美文网首页
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