一、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调用分成三步:

  1. Java层声明native方法——比如 public native int encodeFrame(byte[] data);
  2. 生成头文件——用 javac -h 命令生成对应的C头文件
  3. 实现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* (需转换)

这里要注意:jstringjbyteArray 不能直接当C字符串或数组用。需要用 GetStringUTFCharsGetByteArrayElements 来获取指针。用完记得释放,不然内存泄漏。

三、FFmpeg集成:编译与链接

3.1 为什么选FFmpeg

FFmpeg是目前最强大的音视频处理库,没有之一。它支持几乎所有的音视频格式,从MP4到FLV,从H.264到HEVC,全都能处理。说白了,做Android音视频开发,FFmpeg是绕不开的。

3.2 编译FFmpeg for Android

编译FFmpeg到Android平台,我建议用NDK交叉编译。具体步骤:

  1. 下载FFmpeg源码(我一般用4.4版本,稳定)
  2. 配置编译选项——只编译需要的模块,减少包体积
  3. 用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);

这里我用了 presetcrf 参数。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音视频开发知识体系 NDK音视频开发 JNI调用 native方法声明 生成头文件 C/C++实现 FFmpeg集成 NDK交叉编译 CMake链接 x264编码器 preset参数 crf画质控制 fdk-aac编码器 采样格式匹配 码率控制

这张图把整个NDK音视频开发的核心链路都串起来了。从JNI调用开始,到FFmpeg集成,再到x264和fdk-aac两个编码器。每一步都有坑,但也都有解法。

七、总结

这一章我们走完了NDK音视频开发的完整流程。JNI调用是基础,FFmpeg集成是骨架,x264和fdk-aac是血肉。说白了,掌握了这些,你就能在Android上做真正的音视频处理了。

我个人觉得,学习NDK开发最重要的不是背API,而是理解底层的工作原理。当你遇到崩溃、内存泄漏、编码失败这些问题时,能快速定位到问题根源。嗯,这才是真正的能力。

最后说一句:别怕踩坑。我踩过的坑比你多得多,但每次踩完,能力就提升一截。加油。


公众号:蓝海资料掘金营,微信deep3321