简介这套 ijkplayer 0.8.8 编译文件面向需要在 Android 端集成跨平台播放器的音视频开发者主要解决多 CPU 架构下动态库与依赖库的适配问题。压缩包共 1581 个文件约 49.08MB包含 24 个 so 动态库、20 个 a 静态库以及 java 源码、xml 配置、gradle 与 mk 构建脚本等so 对应不同设备架构a 文件提供播放器核心与音频处理等静态依赖源码和构建脚本则便于对照工程组织方式。资源与 B 站项目实践相关基于 FFmpeg 构建支持 RTMP、HLS、HTTP 等常见流媒体协议可服务于直播、点播和短视频等播放场景目前已有 1748 人学习下载。读者可据此快速完成播放器依赖接入排查 NDK 编译、静态库链接和多架构打包中的问题也可作为自定义解码参数、播放控制与性能优化的工程底稿对理解目录层级、依赖关系和构建流程有直接参考价值。1. 一个压缩包救了一个要提 targetSdk 的老项目上周接手一个维护了三年的投屏类 App播放内核还停留在ijkplayer .so的预编译版本上业务侧要求把 targetSdk 提到 35一编译就卡在 native 库源码仓库的 submodule 拉不全ndk-build 脚本在新版 NDK 上直接报错ffmpeg 交叉编译那套工具链早就不认新规则了。翻备份盘真正救场的是一份 ijkplayer 0.8.8 的预编译产物几个 ABI 目录每个目录三个 .so解压即用不用再碰 configure 和 toolchain 文件。这份东西的价值不在于“新”而在于它是一份已经编译好、符号齐全、能直接塞进jniLibs的成品——对那些不想为了改一行播放逻辑就重建整套 NDK 环境的团队来说这就是后悔药。适合两类人一类是老项目续命必须锁死内核版本另一类是想快速验证播放链路的方案选型先跑通再谈自编译。2. 拆包看货三个 .so 各自背了什么责任拿到预编译包先别急着往工程里丢。我一般会先摊开目录看结构因为这决定了后面 Gradle 怎么配、加载顺序怎么排、出问题该看哪一层。2.1 进程内分层Java 壳、JNI 桥、C 内核ijkplayer 在 Android 上的运行结构是三层。最上面是IjkMediaPlayer这个 Java 类它继承自AbstractMediaPlayer对外暴露setDataSource、prepareAsync、start这些和系统MediaPlayer几乎同名的方法所以业务代码迁移成本很低。中间是 JNI 桥native_setup、native_setOption、_prepareAsync这些 native 方法把 Java 侧的调用翻译成 C 侧的FFPlayer结构体操作。最底下是 C 内核也就是预编译包里那三个 .so。这三个 .so 的依赖关系是单向的加载顺序错了就是UnsatisfiedLinkError.so职责依赖体积占比libijkffmpeg.so裁剪过的 ffmpeg协议、解封装、软解、重采样无最大通常占七到八成libijksdl.so音频/视频输出抽象ANativeWindow、AudioTrack/OpenSL ES、时钟ijkffmpeg中等libijkplayer.soJNI 桥 播放管线改自 ffplay.cFFPlayer就在这里ijkffmpeg、ijksdl较小理解这个分层的实际意义在于定位问题。播放卡顿、花屏、解不出来多半在 ijkffmpeg 这一层属于解码器或码流识别的问题黑屏但有声音、Surface 切回来不刷新基本在 ijksdl 的输出层setOption不生效、回调不触发看 ijkplayer 这一层。分清楚之后再抓 log比盲目改参数高效得多。2.2 ABI 选型别一股脑把四个架构全带上预编译包一般会给出armeabi-v7a、arm64-v8a、x86、x86_64四套。很多人的第一反应是全带上反正能跑。代价是 APK 体积直接翻倍而且从 2023 年起主流应用商店对 64 位的要求越来越硬32 位包在很多新设备上根本装不上。选型我遵循三条线上包只保留arm64-v8a和armeabi-v7a。前者覆盖新机器后者兜住还在服役的老设备尤其是一些定制行业终端。x86/x86_64只在 debug 变体里开给模拟器用。release 里带上纯属浪费体积而且会让某些加固/扫描工具报出多余的告警。如果确定业务不覆盖 32 位设备比如只做自家定制硬件干脆只留arm64-v8aAPK 能省下一大截首次安装也快。有一点要提前确认预编译包是不是真的齐全。有些流传的包只放了armeabi-v7aarm64-v8a目录是空的或者干脆没有装到 64 位设备上就是findLibrary returned null。解压后先ls一遍四个目录确认每个目录里都是三个 .so别到集成阶段才发现缺文件。2.3 能力位这份 ffmpeg 到底裁掉了什么预编译的libijkffmpeg.so是按某套--enable-/--disable-参数裁出来的不是全量 ffmpeg。协议、封装格式、解码器都是白名单式的。这一点必须在动手前探明否则会出现“代码没问题、就是放不了”的黑匣子现象。最省事的探法是对着 .so 里的字符串做匹配# 看这份 so 支持哪些协议与封装字符串表里会留下注册名 strings libs/jniLibs/arm64-v8a/libijkffmpeg.so | grep -E ^(rtmp|rtsp|hls|http|https|file)$ | sort -u # 看解码器是否编进去了 strings libs/jniLibs/arm64-v8a/libijkffmpeg.so | grep -E ^(h264|hevc|aac|mp3|flv|mov,mp4) | sort -ustrings命中的名字来自 ffmpeg 里AVInputFormat/AVCodec的注册表能对上基本就说明该能力编进去了对不上不代表一定没有部分符号被 strip 过但一旦对不上又确实播不了那大概率就是裁剪掉了。常见的裁剪坑是 HLS 和 HEVC。有的包里hls相关的 demuxer 没开播 m3u8 会直接在prepare阶段抛MEDIA_ERROR_UNKNOWNlog 里能看到Protocol not found或Failed to open file。另外 HEVC 软解是否编入也影响很大如果只有硬解路径、设备又不支持 HEVC 硬解那就彻底没戏。这两项探明之后再决定要不要换包或者走自编译能省掉后面大量的无效调试。3. 接进工程Gradle 目录、加载顺序与打包验证3.1 jniLibs 目录结构与 abiFilters预编译的 .so 不用走 CMake直接按 ABI 目录丢进jniLibs就行。关键是目录层级必须是jniLibs/ABI/xxx.so多一层少一层都会导致打不进 APK。android { defaultConfig { ndk { // 只保留线上真正要覆盖的 ABI四个全带会让包体积翻倍 abiFilters arm64-v8a, armeabi-v7a } } sourceSets { main { // 把预编译 .so 所在目录登记为 jniLibs 源 jniLibs.srcDirs [libs/jniLibs] } } packaging { jniLibs { // 老版本 AGP 写 useLegacyPackaging true // 表示安装时把 .so 解压到 lib 目录而不是直接从 APK 里 mmap useLegacyPackaging true } } } dependencies { // Java 壳子以本地 aar/jar 形式引入版本必须和 .so 对齐 implementation files(libs/ijkplayer-java-0.8.8.aar) }abiFilters和jniLibs.srcDirs这两项要一起看abiFilters决定最终打哪些 ABIsrcDirs决定从哪儿找。如果只配了前者没配后者Gradle 会去默认的src/main/jniLibs找找不到就静默跳过编译通过但 APK 里没有 .so这个坑非常隐蔽——它不报错。useLegacyPackaging这一行值得单独说。设为true时安装阶段系统会把 .so 从 APK 解压到/data/app/.../lib/abi/System.loadLibrary走的是文件系统路径设为false时直接从 APK 里 mmap安装包更小、安装更快但对 .so 的页对齐有额外要求下一章的 16KB 问题就出在这儿。老包我一般先设true跑通之后再考虑切false省体积。3.2 Java 侧的初始化与加载顺序Java 壳子里IjkMediaPlayer有个静态方法负责依次加载三个 .so。默认实现就是按ijkffmpeg→ijksdl→ijkplayer的顺序调System.loadLibrary。如果你把 .so 改了名字、或者和别的库有冲突可以自己传一个IjkLibLoader进去接管加载过程。public class PlayerFactory { private static boolean sLibReady false; // 加载顺序不能换ijkplayer 依赖 ijsdlijsdl 依赖 ijkffmpeg public static synchronized void ensureLibrary() { if (sLibReady) return; IjkMediaPlayer.loadLibrariesOnce(new IjkMediaPlayer.IjkLibLoader() { Override public void loadLibrary(String libName) throws UnsatisfiedLinkError { // 这里可以打点确认三个库各自的加载耗时 long t0 System.currentTimeMillis(); System.loadLibrary(libName); Log.i(PlayerLib, libName loaded in (System.currentTimeMillis() - t0) ms); } }); sLibReady true; } public static IjkMediaPlayer create(Context ctx) { ensureLibrary(); IjkMediaPlayer player new IjkMediaPlayer(); player.setScreenOnWhilePlaying(true); return player; } }这里有两个参数层面的讲究。一是loadLibrariesOnce只生效一次第二次调用是空操作所以放在Application.onCreate或首次创建播放器时都行但不要以为换了IjkLibLoader还能重新加载——它内部有静态标志位。二是加载本身有耗时libijkffmpeg因为体积大冷启动时解压 重定位在主线程上可能吃掉几十毫秒我一般会在启动页异步预热一次把这段耗时挪到用户看不见的地方。创建之后、setDataSource之前是setOption的唯一有效窗口。这点很多人踩坑把setOption写在prepareAsync之后发现完全不起作用也不报错。原因是这些 option 在native_setup之后、prepare之前被读取进FFPlayer的配置结构体过了这个点再设只是往一个没人读的字段里写值。IjkMediaPlayer player PlayerFactory.create(context); // 所有 setOption 必须在这两行之间完成 player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, mediacodec, 1L); player.setDataSource(url); player.setSurface(surface); player.prepareAsync();3.3 打包之后先验证别等 QA 提 bug写完配置第一件事不是跑起来看画面而是确认 .so 真的进了 APK。这一步花三十秒能省掉后面半小时的UnsatisfiedLinkError排查。# 1. 看 APK 里的 native 库清单确认四个 ABI 只剩你要的那两个 unzip -l app-release.apk | grep lib/ # 2. 检查 APK 的对齐Android 15 起 16KB 页对齐是硬要求 zipalign -c -P 16 -v 4 app-release.apk | tail -5 # 3. 反查单个 .so 的 LOAD 段对齐值0x4000 才是 16KB 对齐 llvm-readelf -l libs/jniLibs/arm64-v8a/libijkplayer.so | grep LOAD第一条命令的输出要能对上lib/arm64-v8a/libijkffmpeg.so、lib/arm64-v8a/libijksdl.so、lib/arm64-v8a/libijkplayer.so这六行两个 ABI 各三行。少一行就说明abiFilters和目录对不上。第三条命令看Align列0x1000是 4KB 对齐0x4000是 16KB 对齐预编译的老包绝大多数是0x1000这就是后面第 5 章那个安装失败的根源。4. 参数调优把首帧时间和卡顿压下去4.1 常用 option 一览setOption的第一个参数是分类决定了这个 key 最终落到FFPlayer里哪个字段或被转成 ffmpeg 的AVDictionary条目。分类搞错值写了也白写。常用的几组我整理成表分类常量key建议值作用与代价OPT_CATEGORY_FORMATprobesize512KB 左右探测码流信息读取的字节数调小首帧快太小会认不出编码OPT_CATEGORY_FORMATanalyzeduration300000微秒分析时长和 probesize 配合一起调小才有效OPT_CATEGORY_FORMATfflagsnobuffer关掉输入缓冲直播场景常用点播慎用OPT_CATEGORY_PLAYERmax-buffer-size15MB 左右缓冲区上限调大抗抖动调小降内存OPT_CATEGORY_PLAYERframedrop1解码跟不上时丢帧保时钟卡顿观感会好很多OPT_CATEGORY_PLAYERpacket-buffering1开启缓冲水位控制弱网下减少反复起播OPT_CATEGORY_PLAYERstart-on-prepared0关掉自动起播改由业务侧在onPrepared后手动startOPT_CATEGORY_PLAYERmediacodec1硬解总开关OPT_CATEGORY_CODECskip_loop_filter48跳过环路滤波降低解码负载画质略降OPT_CATEGORY_PLAYERopensles1音频输出走 OpenSL ES延迟比 AudioTrack 低注意数值类型。probesize和max-buffer-size是 64 位整数必须用long重载写成 int 会被当成另一个重载解析值可能溢出fflags是字符串mediacodec这类开关用long的 0/1。类型不匹配不会报错只会静默不生效这也是setOption最容易被判定为“玄学”的原因。4.2 首帧时间从三秒到一秒以内点播场景用户最敏感的不是码率是点下去到出画面那段黑屏。默认配置下probesize和analyzeduration都偏保守为了保证兼容性会多读一段数据才确定流信息这段等待在弱网下能拉到两三秒。private void tuneForFastStart(IjkMediaPlayer player) { // 探测窗口压到 512KB绝大多数 mp4/hls 够用首帧能快 1s 以上 player.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, probesize, 512 * 1024L); // 分析时长 300ms和 probesize 一起压单压一个没意义 player.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, analyzeduration, 300 * 1000L); // 关闭输入缓冲避免起播前先攒一大段 player.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, fflags, nobuffer); // 缓冲上限 15MB够扛一般网络抖动又不至于吃掉太多内存 player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, max-buffer-size, 15 * 1024 * 1024L); // 解码跟不上时丢帧保证音频时钟不漂 player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, framedrop, 1L); // 不自动起播等 Surface 就绪后再 start避免首帧空转 player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, start-on-prepared, 0L); }这组值的边界在哪里probesize压到 128KB 以下一部分码率波动大的流会出现分辨率识别错误表现为画面被拉伸或者只显示左上角一块analyzeduration压到 100ms 以下带 B 帧的流可能算错时间基音画不同步。所以我不建议一上来就压到极限先在 512KB / 300ms 这个档位观察稳定了再往下试且一定要拿真实的低码率、高码率、变码率三种流分别验证。start-on-prepared设为 0 之后起播时机就交给业务侧了。这个改动看起来只是多写一行start()实际收益是能把“Surface 已 attach”和“数据已缓冲”两件事对齐避免首帧渲染到一个还没就绪的窗口上导致第一帧黑一下再跳出来。4.3 硬解回退与 Surface 生命周期硬解在 0.8.8 上的表现高度依赖芯片和系统版本。同一个 HEVC 流某几台机器播得飞起另几台直接绿屏或者解不出。我的做法是默认开硬解但给它配一条明确的回退路径而不是让用户对着绿屏干等。player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, mediacodec, 1L); player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, mediacodec-all-videos, 1L); // 分辨率中途变化时重建解码器直播切清晰度必须开 player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, mediacodec-handle-resolution-change, 1L); player.setOnErrorListener((mp, what, extra) - { // 硬解初始化失败的典型回调whatMEDIA_ERROR_UNKNOWN if (what IMediaPlayer.MEDIA_ERROR_UNKNOWN !sFallbackDone) { sFallbackDone true; mp.reset(); // 关掉硬解全部走软解重来一次 mp.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, mediacodec, 0L); mp.setDataSource(currentUrl); mp.prepareAsync(); return true; // 消费掉避免再往上抛 } return false; });这里有个顺序陷阱reset()之后所有的 option 都会被清掉包括你之前设的probesize。所以回退路径里必须把tuneForFastStart那套重新走一遍否则第二次起播会退回默认配置表现就是“回退成功了但特别慢”。我一般把 option 设置抽成一个方法reset之后无条件重调。Surface 这块setSurface在prepare前后都可以调但SurfaceView销毁重建比如切后台再回来、或者页面被 RecyclerView 回收之后必须重新setSurface否则就是有声音没画面。更稳妥的做法是surfaceDestroyed时直接release播放器、surfaceCreated时重建代价是位置要自己记住再 seek 回去。两害相权直播类用重建点播类用重设 Surface 加手动续播。5. 排查现场加载期、安装期、运行期各三个坑5.1 加载期的三种翻车现象一java.lang.UnsatisfiedLinkError: dlopen failed: library libijkffmpeg.so not found。原因通常是 APK 里压根没这个文件或者设备 ABI 不在abiFilters里。前者检查unzip -l的清单后者用adb shell getprop ro.product.cpu.abilist看设备支持的架构清单——注意是abilist不是abi64 位设备通常同时列出arm64-v8a,armeabi-v7a但有些纯 64 位设备只列前者。 解决补上对应 ABI 目录的 .so或者把abiFilters加回去。别指望 32 位库能在纯 64 位设备上跑。现象二dlopen failed: cannot locate symbol avcodec_... referenced by libijkplayer.so。这是三个库版本不一致的典型症状——Java 壳子换成了新版本.so 还是旧的符号表对不上。也可能是工程里同时存在两份libijkffmpeg.so比如某个第三方 SDK 也塞了一份打包时被 merge 成了错误的那个。 解决先unzip -l确认只有一份如果确实有多份在packagingOptions里用pickFirst锁定自己的那份。Java aar 的版本必须和 .so 同批混用必翻车。现象三模拟器上跑得好好的真机一加载就崩。原因基本是模拟器走的是x86_64目录真机走arm64-v8a而两个目录里的 .so 不是同一批产物甚至x86_64那份是别人随手补的。 解决把x86_64限制在 debug 变体且只用一份来源可靠的产物release 变体里剔除掉别让它在真机上被误选。5.2 安装期16KB 页对齐这个新坑现象新系统设备上adb install直接失败报INSTALL_FAILED_INVALID_APK提示 native 库未按 16KB 对齐或者装上了但一加载 .so 就 SIGSEGV。原因是 0.8.8 这批产物编译时默认按 4KB 页对齐LOAD 段Align为0x1000而新系统对 64 位 .so 要求 16KB 对齐0x4000。这跟代码无关纯粹是链接期的max-page-size参数问题。 解决有三条路一是重新链接加上-Wl,-z,max-page-size16384用新 NDK 重编二是找已经用新工具链重编过的预编译包替换三是把useLegacyPackaging设为true先绕过去解压安装路径对对齐的要求宽松一些但这只是缓兵之计上架审核该拦还是拦。验证手段就是第 3 章那条llvm-readelf -l | grep LOAD改完再跑一次看到0x4000才算真解决。5.3 运行期花屏、音画不同步、切回来黑屏现象部分设备上 HEVC 流出现绿屏、马赛克、只解码出上半部分。原因是硬解路径在这些芯片的 Codec 实现上有兼容问题尤其是分辨率不是 16 对齐的流。 解决在OnMediaCodecSelectListener里按mime和分辨率做白名单命中可疑组合就直接返回 null 让内核走软解或者更粗暴一点认设备型号命中已知问题机型就全局关硬解。这类问题没有通解只能靠机型灰度慢慢收敛。现象播放几秒后声音和画面越拉越开视频明显慢半拍。原因多半是音频时钟被 OpenSL ES 的输出延迟影响或者framedrop没开导致视频帧堆积。 解决先开framedrop1观察是否缓解再把opensles关掉退回 AudioTrack 对比。如果只在某个流的特定码率下出现那更可能是analyzeduration压得太狠导致时间基算错把探测窗口调回去。现象从后台切回来画面没了但声音还在或者直接整块黑。原因是SurfaceView的Surface已经销毁重建播放器还绑在旧的ANativeWindow上。 解决在surfaceDestroyed里setSurface(null)并暂停surfaceCreated里重新setSurface后start。如果仍然黑就老老实实走 release 重建 seek 的路径。6. 进阶把“能不能用”变成可复现的体检流程预编译包最大的风险不是性能是来源不明带来的一堆未定义行为。我现在拿到任何一份 ijkplayer 0.8.8 的 .so都会先跑一遍静态体检再跑一遍动态体检两次都过了才敢往工程里放。静态体检三条命令前面零散提过串起来是一个固定流程# 1. 架构与依赖确认 ELF 是 aarch64、没有异常的 NEEDED file libijkplayer.so llvm-readelf -d libijkplayer.so | grep NEEDED # 2. 页对齐0x4000 才能上 16KB 设备 llvm-readelf -l libijkplayer.so | grep LOAD # 3. 能力位确认协议/解码器白名单覆盖你的业务流 strings libijkffmpeg.so | grep -E ^(hls|rtmp|hevc|h264)$ | sort -u第一条的NEEDED输出里应该只有liblog.so、libz.so、libc.so、libm.so、libdl.so、libandroid.so这类系统库加上它自己的两个兄弟库。如果看到某个陌生的第三方 so说明这份产物被人二次链接过来源存疑我一般直接弃用。动态体检靠 ijkplayer 自带的日志开关。它内部有一套 ffmpeg 的 log 回调把级别调高之后prepare阶段的每一步——打开输入、探测流信息、找到解码器、初始化解码器——都会打到 logcat 上// 放在 setDataSource 之前级别调到 DEBUG 才能看到探测细节 IjkMediaPlayer.native_setLogLevel(IjkMediaPlayer.IJK_LOG_DEBUG);# 过滤播放器与 ffmpeg 的日志重点看这几类关键字 adb logcat -s ijkmplayer:V ffmpeg:V | grep -E probe|codec|error|drop这里面probe相关的行能告诉你probesize到底读了多少字节才确定流信息是判断首帧优化有没有生效的直接证据codec相关的行能确认最后选的是硬解还是软解以及硬解初始化的返回值。我遇到过一次“硬解开关明明开了但走的是软解”日志里那行mediacodec init failed, fallback to ffmpeg直接把问题指向了机型兼容比猜参数快得多。一个具体技巧把每次验证的probesize、analyzeduration和对应的首帧耗时记成一张小表同一个流在同一台设备上跑三遍取中位数。别只跑一遍就下结论起播耗时受系统负载影响很大第一遍冷启动和第三遍热启动能差一倍。血泪教训是有一回图省事直接把别人给的 .so 丢进jniLibs就开始联调结果三台测试机里有一台装完就闪退查了两小时才发现那份产物的arm64-v8a目录里三个 .so 根本不是同一次编译出来的libijkplayer.so引用的符号在libijkffmpeg.so里压根没有。从那以后我每次集成新的 native 包都强制走一遍上面的静态 动态体检NEEDED、对齐、能力位、日志四关全过才合入。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?