资讯动态

FFmpeg与OpenCV的Android集成:NDK交叉编译与JNI调用实践

发布时间:2026/9/16 3:22:32 来源:尧图企业网站定制
简介面向安卓音视频开发的 FFmpeg 与 OpenCV 工程模板采用 Eclipse 加 JNI 方式构建已经集成 x264、AAC、mp3lame 等常用编码库下载后可直接导入 Eclipse 工程编译运行。资源包包含四百三十三个文件主要有头文件、动态库、源码、编译脚本和界面配置资源等其中一百八十个 hpp 与一百七十五个 h 文件覆盖 C 层接口声明三十二个 so 库提供已编译好的底层功能压缩包总大小约四十九点七九兆字节。目前已有四百九十人学习下载。这个模板解决了自行交叉编译 FFmpeg 与 OpenCV 耗时长、易出错的问题让使用者直接拥有可运行的 NDK 工程框架通过 JNI 层调用底层音视频处理能力快速完成采集、编解码、图像处理等实验。另外示例 APK 和中间编译产物还可以帮助验证环境是否配置成功便于初学者对照工程结构理解编译脚本、头文件与动态库之间的关联关系把精力集中在功能实现上。1. ffmpeg 与 opencv 的开发模板为什么值得直接拿过来用在 Android 上搞音视频最消耗耐心的一步其实是环境搭建。ffmpeg 本身就依赖一堆可选 encoderopencv 又是一套独立的 native 库两者都要用 NDK 交叉编译。很多初学者卡在 configure 阶段不是版本不匹配就是链接顺序出错就算编译成功放到 arm 设备上还会出现 ABI 不兼容、加载失败的问题。这份模板不一样的是它把作者已经跑通的 ffmpeg自带 x264、aac、mp3lame 编码和 opencv 动态库直接做成了一个 Eclipse 工程并用 JNI 的方式从 MainActivity 调用 native 代码最终生成可安装的 demo.apk。你拿到的不是零散 .so而是有完整资源文件、class 和构建配置的工程骨架。对于想快速接触 ffmpeg 和 opencv 实际开发的程序员它能帮你跳过最耗时的编译环节直接进入编码器、滤镜、图像处理这些核心逻辑。2. 交叉编译中的 NDK 工具链与 x264/mp3lame 构建参数在动手改模板前先要理解这些 .so 是怎么来的。Android 端不能直接运行 x86 上编出的动态库必须在 Linux 或 macOS 上用 NDK 的交叉工具链编译出 ARM 指令集的产物。模板里已经编好了但你接手老项目时很可能要自己重新编因为 NDK 版本、API level、ABI 不同。比如模板用的是 armeabi-v7a你要在 arm64 手机上跑就得拿到源码重新编一版。下面我把关键参数和验证方式梳理一遍这个模板能不能为我所用判断标准很简单它的编译配置和你的 NDK 环境是否兼容。2.1 交叉编译工具链选择 clang 还是 gcc新版本 NDKr18 以后已经移除了 gcc默认用 clang。如果你在 Eclipse 老项目里看到arm-linux-androideabi-gcc说明模板对应的 NDK 还在 r17 或更早这通常和最新的 Android 系统不冲突但 SDK 工具链可能不认。常见做法是先用模板自带的旧 NDK 跑通再逐步迁移。我一般会把 NDK 版本记录在 project.properties 里避免以后换机器时“编译能过运行就崩”。对于交叉编译脚本这里有两点要注意第一是 sysroot 要指到对应的 API level比如android-14/arch-armAPI level 决定能调用的系统库版本第二是--cpuarmv7-a会启用 NEON 优化对 opencv 的图像处理很重要。如果没有指定某些浮点运算库可能退化成软浮点性能差别明显。2.2 ffmpeg 的 configure 参数x264、aac、mp3lame 如何集成ffmpeg 默认不包含 x264 和 mp3lame因为它们有独立的许可证协议。要在 Android 下用需要额外下载源码并编出静态库然后在编译 ffmpeg 时通过--enable-libx264 --enable-libmp3lame打开。下面这个脚本是模板生成过程中最常见的配置注意--enable-nonfree是因为 fdk-aac 和 mp3lame 的许可要求#!/bin/bash NDK/path/to/android-ndk HOST_PLATFORMlinux-x86_64 SYSROOT$NDK/platforms/android-14/arch-arm TOOLCHAIN$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/$HOST_PLATFORM ./configure \ --target-osandroid \ --archarm \ --cpuarmv7-a \ --enable-cross-compile \ --sysroot$SYSROOT \ --cc$TOOLCHAIN/bin/arm-linux-androideabi-gcc \ --prefix$PREFIX \ --enable-shared \ --disable-static \ --enable-gpl \ --enable-libx264 \ --enable-libmp3lame \ --enable-libfdk-aac \ --enable-nonfree \ --disable-doc \ --disable-programs \ --disable-avdevice \ --disable-postproc \ --disable-network \ --enable-jni这个脚本里有几个参数需要认真对待。--enable-shared会生成多个 .so 文件比如 libavcodec.so、libavformat.so 等模板为了便于加载通常会合并成一个体积较大的 libffmpeg.so--disable-programs去掉 ffmpeg 命令行工具只保留库接口--disable-network在纯本地处理场景下可以减少依赖。如果你要硬解 H.264需要将--enable-libx264换成或加上硬件解码相关的--enable-mediacodec那是另一套 NDK 接口。下面是模板中涉及到的编码器/库对应关系方便你快速判断手里的 so 是否满足需求库/编码器功能configure 开关许可证要求x264H.264 编码--enable-libx264GPLmp3lameMP3 编码--enable-libmp3lameLGPLfdk-aacAAC 编码--enable-libfdk-aacnonfreeopencv图像处理cmake 编译BSD2.3 opencv 的 Android 编译与 STL 依赖陷阱opencv 在 Android 上的编译走的是 CMake模板中直接提供了编译好的libopencv_java3.so它把 core、imgproc、objdetect 等模块打包到一起。自己编译时最容易踩的坑是 STL 选择如果 java 层抛出std::bad_alloc或者 JNI 崩溃很可能是 opencv 用的 STL 和 NDK 默认不一致。常见做法是在Application.mk里指定APP_STL : c_shared APP_ABI : armeabi-v7a然后确保所有模块都用同一个 STL 版本。模板里的 opencv 库如果没有特殊说明通常是 GNU STL 编的和现代 NDK 的 c_shared 混用时会有符号冲突所以拿到模板后要先用arm-linux-androideabi-readelf -d libopencv_java3.so | grep NEEDED确认依赖。自己用 CMake 编译 opencv 也是一条常见路径关键参数如下cmake -DCMAKE_TOOLCHAIN_FILE$ANDROID_NDK/build/cmake/android.toolchain.cmake \ -DANDROID_ABIarmeabi-v7a \ -DANDROID_PLATFORMandroid-21 \ -DANDROID_STLc_shared \ -DWITH_OPENCLOFF \ ..ANDROID_STLc_shared会让编译产物动态链接 libc_shared.so这比静态 STL 小但要求 apk 里同时打包这个辅助库。如果你选择了c_static链接时会省去一个 so但多个动态库同时用同一份静态 STL 可能引发符号重复。模板里的库如果依赖了 libgnustl_shared.so则 loadLibrary 时必须显式先加载它否则 dlopen 会失败。2.4 验证 so 库完整性的两个命令不管是从模板拷贝还是自己重新编译我都建议先做一次静态检查。使用 NDK 工具链里的readelf查看动态库依赖看到 liblog.so、libstdc.so 这类系统库是正常的但如果出现 libgnustl_shared.so就说明 opencv 依赖了非默认 STL在System.loadLibrary时也必须同时加载那个库。另一个常用命令是nm -D libffmpeg.so | grep x264如果能搜到x264_encoder_open之类的符号说明 x264 确实被链接进去了而不是一个空壳。3. Eclipse 工程里的 JNI 调用链从 resources.ap_ 到 native 方法模板里那堆文件不是随便放进去的它们构成了一个最小可运行的 Eclipse Android 工程。理解文件的角色才能在替换或扩展时知道动哪里。3.1 工程文件清单与作用文件说明resources.ap_Eclipse/ADT 编译过程中生成的资源打包文件内含 layout、drawable、string 等资源demo.apk最终生成的安装包包含了编译后的 DEX 和多个 .so 库MainActivity.class应用入口类的字节码声明了 native 方法R.class / R$*.class资源索引类Java 代码中通过 R.id.xxx 访问控件libs/armeabi-v7a/*.so预编译的 ffmpeg、opencv 及 JNI 桥接库多数人拿到模板后会直接用 Eclipse 打开工程目录替换成自己的包名和 Activity。这里有个容易忽略的点resources.ap_ 是中间产物如果你改了 layout 或 res 下的文件ADT 会重新生成所以不需要手改但如果只拿到这个文件而没有原 res 目录建议重新建工程再导入。3.2 Java 层 native 方法声明与 loadLibrary 顺序MainActivity 里通常会这样加载public class MainActivity extends Activity { static { System.loadLibrary(ffmpeg); // 先加载基础音视频库 System.loadLibrary(opencv_java3); // 再加载 opencv它依赖前者吗 System.loadLibrary(native-lib); // 最后加载自己的 JNI 桥接 } public native String getVersion(); public native int[] processFrame(int[] rgba, int width, int height); }loadLibrary 顺序很重要。opencv 并不直接依赖 ffmpeg所以理论上无所谓但如果你的桥接库同时引用了两者的符号链接器会在加载时解析符号三个库必须都出现在类加载器路径下。System.loadLibrary会按名字去libs/abi目录找所以一旦你新增一个库就必须保证它放在 libs 下并在 Application.mk 里声明。提示如果项目开启 ProGuard必须保留 MainActivity 的 native 方法名否则混淆后 JVM 找不到 JNI 入口。通常加-keepclasseswithmembernames class * { native methods; }。3.3 C 层 JNI 实现与函数签名JNI 的桥接函数名必须严格遵循Java_包名_类名_方法名的规则否则会报UnsatisfiedLinkError。例如 MainActivity 在包com.example.avtemplate下native 方法为getVersionC 端函数就写成#include jni.h #include libavcodec/avcodec.h #include opencv2/core/core.hpp JNIEXPORT jstring JNICALL Java_com_example_avtemplate_MainActivity_getVersion(JNIEnv *env, jobject thiz) { const char *config avcodec_configuration(); return (*env)-NewStringUTF(env, config); }这个函数返回的avcodec_configuration()是 ffmpeg 编译时的 configure 命令字符串可以直接在 App 里显示验证 x264 和 mp3lame 是否包含。NewStringUTF会生成一个 JVM 字符串对象不需要手动释放但用完后 Java 层可以getBytes保存。C 项目里env是JNIEnv*而 C 项目里是JNIEnv**模板若用纯 C 编写记得用(*env)-NewStringUTF而不是env-NewStringUTF两者混用是常见编译报错。3.4 Android.mk 与 Application.mk 的编写约定Native 代码构建依赖 Android.mk。模板中桥接库的 makefile 大致长这样LOCAL_PATH : $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE : native-lib LOCAL_SRC_FILES : native-lib.c LOCAL_C_INCLUDES : $(LOCAL_PATH)/include LOCAL_LDLIBS : -llog -landroid LOCAL_SHARED_LIBRARIES : libffmpeg libopencv_java3 include $(BUILD_SHARED_LIBRARY)注意LOCAL_SHARED_LIBRARIES只是告诉链接器依赖关系实际把哪些 .so 打进 apk 由 Application.mk 和 Eclipse 构建脚本控制。如果发现安装后dlopen failed先检查Application.mk中的APP_ABIAPP_ABI : armeabi-v7a APP_STL : c_sharedarmeabi-v7a是 32 位 ARM 里兼容性最好的指令集几乎所有 Android 设备都支持。如果模板没有提供 arm64-v8a 的 so你又在 64 位系统上安装加载时系统会因为找不到 libffmpeg.so 而直接抛异常。这时候只能把 app 的android:extractNativeLibstrue配合 32 位兼容模式或者自己重新编译 64 位版本。4. 关联 SDK/NDK 后ffmpeg 与 opencv 的构建错误排查这一章针对 Eclipse 环境给出具体操作和排错思路。Eclipse 虽老但在维护旧的音视频项目时仍有一席之地就算你最终要迁移到 Android Studio这里的排查逻辑也一样适用。4.1 Eclipse 中指定 SDK 和 NDK 路径的两种方式Eclipse 开发 Android 需要安装 ADT 插件。安装完成后在 Window - Preferences - Android 里填入 SDK 位置Eclipse 会自动识别已安装的 Platform 和 Build-Tools。NDK 路径不在这里而是在项目根目录的 local.properties 里声明sdk.dir/Applications/eclipse/android-sdk-mac ndk.dir/opt/android-ndk-r10e注意 local.properties 里的路径必须使用正斜杠Windows 下也不要写成C:\否则 ndk-build 的脚本解析会报错。如果你没有这个文件Eclipse 会尝试从环境变量ANDROID_NDK_HOME读取。实际模板中经常只提供local.properties.example拿到后要自己复制一份并改成真实路径。4.2 用 ndk-build 触发 native 编译的 builder 配置模板工程会自动检测 jni 目录下的 Android.mk 并调用 ndk-build。也可以手动在项目上选择 Properties - Builders - New添加一个 Program 类型的 BuilderCommand 指向 ndk-build.cmdArguments 设为构建参数。常见的构建指令ndk-build clean ndk-build NDK_DEBUG1 APP_ABIarmeabi-v7aNDK_DEBUG1会生成带调试信息的 so便于在 NDK 层面打断点。APP_ABI在这里作为命令行参数会覆盖 Application.mk 里的设置适合临时切换 ABI 验证问题。构建完成后确认 libs/armeabi-v7a 下有对应 so。如果 libs 里只有 .so 而没有中间文件一般是因为 ndk-build 的外部 builder 输出目录设置错误项目属性里要指定为${workspace_loc:/项目名/libs}。4.3 常见构建错误与运行期异常对照表操作过程里最糟心的就是报错链很长我把模板使用高频错误整理成下表错误现场根因处理办法ndk-build: command not foundNDK 路径未配置检查 local.properties 的 ndk.dirCannot find system/libdl.sosysroot 指向错误的 API level修改 Android.mk 或重新编译 soUnsatisfiedLinkError: findLibrary returned null目标 ABI 没有对应 so检查 libs 下是否有 armeabi-v7a 目录dlopen failed: cannot locate symbol avcodec_register_all桥接库依赖的 ffmpeg 未先加载调整 System.loadLibrary 顺序VerifyError: rejected class ... native methodJNI 函数名与 Java 方法不匹配核对 Java_ 包名类名方法名opencv_java3.so: has text relocations旧库使用 TEXTREL升级 opencv 版本或降低 targetSdkVersion最后一行has text relocations是 Android 6.0 之后的常见问题旧版 so 在编译时未使用-z text检查会被系统拒绝加载。模板如果年代较久大概率会遇到。最简单的临时方案是把targetSdkVersion降到 22 以下但更推荐重新编译 so 时加入-Wl,-z,noexecstack和地址无关代码标志。每次修改 native 代码后建议在 Eclipse 控制台看完整日志不要只看红字。很多 undefined reference 错误会把缺失符号打印成avcodec_register_all此时用nm -D libffmpeg.so | grep avcodec_register确认库内是否真的存在该符号能省下不少排查时间。如果是运行时崩溃用 adb 抓取异常现场adb logcat -s AndroidRuntime:E libav_jni:V dlopen:V通过观察UnsatisfiedLinkError前面几条日志可以看到系统实际尝试加载了哪个路径下的 so。常见误区是只关注 java 层堆栈忽略了dlopen failed里给出的完整 so 路径那才是问题所在。4.4 从 Eclipse 迁移到 Android Studio 时如何复用模板虽然模板是 Eclipse 版但库文件完全可以在 Android Studio 中复用。把 libs/ 下的 .so 移到app/src/main/jniLibs/armeabi-v7a/在 app 的 build.gradle 中指定android { sourceSets { main { jniLibs.srcDirs [src/main/jniLibs] } } }Java 端不需要改System.loadLibrary会自动找到 jniLibs 里的库。要注意的是 NDK 版本Android Studio 的 ndk-build 可能不识别旧版 Android.mk 里的arm-linux-androideabi-gcc这时要么使用 local.properties 里指定的 NDK r10e要么在 build.gradle 里把ndkVersion设为项目实际编译用的版本让 Gradle 自动下载对应工具链。相比 EclipseAS 对进程内崩溃的信息展示更友好配合 Android Studio 的 Logcat 分析dlopen失败能更快定位是 ABI 不匹配还是库依赖问题。5. 在模板上扩展 x264 编码与 mp3lame 转码三个可复用细节当你确认模板跑通接下来就是把 ffmpeg 的能力真正用起来。这里给出三个我实际用过的扩展点它们都能在模板现有 so 的基础上直接改写。5.1 x264 编码器初始化的参数组合AVCodec *codec avcodec_find_encoder_by_name(libx264); AVCodecContext *ctx avcodec_alloc_context3(codec); ctx-bit_rate 1000000; ctx-width 640; ctx-height 480; ctx-time_base (AVRational){1, 25}; ctx-pix_fmt AV_PIX_FMT_YUV420P; av_opt_set(ctx-priv_data, preset, fast, 0); av_opt_set(ctx-priv_data, profile, high, 0); avcodec_open2(ctx, codec, NULL);注意 x264 的 profile 要和设备支持级别对应老设备支持 High 但码率太高会花屏。preset选fast是编码速度与体积的折中如果做实时预览可以改ultrafast并降低bit_rate。time_base必须设置成1/帧率否则编码器计算 PTS 会错乱导致输出视频时长不对。5.2 mp3lame 的 PCM 到 MP3 封装mp3lame 不在 ffmpeg 的 API 里是独立的 libmp3lame 静态库。模板里链接时已经导出了lame_init等符号在你的 JNI 代码中直接 include 后调用lame_t lame lame_init(); lame_set_in_samplerate(lame, 44100); lame_set_num_channels(lame, 2); lame_set_brate(lame, 128); lame_init_params(lame); // 每次读入 PCM buffer调用 lame_encode_buffer_interleaved int out_size lame_encode_buffer_interleaved(lame, pcm_buf, samples, mp3_buf, mp3_buf_size); fwrite(mp3_buf, 1, out_size, out_file);关键点是lame_encode_buffer_interleaved的第三个参数samples是每声道的采样数而不是总样本数。如果把左右声道的总样本数传进去编码出的 MP3 会明显变慢变调。另外编码完成后要调用lame_encode_flush把尾部数据刷出来否则文件末尾会缺失。5.3 opencv Mat 到 AVFrame 的像素格式互转opencv 默认是 BGR 排列ffmpeg 解码后的 AVFrame 是 YUV。我一般用cvtColor先转成 RGB再通过sws_scale转成 YUV420P。注意Mat的数据长度和AVFrame的 stride 不同opencv 的step可能带对齐必须按行拷贝而不是 memcpy 整个 buffer。正确做法是逐行复制for (int i 0; i frame-height; i) { memcpy(frame-data[0] frame-linesize[0] * i, mat.data mat.step * i, frame-width * 3); }这个操作完成后frame-data[0]里就是 RGB888 布局再交给sws_scale做色彩空间转换。三个细节都建立在模板的 so 已经集成了对应符号之上。每次扩展后重新 ndk-build用nm -D验证新符号是否暴露比看运行日志更早发现问题。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价