【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
和頭文件
的路徑是否正確。