【Android 音视频开发:FFmpeg音视频编解码篇】二、Android 引入FFmpeg
- 2020 年 4 月 2 日
- 筆記

【声 明】
首先,这一系列文章均基于自己的理解和实践,可能有不对的地方,欢迎大家指正。 其次,这是一个入门系列,涉及的知识也仅限于够用,深入的知识网上也有许许多多的博文供大家学习了。 最后,写文章过程中,会借鉴参考其他人分享的文章,会在文章最后列出,感谢这些作者的分享。
码字不易,转载请注明出处!
教程代码:【Github传送门】 |
---|
目录
一、Android音视频硬解码篇:
二、使用OpenGL渲染视频画面篇
- 1,初步了解OpenGL ES
- 2,使用OpenGL渲染视频画面
- 3,OpenGL渲染多视频,实现画中画
- 4,深入了解OpenGL之EGL
- 5,OpenGL FBO数据缓冲区
- 6,Android音视频硬编码:生成一个MP4
三、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
,开始下载(文件比较大,可能会比较慢,请耐心等待)。

下载CMake工具
2. 添加 C/C++ 支持
有两种方式:
一是,新建一个新的工程,并勾选
C/C++
支持选项,系统将自动创建一个支持C/C++
编码的工程。二是,在已有的项目上,手动添加所有的添加项来支持
C/C++
编码,其实就是自己手动添加「第一种方式」
中Android Studio
为我们自动创建的那些东西。
首先,通过新建一个新工程的方式,看看 IDE
为我们生成了那些东西。
1)新建 C/C++ 工程
依次点击 File -> New -> New Project
,进入新建工程页面,拉到最后,选择 Native C++
然后按照默认配置,一路 Next -> Next -> Finish
即可。

新建C++工程
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
目录。如下:

so目录
4. 添加 FFmpeg so 的头文件
在编译 FFmpeg
的时候,除了生成 so
外,还会生成对应的 .h
头文件,也就是 FFmpeg
对外暴露的所有接口。

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
和头文件
的路径是否正确。