【Android 音視頻開發打怪升級:FFmpeg音視頻編解碼篇】2、Android 引入FFmpeg

【聲 明】

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

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

教程代碼:【Github傳送門

目錄

1、Android音視頻硬解碼篇:

2、使用OpenGL渲染視頻畫面篇

3、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 是否能夠正常使用。github

1、開啓 Android 原生 C/C++ 支持

在過去,一般使用 makefile 的方式在項目中引入 C/C++ 代碼支持,隨着 Android Studio 的普及,makefile 的方式已經基本被 CMake 替代。web

有了 Android 官方的支持,NDK 層代碼的開發變得更加容易。之前一談到 Android NDK ,許多人就會大驚失色,感受是深不可測的東西,一方面是 makefile 的編寫很難,一方面是 C/C++ 相比 Java 來講,比較晦澀。bash

可是沒必要擔憂,一是有了 CMake ,二是對於 C/C++ 的基本使用其實和 Java 差很少,本系列涉及到的,也都是對 C/C++ 的基礎使用,畢竟,高級的我也不會不是嗎?哈哈哈~~架構

1. 安裝 CMake

首先,須要下載 CMake 相關工具,在 Android Studio 中依次點擊 Tools->SDK Manager->SDK Tools,而後勾選app

CMake : CMake 構建工具框架

LLDB : C/C++ 代碼調試工具ide

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 工程中來演示一遍。

2、引入 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 相關的方法了。

3、使用 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);
            } 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 加入到工程中,並被調用。

若是一切正常,App運行後,就會顯示出 FFmpeg 音視頻編解碼器的信息。

若是由提示 so 或者 頭文件 找不到,須要檢查 CMakeLists.txt 中設置的 so頭文件 的路徑是否正確。

相關文章
相關標籤/搜索