Android集成FFmpeg:预编译ARMv8-A库的避坑指南 简介这是一份面向Android平台ARMv864位架构预编译的FFmpeg多媒体处理库集成了avformat容器解析、avcodec音视频编解码、avfilter过滤器、swresample音频重采样、swscale色彩空间转换、avutil基础工具等核心组件以.a静态库和.so动态库形式提供。集成时只需将.so放入应用jniLibs目录或通过NDK链接.a静态库即可在arm64设备上调用FFmpeg能力省去从源码自行交叉编译的繁琐流程与版本兼容调试成本。压缩包共162个文件包含111个.h头文件、23个.c示例源码、6个.a静态库、6个.so动态库另有.pc链接配置、ffpreset编码预设及makefile等辅助文件整体14.35MB已有606人学习下载。其中.c示例覆盖transcode_aac、transcoding、muxing、demuxing_decoding等典型场景可直接对照学习转码、封装、解封装与解码流程配合头文件与库文件适合有一定Android NDK基础、希望在项目中快速接入FFmpeg的中高级开发者使用也可为自行裁剪编译FFmpeg提供结构参考。1. 预编译 ffmpeg-armv8-a.zip跳过 3 小时编译直接拿到能用的 FFmpeg 库做 Android 音视频开发最耗耐心的不是写逻辑而是编译 FFmpeg。先要装 NDK、改 configure、交叉编译再等上半小时才出库中间冒出来的报错能让人刷两个小时的论坛。ffmpeg-armv8-a.zip 这类预编译包省掉的就是这段折磨——里面已经编好适用于 ARMv8-A也就是 64 位 ARM架构的 FFmpeg 库同时提供.a静态库和.so动态库拿过来直接链接进 CMake 或 Android.mk 工程。它适合三类人急着出 Demo 的、不想维护编译脚本的、还有刚进 NDK 新手村的。它解决的核心问题是把 FFmpeg 从一坨需要交叉编译的源码变成可以放进 IDE 里直接引用的依赖。2. 拆开压缩包.a、.so 与 armv8-a 里的 ABI 门道2.1 .a 和 .so一个链接期绑定一个运行期加载先搞清楚这两个后缀后面链接时才能选对。静态库.a本质是一个 ar 归档文件里面是一堆.o目标文件链接时会被完整地拷贝进最终生成的.so里。用静态库你的 JNI 库会变大但运行期不依赖外部文件部署简单调试时符号都在崩了能直接看到 FFmpeg 内部的函数名。动态库.so则相反编译时只在.so里记一个符号引用运行期由系统加载器把真正的.so文件映射进来。好处是多个模块可以共享同一个库更新库文件不需要重编整个 app坏处是加载阶段如果找不到依赖库或者 ABI 不对直接 dlopen 失败。选型上我一般按这个逻辑判断如果只有一个 JNI 模块且不想每次发版都盯着 APK 里有没有混入多余的.so那就选.a。链接后所有 FFmpeg 符号都合入你自己的libxxx.so最终产物体积大一点但交付省心。如果是插件架构或者同一份 FFmpeg 要被多个.so模块共享那必须用动态库否则每个模块各拷一份静态库代码段翻倍内存也翻倍。还有一种常见误用以为.so可以任意放在 assets 里再解压出来用。Android 的dlopen有严格路径限制自己手动解压到应用私有目录再加载是能跑的但要先按依赖顺序加载所有相关的.so否则符号解析必然失败这个坑我们放到第 5 章细说。静态库和动态库的另一个差异体现在链接时报错的位置.a的符号错误在链接期直接暴露.so的符号错误会滞后到运行期。很多新手遇到.so的 undefined symbol 会一脸懵其实换用.a反而更容易排查。2.2 包内必备三件套库文件、头文件、版本信息打开 ffmpeg-armv8-a.zip一个结构完整的包通常长这样include/ libavcodec/avcodec.h libavformat/avformat.h libavutil/avutil.h libswresample/swresample.h libswscale/swscale.h lib/ libavcodec.a libavcodec.so libavformat.a libavformat.so libavutil.a libavutil.so libswscale.a libswscale.so libswresample.a libswresample.so pkgconfig/ libavcodec.pc libavformat.pc头文件是编译期的契约。你在 JNI 代码里写#include libavcodec/avcodec.h编译器和链接器靠这份头文件认识 FFmpeg 的函数、结构体、宏定义。没有头文件就只能自己手写一堆 extern 声明那很容易对齐错参数类型。pkgconfig目录给 CMake 的FindPkgConfig用如果你打算手写 CMake可以跳过它。版本信息没几个人会看但它是第一个能救场的线索。用grep LIBAVCODEC_VERSION include/libavcodec/version.h查出版本号再和库文件比对。FFmpeg 的 API 破坏性变更不少4.x 和 5.x 之间很多函数签名都变了。最典型的例子av_register_all()在 4.0 里被移除如果头文件来自新版而库来自旧版你调用这个函数时链接直接报 undefined reference反过来旧头文件配新库调用某个新扩展函数时也缺符号。所以拿到包先看一眼版本能避掉一半的玄学问题。2.3 armv8-a 与 arm64-v8aABI 必须精确匹配armv8-a 是 ARM 的 64 位指令集架构Android 生态里对应的 ABI 叫arm64-v8a。一个预编译包只服务一个 ABI这是基础认知。但项目里常常同时支持多 ABI比如默认的abiFilters包含armeabi-v7a和arm64-v8a。armeabi-v7a 对应 ARMv7指令集和寄存器宽度完全不同v7 的.so不能直接在 v8 上跑而 v8 的库也不能放进 v7 的目录。最常见的错误是把 armv8-a 的库放进src/main/jniLibs/arm64-v8a/但 gradle 里没加ndk.abiFilters arm64-v8a导致构建时把所有 ABI 都打进了包安装到 v7 设备上时试图加载不匹配的库直接dlopen failed: libavcodec.so is for EM_AARCH64。所以项目里如果只有这一套 arm64 库一定要在build.gradle里限制 ABIandroid { defaultConfig { ndk { abiFilters arm64-v8a } } }还要注意模拟器。x86 的模拟器没法加载 arm64 的.so必须在 arm64 镜像上跑或者干脆用真机。很多人报模拟器上闪退基本都是这个原因。有一种更隐蔽的情况某些云真机平台提供的高通设备是 64 位系统但 app 进程跑在 32 位兼容模式下这时System.loadLibrary会找arm64-v8a还是armeabi-v7a取决于 APK 打包时选了哪个 ABI不是你设备能不能跑 arm64 说了算。3. 集成到 CMake 工程把 .a 静态库链接进 JNI 层的完整示例3.1 目录放置与 CMakeLists.txt 写法把下载的压缩包解压后我习惯把它放到模块目录下的third_party/ffmpeg-armv8-a/。这样 CMake 用相对路径就能引用不需要改全局环境变量换机器也能直接构建。最终目录是这样的app/src/main/cpp/ CMakeLists.txt native-lib.cpp third_party/ffmpeg-armv8-a/ include/ lib/CMakeLists.txt 的核心写法如下cmake_minimum_required(VERSION 3.18.1) project(videotool) # 预编译库所在目录 set(FFMPEG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/third_party/ffmpeg-armv8-a) # 声明引入的静态库模块 add_library(avcodec STATIC IMPORTED) set_target_properties(avcodec PROPERTIES IMPORTED_LOCATION ${FFMPEG_DIR}/lib/libavcodec.a) add_library(avformat STATIC IMPORTED) set_target_properties(avformat PROPERTIES IMPORTED_LOCATION ${FFMPEG_DIR}/lib/libavformat.a) add_library(avutil STATIC IMPORTED) set_target_properties(avutil PROPERTIES IMPORTED_LOCATION ${FFMPEG_DIR}/lib/libavutil.a) add_library(swscale STATIC IMPORTED) set_target_properties(swscale PROPERTIES IMPORTED_LOCATION ${FFMPEG_DIR}/lib/libswscale.a) add_library(swresample STATIC IMPORTED) set_target_properties(swresample PROPERTIES IMPORTED_LOCATION ${FFMPEG_DIR}/lib/libswresample.a) # 自己的 JNI 模块 add_library(videotool SHARED native-lib.cpp) # 头文件搜索路径 target_include_directories(videotool PRIVATE ${FFMPEG_DIR}/include) # 链接顺序依赖者在前被依赖者在后 target_link_libraries(videotool avformat avcodec swscale swresample avutil z m)IMPORTED_LOCATION明确告诉 CMake 这个库文件的实际路径STATIC和SHARED决定链接期还是运行期绑定。这里最关键的参数是target_include_directories它让编译器能找到libavcodec/avcodec.h。缺了这一句编译时一定会报cannot find include libavcodec/avcodec.h。3.2 链接顺序为什么依赖库要排在右边静态库的链接顺序是第一部翻车点。ld 的链接器是一个单遍扫描器它从左到右读取库文件每次记录下当前未解析的符号。当处理到avformat时如果它引用了avcodec里的符号而avcodec已经在左边处理完了那这些符号到链接结束时仍然是未定义状态于是报undefined reference to avformat_open_input。经验做法是把被依赖的库放到右边。FFmpeg 的依赖链大体是avformat - avcodec - avutil、swscale - avutil、swresample - avutil所以顺序固定为 avformat、avcodec、swscale、swresample、avutil最后再放z压缩库和m数学库。如果你多加了一个 libavfilter那它应该插在 avformat 之前因为它是所有模块里依赖最多的。一个很常见的困惑是把库顺序换一下有时候也能过因为链接器在某个条件下会做多轮扫描。但依赖顺序仍然是个好习惯至少不会偶尔过、偶尔不过。还有如果库文件是.a但你在add_library那里写了SHARED IMPORTEDCMake 会认为这是动态库生成正确链接但运行期找不到.so这种错也值得记一下。3.3 JNI 侧的 C/C 互操作与初始化JNI 代码通常是 C 写的而 FFmpeg 是纯 C 库头文件声明都在 extern C 保护下。如果你不在自己的源码里也包一层C 编译器会把函数名做 name mangling链接器拿 mangled 后的符号去库里找自然找不到。正确写法extern C { #include libavcodec/avcodec.h #include libavformat/avformat.h }这里还有一个历史坑FFmpeg 3.x 时代初始化必须调用av_register_all()来注册所有编解码器和封装器4.0 之后这个函数被移除库在内部自动按需注册。如果你用的预编译包是 4.0 以上版本却还在代码里写了av_register_all编译期直接报use of undeclared identifier。如果非要兼容新旧版本可以用宏判断#if LIBAVCODEC_VERSION_INT AV_VERSION_INT(58, 7, 0) av_register_all(); #endif但这个宏依赖头文件版本如果你头文件是新的直接删掉这行就好了。真正调库的最小示例extern C JNIEXPORT jstring JNICALL Java_com_example_videotool_MainActivity_getFfmpegConfig(JNIEnv *env, jobject) { const char *config avcodec_configuration(); return env-NewStringUTF(config); }avcodec_configuration()返回的是编译时传给 configure 的参数串见它就像看见了库的“体检报告”。你可以在里面确认有没有--enable-decoderh264、有没有开启 mediacodec 硬解、编解码器列表是否完整。这一步建议放在第一个自检函数里它能直接告诉我们这个包够不够用。4. 用 Android.mk 接动态库免重编 App 的共享库方案4.1 用 ndk-build 管理预编译 .so老项目里还有不少在用 ndk-build它的写法跟 CMake 完全两套。Android.mk 里引入一个预编译动态库需要先声明一个PREBUILT_SHARED_LIBRARY模块再让 JNI 模块去引用它LOCAL_PATH : $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE : avcodec LOCAL_SRC_FILES : prebuilt/libavcodec.so LOCAL_EXPORT_C_INCLUDES : $(LOCAL_PATH)/third_party/ffmpeg-armv8-a/include include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE : avformat LOCAL_SRC_FILES : prebuilt/libavformat.so LOCAL_EXPORT_C_INCLUDES : $(LOCAL_PATH)/third_party/ffmpeg-armv8-a/include include $(PREBUILT_SHARED_LIBRARY)然后是 JNI 模块本身include $(CLEAR_VARS) LOCAL_MODULE : videotool LOCAL_SRC_FILES : native-lib.cpp LOCAL_SHARED_LIBRARIES : avcodec avformat avutil swscale swresample include $(BUILD_SHARED_LIBRARY)注意prebuilt/目录是相对于LOCAL_PATH的所以库文件要先复制到prebuilt/下或者直接把LOCAL_SRC_FILES写成相对路径。LOCAL_SHARED_LIBRARIES里列出的模块名必须在前面声明过否则ndk-build会报unknown module。这个机制比较绕但好处是 Android.mk 会自动处理库之间的依赖顺序不需要你手动排。4.2 .so 打进 APK 的路径规则与常见遗漏用 ndk-build 构建出来的.so会被放在libs/arm64-v8a/下但 Android Studio 的 gradle 构建系统不认这个路径。常见做法是把生成的libvideotool.so拷到app/src/main/jniLibs/arm64-v8a/下同时把 FFmpeg 的.so也全部放进去否则 APK 装到真机会报java.lang.UnsatisfiedLinkError: dlopen failed: library libavcodec.so not found这个错误表面上是找不到某个库实际是libvideotool.so里记录了DT_NEEDED libavcodec.so但系统在 APK 的 lib 目录里搜索时没找到。遇到过有些人只拷了自己的libvideotool.soFFmpeg 的库还在libs/目录躺着构建时没有打包进去装到真机就翻车。如果使用 gradle 的外部构建通道可以在build.gradle里指定 jniLibs 来源sourceSets { main { jniLibs.srcDirs src/main/jniLibs } }这样只要把.so带目录丢进jniLibs/arm64-v8a/打包就自动完成。要额外确认 APK 里的路径用unzip -p app-debug.apk lib/arm64-v8a/libavcodec.so查看是否存在。4.3 动态库依赖链与手动 dlopen 的加载顺序FFmpeg 的各个.so之间存在复杂的依赖关系libavformat.so的DT_NEEDED里会有libavcodec.so和libavutil.solibavcodec.so又依赖libavutil.so。当你通过System.loadLibrary(videotool)加载时系统加载器会被动解析这些依赖并自动加载同目录下的库这是最常见的正常路径。但如果你在 C/C 里自己dlopen一个不在 APK 库目录下的.so就要手动控制加载顺序void *util dlopen(/data/local/tmp/libavutil.so, RTLD_NOW | RTLD_GLOBAL); if (util nullptr) { // 报错 } void *codec dlopen(/data/local/tmp/libavcodec.so, RTLD_NOW);第一次加载libavutil.so时用了RTLD_GLOBAL可以让它的符号进入全局符号表后续加载的libavcodec.so才能解析到av_malloc等基础符号。如果第二次dlopen后dlerror()返回undefined symbol: av_malloc就是前面的库没有全局导出。换到RTLD_GLOBAL通常就好了。这种场景常见于把 FFmpeg 库放在 assets 里需要热更的情况必须自己维护加载顺序依赖链每多一层手动加载的库就多一个。5. 避坑与常见问题从链接失败到运行崩溃的五种翻车现场5.1 链接报 undefined reference头文件与库版本错位现象CMake 或者 ndk-build 报一堆undefined reference to avcodec_find_decoder但libavcodec.a文件明明存在。原因最常见是头文件声明了库文件里没有的符号。FFmpeg 版本升级时会删除或改名函数如果头文件是新的库是旧的或者反过来都会出现这种错位。另一个原因是 C 名字改编忘了extern C。解决先用nm -g libavcodec.a | grep avcodec_find_decoder看库里到底有没有这个符号。如果库里确实没有去换头文件或换库不要硬改代码。如果库里有检查#include是否被包在extern C里。我一般还会顺手看一眼pkgconfig里的版本声明跟头文件version.h对上号。5.2 运行期崩溃av_register_all 是过去式现象app 一运行到某个初始化函数就直接 SIGABRTlogcat 里能看到Fatal signal 6 (SIGABRT)有时还会打印assertion codec-init failed。原因代码里调用了av_register_all()。FFmpeg 4.0 起这个函数被移除库的组件注册逻辑改成按需自动注册符号已经不存在了但头文件可能是旧版本编译侥幸通过运行期才炸。解决删掉对av_register_all的调用也不要用宏去兼容旧库。如果是从旧项目移植过来的全局搜索这个函数名包括第三方库的源码确保没有残留。新版初始化只需要确保avformat_open_input和avcodec_find_decoder在合适的时机被调用不再需要手动注册整个系统。5.3 dlopen failed: cannot locate symbol依赖库没有先加载或用了 RTLD_LOCAL现象System.loadLibrary(videotool)抛UnsatisfiedLinkError: dlopen failed: cannot locate symbol av_read_frame但 FFmpeg 的.so全部在 jniLibs 里存在。原因libvideotool.so和libavformat.so之间存在符号依赖但系统加载时没有自动找到某个依赖或者你的 JNI 库是用-Wl,--no-undefined编译的却缺少了显式的链接库声明。在手动dlopen的场景下还可能是第一个库用了RTLD_LOCAL导致第二个库看不到第一个库的符号。解决如果是System.loadLibrary路径确认所有.so都在jniLibs/arm64-v8a/下并且build.gradle指定了abiFilters arm64-v8a。如果是手动dlopen把前置依赖库全部用dlopen(..., RTLD_NOW | RTLD_GLOBAL)加载一遍。另外Android.mk 里链接动态库时不要加LOCAL_ALLOW_UNDEFINED_SYMBOLS : true那会掩盖依赖缺失直到运行期才爆。5.4 解码 H.264 报 codec not found预编译包编进去了哪些功能你不清楚现象avcodec_find_decoder(AV_CODEC_ID_H264)返回 null或者avformat_open_input打开 MP4 文件时报could not find codec parameters。原因预编译包在 configure 阶段没有启用--enable-decoderh264或者为了修整体积把一些 decoders 裁掉了。更多时候是硬解问题想要 Android 硬件解码mediacodec编译时必须--enable-jni --enable-mediacodec很多通用预编译包默认只带软解。解决调用avcodec_configuration()打印完整编译参数看有没有--enable-decoderh264和--enable-mediacodec。如果没有先检查这是不是包体本身的限制——拿一个标准的 h264 mp4 文件直接命令行走一遍解码测试能立刻暴露。若确实没有要么找带齐全功能的版本要么自己在本地编译时加上对应开关。跑通后记得把配置参数存到代码注释里方便下次换包时对比。5.5 低版本设备闪退NDK API level 与预编译库不匹配现象同一套代码在 Android 12 上跑得很欢在 Android 6.0 上启动就崩logcat 报dlopen failed: cannot locate symbol __register_atfork或SIGSYS。原因预编译库如果用了较新 NDK 编译会引用新版本 Bionic 里才有的 libc 符号。系统版本旧__register_atfork不存在动态链接失败。模拟器上很少复现因为模拟器镜像通常较新。解决无能为力的时候只能换更保守的预编译版本或者让编译方用--sysroot指向低版本 API 的 NDK 重新编一次。自己接库的话LOCAL_SDK_VERSION至少设为 21并且Application.mk里控制APP_PLATFORM : android-21。如果是下载的预编译包优先找注明编译 API level 的版本。这条经验让我在选库时养成了先问一句“这个包用哪个 NDK 版本编的”的习惯——它比任何代码审查都能提前暴露一批兼容性问题。6. 拿到库先做三件验证readelf、命令行和启动日志6.1 用 readelf 检查 ABI 和动态依赖解压后不要急着拖进工程先在命令行做一次“验尸”。对.so文件执行readelf -A libavcodec.so输出里会有Tag_CPU_arch: ARMv8这样的标签确认它确实是为 armv8-a 构建的。再看动态依赖readelf -d libavformat.so | grep NEEDED能看到libavutil.so、libavcodec.so等依赖项这些依赖在运行时必须都能找到所以打包时不能让任何一个缺位。顺带用size libavcodec.a或ls -lh看一下库的大小通常带全功能的 FFmpeg 不会很小如果libavcodec.a只有几 MB就要怀疑它裁剪掉了哪些组件。6.2 在 JNI 初始化阶段做自检把验证写在代码里而不是人肉测试是最省心的习惯。我在native-lib.cpp里固定放一个自检函数extern C JNIEXPORT jboolean JNICALL Java_com_example_videotool_MainActivity_verifyFfmpeg(JNIEnv *env, jobject) { // 检查核心解码器是否存在 const AVCodec *h264 avcodec_find_decoder(AV_CODEC_ID_H264); if (h264 nullptr) return JNI_FALSE; // 检查网络模块如果需要 if (avformat_network_init() 0) return JNI_FALSE; // 检查能否打开 demo 文件可替换为你自己的资源路径 AVFormatContext *fmt_ctx nullptr; if (avformat_open_input(fmt_ctx, sdcard/Download/demo.mp4, nullptr, nullptr) 0) { return JNI_FALSE; } avformat_close_input(fmt_ctx); return JNI_TRUE; }avformat_open_input失败时不会只看布尔值最好把错误缓冲传进去打日志char errbuf[AV_ERROR_MAX_STRING_SIZE] {0}; av_strerror(ret, errbuf, sizeof(errbuf)); __android_log_print(ANDROID_LOG_ERROR, FFmpegCheck, open failed: %s, errbuf);这一步能挡住 90% 的“资源白下”问题解码器缺失、文件路径不支持、库版本不兼容全部会在这一层暴露。老设备上还要额外跑一次avcodec_find_decoder循环把支持的所有 codec ID 打印出来留档。6.3 用命令行兜底ffmpeg 自带的解码自测有些压缩包会附带编译好的ffmpeg可执行文件如果没带可以用 JNI 调用代替。有命令行工具时把设备连上 USB 调试执行adb push demo.mp4 /data/local/tmp/ adb shell /data/local/tmp/ffmpeg -i /data/local/tmp/demo.mp4 -c copy -f null -如果看到frame ... time00:00:01.00这样的输出说明 demux 和 mux 链路正常。如果报Unknown decoder h264说明这个包没编 h264 解码器要回头查 configure 参数。没有命令行工具时就靠 6.2 的avcodec_find_decoder配合真实文件验证效果等价。从那以后我每拿一个预编译 FFmpeg 包都强制把 6.1 到 6.3 完整走一遍——先验 ABI再验符号最后用真实视频过一遍流程。整个过程不到五分钟但每次都能在集成前发现一个潜在问题。希望帮到你。本文还有配套的精品资源点击获取