其中有兩個文件非常重要,分別是 native-lib.cpp 、 CMakeLists.txt。
- native-lib.cpp :是一個 C++ 接口文件,在 MainActivity 中聲明的外部方法將在這裏得到實現。
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_ffmpeg_myapplication_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_ffmpeg_myapplication_MainActivity_: 對應的是 com.ffmpeg.myapplication.MainActivity.
stringFromJNI 和 Java 層的方法一致。
最後是兩個參數, JNIEnv *env 和 jobject,分別代表 JNI 的上下文環境和調用這個接口的 Java 的類的實例。
- 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 29
buildToolsVersion "29.0.3"
defaultConfig {
applicationId "com.ffmpeg.myapplication"
minSdkVersion 16
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
cppFlags "-std=c++11"
}
ndk {
abiFilters "armeabi-v7a","x86"
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.10.2"
}
}
}
dependencies {
...
}
最主要的兩個地方是兩個 externalNativeBuild 。
第 1 個 externalNativeBuild 中,可以做一些優化配置,比如只打包包含 armeabi 架構的 so
第 2 個 externalNativeBuild,主要是配置 CMakeLists.txt 的路徑和版本。
Android Studio 爲我們生成的關於 C/C++ 支持的主要就是以上三個地方,有了以上配置,就可以在 MainActivity 頁面中正常的顯示出 Hello from C++ 。
引入 FFmpeg so
將 FFmpeg so 庫放到對應的 CPU 架構目錄,在 app/src/main/ 目錄下,新建文件夾,並命名爲 jniLibs(app/src/main/jniLibs 是 Android Studio 默認的放置 so 動態庫的目錄。),接着,在 jniLibs 目錄下,新建 armeabi-v7a 目錄。
然後,添加 FFmpeg so 的頭文件,在編譯 FFmpeg 的時候,除了生成 so 外,還會生成對應的 .h 頭文件,也就是 FFmpeg 對外暴露的所有接口。
添加、鏈接 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} )
使用 FFmpeg
要檢查 FFmpeg 是否可以使用,可以通過獲取 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")
}
}
}
在 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);
} else {
sprintf(info, "%sencode:", 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[%s]\n", info, c_temp->name);
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 加入到工程中,並被調用。