導入Ffmpeg到項目

目錄結構

其中有兩個文件非常重要,分別是 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 加入到工程中,並被調用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章