Android 集成 FFmpeg (一) 基礎(chǔ)知識(shí)及簡(jiǎn)單調(diào)用

前言

網(wǎng)上關(guān)于 Android 集成 FFmpeg 的文章很多,但大多數(shù)都只介紹了步驟,沒(méi)有說(shuō)明背后的原理,若之前沒(méi)有集成底層庫(kù)的經(jīng)驗(yàn),那就會(huì)“神知無(wú)知”的走一步看一步,出錯(cuò)幾率很大,出錯(cuò)了也不知道原因,然后會(huì)亂猜“這篇教程有問(wèn)題“,“換個(gè)版本估計(jì)可以”,甚至“電腦有問(wèn)題,重裝下系統(tǒng)試試”。

為什么會(huì)出現(xiàn)這種情況,答案很簡(jiǎn)單:欲速則不達(dá),要實(shí)現(xiàn) Android 端集成 FFmpeg 功能,那就要掌握必需的基礎(chǔ)知識(shí),如果連 JNI、NDK都不了解,一上來(lái)就參考幾秒鐘搜出來(lái)的集成步驟開(kāi)始集成,那只會(huì)被各種莫名其妙的異常完虐,如果運(yùn)氣好很快就實(shí)現(xiàn)了功能呢? 在我看來(lái)這是更大的損失,這些你沒(méi)有掌握的知識(shí)如此接近卻又悄悄溜走。

那么在 Android 端集成 FFmpeg 需要掌握哪些基礎(chǔ)知識(shí)呢?個(gè)人認(rèn)為以下內(nèi)容是需要了解的:

  1. JNI
  2. CPU架構(gòu)
  3. 交叉編譯
  4. NDK
  5. FFmpeg 簡(jiǎn)介

以下知識(shí)點(diǎn)闡述是經(jīng)過(guò)反復(fù)推敲的,不是隨意復(fù)制而來(lái),其中融入自己的理解,以個(gè)人易于理解的方式記錄下來(lái),希望能給你帶來(lái)幫助。

1.JNI

JNI,即 Java Native Interface ,是 Java 提供用來(lái)與其他語(yǔ)言通信的 api ,“其他語(yǔ)言”意味不止局限于 C 或 C++ ,也可以調(diào)用除 C 和 C++ 之外的語(yǔ)言,只是大多數(shù)情況下調(diào)用 C 或 C++ ; “通信”意味著 Java 和 其他語(yǔ)言之間可以相互調(diào)用,不止局限于 Java 調(diào)用其他語(yǔ)言,其他語(yǔ)言也可以主動(dòng)調(diào)用 Java .

Java 虛擬機(jī)實(shí)現(xiàn)了跨平臺(tái)特性, 無(wú)法很好的實(shí)現(xiàn)與操作系統(tǒng)相關(guān)的本地操作,而 C 或 C++ 可以,同時(shí)代表著 C 或 C++ 不具備 Java 的跨平臺(tái)能力,那么當(dāng)我們?cè)诔绦蛑惺褂?JNI 功能時(shí),就必須關(guān)注程序的平臺(tái)可移植性, JNI 標(biāo)準(zhǔn)要求本地代碼至少能工作在任何Java 虛擬機(jī)環(huán)境。

簡(jiǎn)而言之,跨平臺(tái)的 Java 調(diào)用了不跨平臺(tái)的 C/C++,使程序喪失了跨平臺(tái)性,這就是 JNI 的副作用,所以可以不使用 JNI 時(shí)就盡量避免。而大多數(shù)不可避免的情況是:已存在用 C/C++ 寫的程序/庫(kù)或者 Java 語(yǔ)言不支持程序所要實(shí)現(xiàn)的特性,比如 ffmpeg 是由 C 編寫的,則必須要通過(guò) JNI 實(shí)現(xiàn)調(diào)用。

JNI 的實(shí)現(xiàn)步驟很簡(jiǎn)單,如下:

  1. 編寫帶有 native 方法的 Java 類
  2. 生成該類擴(kuò)展名為 .h 的頭文件
  3. 創(chuàng)建該頭文件的 C/C++ 文件,實(shí)現(xiàn) native 方法
  4. 將該 C/C++ 文件編譯成動(dòng)態(tài)鏈接庫(kù)
  5. 在Java 程序中加載該動(dòng)態(tài)鏈接庫(kù)

動(dòng)態(tài)鏈接庫(kù)是一組源代碼的模塊,其中包含可供應(yīng)用程序調(diào)用的函數(shù)。比如 Windows 下的 .dll 文件就是一種動(dòng)態(tài)鏈接庫(kù),也就是說(shuō) Java 程序在 Windows 中運(yùn)行,所需的動(dòng)態(tài)鏈接庫(kù)就是 .dll 文件; 如果 Java 程序在 Linux 中運(yùn)行,所需的動(dòng)態(tài)鏈接庫(kù)就是 .so 文件 ,這里 JNI 的副作用已初見(jiàn)端倪,本來(lái)無(wú)視操作系統(tǒng)的 Java ,因?yàn)?JNI ,就要考慮運(yùn)行環(huán)境是 Windows 還是 Linux 。除此之外,還要考慮 CPU 架構(gòu),這也是 Android 中使用 JNI 主要需考慮的 so 庫(kù)兼容型問(wèn)題。

對(duì)于 Java 程序來(lái)說(shuō),需要的僅僅是編譯后的動(dòng)態(tài)鏈接庫(kù),不需要 C/C++ 文件和 .h 頭文件。Android 亦如此,網(wǎng)上很多 Android NDK 教程會(huì)把需要編譯的 C/C++ 源碼放入 Android 工程中,形成類似這樣的工程結(jié)構(gòu):

這對(duì)新手來(lái)說(shuō)可能會(huì)產(chǎn)生誤導(dǎo),誤以為 Android 工程需要 C/C++ 文件或 .h 頭文件或者其他的文件,要清楚的是, Android 工程需要的僅僅是編譯后的 .so 庫(kù),所以我們可以在工程之外編譯完后,只將 .so 庫(kù)移植到工程中。那為什么大多數(shù)教程會(huì)把源碼先移植到 Android 工程中再去編譯呢?目的只有一個(gè):節(jié)省目錄的切換以及 .so 庫(kù)的復(fù)制時(shí)間,實(shí)際上這些時(shí)間微乎其微,我推薦新手將編譯操作于工程外進(jìn)行,更易理解。

2.CPU 架構(gòu)

我們都知道 CPU 是什么,那 CPU 架構(gòu)到底是什么呢?回歸到“架構(gòu)”這個(gè)詞本身含義,CPU 架構(gòu)就是 CPU 的框架結(jié)構(gòu)、設(shè)計(jì)方案,處理器廠商以某種架構(gòu)為基礎(chǔ),生產(chǎn)自己的 CPU,就好比“總-分-總”是文章的一種架構(gòu),多篇文章可以都基于“總-分-總”架構(gòu)。

常見(jiàn)的 CPU 架構(gòu)有 x86、x86-64 以及 arm 等, x86-64 其實(shí)也是基于 x86 架構(gòu),只是在 x86 的基礎(chǔ)上做了一些擴(kuò)展,以支持 64 位程序的應(yīng)用,常見(jiàn)的 Intel 、AMD 處理器都是基于 x86 架構(gòu)的。

而 x86 架構(gòu)主打的是 pc 端,對(duì)于移動(dòng)端,arm 架構(gòu)處于霸主地位 ,由于其體積小、低功耗、低成本、高性能的優(yōu)點(diǎn),被廣泛應(yīng)用在嵌入式系統(tǒng)中,目前大多數(shù)安卓、蘋果手機(jī)的 CPU 都基于 arm 架構(gòu),此處所說(shuō)的 arm 架構(gòu)指 arm 系列架構(gòu),其中包括 ARMv5 、ARMv7 等等。

最后再看 Android 端 , Android 系統(tǒng)目前支持 ARMv5、ARMv7、ARMv8、 x86 、x86_64、MIPS 以及 MIPS64 共七種 CPU 架構(gòu),也就是說(shuō)除此之外其他 CPU 架構(gòu)的硬件并不能運(yùn)行 Android 系統(tǒng)。

3.交叉編譯

在某個(gè)平臺(tái)上,編譯該平臺(tái)的可執(zhí)行程序,叫做本地編譯,比如在 Windows 平臺(tái)上編譯 Windows 自身的可執(zhí)行程序;在 x86 平臺(tái)上,編譯 x86 平臺(tái)自身的可執(zhí)行程序。

在某個(gè)平臺(tái)上,編譯另一種平臺(tái)的可執(zhí)行程序,就是交叉編譯,比如在 x86 平臺(tái)上,編譯 arm 平臺(tái)的可執(zhí)行程序,這也是 Android 端使用最多的交叉編譯類型。

在交叉編譯時(shí),由于主機(jī)與目標(biāo)的體系架構(gòu)、環(huán)境不同,所以交叉編譯比本地編譯復(fù)雜很多,需要一些工具來(lái)解決主機(jī)與目標(biāo)不同特性的問(wèn)題,這些工具構(gòu)成的工具集就叫做交叉編譯鏈。

既然交叉編譯比本地復(fù)雜很多,那為什么不使用本地編譯,比如在 arm 平臺(tái)編譯 arm 平臺(tái)的可執(zhí)行程序呢?這是因?yàn)槟繕?biāo)平臺(tái)存儲(chǔ)空間和計(jì)算能力通常是有限的,而編譯過(guò)程需要較大的存儲(chǔ)空間和較快的計(jì)算能力,但目標(biāo)平臺(tái)無(wú)法提供。

4.NDK

我們需要的是 arm 平臺(tái)的動(dòng)態(tài)庫(kù),而這一編譯過(guò)程往往是在 x86 平臺(tái)上進(jìn)行,所以屬于交叉編譯,需要交叉編譯鏈來(lái)實(shí)現(xiàn),所以 NDK(Native Development Kit )中提供了交叉編譯鏈,方便開(kāi)發(fā)。

Android 中包括七種 CPU 架構(gòu),NDK 中自然就有與之對(duì)應(yīng)的交叉編譯鏈,以下是 Android 官網(wǎng)對(duì)此的表格描述:

除此之外,NDK 還提供了一些原生標(biāo)頭和共享庫(kù)文件,包括 C/C++ 支持庫(kù)、從 C/C++ 代碼中可以向 Android 系統(tǒng)輸出日志的 < android/log.h > 等等,可以點(diǎn)擊這里了解更多,總之,NDK 是用來(lái)幫助我們實(shí)現(xiàn)交叉編譯的工具。

在實(shí)際使用時(shí),比較重要的是 Android.mk 語(yǔ)法,內(nèi)容并不多,但你必須了解,不然只復(fù)制別人的配置很容易出錯(cuò),關(guān)鍵是你無(wú)法真正的掌握這部分知識(shí),而最好的學(xué)習(xí)方法就是仔細(xì)閱讀幾遍 Android.mk 官網(wǎng)教程

另外還需要了解什么是 ABI ,ABI 即 application binary interface ,應(yīng)用程序二進(jìn)制接口,顧名思義,“二進(jìn)制接口”說(shuō)明這是程序與系統(tǒng)之間的底層接口,它定義了程序如何與系統(tǒng)交互。我們應(yīng)該指定每個(gè) CPU 架構(gòu)所對(duì)應(yīng)的 ABI,所以 Android 中就出現(xiàn)了 armeabi 、armeabi-v7a、arm64-v8a、x86、x86_64、mips 以及 mips64 目錄來(lái)區(qū)分不同的 ABI ,我們將編譯好的動(dòng)態(tài)庫(kù)放入對(duì)應(yīng) CPU 架構(gòu)的 ABI 目錄中就可以了。

掌握了以上知識(shí)點(diǎn),才能知道 Android 集成 FFmpeg本質(zhì)上是在做什么,為什么要這樣做。不只是集成 FFmpeg,這些知識(shí)對(duì)于任何底層庫(kù)的集成都是通用、必要的。

5.FFmpeg 簡(jiǎn)介

FFmpeg 是一套可以用來(lái)記錄、轉(zhuǎn)換數(shù)字音頻視頻,并能將其轉(zhuǎn)化為流的開(kāi)源計(jì)算機(jī)程序

FFmpeg 被很多開(kāi)源項(xiàng)目和軟件使用,比如暴風(fēng)影音、QQ影音、格式工廠等,另外在我常用的 App 中也發(fā)現(xiàn)了它的身影:

其實(shí)寫到這里,有點(diǎn)猶豫非操作步驟的內(nèi)容是否敘述過(guò)多了,后來(lái)想想,這正是這篇文章的初衷,盡量全面清楚,透過(guò)表面操作看本質(zhì),另外最重要的一個(gè)目標(biāo),就是爭(zhēng)取做到授人以漁,我相信這也是大家對(duì)所有技術(shù)教程文章的一個(gè)美好愿景。

下面分為 3 個(gè)部分介紹 FFmpeg的編譯及 Android 端的簡(jiǎn)單調(diào)用:

  1. 準(zhǔn)備
  2. 編譯 FFmpeg
  3. 編譯動(dòng)態(tài)庫(kù)及調(diào)用

1.準(zhǔn)備

  • Linux 環(huán)境(Ubuntu 16.04):從個(gè)人經(jīng)驗(yàn)而談,準(zhǔn)備 Linux 環(huán)境比 Windows 環(huán)境編譯的工作量小。
  • 下載NDK(android-ndk-r14b) :網(wǎng)上有些教程美言曰“NDK向后兼容”,其實(shí)查閱 NDK 版本更新說(shuō)明就能解決。
  • 下載 FFmpeg (ffmpeg-3.3.3): 官網(wǎng)下載鏈接:https://ffmpeg.org/download.html

2.編譯 FFmpeg

編譯環(huán)境為 x86 的 Linux ,運(yùn)行環(huán)境為 arm 架構(gòu)的 Android 系統(tǒng),目標(biāo)是把 FFmpeg 源碼編譯成 Android 端可調(diào)用的動(dòng)態(tài)庫(kù),這屬于交叉編譯,所以需要 NDK 提供的交叉編譯工具,這是這一步驟的本質(zhì)意義。

Android 工程中只支持導(dǎo)入 .so 結(jié)尾的動(dòng)態(tài)庫(kù),形如:libavcodec-57.so 。但是FFmpeg 編譯生成的動(dòng)態(tài)庫(kù)默認(rèn)格式為 xx.so.版本號(hào) ,形如:libavcodec.so.57 , 所以需要修改 FFmpeg 根目錄下的 configure 文件,使其生成以 .so 結(jié)尾格式的動(dòng)態(tài)庫(kù):

# 將 configure 文件中的:
SLIBNAME_WITH_MAJOR='$(SLIBNAME).$(LIBMAJOR)' 
LIB_INSTALL_EXTRA_CMD='$$(RANLIB) "$(LIBDIR)/$(LIBNAME)"' 
SLIB_INSTALL_NAME='$(SLIBNAME_WITH_VERSION)' 
SLIB_INSTALL_LINKS='$(SLIBNAME_WITH_MAJOR) $(SLIBNAME)'

#替換為:
SLIBNAME_WITH_MAJOR='$(SLIBPREF)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)'
LIB_INSTALL_EXTRA_CMD='$$(RANLIB) "$(LIBDIR)/$(LIBNAME)"'
SLIB_INSTALL_NAME='$(SLIBNAME_WITH_MAJOR)'
SLIB_INSTALL_LINKS='$(SLIBNAME)'

在編譯 FFmpeg 之前,我們需要修改 FFmpeg 的編譯選項(xiàng),主要目的如下:

  • 規(guī)定編譯方式,使其通過(guò)交叉編譯生成我們需要的動(dòng)態(tài)庫(kù)。
  • 選擇所需功能,針對(duì)需求定制 FFmpeg 功能,精簡(jiǎn)動(dòng)態(tài)庫(kù)。

比如我們需要對(duì) mp3 文件進(jìn)行剪切、合并等操作,則應(yīng)開(kāi)啟 mp3 格式編碼與解碼功能( FFmpeg 本身不支持 mp3 格式編碼,需要引入 libmp3lame 庫(kù))。

怎么修改 FFmpeg 的編譯選項(xiàng)呢?在 FFmpeg 根目錄下通過(guò) ./configure 命令進(jìn)行設(shè)置,但是為了方便記錄與修改,我們選擇在根目錄下建立一個(gè)腳本文件來(lái)運(yùn)行 ./configure 命令。

針對(duì)所需功能,腳本文件如下 :

#!/bin/bash  
NDK=/home/yhao/Android/android-ndk-r14b
SYSROOT=$NDK/platforms/android-14/arch-arm/
TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64

CPU=arm
PREFIX=$(pwd)/Android/$CPU
MP3LAME=/home/yhao/sf/lame-3.99.5/android

./configure \
    --prefix=$PREFIX \          #規(guī)定編譯文件在哪里生成
    --enable-cross-compile \    #啟用交叉編譯方式
    --cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \  #交叉編譯鏈
    --target-os=linux \         #目標(biāo)系統(tǒng)
    --arch=arm \                #目標(biāo)平臺(tái)架構(gòu)
    --sysroot=$SYSROOT \        #交叉編譯環(huán)境
    --extra-cflags="-I${MP3LAME}/include" \                 #額外需要的頭文件
    --extra-ldflags="-L${MP3LAME}/lib" \                    #額外需要的庫(kù)                 
    --enable-shared \           #生成動(dòng)態(tài)庫(kù)(共享庫(kù))
    --disable-static \          #禁止生成靜態(tài)庫(kù)
    --disable-doc \             #禁用不需要的功能,下同
    --disable-ffserver \
    --disable-parsers \
    --disable-protocols \
    --disable-indevs \
    --disable-bsfs \
    --disable-muxers \
    --disable-demuxers \
    --disable-hwaccels \
    --disable-decoders \
    --disable-encoders \
    --enable-parser=mpegaudio \ #啟用需要的功能,下同
    --enable-protocol=http \
    --enable-protocol=file \
    --enable-libmp3lame \
    --enable-encoder=libmp3lame \
    --enable-encoder=png \
    --enable-demuxer=mp3 \
    --enable-muxer=mp3 \
    --enable-decoder=mjpeg \
    --enable-decoder=mp3

除強(qiáng)迫癥風(fēng)格的注釋之外,再對(duì)個(gè)別配置進(jìn)行說(shuō)明:

  • --sysroot=$SYSROOT : 前言中提到 NDK 除了提供 交叉編譯鏈 以外,還提供一些原生標(biāo)頭和共享庫(kù)文件,通過(guò)此配置指定了交叉編譯環(huán)境,使其在編譯過(guò)程中能夠引用到 NDK 提供的原生標(biāo)頭和共享庫(kù)文件,其中 android-14 目錄指定了生成動(dòng)態(tài)庫(kù)最低支持的 Android 版本,嗯~ ,這里可以說(shuō)它是向后兼容的。

  • --target-os=linux :Android 內(nèi)核為 Linux ,故在此指定為 linux ,如果要編譯的目標(biāo)系統(tǒng)為 ios ,則指定為 darwin 。

  • --cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- : 類似于通配符方式指定 bin 目錄下以 arm-linux-androideabi-
    開(kāi)頭的交叉編譯工具,假如不支持這種配置方式則需分別指定,比如在交叉編譯 libmp3lame 時(shí)就是分別指定的:

這里寫圖片描述
  • --extra-cflags 和 --extra-ldflags : 由于開(kāi)啟 mp3 編碼需要引入 libmp3lame 庫(kù),所以需要指定編譯好的 libmp3lame 頭文件和庫(kù)文件的路徑,這樣在編譯時(shí)才能正確引用到 libmp3lame 。這里是我編譯好的 libmp3lame 庫(kù),下載后指定對(duì)應(yīng)路徑即可 。

  • FFmpeg 功能的開(kāi)啟和禁用 : 在 FFmpeg 源碼根目錄下通過(guò) ./configure --help 命令查看所有配置選項(xiàng),針對(duì)需求配置,這里針對(duì) mp3 文件操作的功能進(jìn)行配置,禁用了很多不需要的功能,大幅精簡(jiǎn)動(dòng)態(tài)庫(kù),從而減小 APK 大小。

接下來(lái)運(yùn)行該腳本文件使配置生效,比如我的腳本文件名為 config.sh :

sh config.sh

運(yùn)行成功后會(huì)輸出生效的配置,可以看到此時(shí)支持的編解碼:

這里寫圖片描述

ok ,現(xiàn)在已經(jīng)配置完編譯選項(xiàng)了,接下來(lái)就可以開(kāi)始編譯了啦~

sudo make -j4

編譯完成不要忘記安裝:

sudo make install

然后就可以看到成功生成:

這里寫圖片描述

3.編譯動(dòng)態(tài)庫(kù)及調(diào)用

你可能會(huì)疑問(wèn),上一步已經(jīng)編譯 FFmpeg 源碼生成動(dòng)態(tài)庫(kù)了,為什么這一步還是“編譯動(dòng)態(tài)庫(kù)”呢?其實(shí)這個(gè)問(wèn)題等效于:上一步中生成的動(dòng)態(tài)庫(kù)可以直接在 Android 工程中使用嗎?

答案是否定的,回到文章開(kāi)頭 JNI 的使用步驟,在編寫帶有 native 方法的 Java 類后,緊接著就是用 C/C++ 實(shí)現(xiàn)本地接口,這是 Java 與 C/C++ 交互的必要通道。

所以接下來(lái)需要編寫本地接口,在本地接口中調(diào)用上一步編譯好的 FFmpeg 動(dòng)態(tài)庫(kù),然后將本地接口也編譯成動(dòng)態(tài)庫(kù),供 Android 調(diào)用,這一步還需要“編譯動(dòng)態(tài)庫(kù)”。

網(wǎng)上有些教程在這一步把 FFmpeg 源碼、動(dòng)態(tài)庫(kù)全部復(fù)制到 Android 工程中,然后在工程中新建本地接口、mk文件...... 花里胡哨的看的我頭皮發(fā)麻~ 這一步我推薦在 Android 工程之外進(jìn)行,直到生成最終可用的動(dòng)態(tài)庫(kù)之后,再拷貝到 Android 工程中直接使用。

首先新建一個(gè)文件夾,取名隨意,比如 “ndkBuild ”,這個(gè)目錄就作為我們的工作空間,然后在 ndkBuild 下新建 jni 文件夾, 作為編譯工作目錄。 OK~ 接下來(lái)按照前言中的 jni 步驟來(lái)劃分操作:

1. 編寫帶有 native 方法的 Java 類

package com.jni;

public class FFmpeg {
    
    public static native void run();

}

2. 生成該類擴(kuò)展名為 .h 的頭文件

在 Android Studio 的 Terminal 中 切換到 java 目錄下,運(yùn)行 javah 命令生成頭文件:

javah -classpath .  com.jni.FFmpeg

網(wǎng)上很多教程需要先生成 .class 文件,而我在 java 1.8 環(huán)境下親測(cè)以上一句命令即可。

3. 創(chuàng)建該頭文件的 C/C++ 文件,實(shí)現(xiàn) native 方法

將生成的 com_jni_FFmpeg.h 文件剪切到 ndkBuild 的 jni 目錄下,創(chuàng)建對(duì)應(yīng)的 C 文件 com_jni_FFmpeg.c :

#include <android/log.h>
#include "com_jni_FFmpeg.h"

#include <stdlib.h>
#include <stdbool.h>
#include <stdio.h>
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavfilter/avfilter.h"

JNIEXPORT void JNICALL Java_com_jni_FFmpeg_run(JNIEnv *env, jclass obj) {
  
    char info[40000] = {0};
    av_register_all();
    AVCodec *c_temp = av_codec_next(NULL);
    while(c_temp != NULL){
       if(c_temp->decode!=NULL){
          sprintf(info,"%s[Dec]",info);
       }else{
          sprintf(info,"%s[Enc]",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);
       c_temp=c_temp->next;
    }
__android_log_print(ANDROID_LOG_INFO,"myTag","info:\n%s",info);
}

這段程序用于輸出 FFmpeg 支持的編解碼信息,通過(guò) < android/log.h > 的 __android_log_print 方法可以直接將信息輸出到 Android Studio 的 logcat 。

4. 將該 C/C++ 文件編譯成動(dòng)態(tài)鏈接庫(kù)

編譯 FFmpeg 源碼時(shí)的實(shí)際入口是通過(guò) FFmpeg 提供的 makefile ,而在這一步,將直接使用 NDK 提供的編譯方法,需要提供 Application.mk 和 Android.mk 文件。

首先在 jni 目錄下創(chuàng)建 Application.mk 文件 :

APP_ABI := armeabi
APP_PLATFORM=android-14

APP_ABI 表示編譯生成 armeabi 架構(gòu)的 so 庫(kù),APP_PLATFORM 表示最低支持的 Android 版本。

然后在 jni 目錄下創(chuàng)建 Android.mk 文件:

LOCAL_PATH:= $(call my-dir)

INCLUDE_PATH:=/home/yhao/sf/ffmpeg-3.3.3/Android/arm/include
FFMPEG_LIB_PATH:=/home/yhao/sf/ffmpeg-3.3.3/Android/arm/lib

include $(CLEAR_VARS)
LOCAL_MODULE:= libavcodec
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libavcodec-57.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)
 
include $(CLEAR_VARS)
LOCAL_MODULE:= libavformat
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libavformat-57.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)
 
include $(CLEAR_VARS)
LOCAL_MODULE:= libswscale
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libswscale-4.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)
 
include $(CLEAR_VARS)
LOCAL_MODULE:= libavutil
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libavutil-55.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)
 
include $(CLEAR_VARS)
LOCAL_MODULE:= libavfilter
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libavfilter-6.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)
 
include $(CLEAR_VARS)
LOCAL_MODULE:= libswresample
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libswresample-2.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE:= libpostproc
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libpostproc-54.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE:= libavdevice
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libavdevice-57.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := ffmpeg
LOCAL_SRC_FILES := com_jni_FFmpeg.c 
LOCAL_C_INCLUDES := /home/yhao/sf/ffmpeg-3.3.3
LOCAL_LDLIBS := -lm -llog
LOCAL_SHARED_LIBRARIES := libavcodec libavfilter libavformat libavutil libswresample libswscale libavdevice
include $(BUILD_SHARED_LIBRARY)

對(duì)于 Android.mk 文件前面提到過(guò),最好的學(xué)習(xí)方法就是仔細(xì)閱讀幾遍 Android.mk 官網(wǎng)教程 。 簡(jiǎn)要說(shuō)明一下,此文件將之前編譯好的 FFmepg 動(dòng)態(tài)庫(kù)通過(guò) NDK 提供的預(yù)編譯方式編譯,通過(guò) prebuilt 這個(gè)單詞也能猜到它的含義,點(diǎn)擊了解 NDK 預(yù)編譯, 最后將 com_jni_FFmpeg.c 編譯成名為 ffmpeg 的動(dòng)態(tài)庫(kù)。

此時(shí) jni 目錄下應(yīng)有以下四個(gè)文件:

請(qǐng)無(wú)視 Android.mk 文件上的小鎖兒~ 。然后在 jni 目錄下運(yùn)行 (注意要把 ndk 添加到環(huán)境變量) :

ndk-build

大功告成 ,此時(shí)在 ndkBuild 目錄下生成了 libs 和 obj 目錄,而 Android 需要的最終的動(dòng)態(tài)庫(kù)就在 libs 目錄下:

5. 在Java 程序中加載該動(dòng)態(tài)鏈接庫(kù)

將 libs 目錄下的 armeabi 文件夾整體拷貝到 Android Studio 工程的 libs 文件夾下,當(dāng)然如果你的工程已經(jīng)存在 armeabi 目錄,就把該目錄下的動(dòng)態(tài)庫(kù)拷貝到工程的 armeabi 目錄下。

在 FFmpeg 中加載動(dòng)態(tài)庫(kù):

package com.jni;


public class FFmpeg {

    static {
        System.loadLibrary("avutil-55");
        System.loadLibrary("avcodec-57");
        System.loadLibrary("avformat-57");
        System.loadLibrary("avdevice-57");
        System.loadLibrary("swresample-2");
        System.loadLibrary("swscale-4");
        System.loadLibrary("postproc-54");
        System.loadLibrary("avfilter-6");
        System.loadLibrary("ffmpeg");
    }

    public static native void run();
}

記得在應(yīng)用的 build.gradle 文件中 android 節(jié)點(diǎn)下添加動(dòng)態(tài)庫(kù)加載路徑:

    sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
        }
    }

然后在程序中調(diào)用 FFmpeg.run() 方法 ,可以看到 logcat 輸出了 FFmpeg 的編解碼信息:

總結(jié)

這篇文章較多篇幅介紹了 JNI 相關(guān)的基礎(chǔ)知識(shí),對(duì) JNI 經(jīng)驗(yàn)缺乏的人應(yīng)該會(huì)有較多幫助,尤其是交叉編譯和 CPU 架構(gòu)部分,如果一個(gè)月前的我看到估計(jì)都感激涕零了,當(dāng)初一臉茫然的按照網(wǎng)上的教程集成,殊不知不了解這些知識(shí)是真的愣頭青。

此文中僅僅實(shí)現(xiàn)了 Android 端獲取 FFmpeg 編解碼信息,而要實(shí)際使用的話,就要掌握 FFmpeg 提供的函數(shù)或者通過(guò)命令方式調(diào)用,后者難度較小,另外還有 libmp3lame 庫(kù)的編譯,下篇文章一起總結(jié)。

關(guān)注公眾號(hào),Get 更多知識(shí)點(diǎn)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,185評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 175,656評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,647評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,446評(píng)論 6 405
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 54,951評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,189評(píng)論 0 287
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,718評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,602評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,800評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,045評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,419評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,671評(píng)論 1 281
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,420評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,755評(píng)論 2 371

推薦閱讀更多精彩內(nèi)容