一、NDK音视频开发:从JNI到编码器集成
说实话,NDK开发这块,我当年刚接触的时候也是一头雾水。尤其是音视频处理,C++代码和Java代码来回跳转,稍不留神就崩了。但等你真正理解了JNI的调用机制,再把FFmpeg、x264、fdk-aac这些库集成进来,你会发现——嗯,其实也没那么玄乎。
这一章,我就带你走一遍完整的流程。从JNI怎么调用C/C++代码,到怎么把FFmpeg编译进Android项目,再到集成x264和fdk-aac这两个硬核编码器。每一步我都会结合自己的踩坑经历来讲,希望能帮你少走弯路。
二、JNI调用:Java和C++的桥梁
2.1 JNI的基本原理
JNI,全称Java Native Interface。说白了,就是让Java代码能调用C/C++代码的一套标准接口。为什么音视频处理要用JNI?因为Java在底层内存操作、编解码性能上,确实不如C/C++来得直接。
我个人习惯把JNI调用分成三步:
- Java层声明native方法——比如
public native int encodeFrame(byte[] data); - 生成头文件——用
javac -h命令生成对应的C头文件 - 实现C/C++函数——在C代码里写真正的逻辑
核心要点:JNI函数命名有严格规则——Java_包名_类名_方法名。少一个下划线都找不到函数。
2.2 我在项目中遇到的JNI坑
我曾经在一个项目里,因为JNI函数名写错了一个字母,导致运行时一直报 UnsatisfiedLinkError。查了半天才发现是包名里的下划线没转义。记住:包名里的 _ 在JNI函数名里要写成 _1。
举个例子:
// Java层
package com.example.media;
public class Encoder {
public native int encodeFrame(byte[] data);
}
// JNI函数名应该是
JNIEXPORT jint JNICALL
Java_com_example_media_Encoder_encodeFrame(JNIEnv *env, jobject thiz, jbyteArray data)
小技巧:别手写JNI函数名。用 javac -h 生成头文件,然后直接复制函数签名。我每次都是这么干的,从不出错。
2.3 JNI数据类型映射
Java和C++的数据类型不一样,JNI做了一层映射。你想想看,如果Java传一个int过来,C++直接当int用,那肯定不行。JNI里用 jint 来对应Java的int。
| Java类型 | JNI类型 | C/C++类型 |
|---|---|---|
| boolean | jboolean | unsigned char |
| byte | jbyte | signed char |
| int | jint | int |
| long | jlong | long long |
| float | jfloat | float |
| double | jdouble | double |
| String | jstring | const char* (需转换) |
| byte[] | jbyteArray | jbyte* (需转换) |
这里要注意:jstring 和 jbyteArray 不能直接当C字符串或数组用。需要用 GetStringUTFChars 和 GetByteArrayElements 来获取指针。用完记得释放,不然内存泄漏。
三、FFmpeg集成:编译与链接
3.1 为什么选FFmpeg
FFmpeg是目前最强大的音视频处理库,没有之一。它支持几乎所有的音视频格式,从MP4到FLV,从H.264到HEVC,全都能处理。说白了,做Android音视频开发,FFmpeg是绕不开的。
3.2 编译FFmpeg for Android
编译FFmpeg到Android平台,我建议用NDK交叉编译。具体步骤:
- 下载FFmpeg源码(我一般用4.4版本,稳定)
- 配置编译选项——只编译需要的模块,减少包体积
- 用NDK的交叉编译工具链编译
这里给一个我常用的编译脚本片段:
#!/bin/bash
NDK=/path/to/android-ndk-r21e
TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/linux-x86_64
API=21
./configure \
--target-os=android \
--arch=arm64 \
--cross-prefix=$TOOLCHAIN/bin/aarch64-linux-android- \
--cc=$TOOLCHAIN/bin/aarch64-linux-android$API-clang \
--enable-shared \
--disable-static \
--disable-doc \
--disable-programs \
--enable-encoder=h264 \
--enable-encoder=aac \
--enable-decoder=h264 \
--enable-decoder=aac \
--enable-libx264 \
--enable-libfdk-aac
注意:编译时一定要加上 --enable-libx264 和 --enable-libfdk-aac,否则后面集成编码器时会找不到库。我曾经因为漏了这两个参数,编译出来的FFmpeg死活不支持H.264编码,折腾了两天。
3.3 在Android项目中集成FFmpeg
编译完成后,你会得到一堆 .so 文件。把它们放到 app/src/main/jniLibs/arm64-v8a/ 目录下。然后在CMakeLists.txt里链接:
add_library(avcodec SHARED IMPORTED)
set_target_properties(avcodec PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/../jniLibs/arm64-v8a/libavcodec.so)
target_link_libraries(native-lib
avcodec
avformat
avutil
swresample
x264
fdk-aac)
嗯,这里要注意链接顺序。FFmpeg的库之间有依赖关系,顺序错了会报未定义符号。我一般按 avformat → avcodec → avutil → swresample 的顺序来。
四、x264编码器:H.264视频编码
4.1 x264是什么
x264是一个开源的H.264/AVC视频编码器。它的压缩效率高,画质好,是目前最流行的视频编码方案之一。说白了,你看到的绝大多数MP4视频,底层都是x264编码的。
4.2 在FFmpeg中使用x264
集成x264后,在FFmpeg里调用它很简单。我直接上代码:
// 查找x264编码器
AVCodec *codec = avcodec_find_encoder_by_name("libx264");
if (!codec) {
// 处理错误
}
// 创建编码上下文
AVCodecContext *ctx = avcodec_alloc_context3(codec);
ctx->width = 1920;
ctx->height = 1080;
ctx->time_base = (AVRational){1, 30};
ctx->framerate = (AVRational){30, 1};
ctx->pix_fmt = AV_PIX_FMT_YUV420P;
// 设置x264参数
av_opt_set(ctx->priv_data, "preset", "medium", 0);
av_opt_set(ctx->priv_data, "crf", "23", 0);
// 打开编码器
avcodec_open2(ctx, codec, NULL);
这里我用了 preset 和 crf 参数。preset 控制编码速度和压缩率的平衡,crf 控制画质。数值越小画质越好,文件也越大。我个人习惯用 crf 23,画质和体积都比较均衡。
避坑指南:我曾经在低端手机上用 preset veryfast 编码,结果画面出现大量马赛克。后来换成 preset medium,问题就解决了。所以别一味追求速度,画质也很重要。
五、fdk-aac编码器:AAC音频编码
5.1 fdk-aac的优势
fdk-aac是Android平台上最推荐的AAC编码器。它的音质好,码率控制精准,而且支持HE-AAC等高级特性。相比FFmpeg自带的aac编码器,fdk-aac的编码质量明显更高。
5.2 集成与使用
集成fdk-aac的方式和x264类似。编译FFmpeg时加上 --enable-libfdk-aac,然后在代码里调用:
// 查找fdk-aac编码器
AVCodec *codec = avcodec_find_encoder_by_name("libfdk_aac");
if (!codec) {
// 处理错误
}
// 创建编码上下文
AVCodecContext *ctx = avcodec_alloc_context3(codec);
ctx->sample_fmt = AV_SAMPLE_FLT_S16;
ctx->sample_rate = 44100;
ctx->channel_layout = AV_CH_LAYOUT_STEREO;
ctx->bit_rate = 128000;
// 打开编码器
avcodec_open2(ctx, codec, NULL);
这里要注意:sample_fmt 必须用 AV_SAMPLE_FLT_S16,这是fdk-aac支持的格式。我之前用 AV_SAMPLE_FLT_FLTP,结果编码出来的音频全是噪音,查了半天才发现是采样格式不匹配。
六、知识体系总览
为了让你更直观地理解这一章的知识结构,我画了一张图:
这张图把整个NDK音视频开发的核心链路都串起来了。从JNI调用开始,到FFmpeg集成,再到x264和fdk-aac两个编码器。每一步都有坑,但也都有解法。
七、总结
这一章我们走完了NDK音视频开发的完整流程。JNI调用是基础,FFmpeg集成是骨架,x264和fdk-aac是血肉。说白了,掌握了这些,你就能在Android上做真正的音视频处理了。
我个人觉得,学习NDK开发最重要的不是背API,而是理解底层的工作原理。当你遇到崩溃、内存泄漏、编码失败这些问题时,能快速定位到问题根源。嗯,这才是真正的能力。
最后说一句:别怕踩坑。我踩过的坑比你多得多,但每次踩完,能力就提升一截。加油。
公众号:蓝海资料掘金营,微信deep3321