ijkplayer编译:在2022年使用M1芯片遭受毒打
引言
在接手的一个旧项目中,有多处用到视频播放的能力,项目中使用的是一个叫universalvideoview的三方库,性能确实差,视频加载得也太慢了,正好碰上项目需求不是很紧张的时间窗口,准备花些时间换成广受好评的ijkplayer。这也让我开始了逐渐暴躁的旅程。
本来对ijkplayer了解不多,一查发现有现成的官方依赖,谁还想要自己编译啊,直接拿来用呗。
gradle添加依赖:
// 这里对应有多个指令集的支持,依个人需求添加
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
开始调试,简单封一个View,挂到Activity上
ijkplayer本身提供的是流加载、缓存,视频解码的能力,并不负责绘制和各种交互。需要我们自己提供一个 SurfaceView 供其最终渲染,这里搞一个简单的实现:
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 对象,通过它来对视频进行控制,暂停、播放、拖动时间等
private var mediaPlayer: IMediaPlayer? = null
init {
mSurfaceView.holder.addCallback(MySurfaceCallback())
addView(mSurfaceView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
}
// 搞一个监听,方便调试,也可以触发回调UI前做一些调试
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 网络地址或本地文件路径
*/
fun loadVideo(videoPath: String?) {
if(mediaPlayer?.dataSource.isNullOrBlank() && videoPath.isNullOrBlank())
return
rebuildPlayer()
try {
if(!videoPath.isNullOrBlank()) mediaPlayer?.dataSource = videoPath
// 如果我们的服务器需要进行一些头信息的认证,如User-Agent、referer,可以使用这个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()
}
}
// 释放上一个资源,重建 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 发生变化时需要重新绑定 MediaPlayer
loadVideo(null)
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
// do nothing
}
}
interface PlayerListener : IMediaPlayer.OnPreparedListener, IMediaPlayer.OnInfoListener,
IMediaPlayer.OnSeekCompleteListener, IMediaPlayer.OnBufferingUpdateListener, IMediaPlayer.OnErrorListener
}
好!就这么个玩意,可以直接拿去用啦!
加到布局文件:
<com.simple.player.IJKVideoPlayer
android:id="@+id/videoPlayer"
android:layout_width="match_parent"
android:layout_height="match_parent" />
在activity里拿到view实例,加载资源,就可以看到视频辣!
videoPlayer.loadVideo(videoPath)
诶?等了好久,还没有看到东西,是不是那里不对?
然后发现我们写的回调中,onError被调用,其中第二参数 what = 10000,第三参数 extra = 0。经过查询,这种情况一般会出现在找不到资源的情况,
不对啊,之前用另一个库,同一个资源,都能正常播放的啊。仔细翻找日志,找到了下面这句:
W/IJKMEDIA: https protocol not found, recompile FFmpeg with openssl, gnutls or securetransport enabled.
提示的已经很清楚了,不支持 https 协议,请带着 openssl 重新编译 FFmpeg。
官方编译的库不支持https,需要自己下载源码重新编译了,那就开始吧。
开始编译,受苦旅程开始
首先是拿到ijkplayer的基础代码,可以从github上下载压缩包,
github主页:https://github.com/bilibili/ijkplayer.git
也可以直接git拉取:
git clone https://github.com/bilibili/ijkplayer.git
然后,就开始了一步一个坑的过程。
本来这是一个有非常多使用者,也有很多人分享参考经验的库,应该是有非常多避坑心得的。然而还是太天真,当别人的经验和自己的操作,之间隔了以年为单位的时间时,各种环境、硬件的条件下,简直没有一步是顺利的。
0. yasm 环境安装(必要性存疑)
我在准备的初期有看到很多文章中都提到,需要具备yasm环境。有用brew命令装的,有用yum命令的,然而这俩我都没有🥸。我这边使用的是下载源码自行编译安装的方式。
官方下载: http://www.tortall.net/projects/yasm/releases/
从这个网页找到需要的版本下载,很久不维护了,最新的就是2014年的1.3.0版本了,mac下载 **yasm-1.3.0.tar.gz **
下载完成后解压,进入解压后的目录,依次执行以下命令:
sudo ./configure
sudo make
sudo make install
过程中没有报错的话,使用 yasm --version 验证是否安装成功。
我这边后来试过把 yasm 卸了,再次编译也可以成功,所以 ijk 的编译过程应该是不需要这个东西的?你可以跳过这步进行下面的流程。
是在不行在回头来安装嘛🤪。
1.真实的源代码拉取
上面我们下载的代码,只是一个壳子,封装了一些命令而已,实际的源代码并没有在里面。最重要的是,我们的目标是集成 openssl,重新编译 FFmpeg。所以就需要 openssl 、FFmpeg、ijkplayer 的源代码,更不要说 FFmpeg 还需要依赖 libyuv、soundtouch。
要完成这一步,我们需要执行 ijkplayer 根目录下的两个脚本:
// 进入下载的源码根目录
cd ijkplayer-source
// 执行命令下载 openssl 、FFmpeg、ijkplayer 源码和依赖
./init-android-openssl.sh
./init-android.sh
受网络因素限制,过程会非常漫长,而且动不动就会报错,见到下面这些:
这样的:
== 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
看到这些,别灰心,再试一遍,已经下载完的不会删除。
可能会需要一遍又一遍地执行,总会全部下载完的,只有所有库都下载完,才能开始下一步的编译。
2.开始编译 openssl
这边文章记录的是在mac上的编译过程, gcc、g++、make 环境的准备就不在赘述。
身为一个android开发,ndk环境已经具备,就按照网上教程执行了下面的命令:
// 进入固定目录 [源码根目录/android/contrib]
cd android/contrib
// 编译 openssl 和 ffmpeg,这个命令是所有指令集,
// 当然也可以指定,如:./compile-openssl.sh arm64
./compile-openssl.sh all
./compile-ffmpeg.sh all
// 如果上面的的命令中途失败,最好清除一下编译中间物。
./compile-openssl.sh clean
./compile-ffmpeg.sh clean
必须保证 NDK 环境变量存在,否则会看到这个:
You must define ANDROID_NDK before starting.
They must point to your NDK directories.
可以使用命令临时指定:
export ANDROID_NDK=/Users/xxx/env/android-sdk/ndk/23.1.7779620
环境正常后再次 ./compile-openssl.sh all ,我看到了这个:
IJK_NDK_REL=23.1.7779620
You need the NDKr10e or later
是我的 NDK 版本低了?好家伙,原来是太高了,高出了好多年,根本不支持。
在 /android/contrib 下,有一个 do-detect-env.sh 脚本,编译前会通过它检查ndk版本,它里面可以看到支持的版本:
case "$IJK_NDK_REL" in
10e*)
、、、、、、
case "$IJK_NDK_REL" in
11*|12*|13*|14*)
、、、、、、
然后我选了一个相对高一些的版本,r14b,从下面链接可以下载:
历史版本链接:https://developer.android.google.cn/ndk/downloads/older_releases
重设环境变量,重新编译,于是乎我又看见这个:
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
查了好久也找不到具体原因,老老实实下载一个支持的最低版本,r10e。
重设环境变量,重新编译,
export ANDROID_NDK=/Users/xxx/env/android-sdk/ndk/android-ndk-r10e
./compile-openssl.sh clean
./compile-openssl.sh all
在日志的最后,看见这些就是成功了:
--------------------
[*] 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
一顿编译之后,出现了如下错误:
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....
这都是什么鬼?简直心态爆炸!!!
多方查询、试错后,终于找到一个解决方案:把我们要编译的对应指令集下的,libavcodec/hevc_mvs.c 中所有名为 B0、xB0、yB0 的变量及引用,改成小写 b0 xb0 yb0。
例如我们只编译arm64,需要修改文件就在:
源码根目录/android/contrib/ffmpeg-arm64/libavcodec/hevc_mvs.c,
如果要编译 all,就需要修改所有指令集对应文件。
修改后再次clean并执行编译,看到下面的日志,就是成功了:
--------------------
[*] create files for shared ffmpeg
--------------------
--------------------
[*] Finished
--------------------
# to continue to build ijkplayer, run script below,
sh compile-ijk.sh
3.编译 ijkplayer,输出成功成果物
openssl 和 ffmpeg 都编译成功后,会产生静态链接库 .a 文件供 ijkplayer 编译使用,有兴趣的可以去 /android/contrib/build/xxxx/output/lib 路径下查看。
执行脚本编译 ijkplayer :
// 上面我们是在 /android/contrib 下,需要退回到 /android 目录
cd ..
// 同样也可以指定具体的指令集,如 ./compile-ijk.sh arm64
./compile-ijk.sh all
正常情况下到这里根本没有悬念的成功了,但我感觉这玩意就是来搞我心态的,出现了:
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 的指令集!
到最后一步了,不认cpu了,我直接好家伙!螺旋升天,原地爆炸!!!
既然开始了,含着泪也要走完啊,又是一顿查,发现可以指定为兼容模式以 x86_64模式运行,需要修改ndk目录下的 ndk-build 文件:
原文件内容:
#!/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
但是我打开我的文件一看,傻眼了,这些是啥玩意,跟说的根本不一样啊,这怎么改?
别人的文件只有三行,我的却有三百多行...
硬着头皮读一读吧,看有什么指令集相关的代码,发现了在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) 这里加一个,改成 x86_64|amd64|arm64)
重新编译 ijk ,成功。我这里只编了一个指令集,./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版本,可能会遇到 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.
这种情况就需要删除 ndk/prebuilt 下平台指令集中的 awk 程序。
示例路径:/Users/xxx/env/android-sdk/ndk/android-ndk-r14b/prebuilt/darwin-x86_64/bin/awk
找到文件,把它删除或改个名字都行。
最后,编译完成,成果物的简化使用
我看到的其他文章,基本在编译完成后都是在 android gradle 项目中引入编译输出的模块使用,如果我们不要对 ijkplayer 的java层进行定制的话,可以直接使用官方的java层,同时使用我们编译的so库就行了。
// 只添加 java 库依赖,把我们自己编译的so库打包里就行
implementation 'tv.danmaku.ijk.media:ijkplayer-java:0.8.8'
so的输出路径:源码根目录/android/ijkplayer/ijkplayer-arm64/src/main/libs/arm64-v8a
其他指令集同理。