引言
在接手的一個(gè)舊項(xiàng)目中铐伴,有多處用到視頻播放的能力逝薪,項(xiàng)目中使用的是一個(gè)叫universalvideoview的三方庫踱启,性能確實(shí)差报账,視頻加載得也太慢了,正好碰上項(xiàng)目需求不是很緊張的時(shí)間窗口埠偿,準(zhǔn)備花些時(shí)間換成廣受好評(píng)的ijkplayer透罢。這也讓我開始了逐漸暴躁的旅程。
本來對(duì)ijkplayer了解不多冠蒋,一查發(fā)現(xiàn)有現(xiàn)成的官方依賴羽圃,誰還想要自己編譯啊,直接拿來用唄抖剿。
gradle添加依賴:
// 這里對(duì)應(yīng)有多個(gè)指令集的支持统屈,依個(gè)人需求添加
implementation 'tv.danmaku.ijk.media:ijkplayer-java:0.8.8'
implementation 'tv.danmaku.ijk.media:ijkplayer-armv7a:0.8.8'
implementation 'tv.danmaku.ijk.media:ijkplayer-arm64:0.8.8'
參考官方github主頁:https://github.com/bilibili/ijkplayer.git
開始調(diào)試,簡(jiǎn)單封一個(gè)View牙躺,掛到Activity上
ijkplayer本身提供的是流加載愁憔、緩存,視頻解碼的能力孽拷,并不負(fù)責(zé)繪制和各種交互吨掌。需要我們自己提供一個(gè) SurfaceView 供其最終渲染,這里搞一個(gè)簡(jiǎn)單的實(shí)現(xiàn):
class IJKVideoPlayer : FrameLayout {
private val TAG = "IJKVideoPlayer"
constructor(context: Context): super(context)
constructor(context: Context, attrs: AttributeSet): super(context, attrs)
constructor(context: Context, attrs: AttributeSet, styleAttr: Int): super(context, attrs, styleAttr)
var listener: PlayerListener? = null
private var mSurfaceView: SurfaceView = SurfaceView(context)
// mediaPlayer 對(duì)象,通過它來對(duì)視頻進(jìn)行控制膜宋,暫停窿侈、播放、拖動(dòng)時(shí)間等
private var mediaPlayer: IMediaPlayer? = null
init {
mSurfaceView.holder.addCallback(MySurfaceCallback())
addView(mSurfaceView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
}
// 搞一個(gè)監(jiān)聽秋茫,方便調(diào)試史简,也可以觸發(fā)回調(diào)UI前做一些調(diào)試
private val internalListener = object : PlayerListener {
override fun onPrepared(p0: IMediaPlayer?) {
Log.d(TAG, "onPrepared")
listener?.onPrepared(p0)
}
override fun onInfo(p0: IMediaPlayer?, p1: Int, p2: Int): Boolean {
Log.d(TAG, "onInfo --$p1, --$p2")
listener?.let {
return it.onInfo(p0, p1, p2)
}
return false
}
override fun onSeekComplete(p0: IMediaPlayer?) {
Log.d(TAG, "onSeekComplete")
listener?.onSeekComplete(p0)
}
override fun onBufferingUpdate(p0: IMediaPlayer?, p1: Int) {
Log.d(TAG, "onBufferingUpdate --$p1")
listener?.onBufferingUpdate(p0, p1)
}
override fun onError(p0: IMediaPlayer?, p1: Int, p2: Int): Boolean {
Log.d(TAG, "onError --$p1, --$p2")
listener?.let {
return it.onError(p0, p1, p2)
}
return false
}
}
/**
* 加載視頻,開始播放
* @param videoPath 網(wǎng)絡(luò)地址或本地文件路徑
*/
fun loadVideo(videoPath: String?) {
if(mediaPlayer?.dataSource.isNullOrBlank() && videoPath.isNullOrBlank())
return
rebuildPlayer()
try {
if(!videoPath.isNullOrBlank()) mediaPlayer?.dataSource = videoPath
// 如果我們的服務(wù)器需要進(jìn)行一些頭信息的認(rèn)證肛著,如User-Agent圆兵、referer,可以使用這個(gè)api加載資源枢贿。
// if(!videoPath.isNullOrBlank()) mediaPlayer?.setDataSource(context, Uri.parse(videoPath), headerMap)
} catch (e: IOException) {
e.printStackTrace()
}
mediaPlayer?.setDisplay(mSurfaceView.holder)
mediaPlayer?.prepareAsync()
}
/**
* 釋放資源
*/
fun onDestroy() {
mediaPlayer?.apply {
stop()
setDisplay(null)
release()
}
}
// 釋放上一個(gè)資源殉农,重建 mediaPlayer
private fun rebuildPlayer() {
mediaPlayer?.apply {
stop()
setDisplay(null)
release()
}
mediaPlayer = IjkMediaPlayer().apply {
native_setLogLevel(IjkMediaPlayer.IJK_LOG_DEBUG)
// 硬件解碼
setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 1)
setOnPreparedListener(internalListener)
setOnInfoListener(internalListener)
setOnSeekCompleteListener(internalListener)
setOnBufferingUpdateListener(internalListener)
setOnErrorListener(internalListener)
}
}
private inner class MySurfaceCallback : SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) {
// do nothing
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
// view 發(fā)生變化時(shí)需要重新綁定 MediaPlayer
loadVideo(null)
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
// do nothing
}
}
interface PlayerListener : IMediaPlayer.OnPreparedListener, IMediaPlayer.OnInfoListener,
IMediaPlayer.OnSeekCompleteListener, IMediaPlayer.OnBufferingUpdateListener, IMediaPlayer.OnErrorListener
}
好!就這么個(gè)玩意局荚,可以直接拿去用啦超凳!
加到布局文件:
<com.simple.player.IJKVideoPlayer
android:id="@+id/videoPlayer"
android:layout_width="match_parent"
android:layout_height="match_parent" />
在activity里拿到view實(shí)例,加載資源耀态,就可以看到視頻辣轮傍!
videoPlayer.loadVideo(videoPath)
誒?等了好久首装,還沒有看到東西金麸,是不是那里不對(duì)?
然后發(fā)現(xiàn)我們寫的回調(diào)中簿盅,onError被調(diào)用,其中第二參數(shù) what = 10000揍魂,第三參數(shù) extra = 0桨醋。經(jīng)過查詢,這種情況一般會(huì)出現(xiàn)在找不到資源的情況现斋,
不對(duì)啊喜最,之前用另一個(gè)庫,同一個(gè)資源庄蹋,都能正常播放的啊瞬内。仔細(xì)翻找日志,找到了下面這句:
W/IJKMEDIA: https protocol not found, recompile FFmpeg with openssl, gnutls or securetransport enabled.
提示的已經(jīng)很清楚了限书,不支持 https 協(xié)議虫蝶,請(qǐng)帶著 openssl 重新編譯 FFmpeg。
官方編譯的庫不支持https倦西,需要自己下載源碼重新編譯了能真,那就開始吧。
開始編譯,受苦旅程開始
首先是拿到ijkplayer的基礎(chǔ)代碼粉铐,可以從github上下載壓縮包疼约,
github主頁:https://github.com/bilibili/ijkplayer.git
也可以直接git拉取:
git clone https://github.com/bilibili/ijkplayer.git
然后蝙泼,就開始了一步一個(gè)坑的過程程剥。
本來這是一個(gè)有非常多使用者,也有很多人分享參考經(jīng)驗(yàn)的庫汤踏,應(yīng)該是有非常多避坑心得的织鲸。然而還是太天真,當(dāng)別人的經(jīng)驗(yàn)和自己的操作茎活,之間隔了以年為單位的時(shí)間時(shí)昙沦,各種環(huán)境、硬件的條件下载荔,簡(jiǎn)直沒有一步是順利的盾饮。
0. yasm 環(huán)境安裝(必要性存疑)
我在準(zhǔn)備的初期有看到很多文章中都提到,需要具備yasm環(huán)境懒熙。有用brew命令裝的丘损,有用yum命令的,然而這倆我都沒有??工扎。我這邊使用的是下載源碼自行編譯安裝的方式徘钥。
官方下載: http://www.tortall.net/projects/yasm/releases/
從這個(gè)網(wǎng)頁找到需要的版本下載,很久不維護(hù)了肢娘,最新的就是2014年的1.3.0版本了呈础,mac下載 **yasm-1.3.0.tar.gz **
下載完成后解壓,進(jìn)入解壓后的目錄橱健,依次執(zhí)行以下命令:
sudo ./configure
sudo make
sudo make install
過程中沒有報(bào)錯(cuò)的話而钞,使用 yasm --version 驗(yàn)證是否安裝成功。
我這邊后來試過把 yasm 卸了拘荡,再次編譯也可以成功臼节,所以 ijk 的編譯過程應(yīng)該是不需要這個(gè)東西的?你可以跳過這步進(jìn)行下面的流程珊皿。
是在不行在回頭來安裝嘛??网缝。
1.真實(shí)的源代碼拉取
上面我們下載的代碼,只是一個(gè)殼子蟋定,封裝了一些命令而已粉臊,實(shí)際的源代碼并沒有在里面。最重要的是驶兜,我們的目標(biāo)是集成 openssl维费,重新編譯 FFmpeg果元。所以就需要 openssl 、FFmpeg犀盟、ijkplayer 的源代碼而晒,更不要說 FFmpeg 還需要依賴 libyuv、soundtouch阅畴。
要完成這一步倡怎,我們需要執(zhí)行 ijkplayer 根目錄下的兩個(gè)腳本:
// 進(jìn)入下載的源碼根目錄
cd ijkplayer-source
// 執(zhí)行命令下載 openssl 、FFmpeg贱枣、ijkplayer 源碼和依賴
./init-android-openssl.sh
./init-android.sh
受網(wǎng)絡(luò)因素限制监署,過程會(huì)非常漫長(zhǎng),而且動(dòng)不動(dòng)就會(huì)報(bào)錯(cuò)纽哥,見到下面這些:
這樣的:
== pull openssl base ==
Fetching origin
fatal: unable to access 'https://github.com/Bilibili/openssl.git/': LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to github.com:443
error: Could not fetch origin
或者這樣的:
== pull openssl fork x86_64 ==
Fetching origin
error: RPC failed; curl 28 LibreSSL SSL_read: Operation timed out, errno 60
fatal: expected flush after ref listing
error: Could not fetch origin
看到這些钠乏,別灰心,再試一遍春塌,已經(jīng)下載完的不會(huì)刪除晓避。
可能會(huì)需要一遍又一遍地執(zhí)行,總會(huì)全部下載完的只壳,只有所有庫都下載完俏拱,才能開始下一步的編譯。
2.開始編譯 openssl
這邊文章記錄的是在mac上的編譯過程吼句, gcc锅必、g++、make 環(huán)境的準(zhǔn)備就不在贅述惕艳。
身為一個(gè)android開發(fā)搞隐,ndk環(huán)境已經(jīng)具備,就按照網(wǎng)上教程執(zhí)行了下面的命令:
// 進(jìn)入固定目錄 [源碼根目錄/android/contrib]
cd android/contrib
// 編譯 openssl 和 ffmpeg远搪,這個(gè)命令是所有指令集劣纲,
// 當(dāng)然也可以指定,如:./compile-openssl.sh arm64
./compile-openssl.sh all
./compile-ffmpeg.sh all
// 如果上面的的命令中途失敗终娃,最好清除一下編譯中間物。
./compile-openssl.sh clean
./compile-ffmpeg.sh clean
必須保證 NDK 環(huán)境變量存在蒸甜,否則會(huì)看到這個(gè):
You must define ANDROID_NDK before starting.
They must point to your NDK directories.
可以使用命令臨時(shí)指定:
export ANDROID_NDK=/Users/xxx/env/android-sdk/ndk/23.1.7779620
環(huán)境正常后再次 ./compile-openssl.sh all 棠耕,我看到了這個(gè):
IJK_NDK_REL=23.1.7779620
You need the NDKr10e or later
是我的 NDK 版本低了?好家伙,原來是太高了,高出了好多年或链,根本不支持付秕。
在 /android/contrib 下,有一個(gè) do-detect-env.sh 腳本坪它,編譯前會(huì)通過它檢查ndk版本操刀,它里面可以看到支持的版本:
case "$IJK_NDK_REL" in
10e*)
摔刁、瓤荔、净蚤、、输硝、今瀑、
case "$IJK_NDK_REL" in
11*|12*|13*|14*)
、点把、橘荠、、郎逃、哥童、
然后我選了一個(gè)相對(duì)高一些的版本,r14b褒翰,從下面鏈接可以下載:
歷史版本鏈接:https://developer.android.google.cn/ndk/downloads/older_releases
重設(shè)環(huán)境變量贮懈,重新編譯,于是乎我又看見這個(gè):
making links in crypto/objects...
objects.h => ../../include/openssl/objects.h
obj_mac.h => ../../include/openssl/obj_mac.h
making links in crypto/md4...
make: ../../util/mklink.pl: Command not found
make: *** [links] Error 127
make: *** [links] Error 1
make: *** [links] Error 1
--------------------
[*] compile openssl
--------------------
making depend in crypto...
/bin/sh: /util/domd: No such file or directory
make: *** [local_depend] Error 127
make: *** [depend] Error 1
查了好久也找不到具體原因影暴,老老實(shí)實(shí)下載一個(gè)支持的最低版本错邦,r10e。
重設(shè)環(huán)境變量型宙,重新編譯撬呢,
export ANDROID_NDK=/Users/xxx/env/android-sdk/ndk/android-ndk-r10e
./compile-openssl.sh clean
./compile-openssl.sh all
在這個(gè)過程中,你可能會(huì)遇見這個(gè)彈窗:相信我魂拦,不要嘗試去設(shè)置里一個(gè)一個(gè)允許了,因?yàn)檫@只是個(gè)開始搁嗓,后面還有無數(shù)這玩意等著你芯勘,執(zhí)行如下命令,跳過這個(gè)檢查:
sudo spctl --master-disable
再次編譯腺逛,在日志的最后荷愕,看見這些就是成功了:
--------------------
[*] link openssl
--------------------
--------------------
[*] Finished
--------------------
# to continue to build ffmpeg, run script below,
sh compile-ffmpeg.sh
# to continue to build ijkplayer, run script below,
sh compile-ijk.sh
3.openssl編譯完成后,編譯 ffmpeg
./compile-ffmpeg.sh clean
./compile-ffmpeg.sh all
一頓編譯之后棍矛,出現(xiàn)了如下錯(cuò)誤:
libavcodec/hevc_mvs.c:207:15: error: 'x0000000' undeclared (first use in this function)
TAB_MVF(((x ## v) >> s->ps.sps->log2_min_pu_size), \
^
libavcodec/hevc_mvs.c:204:34: note: in definition of macro 'TAB_MVF'
tab_mvf[(y) * min_pu_width + x]
^
libavcodec/hevc_mvs.c:274:16: note: in expansion of macro 'TAB_MVF_PU'
(cand && !(TAB_MVF_PU(v).pred_flag == PF_INTRA))
^
libavcodec/hevc_mvs.c:683:24: note: in expansion of macro 'AVAILABLE'
is_available_b0 = AVAILABLE(cand_up_right, B0) &&
^
make: *** [libavcodec/hevc_mvs.o] Error 1
make: *** Waiting for unfinished jobs....
這都是什么鬼安疗?簡(jiǎn)直心態(tài)爆炸!9晃荐类!
多方查詢、試錯(cuò)后茁帽,終于找到一個(gè)解決方案:把我們要編譯的對(duì)應(yīng)指令集下的玉罐,libavcodec/hevc_mvs.c 中所有名為 B0屈嗤、xB0、yB0 的變量及引用吊输,改成小寫 b0 xb0 yb0饶号。
例如我們只編譯arm64,需要修改文件就在:
源碼根目錄/android/contrib/ffmpeg-arm64/libavcodec/hevc_mvs.c璧亚,
如果要編譯 all讨韭,就需要修改所有指令集對(duì)應(yīng)文件。
修改后再次clean并執(zhí)行編譯癣蟋,看到下面的日志透硝,就是成功了:
--------------------
[*] create files for shared ffmpeg
--------------------
--------------------
[*] Finished
--------------------
# to continue to build ijkplayer, run script below,
sh compile-ijk.sh
3.編譯 ijkplayer,輸出成功成果物
openssl 和 ffmpeg 都編譯成功后疯搅,會(huì)產(chǎn)生靜態(tài)鏈接庫 .a 文件供 ijkplayer 編譯使用濒生,有興趣的可以去 /android/contrib/build/xxxx/output/lib 路徑下查看。
執(zhí)行腳本編譯 ijkplayer :
// 上面我們是在 /android/contrib 下幔欧,需要退回到 /android 目錄
cd ..
// 同樣也可以指定具體的指令集罪治,如 ./compile-ijk.sh arm64
./compile-ijk.sh all
正常情況下到這里根本沒有懸念的成功了,但我感覺這玩意就是來搞我心態(tài)的礁蔗,出現(xiàn)了:
xxxx-xxxxx android % ./compile-ijk.sh all
profiler build: NO
ERROR: Unknown host CPU architecture: arm64
/Users/wwf/Desktop/projects/ijkplayer/android
profiler build: NO
ERROR: Unknown host CPU architecture: arm64
/Users/wwf/Desktop/projects/ijkplayer/android
profiler build: NO
ERROR: Unknown host CPU architecture: arm64
/Users/wwf/Desktop/projects/ijkplayer/android
profiler build: NO
ERROR: Unknown host CPU architecture: arm64
/Users/wwf/Desktop/projects/ijkplayer/android
profiler build: NO
ERROR: Unknown host CPU architecture: arm64
焯觉义!我這mac是 M1 芯片的,arm64 的指令集浴井!
到最后一步了晒骇,不認(rèn)cpu了,我直接好家伙磺浙!螺旋升天洪囤,原地爆炸!K貉酢瘤缩!
既然開始了,含著淚也要走完啊伦泥,又是一頓查剥啤,發(fā)現(xiàn)可以指定為兼容模式以 x86_64模式運(yùn)行,需要修改ndk目錄下的 ndk-build 文件:
原文件內(nèi)容:
#!/bin/sh
DIR="$(cd "$(dirname "$0")" && pwd)"
$DIR/build/ndk-build "$@"
修改為:
#!/bin/sh
DIR="$(cd "$(dirname "$0")" && pwd)"
arch -x86_64 /bin/bash $DIR/build/ndk-build "$@"
文件路徑:/Users/xxx/env/android-sdk/ndk/android-ndk-r10e/ndk-build
但是我打開我的文件一看不脯,傻眼了府怯,這些是啥玩意,跟說的根本不一樣啊跨新,這怎么改富腊?
別人的文件只有三行域帐,我的卻有三百多行...
硬著頭皮讀一讀吧赘被,看有什么指令集相關(guān)的代碼,發(fā)現(xiàn)了在140多行處肖揣,有如下邏輯:
HOST_ARCH=$(uname -m)
case $HOST_ARCH in
i?86) HOST_ARCH=x86;;
x86_64|amd64) HOST_ARCH=x86_64;;
*) echo "ERROR: Unknown host CPU architecture: $HOST_ARCH"
exit 1
esac
log "HOST_ARCH=$HOST_ARCH"
把 x86_64|amd64) 這里加一個(gè)民假,改成 x86_64|amd64|arm64)
重新編譯 ijk ,成功龙优。我這里只編了一個(gè)指令集羊异,./compile-ijk.sh arm64,日志如下:
[arm64-v8a] Compile++ : ijksoundtouch <= BPMDetect.cpp
[arm64-v8a] Compile++ : ijksoundtouch <= PeakFinder.cpp
[arm64-v8a] Compile++ : ijksoundtouch <= SoundTouch.cpp
[arm64-v8a] Compile++ : ijksoundtouch <= mmx_optimized.cpp
[arm64-v8a] Compile++ : ijksoundtouch <= ijksoundtouch_wrap.cpp
[arm64-v8a] StaticLibrary : libcpufeatures.a
[arm64-v8a] StaticLibrary : libijkj4a.a
[arm64-v8a] StaticLibrary : libandroid-ndk-profiler.a
[arm64-v8a] StaticLibrary : libijksoundtouch.a
[arm64-v8a] StaticLibrary : libyuv_static.a
[arm64-v8a] SharedLibrary : libijksdl.so
[arm64-v8a] SharedLibrary : libijkplayer.so
[arm64-v8a] Install : libijksdl.so => libs/arm64-v8a/libijksdl.so
[arm64-v8a] Install : libijkplayer.so => libs/arm64-v8a/libijkplayer.so
附:如果使用的是較 r10e 高的ndk版本彤断,可能會(huì)遇到 Host 'awk' tool is outdated :
xxxx-xxxxx android % ./compile-ijk.sh all
profiler build: NO
Android NDK: Host 'awk' tool is outdated. Please define NDK_HOST_AWK to point to Gawk or Nawk !
/Users/wwf/Desktop/env/android-sdk/ndk/android-ndk-r14b/build/core/init.mk:391: *** Android NDK: Aborting. . Stop.
這種情況就需要?jiǎng)h除 ndk/prebuilt 下平臺(tái)指令集中的 awk 程序野舶。
示例路徑:/Users/xxx/env/android-sdk/ndk/android-ndk-r14b/prebuilt/darwin-x86_64/bin/awk
找到文件,把它刪除或改個(gè)名字都行宰衙。
最后平道,編譯完成,成果物的簡(jiǎn)化使用
我看到的其他文章供炼,基本在編譯完成后都是在 android gradle 項(xiàng)目中引入編譯輸出的模塊使用一屋,如果我們不要對(duì) ijkplayer 的java層進(jìn)行定制的話,可以直接使用官方的java層袋哼,同時(shí)使用我們編譯的so庫就行了冀墨。
// 只添加 java 庫依賴,把我們自己編譯的so庫打包里就行
implementation 'tv.danmaku.ijk.media:ijkplayer-java:0.8.8'
so的輸出路徑:源碼根目錄/android/ijkplayer/ijkplayer-arm64/src/main/libs/arm64-v8a
其他指令集同理涛贯。