简介:面向 Android 音视频开发者的 FFmpeg 预编译库合集,针对 ARMv8(arm64-v8a)架构优化,解决在 64 位 ARM 设备上集成音视频处理能力时自行交叉编译成本高、易出错的问题。包内共 162 个文件,以 111 个 C 头文件与 23 个 C 源文件为主,便于查阅接口与参考调用;同时提供 avformat、avcodec、avfilter、avutil、swscale、swresample 六个静态库及对应动态库,并附 pkg-config 配置与示例转码、封装、解封装代码,压缩包约 14.35MB。已有 614 人学习下载。读者可直接将动态库放入 jniLibs、静态库链接进可执行文件,快速完成容器解析、H.264/H.265 与 AAC 编解码、滤镜处理、采样率与色彩空间转换等模块的接入,并借助示例代码理解典型调用流程,减少从零编译与踩坑的时间。
1. 拿到 ffmpeg-armv8-a.zip:一份能直接链进 Android 工程的预编译静态库
如果你在 Android 上做过音视频,大概率经历过这种场面:CMake 里find_package(FFmpeg)死活找不到,自己拉源码交叉编译又卡在 NDK 版本、汇编器、--target-os参数上,折腾一整天编出来的.a一链接就是满屏undefined reference to 'avcodec_...'。这份ffmpeg-armv8-a.zip就是冲着这个痛点来的——它是一套面向 arm64-v8a 架构、已经编译好的 FFmpeg 静态库集合,核心产物是.a文件,配套头文件,目标是让你跳过交叉编译这一关,直接把库塞进 JNI 工程里跑起来。它适合两类人:一是需要快速在 Android 端验证解码、转码、抽帧逻辑的开发者;二是想搞清楚.a静态链接和.so动态加载到底差在哪、链接顺序为什么能决定生死的工程师。下面按「这套库是什么结构 → 怎么接进工程 → 链接阶段怎么排错 → 怎么验证真的能用」的顺序拆。
2. 拆包看结构:arm64-v8a 静态库的目录约定与链接模型
拿到压缩包先别急着往工程里拷,先搞清楚里面装了什么。FFmpeg 编译产物在 Android 场景下通常分两套:静态库.a和动态库.so。这份包给的是.a,意味着最终产物会把 FFmpeg 的代码一起打进你的libxxx.so里,运行时不再依赖额外的 FFmpeg 动态库。这个选择有明确代价和收益,得先想清楚。
2.1 静态库与动态库的取舍:为什么这份包给的是 .a
.a本质是一堆.o目标文件的归档(archive),链接器在链接阶段按需把用到的.o抽出来合并进最终产物。.so则是运行时加载,你的库和 FFmpeg 库是两个独立文件,靠符号表在加载时解析。
选.a的典型理由:
- 分发简单,最终只有一个
libplayer.so,不用管 FFmpeg 的.so放哪、System.loadLibrary顺序对不对。 - 可以裁掉不用的模块,链接器只抽取被引用的目标文件,体积比整包
.so小。 - 避免多
.so之间的符号冲突和加载顺序问题。
代价也很直接:
- 链接命令变长,依赖顺序敏感,顺序错了就是
undefined reference。 - 每次改 FFmpeg 配置都要重新链接整个产物,增量编译收益低。
- 多个模块各自静态链接 FFmpeg 会导致符号重复,最终包体膨胀。
常见做法是:单个 App 内只有一处使用 FFmpeg,用.a;如果多个独立.so都要用,考虑抽一个公共.so出来,别各自静态链接。
2.2 目录结构与头文件、库文件的对应关系
解包后一般能看到类似这样的布局(不同打包者命名略有差异,以实际为准):
ffmpeg-armv8-a/ ├── include/ │ ├── libavcodec/ │ ├── libavformat/ │ ├── libavutil/ │ ├── libswscale/ │ └── libswresample/ └── lib/ ├── libavcodec.a ├── libavformat.a ├── libavutil.a ├── libswscale.a └── libswresample.ainclude下是各模块的公开头文件,lib下是对应的静态库。这里有个容易忽略的点:FFmpeg 各模块之间存在依赖方向,libavformat依赖libavcodec,libavcodec依赖libavutil,libswscale依赖libavutil。链接时被依赖的库要放在依赖者的右边,这是静态链接的基本规则,后面排错章节会反复用到。
先确认一下库的架构,别拿到 x86_64 的包往 arm64 工程里塞:
# 查看静态库中包含的目标文件架构信息 file lib/libavcodec.a # 更精确地看某个 .o 的 ELF 头 ar t lib/libavcodec.a | head -5 ar x lib/libavcodec.a $(ar t lib/libavcodec.a | head -1) file $(ar t lib/libavcodec.a | head -1)file输出里应该出现aarch64或ARM aarch64字样。如果看到x86-64,说明包拿错了,后面所有链接都会以架构不匹配告终。这一步花三十秒,能省掉后面半小时的困惑。
2.3 用 CMake 把 .a 接进 JNI 工程
Android 现在主流是 CMake,下面给一份能直接改改就用的CMakeLists.txt骨架。关键在IMPORTED目标声明和target_link_libraries的顺序。
cmake_minimum_required(VERSION 3.18) project(ffmpeg_demo LANGUAGES C CXX) # 指向解包后的 ffmpeg-armv8-a 目录 set(FFMPEG_ROOT ${CMAKE_SOURCE_DIR}/third_party/ffmpeg-armv8-a) # 声明导入的静态库目标,IMPORTED 表示不参与本工程编译 add_library(avcodec STATIC IMPORTED) set_target_properties(avcodec PROPERTIES IMPORTED_LOCATION ${FFMPEG_ROOT}/lib/libavcodec.a) add_library(avformat STATIC IMPORTED) set_target_properties(avformat PROPERTIES IMPORTED_LOCATION ${FFMPEG_ROOT}/lib/libavformat.a) add_library(avutil STATIC IMPORTED) set_target_properties(avutil PROPERTIES IMPORTED_LOCATION ${FFMPEG_ROOT}/lib/libavutil.a) add_library(swscale STATIC IMPORTED) set_target_properties(swscale PROPERTIES IMPORTED_LOCATION ${FFMPEG_ROOT}/lib/libswscale.a) add_library(swresample STATIC IMPORTED) set_target_properties(swresample PROPERTIES IMPORTED_LOCATION ${FFMPEG_ROOT}/lib/libswresample.a) add_library(native-lib SHARED native-lib.cpp) target_include_directories(native-lib PRIVATE ${FFMPEG_ROOT}/include) # 链接顺序:被依赖的放右边,avutil 永远最后 target_link_libraries(native-lib avformat avcodec swscale swresample avutil android log z # FFmpeg 常依赖 zlib,缺了会报 inflate 相关未定义 m)逻辑说明:add_library(... STATIC IMPORTED)告诉 CMake 这是一个外部已编译好的静态库,IMPORTED_LOCATION指向实际文件路径。target_link_libraries里的顺序不是随便写的——链接器从左到右扫描,遇到未解析符号会去右边的库里找,所以avformat在最左、avutil在最右。z和m是 FFmpeg 常见的系统依赖,zlib用于部分格式的解压,libm用于数学函数,漏掉它们会在链接末期冒出一批undefined reference to 'inflate'或'pow'。
参数说明:FFMPEG_ROOT建议用相对路径配合CMAKE_SOURCE_DIR,避免换机器后绝对路径失效。如果你的工程用externalNativeBuild配置,把这段放进src/main/cpp/CMakeLists.txt,并在build.gradle里指定abiFilters 'arm64-v8a',别让 Gradle 去编其他 ABI,否则 x86 那套会因为没有对应库直接失败。
3. 链接阶段的硬仗:undefined reference 的成因与逐条排查
静态库接入工程后,九成的失败都发生在链接阶段,而且报错信息往往指向一个你根本没直接调用的函数。这一章把最常见的几类问题拆开讲,每条都给出现象、原因和改法。
3.1 链接顺序错误:为什么把 avutil 放前面就炸
现象:链接时报undefined reference to 'av_malloc'、'av_free'之类,但你明明在target_link_libraries里写了avutil。
原因:静态链接器是单遍从左到右扫描的。假设顺序写成avutil avcodec avformat,链接器先扫avutil,此时还没有任何未解析符号(因为avcodec还没被扫到),于是avutil里被avcodec需要的那些.o不会被抽取;等扫到avcodec产生对av_malloc的引用时,avutil已经扫过去了,不会再回头。
解决:把顺序改成依赖者在前、被依赖者在后,即avformat avcodec swscale swresample avutil。如果确实无法调整顺序(比如某些构建系统自动排序),可以用链接器选项强制多遍扫描:
# 让链接器对静态库做多遍解析,代价是链接变慢 target_link_options(native-lib PRIVATE "-Wl,--start-group") target_link_libraries(native-lib avutil avcodec avformat) target_link_options(native-lib PRIVATE "-Wl,--end-group")--start-group和--end-group之间的库会被反复扫描直到符号稳定,这是兜底手段,正常情况还是把顺序写对更干净。
3.2 缺失系统依赖:libz、libm、liblog 的补链
现象:FFmpeg 自身的符号都解析了,但报undefined reference to 'inflateInit2_'、'log10'、'__android_log_print'。
原因:FFmpeg 编译时启用了 zlib 支持(很多封装格式需要),数学函数来自libm,Android 日志来自liblog。这些不会自动带上,得手动加。
解决:在target_link_libraries末尾补上z m log android。注意log和android是 NDK 提供的系统库,直接写名字即可,CMake 会从 NDK sysroot 里找。如果还报pthread相关,补pthread(新版 NDK 里 pthread 已并入 libc,通常不需要单独写)。
3.3 C++ 名称修饰与 extern "C":头文件包含方式导致的符号错位
现象:报错形如undefined reference to 'avcodec_open2(AVCodecContext*, AVCodec const*, AVDictionary**)',注意括号里带了参数类型——这是 C++ 修饰后的符号。
原因:FFmpeg 是 C 库,头文件里没有extern "C"包裹(或者你包含方式不对),在.cpp文件里直接#include <libavcodec/avcodec.h>会让编译器按 C++ 规则修饰函数名,而库里的符号是 C 的未修饰名,自然对不上。
解决:包含 FFmpeg 头文件时用extern "C"包起来:
extern "C" { #include <libavcodec/avcodec.h> #include <libavformat/avformat.h> #include <libavutil/avutil.h> }如果头文件本身已经带了extern "C"(新版 FFmpeg 部分头文件有),重复包裹也不会出错,但养成习惯更稳。另一个坑是混用:一个.cpp里包了,另一个没包,链接时一半符号对得上一半对不上,报错看着很随机,实际就是包含方式不一致。
3.4 重复符号与 LTO:多个模块各自静态链接的冲突
现象:链接报multiple definition of 'av_...',或者最终包体异常大。
原因:工程里有两个以上.so各自静态链接了同一份 FFmpeg.a,符号在最终合并时重复;或者开启了 LTO(链接时优化),静态库的.o与 LTO 的中间表示不兼容。
解决:优先收敛到单一模块使用 FFmpeg,其他模块通过 JNI 接口调用;确实需要多处使用,抽一个libffmpeg-wrapper.so出来共享。LTO 问题则在 CMake 里对导入库关闭 LTO:
set_target_properties(avcodec PROPERTIES INTERPROCEDURAL_OPTIMIZATION FALSE)对每个导入目标都设一遍,或者全局set(CMAKE_INTERPROCEDURAL_OPTIMIZATION FALSE)后再单独给需要 LTO 的目标开。
4. 避坑与排查:五条血泪经验
这一章单独拎出来,因为下面这几条是实际接入时最耗时间的,每条都按「现象 → 原因 → 解决」写清楚,遇到时直接对号入座。
4.1 现象:编译通过,运行时 dlopen 失败或直接崩溃
原因:.a静态链接进libnative-lib.so后,如果 FFmpeg 编译时带了某些运行时依赖(比如动态加载的编解码器、外部.so插件),运行时找不到。更常见的是 ABI 不匹配——工程编了armeabi-v7a,但包里只有arm64-v8a的库,Gradle 打包时静默跳过,运行时加载失败。
解决:build.gradle里显式限定abiFilters 'arm64-v8a',并在Application.mk或 CMake 里确认ANDROID_ABI一致。用readelf -d libnative-lib.so看NEEDED项,确认没有意外的动态依赖。
4.2 现象:undefined reference指向的函数在头文件里明明有声明
原因:声明在头文件里,但对应实现所在的.a没被链接,或者该功能在编译 FFmpeg 时被--disable-xxx裁掉了。预编译包常见的坑是裁剪了某些模块(比如--disable-encoders),头文件还在,符号没了。
解决:用nm查符号是否真的在库里:
# 查某个符号是否存在于静态库中,T 表示已定义文本符号 nm -C lib/libavcodec.a | grep avcodec_open2如果输出是U(未定义)或干脆没有,说明该功能没编进去,只能换包或自己重编。这一步能快速区分「链接问题」和「库本身缺功能」。
4.3 现象:链接时间暴涨,增量编译几乎失效
原因:静态库每次都要重新参与链接,FFmpeg 的.a体积不小,链接器要处理大量目标文件。加上--start-group多遍扫描会更慢。
解决:把 FFmpeg 相关目标用IMPORTED声明(不参与编译),只在最终链接时引入;开发阶段可以临时把不用的模块从target_link_libraries里去掉,比如只做解码就暂时不链swscale。另外确认没有误开-Wl,--whole-archive,那会把整个.a全量拉进来,体积和链接时间双爆。
4.4 现象:日志里avformat_open_input返回负数,但错误码查不到含义
原因:FFmpeg 的错误码是负值,直接打印数字没用,得用av_strerror转成可读信息。很多人第一次接 FFmpeg 时对着-2、-5发呆。
解决:统一封装错误打印:
extern "C" { #include <libavutil/error.h> } void log_ffmpeg_error(int ret) { char buf[AV_ERROR_MAX_STRING_SIZE] = {0}; av_strerror(ret, buf, sizeof(buf)); __android_log_print(ANDROID_LOG_ERROR, "FFmpeg", "err=%d msg=%s", ret, buf); }AV_ERROR_MAX_STRING_SIZE是 FFmpeg 定义的安全缓冲区大小,别自己拍一个长度。所有返回int的 FFmpeg 接口都该走这个封装,排错效率差好几倍。
4.5 现象:换 NDK 版本后原本能编的工程突然报汇编错误
原因:FFmpeg 的汇编优化(NEON)对汇编器和 NDK 工具链版本敏感,预编译包是用某个 NDK 版本编的,换到差异较大的 NDK 后,链接阶段可能因为.o里的重定位或指令集特性不兼容而失败。
解决:锁定 NDK 版本,在build.gradle里写死ndkVersion "...",团队内统一。如果必须升级 NDK,优先找对应版本重编的 FFmpeg 包,别硬链。这条没有捷径,属于工具链层面的硬约束。
5. 验证与进阶:用最小解码回路确认库真的可用
库链上了不代表能用,得跑一个最小回路验证。最直接的方式是写一个只做「打开输入 → 找到视频流 → 读一帧 → 释放」的 JNI 函数,把关键返回值都打出来。下面这段是骨架,重点看错误处理和资源释放顺序。
extern "C" { #include <libavformat/avformat.h> #include <libavcodec/avcodec.h> } extern "C" JNIEXPORT jint JNICALL Java_com_example_ffmpegdemo_NativeProbe_probe(JNIEnv *env, jobject, jstring path) { const char *url = env->GetStringUTFChars(path, nullptr); AVFormatContext *fmt_ctx = nullptr; // 打开输入,失败直接返回错误码 int ret = avformat_open_input(&fmt_ctx, url, nullptr, nullptr); if (ret < 0) { log_ffmpeg_error(ret); env->ReleaseStringUTFChars(path, url); return ret; } // 探测流信息,这一步不做的话 stream 信息是空的 ret = avformat_find_stream_info(fmt_ctx, nullptr); if (ret < 0) { log_ffmpeg_error(ret); avformat_close_input(&fmt_ctx); env->ReleaseStringUTFChars(path, url); return ret; } int video_idx = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0); if (video_idx < 0) { log_ffmpeg_error(video_idx); avformat_close_input(&fmt_ctx); env->ReleaseStringUTFChars(path, url); return video_idx; } AVStream *st = fmt_ctx->streams[video_idx]; __android_log_print(ANDROID_LOG_INFO, "FFmpeg", "codec=%s w=%d h=%d", avcodec_get_name(st->codecpar->codec_id), st->codecpar->width, st->codecpar->height); avformat_close_input(&fmt_ctx); env->ReleaseStringUTFChars(path, url); return 0; }逻辑说明:avformat_open_input负责打开容器并初始化AVFormatContext,avformat_find_stream_info补齐流参数(不调用的话codecpar里的宽高、采样率可能是 0),av_find_best_stream挑出最佳视频流。每一步失败都要先释放已分配资源再返回,avformat_close_input会同时释放fmt_ctx并置空,这是 FFmpeg 的标准释放方式。参数上,av_find_best_stream的wanted_stream_nb传 -1 表示自动选择,related_stream传 -1 表示不关联其他流。
验证通过的标准:日志里能打出正确的编码器名和分辨率,且连续调用多次不崩溃、内存不持续增长。用adb logcat过滤FFmpeg标签观察,再用dumpsys meminfo看进程内存曲线,跑一百次探测看是否稳定。
进阶一点的做法是把这套探测逻辑封装成一个ProbeResult结构体回传 Java 层,避免每次都在 JNI 里拼字符串。另外,如果后续要做解码,avcodec_find_decoder和avcodec_open2的返回值同样要走av_strerror,别偷懒直接if (ret < 0) return,否则线上出问题时你手里只有一个负数。
从那以后我每次接新的预编译 FFmpeg 包,都强制先跑一遍nm查关键符号、再跑一遍最小探测回路,两步都过了才往业务代码里接。这套习惯帮我挡掉过好几次「库是残的」和「ABI 拿错了」的翻车。希望帮到你。
本文还有配套的精品资源,点击获取