­

【Android 音視頻開發:FFmpeg音視頻編解碼篇】二、Android 引入FFmpeg

【聲 明】

首先,這一系列文章均基於自己的理解和實踐,可能有不對的地方,歡迎大家指正。 其次,這是一個入門系列,涉及的知識也僅限於夠用,深入的知識網上也有許許多多的博文供大家學習了。 最後,寫文章過程中,會借鑒參考其他人分享的文章,會在文章最後列出,感謝這些作者的分享。

碼字不易,轉載請註明出處!

教程代碼:【Github傳送門】

目錄

一、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 ,開始下載(文件比較大,可能會比較慢,請耐心等待)。

下載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 objectKotlin 中表示靜態代碼塊,類似 Java 中的 static { },其中的代碼有且只會被執行一次。

接着在 init{} 方法中,加載了 C/C++ 代碼編譯成的 so 庫: native-lib

往上一句代碼,用 external 聲明了一個外部引用的方法 stringFromJNI() ,這個方法和 C/C++ 層的代碼是對應的。

最終在最上面的 onCreate 中,將從 C/C++ 層返回的 String 顯示出來。

  • 第二,創建了一個 cpp 文件包

其中有兩個文件非常重要,分別是 native-lib.cppCMakeLists.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 *envjobject,分別代表 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頭文件 的路徑是否正確。