简介:面向安卓音视频开发的 FFmpeg 与 OpenCV 工程模板,采用 Eclipse 加 JNI 方式构建,已经集成 x264、AAC、mp3lame 等常用编码库,下载后可直接导入 Eclipse 工程编译运行。资源包包含四百三十三个文件,主要有头文件、动态库、源码、编译脚本和界面配置资源等,其中一百八十个 hpp 与一百七十五个 h 文件覆盖 C++ 层接口声明,三十二个 so 库提供已编译好的底层功能,压缩包总大小约四十九点七九兆字节。目前已有四百九十人学习下载。这个模板解决了自行交叉编译 FFmpeg 与 OpenCV 耗时长、易出错的问题,让使用者直接拥有可运行的 NDK 工程框架,通过 JNI 层调用底层音视频处理能力,快速完成采集、编解码、图像处理等实验。另外示例 APK 和中间编译产物还可以帮助验证环境是否配置成功,便于初学者对照工程结构理解编译脚本、头文件与动态库之间的关联关系,把精力集中在功能实现上。
1. ffmpeg 与 opencv 的开发模板,为什么值得直接拿过来用
在 Android 上搞音视频,最消耗耐心的一步其实是环境搭建。ffmpeg 本身就依赖一堆可选 encoder,opencv 又是一套独立的 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
新版本 NDK(r18 以后)已经移除了 gcc,默认用 clang。如果你在 Eclipse 老项目里看到arm-linux-androideabi-gcc,说明模板对应的 NDK 还在 r17 或更早,这通常和最新的 Android 系统不冲突,但 SDK 工具链可能不认。常见做法是先用模板自带的旧 NDK 跑通,再逐步迁移。我一般会把 NDK 版本记录在 project.properties 里,避免以后换机器时“编译能过,运行就崩”。
对于交叉编译脚本,这里有两点要注意:第一是 sysroot 要指到对应的 API level,比如android-14/arch-arm,API level 决定能调用的系统库版本;第二是--cpu=armv7-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_PLATFORM=linux-x86_64 SYSROOT=$NDK/platforms/android-14/arch-arm TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/$HOST_PLATFORM ./configure \ --target-os=android \ --arch=arm \ --cpu=armv7-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 开关 | 许可证要求 |
|---|---|---|---|
| x264 | H.264 编码 | --enable-libx264 | GPL |
| mp3lame | MP3 编码 | --enable-libmp3lame | LGPL |
| fdk-aac | AAC 编码 | --enable-libfdk-aac | nonfree |
| opencv | 图像处理 | cmake 编译 | BSD |
2.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_ABI=armeabi-v7a \ -DANDROID_PLATFORM=android-21 \ -DANDROID_STL=c++_shared \ -DWITH_OPENCL=OFF \ ..ANDROID_STL=c++_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 方法为getVersion,C 端函数就写成:
#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_ABI:
APP_ABI := armeabi-v7a APP_STL := c++_sharedarmeabi-v7a是 32 位 ARM 里兼容性最好的指令集,几乎所有 Android 设备都支持。如果模板没有提供 arm64-v8a 的 so,你又在 64 位系统上安装,加载时系统会因为找不到 libffmpeg.so 而直接抛异常。这时候只能把 app 的android:extractNativeLibs="true"配合 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 类型的 Builder,Command 指向 ndk-build.cmd,Arguments 设为构建参数。常见的构建指令:
ndk-build clean && ndk-build NDK_DEBUG=1 APP_ABI=armeabi-v7aNDK_DEBUG=1会生成带调试信息的 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 found | NDK 路径未配置 | 检查 local.properties 的 ndk.dir |
Cannot find system/libdl.so | sysroot 指向错误的 API level | 修改 Android.mk 或重新编译 so |
UnsatisfiedLinkError: findLibrary returned null | 目标 ABI 没有对应 so | 检查 libs 下是否有 armeabi-v7a 目录 |
dlopen failed: cannot locate symbol "avcodec_register_all" | 桥接库依赖的 ffmpeg 未先加载 | 调整 System.loadLibrary 顺序 |
VerifyError: rejected class ... native method | JNI 函数名与 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 自动下载对应工具链。相比 Eclipse,AS 对进程内崩溃的信息展示更友好,配合 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验证新符号是否暴露,比看运行日志更早发现问题。
本文还有配套的精品资源,点击获取