首先,這一系列文章均基於本身的理解和實踐,可能有不對的地方,歡迎你們指正。
其次,這是一個入門系列,涉及的知識也僅限於夠用,深刻的知識網上也有許許多多的博文供你們學習了。
最後,寫文章過程當中,會借鑑參考其餘人分享的文章,會在文章最後列出,感謝這些做者的分享。html
碼字不易,轉載請註明出處!linux
教程代碼:【Github傳送門】 |
---|
使用
GCC
或CLANG
交叉編譯出Android平臺可使用的FFmpeg so庫。爲了很好的邁出FFmpeg
開發的第一步,不只要知其然,更要知其因此然。不只要知道怎麼樣能成功編譯,更要知道爲何能成功編譯。在開始動手以前,建議先通讀整篇文章,相信本文定可讓你有所感悟。android
網上其實已經有不少的關於FFmpeg so庫編譯的分享,可是大部分都是直接把配置文件的內容貼出來。我想大部分去搜索 「如何編譯FFmpeg so庫」的人,對交叉編譯這個東東都是比較陌生的。git
特別對於移動端開發者來講,大部分人大多數時候都是在Java層作開發,不多接觸到NDK層的東西。若是直接去看一份交叉編譯的配置,估計會很上頭。github
一般狀況下,在一篇FFmpeg編譯的文章下面都會有不少的相似「爲何按照樓主的配置仍是沒法編譯成功?」的評論,那爲何人家能夠編譯成功,咱們copy下來卻不能夠呢?shell
緣由有很是多,大部分其實集中在如下幾個方面:bash
1. 無腦copy,祈求有一個傻瓜式的配置能夠成功編譯;
2. FFmpeg版本和NDK版本不少,每個版本均可能須要不同的配置;
3. 不瞭解每一個配置項的意義,即便好運配置對了, 可是稍微一修改,又沒法正常編譯了。
複製代碼
爲何FFmpeg讓人以爲很難搞?架構
我想主要是由於邁出第一步就很困難,連so庫都編譯不出來,後面的都是扯淡了。app
引自百度百科的定義:交叉編譯,是在一個平臺上生成另外一個平臺上的可執行代碼。cors
什麼意思呢?說白了,就是在一個機器上生成一個程序,這個程序能夠跑在另一個機器上。舉慄:在PC上編譯一個apk,這個apk能夠跑在Android手機上,這其實就是一個交叉編譯的過程。
咱們知道,PC上的軟件是直接在PC上編譯生成的,那爲何Android上的軟件不能在Android上本身編譯生成呢?
理論上是能夠,可是Android手機上的資源有限啊,在PC上編譯一個apk都要那麼久,你能夠想象在Android手機上編譯一個apk要多久嗎?或者你能想象在手機上敲代碼的情景嗎?
那咱們會想既然PC上資源那麼豐富,那可不能夠利用PC來編譯出在手機上能夠運行的軟件呢?
因而,交叉編譯出現。
咱們知道PC上的環境和手機上的運行環境是絕然不一樣的,若是使用PC上的環境直接編譯的話,能夠想象這個編譯出來的App,分分鐘就會掛掉。
因此,交叉編譯最重要的是,要配置好編譯過程當中使用到的相關的環境,而這個環境其實就是目標機器(好比Android手機)正在運行的環境。
對於C/C++的編譯,一般有兩個工具 GCC
和 CLANG
。
GCC
可能你們都有據說過,這是一個老牌的編譯工具,不只能夠編譯C/C++,也能夠編譯Java,Object-C,Go等語言。
CLANG
則是一個效率更高的C/C++編譯工具,而且兼容GCC,Google在很早之前就開始建議使用clang進行編譯,而且在 ndk 17
之後,把 GCC
移除了,全面推行使用 CLANG
。
鼎鼎大名的FFmpeg,不說在音視頻界如雷貫耳,就算一個不開發音視頻的開發者也都是略有耳聞。
官方簡介
A complete, cross-platform solution to record, convert and stream audio and video.
翻譯過來就是:FFmpeg是一套集錄制、轉換以及流化音視頻的完整的跨平臺解決方案。
從這段簡介能夠看到FFmpeg有如下特色:
從前面的介紹,基本上能夠總結出FFmepg編譯的基本流程:
流程就是這麼簡單,接下來就來詳細看看,如何經過 CLANG
和 GCC
兩種方式來編譯。
注:本文編譯平臺爲Mac,建議使用Mac或者Linux進行編譯,聽說Windows有不少坑。
Android 的 NDK
已經迭代了不少版本,在 r17c
之後,Google正式移除 GCC
,再也不支持 GCC
,新版本的 NDK
都是使用 CLANG
進行編譯。
這裏就使用目前最新的 NDK r20b
版原本編譯。
NDK
下載地址:Android-NDK
最主要的就是這兩個路徑:
編譯工具鏈目錄:
toolchains/llvm/prebuilt/darwin-x86_64/bin
交叉編譯環境目錄:
toolchains/llvm/prebuilt/darwin-x86_64/sysroot
複製代碼
根據不一樣的CPU架構區和不一樣的Android版本,區分了不一樣的clang工具,根據本身須要選擇就行了。
本文選擇 CPU 架構 armv7a
,Android版本 21
:
armv7a-linux-androideabi21-clang
armv7a-linux-androideabi21-clang++
複製代碼
在 toolchains/llvm/prebuilt/darwin-x86_64/sysroot
目錄下,包含了兩個目錄: usr/include
,usr/lib
,分別對應了 頭文件
和 庫文件
。
FFmpeg官網下載,直接DownLoad便可。
本文使用的是目前最新的版本 ffmpeg-4.2.2
。
下載好源碼後,進入根目錄,找到一個名爲 congfigure
的文件,這是一個shell腳本,用於生成一些 FFmpeg
編譯須要的配置文件。
這個文件很是重要,
FFmpeg
的編譯配置就是靠它完成的。 後面咱們將對其中一些重要的內容進行分析,這是理解FFmpeg
編譯配置的關鍵。
有了以上基礎之後,就能夠對FFmpeg進行編譯了。
cross_prefix_clang
參數打開(注:不是雙擊運行) ffmpeg-4.2.2
根目錄下的 configure
文件,搜索 CMDLINE_SET
,能夠找到如下代碼,而後新增一個命令行選項:cross_prefix_clang
CMDLINE_SET=" $PATHS_LIST ar arch as assert_level build_suffix cc objcc cpu cross_prefix # 新增命令行參數 cross_prefix_clang custom_allocator cxx dep_cc # 省略其餘..... "
複製代碼
搜索 ar_default="${cross_prefix}${ar_default}"
, 找到如下代碼
ar_default="${cross_prefix}${ar_default}"
cc_default="${cross_prefix}${cc_default}"
cxx_default="${cross_prefix}${cxx_default}"
nm_default="${cross_prefix}${nm_default}"
pkg_config_default="${cross_prefix}${pkg_config_default}"
複製代碼
將中間兩行修改成
ar_default="${cross_prefix}${ar_default}"
#------------------------------------------------
cc_default="${cross_prefix_clang}${cc_default}"
cxx_default="${cross_prefix_clang}${cxx_default}"
#------------------------------------------------
nm_default="${cross_prefix}${nm_default}"
pkg_config_default="${cross_prefix}${pkg_config_default}"
複製代碼
至於爲何這麼修改,將在後面的 configure
分析中詳細講解
在 ffmpeg-4.2.2
根目錄下新建 shell
腳本,命名爲: build_android_clang.sh
#!/bin/bash
set -x
# 目標Android版本
API=21
CPU=armv7-a
#so庫輸出目錄
OUTPUT=/Users/cxp/Desktop/FFmpeg/ffmpeg-4.2.2/android/$CPU
# NDK的路徑,根據本身的NDK位置進行設置
NDK=/Users/cxp/Desktop/FFmpeg/android-ndk-r20b
# 編譯工具鏈路徑
TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/darwin-x86_64
# 編譯環境
SYSROOT=$TOOLCHAIN/sysroot
function build
{
./configure \
--prefix=$OUTPUT \
--target-os=android \
--arch=arm \
--cpu=armv7-a \
--enable-asm \
--enable-neon \
--enable-cross-compile \
--enable-shared \
--disable-static \
--disable-doc \
--disable-ffplay \
--disable-ffprobe \
--disable-symver \
--disable-ffmpeg \
--sysroot=$SYSROOT \
--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
--cross-prefix-clang=$TOOLCHAIN/bin/armv7a-linux-androideabi$API- \
--extra-cflags="-fPIC"
make clean all
# 這裏是定義用幾個CPU編譯
make -j12
make install
}
build
複製代碼
這個shell腳本,大致上其實仍是很容易懂的,好比
--disabble-static
--enable-shared
分別用於禁止輸出靜態庫,以及輸出動態庫;
--arch
--cpu
用於配置輸出的so庫是什麼架構的;
--prefix
用於配置輸出的so庫的存放路徑。
接下來重點來說一下幾個選項:
--target-os=android
:在舊版本的 FFmpeg
中,對Android平臺的支持並非很完善,並無 android
這個target,因此在一些比較老的文章中都會提到,編譯Android平臺的so庫,須要對 configure
作如下修改,不然會按照 linux
標準的方式輸出so庫,其命名方式和Android的so不同,Android是沒法加載的。
SLIBNAME_WITH_VERSION='$(SLIBNAME).$(LIBVERSION)'
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_VERSION='$(SLIBNAME).$(LIBVERSION)'
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加入了 android
這個 target
。因此咱們不再須要手動去修改了
。
--sysroot=$SYSROOT
: 用於配置交叉編譯環境的 根路徑
,編譯的時候會默認從這個路徑下去尋找 usr/include
usr/lib
這兩個路徑,進而找到相關的頭文件和庫文件。
r20b
版本的 NDK
系統的頭文件和庫文件就是在 $SYSYROOT/usr/include
和 $SYSYROOT/usr/lib
中。
基本上不少新手在編譯的時候都會出現找不到各類頭文件,致使編譯失敗。因此當編譯出現找不到頭文件的時候,首先要檢查的就是這個路徑。
一點疑問
在使用最新的
ndk r20b
版本進行編譯的時候發現,即便不配置sysroot
也能夠正常編譯,懷疑 Android 的clang
工具是否通過了處理,會自動去尋找對應的路徑。 目前沒有從configure
文件中找到緣由。
若有知情者的,還望告知呀~。
說到 sysroot
就不得不提到另一個參數 -isysyroot
,這個參數也讓我困惑了好久,由於不多文章會提到這個兩個參數的聯繫和區別,然而這個參數也很致使讓人很莫名奇妙的編譯失敗。
介紹 -isysroot
以前,先看看這個 extra-cflags
選項。
這個選項的做用是,給編譯器指定除了 sysroot
以外的頭文件搜索路徑。好比:
--extra-cflags="-I$SYSROOT/usr/include"
# 其中 -I 用於區分不一樣的路徑
複製代碼
而 -isysroot
是這個選項的一個配置。好比
--extra-cflags="-isysroot $SYSROOT"
複製代碼
-isysroot
的做用就是,把後面的路徑設置爲默認的頭文件搜索路徑,這時候,前面 sysroot
配置路徑就再也不做爲 頭文件
默認的搜索路徑了,不過依然是 庫文件
默認的搜索路徑。
能夠看到,這兩個配置從某種程度上說是同樣的:
--extra-cflags="-I$SYSROOT/usr/include"
約等於
--extra-cflags="-isysroot $SYSROOT"
複製代碼
這個和上面的 extra-cflags
做用是相似的,不過是用於配置額外的 庫文件
搜索路徑,如
--extra-ldflags="-L$SYSROOT/usr/lib"
# 其中 -L 用於區分不一樣的路徑
複製代碼
能夠看到 extra-cflags
extra-ldflags
結合起來能夠替代 sysroot
。
這個選項直譯爲 交叉編譯前綴
,指的是交叉編譯工具的前綴。
這個選項常常和另一個選項 cc
一塊兒出現搭配使用。
這是什麼意思呢?網上有的文章對於 cc
這個選項常常出現兩種配置方式:
一種是隻配置 cross-prefix
,沒有配置 cc
,好比本文。
另外一種是既配置 cross-prefix
,又配置 cc
。
好比:
--cc=$TOOLCHAIN/bin/arm-linux-androideabi-gcc \
--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
複製代碼
這是兩種徹底不一樣的配置方式,可是很神奇的是有時候他們都能成功編譯,有時候又會出現找不到編譯鏈工具的錯誤。
爲了搞明白 cross-prefix
cc
這兩個選項的配置到底有什麼影響,到底應該怎麼使用這兩個配置,我特意仔細的去看了 FFmpeg
根目錄下的 configure
配置腳本,找到了一些蛛絲馬跡。
注:如下分析基於ffmpeg-4.2.2版本,其餘版本可能有所不一樣,掌握基本原理便可。
打開(注:不是雙擊運行)configure
shell腳本,首先來看看 configure 是如何獲取用戶配置的編譯選項的。
搜索 for opt do
,能夠找到如下代碼
for opt do
optval="${opt#*=}"
case "$opt" in
--extra-ldflags=*)
add_ldflags $optval
;;
--extra-ldexeflags=*)
add_ldexeflags $optval
;;
--extra-ldsoflags=*)
add_ldsoflags $optval
;;
--extra-ldlibflags=*)
warn "The --extra-ldlibflags option is only provided for compatibility and will be\n"\
"removed in the future. Use --extra-ldsoflags instead."
add_ldsoflags $optval
;;
--extra-libs=*)
add_extralibs $optval
;;
--disable-devices)
disable $INDEV_LIST $OUTDEV_LIST
;;
--enable-debug=*)
debuglevel="$optval"
;;
# 省略中間一些代碼...
*)
optname="${opt%%=*}"
optname="${optname#--}"
optname=$(echo "$optname" | sed 's/-/_/g')
if is_in $optname $CMDLINE_SET; then
eval $optname='$optval'
elif is_in $optname $CMDLINE_APPEND; then
append $optname "$optval"
else
die_unknown $opt
fi
;;
esac
done
複製代碼
這個shell腳本的代碼有不少特有的語法,也不用鑽牛角尖,能大概看明白就能夠了。
for循環的首行 經過分割 =
獲取到用戶設置的選項值 optval
。
下面除了一些特殊的選項,咱們看看最後的通配符 *)
,這段代碼的目的,其實就是把用戶配置的選項和值關聯起來。
好比 --cpu=armv7-a
,前面三行就是把 cpu
分割出來,賦值給 optname
,再把 optval
賦值給 cpu
,說白了就是初始化了 cpu
這個變量爲 armv7-a
。
搜索 android
關鍵字,能夠找到如下代碼
# ffmpeg-4.2.2/configure
if test "$target_os" = android; then
cc_default="clang"
fi
ar_default="${cross_prefix}${ar_default}"
cc_default="${cross_prefix}${cc_default}"
cxx_default="${cross_prefix}${cxx_default}"
nm_default="${cross_prefix}${nm_default}"
pkg_config_default="${cross_prefix}${pkg_config_default}"
複製代碼
當你配置了 --target-os=android
的時候,FFmpeg默認的編譯工具爲 clang
。
cc_default
其實就是配置項 cc
的默認值,能夠看到 cc_default
在這裏和 cross_prefix
作了拼接。這裏就是爲何說 cross_prefix
是交叉編譯工具前綴。
拼接完是這樣的:
cc_defalut=$TOOLCHAIN/bin/arm-linux-androideabi-$cc
複製代碼
看下 ar_default
cc_default
cxx_default
這些默認值是什麼。
搜索 cc_default
能夠找到如下代碼
# ffmpeg-4.2.2/configure
ar_default="ar"
cc_default="gcc"
cxx_default="g++"
host_cc_default="gcc"
複製代碼
能夠看到,FFmpeg 默認的編譯工具是 GCC
。
當你編譯 Android 平臺的庫時,因爲 configure
強制設置 cc_default="clang"
,因此:
當你使用 GCC
做爲編譯工具時,必須配置 cc
選項,或修改 configure
中的 cc_default="clang"
爲 cc_default="gcc"
;
當你使用 CLANG
做爲編譯工具時,能夠不配置 cc
選項。
仔細想一想會發現,爲何當 cc
配置爲下邊的值時,也能夠正常編譯呢?
--cc=$TOOLCHAIN/bin/arm-linux-androideabi-gcc
複製代碼
這時 cc_defalut
不就等於
cc_defalut=$TOOLCHAIN/bin/arm-linux-androideabi-$TOOLCHAIN/bin/arm-linux-androideabi-gcc
複製代碼
這個路徑確定是錯的啊!
這就要來看到底 cc_default
是怎麼使用的了。
搜索 set_default arch
,能夠看到如下代碼,在這裏 configure
從新設置了 cc
的默認值。
set_default arch cc cxx doxygen pkg_config ranlib strip sysinclude \
target_exec x86asmexe nvcc
複製代碼
這裏調用了一個叫 set_default
的函數,來看看這個函數的實現
set_default(){
for opt; do
eval : \${$opt:=\$${opt}_default}
done
}
複製代碼
這也是一個看不太懂的shell語法,大概的意思就是:for循環獲取全部的輸入參數變量,而後給這個變量賦值。
好比 set_default cc
,意思就是 cc=cc_default
,不過有一點要注意的是中間這個符號 :=
。
這個符號相似Java中的三目運算符:
opt != null? opt:opt_defalut
複製代碼
也就是說,若是參數爲空,將 xx_default
賦值給 xx
。
這就能夠解釋上面的疑問了。
--cc=$TOOLCHAIN/bin/arm-linux-androideabi-gcc
複製代碼
set_default cc
等於沒有用了。由於通過 for
循環獲取了用戶的配置之後, cc
不爲空。 set_default
後,cc
的值是不會改變的。
當 cc
不配置的時候,FFmpeg 根據默認的拼接方式,把拼接好的路徑設置給 cc
。
可是,不能配置 cc=gcc
這種,這樣,最後 cc
的值就只有 gcc
,確定是不能正確找到編譯工具的。
corss-prefix-clang
這個選項如今能夠來解釋爲何前面須要修改 configure
配置腳本了。
原始的配置是這樣的
ar_default="${cross_prefix}${ar_default}"
cc_default="${cross_prefix}${cc_default}"
cxx_default="${cross_prefix}${cxx_default}"
nm_default="${cross_prefix}${nm_default}"
pkg_config_default="${cross_prefix}${pkg_config_default}"
複製代碼
也就是說,默認的 cc
ar
nm
路徑前綴是同樣的,可是 Android NDK
的路徑倒是這樣的
看到了不?ar
/nm
和 cc
的前綴是不同的,前者是 arm-linux-androideabi-
, 後者是 armv7a-linux-androideabi16-
。
所以,須要對 cc
和 cxx
兩個前綴進行修改,爲此新加了 cross_prefix_clange
來進行單獨配置。
這裏只是針對 NDK r20b 的狀況,不一樣的 NDK 版本可能有所不一樣,根據這個原理去設置便可。
綜上,解釋了一些編譯 FFmpeg 經常使用的配置選項,而且從原理上弄明白爲什麼要這樣配置,基本上搞清楚了這些,想要組合兩個不一樣版本的FFmpeg和NDK來編譯,都會比較容易實現。
打開cmd終端,cd 到 FFmpeg 所在目錄
輸入 ./build_android_clang.sh
等待編譯完成,將會在 ffmpeg/android/armv7-a目錄下獲得 include
和 lib
兩個目錄,分別是 頭文件
和 so庫文件
目前大部分網上的文章都是使用 GCC
來編譯 FFmpeg
的,下面就來看看如何配置 GCC
的編譯參數。
前面就說過,NDK r17c 之後,Googole 就移除了 GCC,因此要使用 GCC 只能下載 r17c 及之前的版本,本文使用 r17c 來編譯。
根據本身編譯平臺選擇對應的版本:NDK r17c
本文選擇的是 Mac 版本:Mac OS X。
和 NDK r20b
相比,NDK r17c
的目錄稍微有些變化。
# 庫文件路徑
android-ndk-r17c/platforms/android-21/arch-arm/usr/lib
複製代碼
# 頭文件路徑
android-ndk-r17c/sysroot/usr/include
複製代碼
android-ndk-r17c/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin
複製代碼
能夠看到,Google 將 頭文件
和 庫文件
分離了,這也是不少新手在編譯的時候一直沒有配對路徑,致使編譯失敗的緣由。
FFmpeg 的版本依然是使用上面的 ffmpeg-4.2.2
, 固然,此次不須要修改 configure
了。
根據前面介紹的知識,很容易就能寫出編譯配置了
在 ffmpeg-4.2.2
根目錄新建腳本: build_android_gcc.sh
#!/bin/bash
set -x
API=21
CPU=armv7-a
#so庫輸出目錄
OUTPUT=/Users/cxp/Desktop/FFmpeg/ffmpeg-4.2.2/android/$CPU
# NDK的路徑,根據本身的安裝位置進行設置
NDK=/Users/cxp/Desktop/FFmpeg/android-ndk-r17c
# 庫文件
SYSROOT=$NDK/platforms/android-$API/arch-arm
# 頭文件
ISYSROOT=$NDK/sysroot/usr/include
# 彙編頭文件
ASM=$ISYSROOT/arm-linux-androideabi
TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64
function build
{
./configure \
--prefix=$OUTPUT \
--target-os=android \
--arch=arm \
--cpu=armv7-a \
--enable-asm \
--enable-cross-compile \
--enable-shared \
--disable-static \
--disable-doc \
--disable-ffplay \
--disable-ffprobe \
--disable-symver \
--disable-ffmpeg \
--sysroot=$SYSROOT \
--cc=$TOOLCHAIN/bin/arm-linux-androideabi-gcc \
--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
--extra-cflags="-I$ISYSROOT -I$ASM -fPIC"
make clean all
# 這裏是定義用幾個CPU編譯
make -j12
make
make install
}
build
複製代碼
能夠看到,在基本上配置和使用 CLANG
進行編譯差很少。
有如下不一樣:
cc
配置。由於若是不配置 cc
默認爲 clang
(參考前文的分析);extra-cflags
的配置,由於 SYSROOT
中只包含了 庫文件
,須要額外配置 頭文件
的搜索路徑;彙編頭文件
的路徑也不在 SYSROOT
中,也須要額外配置 ASM
。打開 cmd 終端,cd 到 ffmpeg-4.2.2 目錄
執行 ./build_android_gcc.sh
經過對 configure
的分析,可讓咱們更加清晰的理解每一個參數配置項的意義,以及如何搭配使用這些配置。只要清楚了各個配置的含義,不管版本怎麼變化,均可很快的寫出編譯腳本。
當了,本文只是介紹了最基礎的配置方案,你還能夠經過更多的 --disable-xxx
選項實現對 FFmpeg
的裁剪,或者經過 --enable-xxx
選項,開啓一些高級功能。