简介:面向 Android 音视频开发者的 FFmpeg 预编译库合集,针对 ARMv8(arm64-v8a)架构优化,解决在 64 位 ARM 设备上集成音视频编解码能力时自行交叉编译成本高、易出错的问题。压缩包共 162 个文件,以 111 个 C 头文件、23 个 C 源文件为主,辅以 6 个 .a 静态库、6 个 .so 动态库及 6 个 pkg-config 配置文件和若干示例源码,整体约 14.35MB。库文件覆盖 avformat 封装格式解析、avcodec 编解码、avfilter 滤镜处理、swresample 音频重采样、swscale 色彩空间转换与 avutil 基础工具等核心组件,并附带转码、封装、解封装等示例代码,便于对照理解各模块调用方式。已有 614 人学习下载,适合需要快速将 FFmpeg 接入 Android 工程、研究库结构与接口用法的中高级开发者参考。
1. 拿到一份 ffmpeg-armv8-a.zip,先别急着解压
移动端做音视频的同行大概都遇到过这个场景:Android 工程里要转码、要抽帧、要混音,Java 层调 MediaCodec 能应付一部分,但一旦碰上格式兼容、滤镜链、精确裁剪这类活,最后还是得把 ffmpeg 请进来。而官方源码直接编出来的产物是动态库,塞进 APK 里体积大、加载路径还得跟 System.loadLibrary 对齐。这时候一份预编译好的 armv8-a 静态库压缩包,价值就出来了——它把 configure、交叉编译、裁剪 codec 这些脏活提前干完,你拿到的是能直接链进 so 的 .a 文件。
ffmpeg-armv8-a.zip 就是这类产物:面向 ARMv8-A(也就是 64 位 ARM,Android 上的 arm64-v8a)架构的 ffmpeg 静态库打包。它解决的核心问题是「不想自己搭 NDK 工具链、不想啃 configure 参数,又要一个能跑在 64 位 ARM 上的 ffmpeg 内核」。适合谁?做 Android 音视频 SDK 的、要给 App 加本地转码能力的、或者在做嵌入式 ARM 板子上跑媒体处理的从业者。但先说清楚:静态库不是万能药,它跟 .so 的取舍、ABI 匹配、license 合规这几件事,后面几章会一条条拆开讲,别拿到包就直接往 jniLibs 里扔。
2. 静态库 .a 与动态库 .so:这份包为什么给的是 .a
2.1 两种链接方式的真实差别
很多人第一次接触 ffmpeg 集成,看到网上教程一半让你放 .so 到 jniLibs,一半让你链 .a,就懵了。差别不在「哪个更高级」,而在链接时机和部署形态。
.so 是共享库,运行时由动态链接器加载,APK 里每个 ABI 目录下都得放一份,优点是多个模块可以共用同一份,升级库文件不用重编上层;缺点是文件数量多、加载有路径依赖、符号冲突时排查麻烦。.a 是静态库,本质是一堆 .o 的归档,编译期被链接器整个或按需抽出来塞进你最终的 .so 里,产物只有一个 so,部署干净,符号在编译期就解析完,运行时没有「找不到 libavcodec.so」这种玄学问题。
代价也明确:静态链接会把用到的目标代码复制进最终产物,如果裁剪不当,so 体积会膨胀;而且多个静态库之间如果有重复符号,链接器会报 multiple definition。所以这份 armv8-a 的 .a 包,正确的用法不是「全部链进去」,而是按你实际用到的模块挑着链。
2.2 判断这份包能不能用:三个先决检查
解压后别急着写 CMakeLists,先做三件事。
第一,确认架构。armv8-a 对应 Android 的 arm64-v8a,如果你的工程 minSdk 以下还要兼容 armeabi-v7a,这份包只能覆盖 64 位那一半,32 位得另找或自己编。用 file 命令看归档里的对象文件:
# 解压后进入目录,抽查一个 .a 里的对象架构 unzip ffmpeg-armv8-a.zip -d ffmpeg-armv8 cd ffmpeg-armv8 ar t libavcodec.a | head -5 # 抽出第一个 .o 看架构 ar x libavcodec.a $(ar t libavcodec.a | head -1) file *.o输出里出现ELF 64-bit LSB relocatable, ARM aarch64才算对得上。如果显示 32-bit ARM,说明包名和内容不符,直接弃用。
第二,确认头文件齐全。静态库必须配头文件才能编译,检查 include 目录下是否有 libavcodec、libavformat、libavutil、libswscale、libswresample 这几个子目录,缺一个后面编译就过不去。
第三,确认编译选项一致性。这一步最容易被忽略:如果这份 .a 是用-fPIC编的,你上层 so 也必须开 PIC;如果它链接了 libc++ 的某个版本,你的工程 NDK 版本差太远,链接期会报 undefined reference to std::__ndk1 之类。常见做法是先写个最小 demo 链一下,能过再往正式工程搬。
提示:静态库的 ABI 兼容性比动态库更苛刻,因为它把符号解析推迟到了你工程的链接阶段,任何编译选项不一致都会在这一步暴露。
2.3 用 CMake 把 .a 链进自己的 so
Android 现在主流是 CMake,下面这段是能抄的骨架。假设解压后目录结构是ffmpeg-armv8/lib/*.a和ffmpeg-armv8/include/。
cmake_minimum_required(VERSION 3.18) project(nativemedia CXX) # 头文件目录,供 #include <libavcodec/avcodec.h> 使用 include_directories(${CMAKE_SOURCE_DIR}/ffmpeg-armv8/include) # 把静态库声明为 imported target,路径按实际解压位置改 set(FFMPEG_LIB_DIR ${CMAKE_SOURCE_DIR}/ffmpeg-armv8/lib) add_library(avcodec STATIC IMPORTED) set_target_properties(avcodec PROPERTIES IMPORTED_LOCATION ${FFMPEG_LIB_DIR}/libavcodec.a) # avformat、avutil、swscale、swresample 同理各声明一个 add_library(nativemedia SHARED native-lib.cpp) target_link_libraries(nativemedia avformat avcodec avutil swscale swresample android log z)逻辑说明:IMPORTED告诉 CMake 这个库已经存在、不要去找源码编译;IMPORTED_LOCATION必须写绝对路径或基于CMAKE_SOURCE_DIR的路径,写相对路径在子目录里会失效。链接顺序有讲究——avformat 依赖 avcodec,avcodec 依赖 avutil,所以 avformat 放最前、avutil 放最后,顺序反了会出现 undefined reference,这是静态链接的经典坑。z是 zlib,ffmpeg 处理 png、字幕压缩时会用到,漏了会在链接期报 inflate 相关符号缺失。
参数怎么改:如果你的工程只要解码不要编码,可以把 avcodec 换成裁剪版;如果不用滤镜,swscale 和 swresample 可以去掉,能省不少体积。但去掉之前先用nm -u看依赖,别凭感觉删。
3. 从解压到跑通第一个解码调用:可复现的最小链路
3.1 目录规整与头文件引用
拿到包后我一般先做一次目录规整,把散落的 .a 和 include 摆成固定结构,后面换 NDK 版本或换架构时只改路径不改代码。
# 规整成 lib/<abi>/ 与 include/ 两棵树 mkdir -p app/src/main/cpp/ffmpeg/include mkdir -p app/src/main/cpp/ffmpeg/lib/arm64-v8a cp -r ffmpeg-armv8/include/* app/src/main/cpp/ffmpeg/include/ cp ffmpeg-armv8/lib/*.a app/src/main/cpp/ffmpeg/lib/arm64-v8a/这样做的原因是:Android Gradle Plugin 对 jniLibs 有默认 ABI 目录约定,虽然静态库不走 jniLibs,但保持lib/<abi>/的层级,将来要加 armeabi-v7a 时直接并列一个目录,CMake 里用${ANDROID_ABI}变量就能自动切换,不用改脚本。
头文件引用上有个细节:ffmpeg 的头文件互相包含时用的是"libavutil/..."这种相对路径,所以 include 目录必须指到include/这一层,而不是include/libavcodec/。指错了会报libavutil/avconfig.h: No such file。
3.2 一个最小解码 demo 的代码骨架
链路通了之后,写个最小 demo 验证库真的能用。下面这段只做「打开文件、找到视频流、读一帧」三件事,够验证链接和基本 API 可用性。
extern "C" { #include <libavformat/avformat.h> #include <libavcodec/avcodec.h> } // 返回值:0 成功,负数失败,调用方负责 avformat_close_input int probeFirstVideoFrame(const char* path) { AVFormatContext* fmt = nullptr; // 打开输入,第三个参数留空表示自动探测格式 if (avformat_open_input(&fmt, path, nullptr, nullptr) < 0) return -1; if (avformat_find_stream_info(fmt, nullptr) < 0) { avformat_close_input(&fmt); return -2; } // 找第一条视频流,返回流索引 int idx = av_find_best_stream(fmt, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0); if (idx < 0) { avformat_close_input(&fmt); return -3; } AVCodecParameters* par = fmt->streams[idx]->codecpar; const AVCodec* dec = avcodec_find_decoder(par->codec_id); if (!dec) { avformat_close_input(&fmt); return -4; } AVCodecContext* ctx = avcodec_alloc_context3(dec); avcodec_parameters_to_context(ctx, par); if (avcodec_open2(ctx, dec, nullptr) < 0) { avcodec_free_context(&ctx); avformat_close_input(&fmt); return -5; } // 到这里说明解码器初始化成功,库可用 avcodec_free_context(&ctx); avformat_close_input(&fmt); return 0; }逻辑说明:avformat_open_input负责容器层解析,av_find_best_stream挑默认视频流,avcodec_find_decoder按 codec_id 找解码器实现——这一步能返回非空,就证明你链进来的 libavcodec.a 里确实包含该解码器,裁剪没裁过头。参数上,avcodec_parameters_to_context把流参数拷进解码上下文,新版 ffmpeg 必须走这一步,老教程里直接访问stream->codec的写法已经废弃,照抄会编译不过。
失败时看什么:返回 -1 多半是路径或权限问题;-4 说明这份 .a 没编进对应解码器,得换包或自己补编;-5 常见于解码器依赖的硬件上下文缺失,先换软解验证。
3.3 验证产物:用 readelf 和 nm 确认符号
编译出 so 之后,别只看「编译成功」就收工。用 NDK 里的工具链查一遍,确认 ffmpeg 符号真的进去了、没有意外的动态依赖。
# 查最终 so 是否还动态依赖 libav* $NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android-readelf -d libnativemedia.so | grep NEEDED # 查关键符号是否已静态链入 $NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android-nm -D libnativemedia.so | grep avcodec_open2NEEDED列表里如果还出现 libavcodec.so,说明链接时优先命中了动态库,静态库没生效,检查 CMake 里是不是同时存在 .so 路径。nm -D能查到avcodec_open2,说明符号已进入动态符号表,运行时 JNI 调用不会报找不到。这两条命令是我每次集成静态库的固定动作,比在设备上盲跑省时间。
4. 裁剪与体积:静态链接最容易翻车的三个参数
4.1 按需链接与 --gc-sections
静态库全量链进去,so 轻松破 20MB。控制体积的第一招是链接器的 section 回收。
target_link_options(nativemedia PRIVATE -Wl,--gc-sections -Wl,--exclude-libs,ALL)--gc-sections让链接器丢弃没有被引用的函数段,前提是编译时开了-ffunction-sections -fdata-sections——这两个开关得在编 ffmpeg 时就带上,如果这份预编译包没带,gc-sections 效果会打折。--exclude-libs,ALL把静态库里的符号从最终 so 的导出表里隐藏,既减小动态符号表,也避免和别的库符号冲突。参数含义:ALL 表示所有静态库,也可以写成具体库名只隐藏部分。
4.2 用 strip 去掉调试符号
预编译包里的 .a 往往带调试信息,链进 so 后体积虚高。发布版务必 strip。
# 对最终 so 做 strip,保留动态符号 $NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip --strip-unneeded libnativemedia.so注意--strip-unneeded和--strip-all的区别:后者会把动态符号表也干掉,JNI 的Java_xxx入口就没了,运行时直接崩。用 unneeded 只删链接不需要的符号,安全。这一步在 CI 里做成构建后置步骤,别手工漏。
4.3 版本与 license 的边界
ffmpeg 的 license 取决于编译时开的组件。如果这份包开了--enable-gpl(比如带了 x264),你的整个 App 就受 GPL 约束,闭源分发会有合规风险。判断方法:看包里有没有 libx264、libx265 这类外部编码库的 .a,或者查头文件里CONFIG_GPL宏。
# 查配置宏,判断是否 GPL 构建 grep -r "CONFIG_GPL" ffmpeg-armv8/include/libavutil/avconfig.h如果显示 1,商用闭源项目要谨慎,常见做法是换 LGPL 构建的包,或者把 ffmpeg 相关功能拆成独立进程通过 socket 通信来隔离。这条不是技术坑,是法务坑,但翻车代价比编译错误大得多。
5. 避坑清单:静态库集成里那些血泪经验
5.1 现象:链接报 undefined reference to__android_log_print
原因:ffmpeg 内部有日志回调走 Android log,但你的 target_link_libraries 里漏了log。解决:在链接列表补上log,顺序放在 ffmpeg 库之后。
5.2 现象:运行时报dlopen failed: cannot locate symbol "avcodec_version"
原因:你以为链的是静态库,实际 CMake 里某个 imported target 的路径指到了 .so,或者系统里存在同名动态库被优先加载。解决:用 3.3 节的 readelf 查 NEEDED,确认没有 libav* 动态依赖;检查 IMPORTED_LOCATION 后缀是不是 .a。
5.3 现象:编译过、链接过,一调用就 SIGSEGV 在 av_malloc
原因:NDK 版本与预编译包的 libc++ 不匹配,或者 .a 编译时用了不同的 STL(c++_static 对 c++_shared)。解决:统一 STL,在 build.gradle 里显式写arguments "-DANDROID_STL=c++_shared",并确认 ffmpeg 包也是同一 STL 编的;实在对不上就自己重编。
5.4 现象:armeabi-v7a 设备上装完直接闪退
原因:只放了 arm64-v8a 的 .a,32 位设备加载不到对应 so。解决:Gradle 里用 abiFilters 明确只出 arm64-v8a,或者补一份 32 位包;不要指望静态库能跨 ABI 复用。
5.5 现象:so 体积比预期大了一倍
原因:同时链了静态库和动态库,或者没开 gc-sections、没 strip。解决:按第 4 章三步走,先查 NEEDED 去重,再开 section 回收,最后 strip,通常能砍掉 40% 以上。
6. 进阶:把这份包接进 CI 并做一次符号体检
单机跑通只是第一步,团队协作里真正省心的是把「解压、校验架构、链接、体积检查」固化成脚本。我一般会在 CI 里加一个前置任务,对下载到的 ffmpeg-armv8-a.zip 做完整性校验,避免有人传了个 32 位包混进流水线。
#!/usr/bin/env bash set -euo pipefail ZIP=$1 WORK=$(mktemp -d) unzip -q "$ZIP" -d "$WORK" # 抽查所有 .a 的架构,任何一个不是 aarch64 就失败 for lib in "$WORK"/lib/*.a; do obj=$(ar t "$lib" | head -1) (cd "$WORK" && ar x "$lib" "$obj") arch=$(file "$WORK/$obj" | grep -c "ARM aarch64" || true) if [ "$arch" -eq 0 ]; then echo "ABI mismatch in $lib"; exit 1 fi done echo "all libs are arm64-v8a"这段脚本的关键在set -euo pipefail:任何一步失败立即中断,避免半成品继续往下走。ar x抽对象文件时要在归档所在目录执行,否则路径找不到,这是 shell 里的小陷阱。架构校验用file的输出匹配ARM aarch64,比解析 ELF 头省事,够用。
再补一个体积基线检查,防止某次提交悄悄把 so 撑大:
MAX=12000000 # 12MB 基线,按项目实际调 SIZE=$(stat -c%s libnativemedia.so) if [ "$SIZE" -gt "$MAX" ]; then echo "so size $SIZE exceeds baseline $MAX"; exit 1 fi参数怎么定:基线取当前发布版体积上浮 10%,太紧会频繁误报,太松失去意义。这个检查配合第 4 章的 strip 步骤,基本能挡住「忘了裁剪就提交」这类事故。
还有一个验证技巧值得单独说:用nm -u列出最终 so 的未定义符号,过滤掉系统库该提供的(libc、liblog、libm、libdl、libz),剩下的如果还有 av* 开头,说明静态链接没闭合,运行时必崩。这条命令比在真机上反复装包快得多,我把它当成集成后的固定体检项。
$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android-nm -u libnativemedia.so \ | grep -E "av|sws|swr" || echo "no unresolved ffmpeg symbols"输出no unresolved ffmpeg symbols才算真正闭合。从那以后我每次集成新的预编译包,都强制走一遍「架构校验 → 链接 → NEEDED 检查 → 未定义符号检查 → 体积基线」这五步,宁可多花五分钟,也不在发版前夜对着闪退日志抓瞎。希望这套流程能帮到你,少走几个我当年踩过的弯路。
本文还有配套的精品资源,点击获取