【Android 音视频开发打怪升级:FFmpeg音视频编解码篇】二、Android 引入FFmpeg - 简...
source link: https://www.jianshu.com/p/2c9918546edc?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
【Android 音视频开发打怪升级:FFmpeg音视频编解码篇】二、Android 引入FFmpeg
首先,这一系列文章均基于自己的理解和实践,可能有不对的地方,欢迎大家指正。
其次,这是一个入门系列,涉及的知识也仅限于够用,深入的知识网上也有许许多多的博文供大家学习了。
最后,写文章过程中,会借鉴参考其他人分享的文章,会在文章最后列出,感谢这些作者的分享。
码字不易,转载请注明出处!
一、Android音视频硬解码篇:
二、使用OpenGL渲染视频画面篇
三、Android FFmpeg音视频解码篇
- 1,FFmpeg so库编译
- 2,Android 引入FFmpeg
- 3,Android FFmpeg视频解码播放
- 4,Android FFmpeg+OpenSL ES音频解码播放
- 5,Android FFmpeg+OpenGL ES播放视频
- 6,Android FFmpeg简单合成MP4:视屏解封与重新封装
- 7,Android FFmpeg视频编码
本文你可以了解到
本文将介绍如何将上一篇文章编译出来的
FFmpeg so
库,引入到Android
工程中,并验证so
是否可以正常使用。
一、开启 Android 原生 C/C++ 支持
在过去,通常使用 makefile
的方式在项目中引入 C/C++
代码支持,随着 Android Studio
的普及,makefile
的方式已经基本被 CMake
替代。
有了 Android
官方的支持,NDK
层代码的开发变得更加容易。以前一谈到 Android NDK
,许多人就会大惊失色,感觉是深不可测的东西,一方面是 makefile
的编写很难,一方面是 C/C++
相比 Java
来说,比较晦涩。
但是不必担心,一是有了 CMake
,二是对于 C/C++
的基本使用其实和 Java
差不多,本系列涉及到的,也都是对 C/C++
的基础使用,毕竟,高级的我也不会不是吗?哈哈哈~~
1. 安装 CMake
首先,需要下载 CMake
相关工具,在 Android Studio
中依次点击 Tools->SDK Manager->SDK Tools
,然后勾选
CMake
: CMake 构建工具
LLDB
: C/C++ 代码调试工具
NDK
: NDK 环境
最后依次点击 OK->OK->Finish
,开始下载(文件比较大,可能会比较慢,请耐心等待)。
2. 添加 C/C++ 支持
有两种方式:
一是,新建一个新的工程,并勾选
C/C++
支持选项,系统将自动创建一个支持C/C++
编码的工程。
二是,在已有的项目上,手动添加所有的添加项来支持
C/C++
编码,其实就是自己手动添加「第一种方式」
中Android Studio
为我们自动创建的那些东西。
首先,通过新建一个新工程的方式,看看 IDE
为我们生成了那些东西。
1)新建 C/C++ 工程
依次点击 File -> New -> New Project
,进入新建工程页面,拉到最后,选择 Native C++
然后按照默认配置,一路 Next -> Next -> Finish
即可。
2)Android Studio 自动生成了什么
生成的工程目录如下:
重点关注上图标注的3个地方:
- 第一,最上层的
MainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Example of a call to a native method
sample_text.text = stringFromJNI()
}
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
external fun stringFromJNI(): String
companion object {
// Used to load the 'native-lib' library on application startup.
init {
System.loadLibrary("native-lib")
}
}
}
很简单,使用过 so
库的应该都看得懂,这里简单说一下。
代码的最下面,companion object
在 Kotlin
中表示静态代码块,类似 Java
中的 static { }
,其中的代码有且只会被执行一次。
接着在 init{}
方法中,加载了 C/C++
代码编译成的 so
库: native-lib
。
往上一句代码,用 external
声明了一个外部引用的方法 stringFromJNI()
,这个方法和 C/C++
层的代码是对应的。
最终在最上面的 onCreate
中,将从 C/C++
层返回的 String
显示出来。
- 第二,创建了一个
cpp
文件包
其中有两个文件非常重要,分别是 native-lib.cpp
、 CMakeLists.txt
。
i. native-lib.cpp
:是一个 C++ 接口文件,在 MainActivity
中声明的外部方法将在这里得到实现。
自动生成 native-lib.cpp
的内容如下:
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_chenlittleping_mynativeapp_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
可以看到,这个 cpp 文件中的方法命名非常的长,不过其实非常简单。
首先是头部固定写法 extern "C" JNIEXPORT jstring JNICALL
:
extern "C"
表示以 C语言
的方式来编译;
jstring
表示该方法返回类型是 Java
层的 String
类型,类似的还是有: void
jint
等;
然后是 Java 层对应方法的映射,即整个方法命名其实是 Java
层对应方法的绝对路径。
其中,最前面的 Java_
是固定写法;
com_chenlittleping_mynativeapp_MainActivity_
: 对应的是 com.chenlittleping.mynativeapp.MainActivity.
,其实就是 .
换为 _
;
stringFromJNI
和 Java 层的方法一致。
最后是两个参数, JNIEnv *env
和 jobject
,分别代表 JNI
的上下文环境和调用这个接口的 Java
的类的实例。
调用这个方法,将会在 C++
层创建一个字符串,并以 Java#String
的类型返回。
ii. CMakeLists.txt
: 也就是构建脚本。内容如下:
# cmake 最低版本
cmake_minimum_required(VERSION 3.4.1)
# 配置so库编译信息
add_library(
# 输出so库的名称
native-lib
# 设置生成库的方式,默认为SHARE动态库
SHARED
# 列出参与编译的所有源文件
native-lib.cpp)
# 查找代码中使用到的系统库
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log)
# 指定编译目标库时,cmake要链接的库
target_link_libraries(
# 指定目标库,native-lib 是在上面 add_library 中配置的目标库
native-lib
# 列出所有需要链接的库
${log-lib})
这是最简单的编译配置,具体见上面的注释。
CMakeLists.txt
的目的就是配置可以编译出 native-lib
so 库的构建信息。
说白了,就是告诉编译器:
- 编译的目标是谁
- 依赖的源文件在哪里找
- 依赖的 `系统或第三方` 的 `动态或静态` 库在哪里找。
- 第三,在 Gradle 文件中注册 CMake 脚本
在 第二步
中,已经把构建 so
库的信息配置好了,接下来要把这些信息注册到 Gradle
中,编译器才会去编译它。
app 的 build.gradle
内容如下:
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 28
buildToolsVersion "29.0.1"
defaultConfig {
applicationId "com.chenlittleping.mynativeapp"
minSdkVersion 19
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// 1) CMake 编译配置
externalNativeBuild {
cmake {
cppFlags ""
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
// 2) 配置 CMakeLists 路径
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.10.2"
}
}
}
dependencies {
// 省略无关代码
//......
}
最主要的两个地方是两个 externalNativeBuild
。
第 1 个 externalNativeBuild
中,可以做一些优化配置,比如只打包包含 armeabi
架构的 so
:
externalNativeBuild {
cmake {
cppFlags ""
}
ndk {
abiFilters "armeabi" //, "armeabi-v7a"
}
}
第 2 个 externalNativeBuild
,主要是配置 CMakeLists.txt
的路径和版本。
Android Studio
为我们生成的关于C/C++
支持的主要就是以上三个地方,有了以上配置,就可以在MainActivity
页面中正常的显示出Hello from C++
。
3) 在已有工程上添加 C/C++
支持
前面就说过,在已有项目上添加 C/C++
支持,就是由我们自己手动添加整个配置。那么根据签名介绍的三个步骤,依葫芦画瓢,就可以添加了。
这里刚好就用添加 FFMpeg so
到本系列文章现有 Demo 工程中来演示一遍。
二、引入 FFmpeg so
1. 新建 cpp 目录
首先,在 app/src/main/
目录下,新建文件夹,并命名为 cpp
。
接着,在 cpp
目录下,右键 New -> C/C++ Source File
,新建 native-lib.cpp
文件。
接着,在 cpp
目录下,右键 New -> File
,新建 CMakeLists.txt
,先将上面 IDE
生成的那份代码粘贴进来, FFmpeg的配置在后面详细讲解。
# CMakeLists.txt
# cmake 最低版本
cmake_minimum_required(VERSION 3.4.1)
# 配置so库编译信息
add_library(
# 输出so库的名称
native-lib
# 设置生成库的方式,默认为SHARE动态库
SHARED
# 列出参与编译的所有源文件
native-lib.cpp)
# 查找代码中使用到的系统库
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log)
# 指定编译目标库时,cmake要链接的库
target_link_libraries(
# 指定目标库,native-lib 是在上面 add_library 中配置的目标库
native-lib
# 列出所有需要链接的库
${log-lib})
2. 将 CMakeLists 配置到 build.gradle 中
android {
// ...
defaultConfig {
// ...
// 1) CMake 编译配置
externalNativeBuild {
cmake {
cppFlags ""
}
}
}
// ...
// 2) 配置 CMakeLists 路径
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.10.2"
}
}
}
// ...
如果只是简单的编写 C/C++
代码,以上基础配置就可以了。
接着来看看本文的重点,如何使用 CMakeLists.txt
引入 FFmpeg
的动态库。
3. 将 FFmpeg so 库放到对应的 CPU 架构目录
在 上一篇文章中,我们编译的 FFmpeg so
库的 CPU
架构为 armv7-a
,所以,我们需要把所有的 so
库放置到 armeabi-v7a
目录下。
首先,在 app/src/main/
目录下,新建文件夹,并命名为 jniLibs
。
app/src/main/jniLibs
是 Android Studio 默认的放置 so 动态库的目录。
接着,在 jniLibs
目录下,新建 armeabi-v7a
目录。
最后把 FFmpeg
编译得到的所有 so
库粘贴到 armeabi-v7a
目录。如下:
4. 添加 FFmpeg so 的头文件
在编译 FFmpeg
的时候,除了生成 so
外,还会生成对应的 .h
头文件,也就是 FFmpeg
对外暴露的所有接口。
在 cpp
目录下,新建 ffmpeg
目录,然后把编译时生成的 include
文件粘贴进来。
5. 添加、链接 FFmpeg so 库
上面已经把 so
和 头文件
放置到对应的目录中了,但是编译器是不会把它们编译、链接、并打包到 Apk
中的,我们还需要在 CMakeLists.txt
中显性的把相关的 so
添加和链接起来。完整的 CMakeLists.txt
如下:
cmake_minimum_required(VERSION 3.4.1)
# 支持gnu++11
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11")
# 1. 定义so库和头文件所在目录,方面后面使用
set(ffmpeg_lib_dir ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
set(ffmpeg_head_dir ${CMAKE_SOURCE_DIR}/ffmpeg)
# 2. 添加头文件目录
include_directories(${ffmpeg_head_dir}/include)
# 3. 添加ffmpeg相关的so库
add_library( avutil
SHARED
IMPORTED )
set_target_properties( avutil
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libavutil.so )
add_library( swresample
SHARED
IMPORTED )
set_target_properties( swresample
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libswresample.so )
add_library( avcodec
SHARED
IMPORTED )
set_target_properties( avcodec
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libavcodec.so )
add_library( avfilter
SHARED
IMPORTED)
set_target_properties( avfilter
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libavfilter.so )
add_library( swscale
SHARED
IMPORTED)
set_target_properties( swscale
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libswscale.so )
add_library( avformat
SHARED
IMPORTED)
set_target_properties( avformat
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libavformat.so )
add_library( avdevice
SHARED
IMPORTED)
set_target_properties( avdevice
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libavdevice.so )
# 查找代码中使用到的系统库
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
# 配置目标so库编译信息
add_library( # Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
native-lib.cpp
)
# 指定编译目标库时,cmake要链接的库
target_link_libraries(
# 指定目标库,native-lib 是在上面 add_library 中配置的目标库
native-lib
# 4. 连接 FFmpeg 相关的库
avutil
swresample
avcodec
avfilter
swscale
avformat
avdevice
# Links the target library to the log library
# included in the NDK.
${log-lib} )
主要看看注释中新加入的 1~4
点。
1)通过 set
方法定义了 so
和 头文件
所在目录,方便后面使用。
其中
CMAKE_SOURCE_DIR
为系统变量,指向CMakeLists.txt
所在目录。ANDROID_ABI
也是系统变量,指向 so 对应的CPU
框架目录:armeabi、armeabi-v7a、x86 ...
set(ffmpeg_lib_dir ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
set(ffmpeg_head_dir ${CMAKE_SOURCE_DIR}/ffmpeg)
2)通过 include_directories
设置头文件查找目录
include_directories(${ffmpeg_head_dir}/include)
3)通过 add_library
添加 FFmpeg 相关的 so
库,以及 set_target_properties
设置 so
对应的目录。
其中,add_library 第一个参数为 so 名字,
SHARED
表示引入方式为动态库引入。
add_library( avcodec
SHARED
IMPORTED )
set_target_properties( avcodec
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libavcodec.so )
4)最后,通过 target_link_libraries
把前面添加进来的 FFMpeg so
库都链接到目标库 native-lib
上。
这样,我们就将 FFMpeg
相关的 so
库都引入到当前工程中了。下面就要来测试一下,是否可以正常调用到 FFmpeg
相关的方法了。
三、使用 FFmpeg
要检查 FFmpeg
是否可以使用,可以通过获取 FFmpeg
基础信息来验证。
1. 在 FFmpegAcrtivity 中添加一个外部方法 ffmpegInfo
把获取到的 FFmpeg
信息显示出来。
class FFmpegActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_ffmpeg_info)
tv.text = ffmpegInfo()
}
private external fun ffmpegInfo(): String
companion object {
init {
System.loadLibrary("native-lib")
}
}
}
2. 在 native-lib.cpp 中添加对应的 JNI 层方法
#include <jni.h>
#include <string>
#include <unistd.h>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavfilter/avfilter.h>
#include <libavcodec/jni.h>
JNIEXPORT jstring JNICALL
Java_com_cxp_learningvideo_FFmpegActivity_ffmpegInfo(JNIEnv *env, jobject /* this */) {
char info[40000] = {0};
AVCodec *c_temp = av_codec_next(NULL);
while (c_temp != NULL) {
if (c_temp->decode != NULL) {
sprintf(info, "%sdecode:", info);
switch (c_temp->type) {
case AVMEDIA_TYPE_VIDEO:
sprintf(info, "%s(video):", info);
break;
case AVMEDIA_TYPE_AUDIO:
sprintf(info, "%s(audio):", info);
break;
default:
sprintf(info, "%s(other):", info);
break;
}
sprintf(info, "%s[%10s]\n", info, c_temp->name);
} else {
sprintf(info, "%sencode:", info);
}
c_temp = c_temp->next;
}
return env->NewStringUTF(info);
}
}
首先,我们看到代码被包裹在 extern "C" { }
当中,和前面的系统创建的稍微有些不同,通过这个大括号包裹,我们就不需要每个方法都添加单独的 extern "C"
开头了。
另外,由于 FFmpeg
是使用 C
语言编写的,所在 C++
文件中引用 #include
的时候,也需要包裹在 extern "C" { }
,才能正确的编译。
方法的新建就不用说了,和前面介绍的命名方法一致。
在方法中,使用 FFmpeg
提供的方法 av_codec_next
,获取到 FFmpeg 的编解码器,然后通过循环遍历,将所有的音视频编解码器信息拼接起来,最后返回给 Java
层。
至此,FFmpeg
加入到工程中,并被调用。
如果一切正常,App运行后,就会显示出 FFmpeg
音视频编解码器的信息。
如果由提示
so
或者头文件
找不到,需要检查CMakeLists.txt
中设置的so
和头文件
的路径是否正确。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK