1. 整体思路:为什么专门写一篇“引入so”的文章
先说个真实感受,很多人在接触FFmpeg时,第一步就卡死在“编译”上了。网上搜到的教程动不动就让你下载NDK、配置交叉编译环境、自己跑脚本编译出全平台的so文件,新手光是配环境就能折腾两天,最后还可能因为网络问题、版本不匹配、路径错误等原因直接放弃。但实际上,项目里真正需要引入FFmpeg时,绝大多数场景根本不需要从源码编译——你只需要拿到编译好的.so文件,放到Android工程里正确调用就行。这就是我今天想聊的核心:在Android项目里引入FFmpeg的.so文件,从拿到so到成功调用的完整链路。
这个操作解决了什么问题呢?说白了,就是把FFmpeg这个强大的音视频处理库,变成你Android应用里一个可以被Java/Kotlin调用的能力模块。最常见的应用场景包括:自定义视频封面截取、音视频格式转换、视频裁剪拼接、直播推流前的音视频编码处理、甚至是在线视频播放时对直播流的特殊处理等等。市面上很多播放器、剪辑工具、看片软件,底层都是靠FFmpeg在干活。
这篇文章适合谁看?两类人:第一类是已经在Android开发上有一两年经验、想在多媒体方向深入的技术同学;第二类是产品里明确有音视频处理需求、但被“编译FFmpeg”这个门槛拦住的后端或跨端开发者。我不写怎么从零编译FFmpeg,因为那是另一套复杂工程,我写的是:当你手上已经有一套so文件(不管是团队里同事编译的、还是第三方渠道获得的),如何把它干净利落地集成进Android工程并且稳定跑起来。
顺便提一句,标题里“引入so文件”这件事,看起来简单,实际上有很多细节坑点,比如ABI匹配、JNI函数命名规则、CMakeLists配置路径、动态库依赖顺序、ffmpeg内部初始化逻辑等等。任何一个环节出错,都会在运行时报出各种诡异的错误。我会把我的实操经历、踩过的坑和排查步骤都写出来,尽量让你少走弯路。
2. 核心细节解析:搞清楚so文件的结构和引入前提
2.1 FFmpeg的so文件到底是什么
很多没深入接触过NDK开发的Android开发者,对.so文件的认知停留在“我知道它是C/C++编译出来的库,但具体怎么用不太清楚”。先把这个概念梳理清楚。
FFmpeg本身是一个庞大的C项目,核心模块包括:
- libavcodec:编解码库,处理各种音视频编码格式的编解码;
- libavformat:封装格式库,负责读音频、视频的容器格式,比如MP4、MKV、FLV这种“箱子”;
- libavfilter:滤镜库,负责做画面缩放、裁剪、旋转、水印等操作;
- libavutil:工具库,相当于公共基础库,其他库都会依赖它;
- libswscale:图像缩放、像素格式转换库;
- libswresample:音频采样率转换、重采样库。
你拿到的FFmpeg.so文件,通常就是以上这些库分别编译出的动态库,另外再加上一个ffmpeg可执行程序对应功能的封装库(有时候会合并、有时候是分离的)。引入到Android工程后,它们会被打包进APK,应用运行时通过JNI去调用这些库里的C函数。
这里有个很关键的细节:多个so之间存在符号依赖关系。比如libavformat可能会调用libavcodec里的符号,libavcodec又依赖libavutil。当Android系统加载这些库时,必须按依赖顺序加载,否则就会出现“undefined symbol”类型的报错。这也是很多新手引入后一运行就崩溃的核心原因之一。
2.2 引入so的两种方式:你有哪套文件
我在实际项目中见过两种“引入.so”的形态,你八成也会遇到其中一种。
第一种:团队内部编译好了一套完整的FFmpeg so文件,包含多个模块库,你只需要把它们拷贝进工程,配置一下CMakeLists.txt,然后通过JNI调用。
第二种:第三方SDK只给你一个封装好的so,比如一个叫libffmpeg_lib.so的文件,里面已经把FFmpeg核心库和JNI封装层合并在一起了。这种情况下你需要做的只是拷贝so、写一个配套的native接口类、加载System.loadLibrary("ffmpeg_lib"),甚至都不需要自己写CMake配置。
两者的区别在于:第一种是“裸的FFmpeg”,所有底层能力暴露出来,你需要自己写JNI层去映射、转码、回调;第二种是“封装过的FFmpeg”,对外接口通常只有几个Java方法,调用起来非常省事,但灵活性大幅下降。
从做项目的角度,我更建议你优先拿第一种完整so库。虽然刚开始写JNI层会麻烦一些,但它的灵活性在你后面加需求时(比如新增音频滤镜、自定义RTMP推流逻辑)会体现出巨大价值。封装过的SDK一旦需要调底层能力,往往会卡在“缺少接口”这个死路上。
2.3 ABI匹配问题:so不是随便拷贝就能用
这是我在几次项目中反复提醒自己的一件事,也是新手最容易忽视的问题。
ABI(Application Binary Interface)决定了CPU如何执行二进制程序。Android的常见ABI有arm64-v8a、armeabi-v7a、x86、x86_64。其中arm64-v8a对应的是64位ARM架构处理器,现在几乎所有主流手机都是这个;armeabi-v7a是32位ARM架构,一些老设备、低端机或者特殊硬件还在用;x86/x86_64主要对应的是模拟器。
FFmpeg编译完成后,针对不同ABI产出的so文件是完全不同的二进制。你必须在工程里为每个ABI目录放入对应架构的.so文件。如果一个arm64-v8a的设备加载了一个armeabi-v7a的库,轻则直接用不了,重则直接崩溃闪退。
我习惯的做法是,在项目里创建jniLibs/arm64-v8a、jniLibs/armeabi-v7a两个目录,只放这两种主流Android真机架构的so。至于x86架构,如果团队没有实际在模拟器上跑多媒体的需求,我会选择忽略。注意:如果你某天突然要上模拟器调试,还得补上x86的so,因为模拟器默认是x86架构,没有对应so就会加载失败。
这里还有一个坑:如果工程里同时存在多个模块,而每个模块里有不同ABI的so,Android打包时默认的ABI策略可能会导致APK体积猛增。所以在build.gradle里,通常建议用abiFilters指定只打包你需要的ABI类型,而不是全部丢进去。
3. 实操过程:从零开始把FFmpeg so接进Android工程
3.1 准备工作:确定so文件清单和项目结构
假设我现在拿到了一套团队内编译好的FFmpeg so,文件列表是:
- libavcodec.so
- libavformat.so
- libavfilter.so
- libavutil.so
- libswresample.so
- libswscale.so
我先在Android工程里建好目录结构。通常做法是在 app/src/main/ 下创建 jniLibs 文件夹,再按ABI分好子目录:
app/ ├── src/main/ │ ├── java/com/example/ffmpegdemo/ │ ├── jniLibs/ │ │ ├── arm64-v8a/ │ │ │ ├── libavcodec.so │ │ │ ├── libavformat.so │ │ │ ├── libavfilter.so │ │ │ ├── libavutil.so │ │ │ ├── libswresample.so │ │ │ └── libswscale.so │ │ └── armeabi-v7a/ │ │ ├── libavcodec.so │ │ ├── libavformat.so │ │ ├── libavfilter.so │ │ ├── libavutil.so │ │ ├── libswresample.so │ │ └── libswscale.so如果你手里的so是别人通过AAR或者Module形式提供的,那就不需要自己维护jniLibs目录了,依赖引入时库内部会自己带so文件。但如果是直接给的so文件包,手动放进jniLibs就是最直观的方式。
3.2 手写JNI封装层:让Java能调C库
光有so文件还不够,Java层没法直接调用C函数。你需要写一个JNI(Java Native Interface)桥梁,让Java方法能映射到C函数上。这个过程说复杂也复杂,说简单也简单,我拆解一下核心步骤。
第一步,写一个Java类,声明native方法:
public class FFmpegNative { static { // 注意加载顺序,libavutil是关键,优先于其他库 System.loadLibrary("avutil"); System.loadLibrary("avcodec"); System.loadLibrary("avformat"); System.loadLibrary("avfilter"); System.loadLibrary("swscale"); System.loadLibrary("swresample"); System.loadLibrary("ffmpeg_wrapper"); } public native int run(String[] argv); }这里有个重要的经验点:System.loadLibrary的加载顺序,按依赖关系从底层向上层逐个加载。libavutil最先,其次libavcodec和libavformat,swscale和swresample可以放在后面,自定义封装库最后加载。如果乱序加载,比如先loadLibrary("avformat")再loadLibrary("avutil"),在Android上通常是ok的,因为链接器会在全局符号表里找依赖,但某些精简过的so或使用特定编译参数时可能会报符号找不到,所以还是老老实实按依赖顺序写。
第二步,在C/C++层写对应的JNI函数实现。这里我用的是NDK开发里最常见的做法——写一个包装C文件,编译出libffmpeg_wrapper.so(如果你打算直接把JNI逻辑编进so里,就不需要这一步,直接把JNI实现和ffmpeg一起编译即可)。
wrapper的JNI函数实现大致是这个样子:
#include <jni.h> #include <string.h> extern "C" int run_ffmpeg(int argc, char **argv); JNIEXPORT jint JNICALL Java_com_example_ffmpegdemo_FFmpegNative_run( JNIEnv *env, jobject thiz, jobjectArray argv_array) { int argc = env->GetArrayLength(argv_array); char **argv = (char **) calloc(argc, sizeof(char *)); for (int i = 0; i < argc; i++) { jstring string = (jstring) env->GetObjectArrayElement(argv_array, i); const char *tmp = env->GetStringUTFChars(string, 0); argv[i] = strdup(tmp); env->ReleaseStringUTFChars(string, tmp); } int result = run_ffmpeg(argc, argv); for (int i = 0; i < argc; i++) { free(argv[i]); } free(argv); return result; }这段代码做的事情很简单:把Java传过来的String数组转换成C语言里的char**,调用FFmpeg的入口函数,最后把内存释放掉。看起来简单,但有一个很容易出错的点:给FFmpeg传参时,argv[0]通常应该传一个固定字符串,哪怕是个空字符串也行。因为FFmpeg内部部分逻辑会读取argv[0]来判断程序名,从而影响某些处理路径。很多人第一次写的时候,直接把真正的参数从argv[0]开始传,结果行为异常,排查半天才反应过来。
第三步,配置CMakeLists.txt,把wrapper源码和FFmpeg头文件、库文件链接在一起。
cmake_minimum_required(VERSION 3.18.1) project("ffmpegdemo") add_library( ffmpeg_wrapper SHARED native-lib.cpp ) # 指定FFmpeg的头文件路径 set(FFMPEG_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/src/main/cpp/include) # 指定FFmpeg的so库存放路径 set(FFMPEG_LIB_DIR ${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}) include_directories(${FFMPEG_INCLUDE_DIR}) add_library(avcodec SHARED IMPORTED) set_target_properties(avcodec PROPERTIES IMPORTED_LOCATION ${FFMPEG_LIB_DIR}/libavcodec.so) add_library(avformat SHARED IMPORTED) set_target_properties(avformat PROPERTIES IMPORTED_LOCATION ${FFMPEG_LIB_DIR}/libavformat.so) add_library(avutil SHARED IMPORTED) set_target_properties(avutil PROPERTIES IMPORTED_LOCATION ${FFMPEG_LIB_DIR}/libavutil.so) target_link_libraries( ffmpeg_wrapper avformat avcodec avutil swresample swscale log )这个CMakeLists有几个关键细节要单独说:
IMPORTED_LOCATION后面不能写成硬编码的绝对路径,因为不同设备编译时路径不同,最好用${CMAKE_SOURCE_DIR}结合${ANDROID_ABI}来动态拼接,这样做的好处是,当你一次性编译多个ABI时,CMake会自动对应到正确的架构目录。add_library(avcodec SHARED IMPORTED)之后,不需要再把这些系统库放进jniLibs了。实际打包时,CMake会在最终的APK里把链接进来的so一起带进去。当然你手动放在jniLibs里的so也会被打进APK,但如果同时通过CMake链接和jniLibs重复引入,可能会导致打包时冲突或so重复,所以规范的做法是:CMake里IMPORTED的库,就不需要再在jniLibs里手动放了。网上很多教程两个都放,有时候确实能运行,但工程维护起来会很乱,后面改ABI版本时容易出各种怪异问题。注意最后一行
log,这是Android的日志库,如果不链接,你在C代码里用__android_log_print写日志就会编译报错。
3.3 build.gradle中的关键配置
写完CMakeLists和JNI源码,接下来是让Gradle认识它们。
在app模块的build.gradle里,你需要这样配置:
android { defaultConfig { externalNativeBuild { cmake { cppFlags "-std=c++11" abiFilters "arm64-v8a", "armeabi-v7a" } } ndk { abiFilters "arm64-v8a", "armeabi-v7a" } } externalNativeBuild { cmake { path "src/main/cpp/CMakeLists.txt" version "3.18.1" } } sourceSets { main { jniLibs.srcDirs = ['src/main/jniLibs'] } } }这里关于abiFilters,实话说,这行配置踩过的坑不少。
ndk.abiFilters和defaultConfig.externalNativeBuild.cmake.abiFilters两处作用不同。前者控制APK最终打包哪些ABI的so,后者控制CMake编译哪些ABI的native库。如果你在ndk.abiFilters里只写了arm64-v8a,但CMake那里没限制,可能出现CMake把所有ABI都编译出来了、打包时却又被过滤掉一部分的情况,白白增加构建时间。我后来统一的做法是两边都写上同样的ABI,避免纠结。
jniLibs.srcDirs这个配置,其实现在AS会默认扫描src/main/jniLibs,所以不写也能工作。但如果你把so放在了一个自定义目录(比如libs/ffmpeg/下面),就必须通过它指定路径,否则so压根不会被打包。这个在一些老项目中尤其常见,很多人把so放在libs目录里,结果怎么打都打不进APK,就是这个原因。
3.4 用命令验证so是否打包成功
到这里,代码层面的工作基本做完。接下来需要验证两件事:so有没有被打进APK、加载时有没有报错。
在Android Studio的Terminal里,可以用一个简单命令检查APK内容:
./gradlew assembleDebug # 如果构建成功,进入APK目录,查看里面的lib文件夹 unzip -l app/build/outputs/apk/debug/app-debug.apk | grep "\.so"正常输出里应该能看到lib/arm64-v8a/libavcodec.so、lib/arm64-v8a/libavformat.so这些文件。如果so没有出现,说明jniLibs的路径配置有问题,或者abiFilters过滤过度了。
验证so能被正常加载,则写个最简单的JUnit或直接在MainActivity里调用:
public class MainActivity extends AppCompatActivity { static { System.loadLibrary("ffmpeg_wrapper"); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); TextView textView = findViewById(R.id.hello_text); if (FFmpegNative.verifyLoad()) { textView.setText("FFmpeg loaded"); } } }这里的verifyLoad()是一个自定义JNI函数,里面可以只返回一个简单的版本号,或者调用FFmpeg的av_version_info()。我第一次接的时候图快,直接在Java层调了一个不存在的函数名,结果一跑就崩,提示UnsatisfiedLinkError,然后才意识到JNI名字映射出了问题。这也是为什么我会建议,拿到so以后先写一个最简单的JNI函数验证通路,再往上叠加真正复杂的功能。这样出了问题,你才能快速判断到底是so加载挂了,还是函数调用方式有误。
4. 常见问题与排查技巧实录
以下这些是自己和身边同事都真实碰到过的坑,单独拉一个章节记录下来,方便你遇到问题时直接对照排查。
4.1 运行时UnsatisfiedLinkError
这是最典型的错误,表现形式是在logcat里出现类似这样的信息:
java.lang.UnsatisfiedLinkError: dlopen failed: library "libavcodec.so" not found这种错误出现时,排查顺序是这样:
- 确认so确实打包进了APK。用前文提到的unzip命令检查,如果没有,查jniLibs路径和sourceSets配置。
- 确认ABI相匹配。在真机上跑,看看手机的CPU架构是arm64-v8a还是armeabi-v7a,对照APK里lib目录下的子文件夹。
- 确认loadLibrary的库名正确。注意System.loadLibrary("avcodec")不需要带lib前缀和.so后缀,很多人习惯写成loadLibrary("libavcodec.so")或者loadLibrary("avcodec.so"),这样系统会认为你在找一个名叫liblibavcodec.so.so的库,当然找不到。
4.2 dlopen failed: cannot locate symbol
这个错误比上一个更隐蔽,它说明so文件找到了,加载也执行了,但在解析符号依赖时出了问题。通常原因是多个so文件之间版本不匹配,或者被系统库加载器解析时缺少某个依赖。
我从实际项目里遇到过的一个典型案例:同事替换了一个新编译的libavformat.so,却没有同步替换libavcodec.so和libavutil.so。新版的libavformat里引用了libavutil新增的某个符号,而旧的libavutil没有导出这个符号,于是运行时就报了cannot locate symbol。
这个问题的排查方法是:在加载所有动态库之后,立刻查看logcat里有没有关于symbol的详细描述,记录下缺失的符号名,然后用readelf命令去检查对应依赖库是否导出该符号:
# 在Linux/macOS上,检查libavutil.so的导出符号 readelf -Ws libavutil.so | grep avutil_version一旦出现missing symbol,优先怀疑是so文件版本与其余库不匹配。最有效的解决方式就是保证整个FFmpeg库栈都来自同一次编译产物,不要混着用。
4.3 JNI函数名与C实现不一致导致的NoSuchMethodError
JNI的命名规则是Java_包名_类名_方法名,包名里的点要换成下划线。如果类名里含有下划线,JNI里会映射成_1这种转义形式。
很多人在包名比较长(比如com.example.app.ffmpeg)时,容易在C文件里把包名或方法名写错。比如Java类里的native方法叫runCommand,C函数却写成了Java_com_example_app_ffmpeg_FFmpegNative_runcommand,字母大小写不一致就会报NoSuchMethodError。
解决这种问题没有太好的捷径,建议在第一次写好JNI封装时,先用一条最简单的native方法跑通,比如:
public native String getFFmpegVersion();C层实现:
JNIEXPORT jstring JNICALL Java_com_example_app_ffmpeg_FFmpegNative_getFFmpegVersion(JNIEnv *env, jobject thiz) { return env->NewStringUTF("ffmpeg-6.0"); }这个简单函数能通,说明JNI命名、加载流程、链接配置都没有问题,后续再扩充真正的业务逻辑就安全得多。
4.4 FFmpeg执行过程中崩溃,报错信息指向内存问题
这种崩溃通常发生在真正调用FFmpeg处理视频流时。比如做转码时,进程直接SIGSEGV退出。根据我的经验,十次里有八次是以下原因导致的:
- 传给FFmpeg的输入文件路径没转换成UTF-8,中文字符或空格路径解析出错。Android上一定要用
GetStringUTFChars转一次,并且要确保文件路径确实存在; - 输入输出格式没指定,FFmpeg内部尝试自动探测,当文件头信息不完整时,可能读到非法内存;
- 调用了FFmpeg的全局初始化但没有退出清理。FFmpeg本身不是线程安全的,如果你的JNI方法是直接在子线程里并行调用,多个转码任务同时跑,会触发对全局状态的并发访问,基本必崩。
我自己的经验是,对于初版集成,务必加全局锁,把JNI的调用限制为单一线程执行。直到你确认FFmpeg的调用链路完全稳定以后,再根据实际需求去考虑多实例并发。后来我在一个项目里踩过一个坑,就是试图在同一个JNI函数里同时执行“裁剪视频”和“导出音频”两个操作,结果第二个操作返回的结果完全不正常,查了很久最后发现是ffmpeg内部某些全局配置被第一次任务污染了。那次之后我再也没有在FFmpeg集成初期放开过并发控制。
4.5 打包体积暴涨问题
FFmpeg的so确实不小,尤其是如果编译时没有裁剪模块。一套全功能FFmpeg的arm64-v8a so文件加起来可能达到几十MB。对于工具型应用这也许可以接受,但对于体量敏感的App,就需要用编译时裁剪模块的方案来瘦身。比如你只做视频转码,不涉及音频,就可以通过编译时禁用libavfilter和libswresample来节省体积。博主在集成时也建议关注下so文件大小,如果特别大,反向说明这个FFmpeg是别人全量编译的,可能会携带很多你用不到的特性。
关于APK体积还有一个优化技巧:开启android:extractNativeLibs="false",让so文件不压缩直接存在APK里。这样APK体积会变小一些(压缩算法本身有去重和存储优化空间),但安装包安装后占用的磁盘空间会变大。这个取舍根据项目情况来定。
4.6 常见问题速查表
为方便排查,把上面几个典型异常整理成一张表,可以保存备用:
| 现象 | 可能原因 | 排查手段 |
|---|---|---|
| UnsatisfiedLinkError: library not found | so未打包进APK / 名称写错 / ABI不匹配 | unzip查看APK内lib目录,核对ABI与库名 |
| cannot locate symbol | so文件版本不一致,依赖符号缺失 | 用readelf检查符号导出,确认整组so来自同一次编译 |
| NoSuchMethodError | JNI函数名映射错误 | 核对Java包名类名方法名与C函数后缀 |
| SIGSEGV崩溃 | 路径字符编码问题 / 全局并发 / 输入文件不完整 | 确认路径转换UTF-8、临时加全局锁、检查文件有效性 |
| APK体积变大 | FFmpeg模块全量编译 | 编译时裁剪模块或用abiFilters减小包体 |
5. 复盘与扩展:so引入后的能力延伸
到这里,基础的“引入so”工作已经全部完成。但实际项目中,引入so文件从来不是为了“能加载它”,而是为了“能调用它干活”。所以在最后,我想聊聊引入完成之后怎么验证能力到位,以及后续还能怎么扩展。
5.1 用一条最简单的命令验证FFmpeg可用性
我建议在完成集成后,先在JNI层调用一个最简单的FFmpeg命令,验证整个链路是通的。比如读取一个视频文件的头部信息并打印出来。
Java层:
String[] argv = new String[]{"ffmpeg", "-i", "/sdcard/Download/test.mp4"}; int result = FFmpegNative.run(argv); Log.d("FFmpegDemo", "result = " + result);C层在run_ffmpeg里最终还是调用ffmpeg的main逻辑。当返回result为0,且logcat里能看到视频的编码信息(时长、分辨率、编码格式)时,说明FFmpeg已经彻底“活”在了你的App里。
这里有一个看似不起眼、但非常关键的点:Android上访问文件路径,尽量用getAbsolutePath()而不是直接写死相对路径。FFmpeg不会理解Android的content://这种URI,它只认传统的文件路径。所以你在调用前,需要把视频文件复制到应用私有目录或者指定存储目录,再拿到绝对路径传给JNI。很多初接FFmpeg的人第一运行就报错,十有八九是卡在“文件找不到”而不是FFmpeg本身的问题。
5.2 后续功能扩展方向
so文件引入成功后,后续扩展空间很大,列举几个我自己做过的方向:
- 视频缩略图生成:调用FFmpeg解码视频某一帧,转成Bitmap,提供给列表页展示。这是最简单也最常见的扩展。
- 音视频转码:把高码率视频转成低码率,或者把MOV转成MP4,这里面涉及编码参数设置、关键帧间隔、码率控制等细节。
- 视频裁剪拼接:通过设置输入起始时间、时长和输出编码参数,实现类似简易剪辑器的功能。
- 滤镜处理:如果你编译时包含了libavfilter,那么可以给视频加字幕、加旋转、加缩放、加色彩调整等滤镜效果。
另外,如果App对性能要求很高,比如要做实时视频处理(直播推流、摄像头滤镜),那就不能直接在Java层频繁调用JNI,要考虑把视频帧数据在C层做buffer管理,甚至直接用Surface传入纹理,让FFmpeg与GPU协作。这个方向涉及的知识就更深了,但引入so文件是这一切的起点。
5.3 关于自己编译的一点点建议
最后坦白聊一下“到底要不要自己编译FFmpeg”这个问题。我的建议是:
如果你只是想在App里用FFmpeg的几个基础功能,完全不需要自己编译。我见过很多开发者一开始雄心勃勃要搞自编译,最后被环境配置、网络下载、编译耗时等搞到怀疑人生。从工程效率角度,直接引入一套可用的so才是“正道”。
但如果你是长期做多媒体底层、对包体积和性能有极致要求的人,建议还是要学会自己编译。因为自己编译可以做到模块裁剪,只保留自己用到的编码器、封装格式,把FFmpeg瘦身到极致。同时还可以针对特定芯片平台做架构优化,比如加入汇编优化指令,让性能翻倍。这个进阶路线确实值得投入,但不该成为你第一次集成FFmpeg的门槛。
我个人在实际操作中还有一个习惯,就是稳定跑通之后,会把涉及FFmpeg的JNI层代码和配置单独抽成一个Module或者AAR。这样后续在其他项目里要用,直接依赖进来就行,不用再重复踩一遍本文里提到的各种坑。这个习惯帮我省了很多事,也推荐你试试。
如果你在引入过程中遇到本文之外的问题,比如某个特定API在JNI里传参传不进去,或者某个格式的so在特定机型上出现异常,可以多抓logcat,尤其是注意加载so之后的“dlopen”相关日志,那往往是最接近问题根源的地方。细节很多,但只要按流程走,先把最简通路跑通,再逐步扩展,基本不会出大问题。