ijkplayer 0.8.8 .so集成指南:Android硬解避坑与播放器实践
简介ijkplayer 0.8.8 编译文件包是针对 Android、iOS 等平台的跨平台播放器核心库源自 B 站开源项目基于 FFmpeg 构建直接提供 armeabi-v7a、arm64-v8a、x86、x86_64 等架构对应的 .so 动态库开发者不用自行折腾编译链集成即可实现 RTMP、HLS、HTTP 等多种流媒体的播放能力大幅降低多媒体开发门槛。整个压缩包共 1581 个文件、约 49MB除 .so 动态库外还包含 .a 静态库、Java 层调用源码、Gradle 构建脚本、XML 配置以及 .mk 等文件覆盖了从底层解码到上层接口、再到工程化集成的完整链路方便开发者按需选用和排查问题。已有 1747 人学习下载该版本在 B 站实际线上播放场景中经过大规模验证稳定性和兼容性较旧版有所提升适合需要自定义播放器行为、优化起播速度或适配不同硬件设备的开发者多架构的编译产物也为后续二次编译和裁剪提供了清晰参照。1. 为什么还要用ijkplayer 0.8.8编译好的.so能省多少事ijkplayer 0.8.8 的编译产物 .so 文件是很多老播放器项目最后稳定停留的版本。这年头自己从头编一遍 ijkplayer 是有代价的先配 NDK 环境再下 ffmpeg 源码还得处理全平台交叉编译参数运气不好卡在编译脚本上就是一下午。而这份直接可用的 .so 包把最耗时的编译环节跳过去了你拿到手要做的只是把它塞进 jniLibs、写几行初始化代码立刻能跑起来。适合正在做短视频、直播、本地视频播放这类 Android 开发的人尤其是那些被老板催着“尽快出一个能播放的这个视频”却不想折腾编译链的开发者。我当年第一次用它从解压到出画面用了不到半小时省下来的时间全用来调播放参数了。2. 解包前先看懂.so构成ijkplayer 0.8.8到底带来了什么2.1 三个核心.so文件的作用与对应关系从资源包里解压出来最常见的 is 目录结构是ijkplayer-arm64/libijkplayer.so、libijksdl.so、libijkffmpeg.so这一组。第一次接触的人容易以为只要 libijkplayer.so 就够了实际上这三个是协同工作的。libijkplayer.so是播放器核心控制类负责播放器的状态机、seek、暂停、播放事件回调libijksdl.so是ijk自己改造过的SDL层负责音频输出、视频渲染、事件循环没有它画面就画不出来libijkffmpeg.so是裁减过的 ffmpeg负责解封装、解码、音视频同步。由于 ijkplayer 是 Java 层通过 JNI 调 native三个库是互相依赖的关系加载顺序上系统会自动处理依赖但你要保证三个文件都放进同一个 ABI 目录缺一个在运行时会报UnsatisfiedLinkError到时候连错误原因都看不明白。我一般会建议先确认这份 .so 是不是带硬解的那一版。0.8.8 的编译产物通常区分 clang 和 gcc 参数最常见的编译选项是--enable-jni--enable-media-codec这意味着 ffmpeg 里带了 Android MediaCodec 的硬解实现。如果没有带这个参数你在 Java 层设置MEDIA_CODEC硬解选项时实际会默默回退到软解表现就是 CPU 占用高、发热但功能正常。怎么判断看libijkffmpeg.so里有没有mediacodec相关符号用nm -D libijkffmpeg.so | grep mediacodec在 Linux 或者 Cygwin 下看一眼有输出就是带硬解的。没有也行后面第4章会讲硬解参数的坑。2.2 从.so文件名看ABI你手里的包覆盖了哪些平台资源包里通常会按 ABI 分目录常见的有armeabi-v7a、arm64-v8a、x86、x86_64。0.8.8 年代比较早很多编译包不带x86_64只有前三个。这里有个容易误判的点x86目录的 .so 主要给模拟器用真机基本用不到armeabi-v7a兼容老设备和部分 64 位系统32位兼容模式arm64-v8a是现在的主流。如果你只做真机发布优先保留arm64-v8a和armeabi-v7a两个目录就行x86可以在 debug 包跑模拟器时用打包 release 时可以排除掉。需要注意0.8.8 版本本身是 20162017 年左右的项目编译选项对新设备兼容性有点“看脸”。有些包会额外带一个ijkplayer.so的符号链接或者特殊命名的 .so比如libijkffmpeg-ff.so这是针对某些机型裁减过的。如果你手里的包只有一个 ABI 目录那说明作者编译时只开了单个架构这种情况下别硬塞到多 ABI 工程里否则会在某些设备上直接找不到库。更靠谱的做法是看包里的命名常见命名格式是ijkplayer-0.8.8/arm64-v8a/libijkplayer.so这种按目录区分比较清爽。2.3 这份资源包的目录结构先认识再动手我习惯拿到手先列一下文件。一个典型的 0.8.8 .so 资源包长这样ijkplayer-0.8.8/ ├── arm64-v8a/ │ ├── libijkplayer.so │ ├── libijksdl.so │ └── libijkffmpeg.so ├── armeabi-v7a/ │ ├── libijkplayer.so │ ├── libijksdl.so │ └── libijkffmpeg.so ├── x86/ │ ├── libijkplayer.so │ ├── libijksdl.so │ └── libijkffmpeg.so └── README.txt# 用 ls -l 看文件大小每个.so大约4-8MB太小可能裁剪过度太大说明没优化 ls -lR ijkplayer-0.8.8/这里每个目录下的三个 .so 必须同时使用不能混搭。比如你用 arm64-v8a 的 libijkplayer.so却拿 armeabi-v7a 的 libijkffmpeg.so加载时轻则异常重则直接崩。我见到过有人为了省空间只保留 libijkplayer.so运行时一直报找不到 ijkffmpeg这种错误不看 logcat 完全猜不出来。再看 README.txt里面通常会写明这份 so 的编译时间、NDK 版本、是否包含 openssl也就是 https 协议支持。0.8.8 默认不带 openssl支持 http、rtmp、本地文件但https://的视频流播放不了。如果你的播放地址是 https 开头得另找带 openssl 的版本或者自己在 Java 层用 okhttp 下载到本地再播放。这个我在第3章还会提到。3. 把.so接进Android工程集成步骤与Gradle配置3.1 放置.so文件jniLibs目录的ABI规则拿到 .so 之后第一件事不是写代码是先把文件放到正确的位置。Android Studio 里默认会识别app/src/main/jniLibs/目录下的 .so也可以直接在build.gradle里通过sourceSets指定。android { compileSdkVersion 30 defaultConfig { applicationId com.example.ijkdemo minSdkVersion 16 targetSdkVersion 30 ndk { abiFilters armeabi-v7a, arm64-v8a } } sourceSets { main { jniLibs.srcDirs [libs] } } }这段配置里abiFilters决定最终打进 apk 的架构如果你的工程里还有别的 .so比如某个支付 SDK 只提供armeabi-v7a这里就要小心了。Android 在安装 apk 的时候如果发现arm64-v8a目录里有任何 .so就会把整个 app 当成 64 位应用运行同时只加载arm64-v8a下的 .so。如果你的支付 SDK 没有 64 位版本而 ijkplayer 有那么 App 在 64 位设备上会找不到支付 SDK 的库导致加载失败。这是很多项目把 ijkplayer 接进来之后突然崩掉的隐藏原因。android { packagingOptions { pickFirst lib/arm64-v8a/libijkplayer.so pickFirst lib/arm64-v8a/libijksdl.so pickFirst lib/arm64-v8a/libijkffmpeg.so } }如果项目里有重复的 .so 来自不同 module用pickFirst解决冲突。我建议把jniLibs.srcDirs指向libs因为资源包解压出来本身是libs/arm64-v8a/xxx.so结构直接放进去最省事。注意如果你用aar方式依赖 ijkplayer那就不用复制 .so 了但这份资源是散装的 .so手动放置更可控。3.2 编写播放器初始化代码加载so与创建IJKMediaPlayer放置好 .so 之后写一个最简播放器。0.8.8 的 Java 层 API 跟官方 ijkplayer demo 一样核心类是IjkMediaPlayer。注意要先在 Java 代码里显式加载三个库虽然很多情况下系统会自动加载但为了排查问题我习惯写清楚public class IjkPlayerFactory { static { System.loadLibrary(ijkffmpeg); System.loadLibrary(ijksdl); System.loadLibrary(ijkplayer); } public static IjkMediaPlayer create() { IjkMediaPlayer player new IjkMediaPlayer(); // 日志级别INFO及以上输出避免刷屏 IjkMediaPlayer.native_setLogLevel(IjkMediaPlayer.INFO); return player; } }这段代码里System.loadLibrary的参数对应不带lib前缀的 .so 文件名。如果加载顺序不对比如先加载 ijkplayer 再加载 ijkffmpeg某些版本会报library libijkffmpeg.so not found。因为 libijkplayer.so 的JNI_OnLoad里会依赖 ffmpeg 的符号系统在加载 ijkplayer 时会尝试解析所有未定义符号如果此时 ffmpeg 还没加载进来就会失败。所以保持固定顺序ijkffmpeg - ijksdl - ijkplayer虽然 Android 7.0 以后对未定义符号的处理放宽了但老版本设备依然敏感。3.3 设置播放地址、硬解与软解切换的参数接下来说怎么让播放器真正跑起来。0.8.8 的IjkMediaPlayer是MediaPlayer的子类可以直接传 urlIjkMediaPlayer player IjkPlayerFactory.create(); player.setDataSource(http://192.168.1.100/sample.flv); player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, mediacodec, 1); player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, mediacodec-auto-rotate, 1); player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, opensles, 0); player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, framedrop, 1); player.prepareAsync();这几个参数的作用mediacodec1把硬解码器打开如果 .so 没编硬解这个设置会被 ffmpeg 忽略播放器自动用软解。mediacodec-auto-rotate是让硬解输出时自动处理旋转角度很多短视频是竖着拍的带 rotation tag不设这个就会出现横屏播放竖幅画面的尴尬。opensles0表示不用 OpenSL ES 音频输出老版本对 OpenSL 支持不稳建议先关掉但某些设备上关掉之后音频延迟会变大后面第5章我再说调法。framedrop1允许在网络卡顿的时候丢帧优先保证音频流畅直播场景必开。代码里prepareAsync是异步准备注册监听事件player.setOnPreparedListener(MediaPlayer::start);如果没监听onPrepared画面不会自动开始。很多人卡在这一步以为设置了 DataSource 就会播实际上必须等 prepared 回调再 start。我见过有人直接在prepareAsync后调start()结果整条状态机直接异常。3.4 处理https源与本地文件绕开openssl缺失的坑前面说过 0.8.8 不带 openssl所以setDataSource(https://...)大概率会回调 onError错误码通常是MEDIA_ERROR_UNKNOWN。解决办法有两个优先推荐用本地代理或者下载到 cache// 伪代码先用 okhttp 下载到临时文件 File videoFile new File(getCacheDir(), temp.mp4); downloadWithOkHttp(url, videoFile); player.setDataSource(videoFile.getAbsolutePath()); player.prepareAsync();另一种做法是自己实现 JNI 层带 openssl 的 .so但这个需要重新编译不是这份资源能解决的。我一般会在项目里封装一层PlayerDataSource如果是 https 逻辑先转成 http 反代地址或者干脆本地缓存。记住 0.8.8 对rtsp支持也不完整需要自己测。4. 避坑加载.so必踩的五个问题4.1 问题一只放了arm64目录32位设备直接闪退现象应用在小米 8、华为 Mate 10 这种 64 位设备上正常在几年前的 32 位设备或者低端 64 位兼容模式设备上打开就崩logcat 显示dlopen failed: library libijkplayer.so not found。原因你的jniLibs下只有arm64-v8a没有armeabi-v7a。32 位应用只能加载armeabi-v7a目录找不到就崩。如果你在build.gradle里写了abiFilters arm64-v8a也是一样。解决把资源包里的armeabi-v7a目录整个拷贝到libs下ndk.abiFilters 同时保留两个ndk { abiFilters armeabi-v7a, arm64-v8a }然后 clean 再 build。注意如果你引用的图片加载库libglide.so只有 arm64那就是另一个问题。这里是只说 ijkplayer 自身三件套必须全平台匹配。4.2 问题二printf日志出来了但播放进度不动现象logcat 能看到ffmpeg打出来的流信息但 setOnPreparedListener 一直不回调进度条一直转播放器不进入 prepared 状态。原因0.8.8 在部分 Android 10 设备上网络权限没动态申请或者使用 http 明文流量被系统拦截时ffmpeg 会卡在avformat_open_input阶段既不报 error 也不回调。因为 ffmpeg 是 native 代码它不遵循 Android 的网络安全配置但系统网络栈会先拒绝明文 http 连接。解决在AndroidManifest.xml里显式开启明文流量application android:usesCleartextTraffictrue ...如果你 targetSdk 28 以上这个属性必须开。另外记得检查INTERNET权限。我遇到过一次是权限全开了但把 url 写成了http://localhost:8080模拟器里的 localhost 是模拟器自己需要10.0.2.2才能访问宿主机这个也坑过半天。4.3 问题三开了硬解反而黑屏关掉就好了现象参数里设了mediacodec1有声音黑屏。关掉硬解用软解一切正常。原因硬解模式下视频帧通过 MediaCodec 输出但 0.8.8 对某些编码格式如 H.265 的一种特殊 profile、或者分辨率不是 16 倍数兼容性有问题。MediaCodec 解码成功但渲染时没有触发GL_SURFACE_CREATED导致画面通路断了。解决不要一刀切全开硬解改成运行时降级player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, mediacodec, 1); player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, mediacodec-auto-rotate, 1);然后在onError里检测到错误码是MEDIA_ERROR_IO或者MEDIA_ERROR_MALFORMED时用软解重试player.reset(); player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, mediacodec, 0); player.setDataSource(url); player.prepareAsync();还有一个经验部分设备上硬解必须配合ijkplayer.setOption(OPT_CATEGORY_PLAYER, override-hw-codec, 1)才有效。0.8.8 里这个参数的语义是强制用硬件解码如果不设某些流会回调错误但不降级。4.4 问题四和另一个播放器库的.so冲突导致崩溃现象项目里既有 ijkplayer又集成了某个短视频 SDK 自带的libffmpeg.so运行时报dlopen failed: cannot locate symbol avcodec_register_all。原因两个 .so 都包含 ffmpeg 的符号但版本不同。Java 层加载后符号表按照先加载优先解析ij zna 到其它库的符号冲突了。解决最简单是让 ijkplayer 单独线程加载且使用System.load指定绝对路径不推荐更常用的方案是把短视频 SDK 的 .so 放在单独的进程里或者干脆放弃 ijkplayer 改用别的播放器。如果必须共存试试用javacpp的Loader.loadLibrary隔离一下这个方案复杂度高。我们实际项目里最后的做法是统一所有 ffmpeg 相关库为一个版本短视频 SDK 自带的不开硬解只处理音频冲突就没了。4.5 问题五官方demo能跑自己工程一加载就崩现象把 ijkplayer 的 so 放进自己项目初始化就error_check失败logcat 里只有JNI DETECTED ERROR IN APPLICATION看不出具体原因。原因多数是 Java 层的IjkMediaPlayer类版本和 .so 不匹配。你是从网上随意下载的 so但 Java 源码用了不同版本的ijkplayer-java模块JNI 方法签名对不上。解决资源包通常会附带对应的 java 类如果没有去 0.8.8 源码目录ijkplayer/ijkplayer-java/src/main/java/tv/danmaku/ijk/media/player找同名文件复制进工程。千万别用最新 master 分支的 Java 层去配 0.8.8 的 .so很多 native 方法都重命名了。我吃过这个亏卡了三个多小时后才想起来对照版本。5. 验证播放与进阶从硬解到音画同步的实测方法5.1 用adb logcat验证解码器是否生效接入后不要眼睛盯着屏幕就算成功我习惯用 logcat 验证。adb logcat | grep -E ijk|IjkMediaPlayer|MediaCodec如果日志里出现MediaCodec: createByCodecName(OMX...)说明硬解生效如果只有VideoDecoder: Software decoding说明 .so 没编硬解或参数没设对。注意 0.8.8 默认 log level 可能会把这些信息过滤掉先 debug 跑一次在初始化处把 level 调到IjkMediaPlayer.DEBUG。播放过程中用adb shell top -H -p pid看 CPU 占用硬解视频的线程 CPU 通常小于 15%软解会占到 30~50%。这种方式比看参数更可靠。5.2 音画同步的“玄学”调整参数0.8.8 在部分设备上会出现音频快视频半秒的情况这时不要去调播放器内部的时间戳试试改音频输出方式。player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, opensles, 0); player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, audio-time-scale, 1.0f); player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, syncronize, 1);注意官方参数名是syncronize错一个字母会被忽略。opensles0时音频走 AudioTrack延迟低但容易卡顿opensles1时延迟稍高但能对齐。不同设备差异很大我最后是在线上埋点统计用户反馈发现开 OpenSL 的话音画不同步投诉少一半就把这个值改为 1 了。还有一个常见参数framedrop2表示丢帧更加激进。直播场景推荐 2点播场景推荐 1因为丢太多帧会遇到视频清晰度下降的投诉。5.3 进阶自己封装一个带断点重连的播放器0.8.8 没有自动重连机制但你可以利用它的onError事件做简单的断线恢复。代码逻辑不复杂player.setOnErrorListener((mp, what, extra) - { if (retryCount 3) { retryCount; mp.reset(); mp.setDataSource(url); mp.prepareAsync(); return true; } return false; });注意prepareAsync之后要重新设置各种 option因为reset()会把参数清空。我在项目里踩过这个坑第一次重连成功第二次后硬解参数丢了播放卡顿严重。5.4 最后的习惯每接一个.so都先查ABI从那次音画不同步排查之后我养成了一个强制习惯任何新拿到手的 .so 资源包先看目录是否三件套齐全、ABI 是否覆盖目标设备再看 README 里有没有写 openssl 和 mediacodec。把这两点记到项目交接文档里后面再多人同时改也不会出幺蛾子。ijkplayer 0.8.8 虽然老但把它当成一个被验证过的黑盒照样能发挥价值。希望这份 .so 资源也能让你少走几步弯路播放器跑起来之后再回头读读 0.8.8 的源码很多困惑会自己解开。本文还有配套的精品资源点击获取