美文网首页
ijkplayer编译:在2022年使用M1芯片遭受毒打

ijkplayer编译:在2022年使用M1芯片遭受毒打

作者: 小风风吖 | 来源:发表于2022-08-04 16:34 被阅读0次

引言

在接手的一个旧项目中,有多处用到视频播放的能力,项目中使用的是一个叫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
但是我打开我的文件一看,傻眼了,这些是啥玩意,跟说的根本不一样啊,这怎么改?

别看了,这图没用.jpg

别人的文件只有三行,我的却有三百多行...
硬着头皮读一读吧,看有什么指令集相关的代码,发现了在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
其他指令集同理。

完结,编译 ijkplayer 真有意思,以后再也不想编了。

转载请注明出处,@via 小风风吖-ijkplayer编译:在2022年使用M1芯片遭受毒打 蟹蟹。

相关文章

网友评论

      本文标题:ijkplayer编译:在2022年使用M1芯片遭受毒打

      本文链接:https://www.haomeiwen.com/subject/ehsywrtx.html