一、先理清概念:NV21 与常见 YUV 格式的区别
NV21:安卓摄像头默认输出格式,存储顺序为「Y 平面 → UV 平面(U 和 V 交替存储,V 在前)」,属于 YUV420SP 类型;
I420/YUV420P:Planar 格式,存储顺序为「Y 平面 → U 平面 → V 平面」(3 个独立平面);
YV12:Planar 格式,存储顺序为「Y 平面 → V 平面 → U 平面」;
转换核心是拆分 UV 分量,按目标格式重新排列。
二、方案 1:Java 层实现(易理解,适合小尺寸图像)
- NV21 转 I420(最常用)
java
/**
* NV21 转 I420(YUV420P)
* @param nv21 原始 NV21 数据
* @param width 图像宽度
* @param height 图像高度
* @return I420 数据
*/
public static byte[] nv21ToI420(byte[] nv21, int width, int height) {
if (nv21 == null || nv21.length != width * height * 3 / 2) {
throw new IllegalArgumentException("NV21 数据长度不匹配");
}
byte[] i420 = new byte[width * height * 3 / 2];
int ySize = width * height;
// 第一步:复制 Y 平面(NV21 和 I420 的 Y 分量完全一致)
System.arraycopy(nv21, 0, i420, 0, ySize);
// 第二步:拆分 NV21 的 UV 平面为 I420 的 U、V 平面
int uvIndex = ySize; // NV21 中 UV 起始位置
int uIndex = ySize; // I420 中 U 起始位置
int vIndex = ySize + (width * height / 4); // I420 中 V 起始位置
// NV21 的 UV 是 VU 交替,步长 2
for (int i = 0; i < width * height / 2; i += 2) {
i420[vIndex++] = nv21[uvIndex++]; // V 分量
i420[uIndex++] = nv21[uvIndex++]; // U 分量
}
return i420;
}
- NV21 转 YV12
php
/**
* NV21 转 YV12
* @param nv21 原始 NV21 数据
* @param width 图像宽度
* @param height 图像高度
* @return YV12 数据
*/
public static byte[] nv21ToYv12(byte[] nv21, int width, int height) {
if (nv21 == null || nv21.length != width * height * 3 / 2) {
throw new IllegalArgumentException("NV21 数据长度不匹配");
}
byte[] yv12 = new byte[width * height * 3 / 2];
int ySize = width * height;
// 复制 Y 平面
System.arraycopy(nv21, 0, yv12, 0, ySize);
int uvIndex = ySize;
int vIndex = ySize; // YV12 中 V 在前
int uIndex = ySize + (width * height / 4); // YV12 中 U 在后
for (int i = 0; i < width * height / 2; i += 2) {
yv12[vIndex++] = nv21[uvIndex++]; // V 分量
yv12[uIndex++] = nv21[uvIndex++]; // U 分量
}
return yv12;
}
三、方案 2:C++ 层实现(性能最优,适合实时采集)
Java 层循环操作字节数组效率低,安卓摄像头实时采集场景建议用 JNI/C++ 实现,下面是完整的 JNI 代码:
- C++ 核心转换函数(nv21_convert.cpp)
java
#include <jni.h>
#include <string.h>
// NV21 转 I420
extern "C" JNIEXPORT jbyteArray JNICALL
Java_com_example_nv21convert_Nv21ConvertUtils_nv21ToI420(
JNIEnv* env,
jobject /* this */,
jbyteArray nv21,
jint width,
jint height) {
// 获取 NV21 数据指针
jbyte* nv21Data = env->GetByteArrayElements(nv21, NULL);
int ySize = width * height;
int totalSize = ySize * 3 / 2;
// 创建 I420 数组
jbyteArray i420Array = env->NewByteArray(totalSize);
jbyte* i420Data = env->GetByteArrayElements(i420Array, NULL);
// 复制 Y 分量
memcpy(i420Data, nv21Data, ySize);
// 拆分 UV 分量
jbyte* nv21UV = nv21Data + ySize;
jbyte* i420U = i420Data + ySize;
jbyte* i420V = i420Data + ySize + ySize / 4;
for (int i = 0; i < ySize / 2; i += 2) {
*i420V++ = *nv21UV++; // V
*i420U++ = *nv21UV++; // U
}
// 释放资源
env->ReleaseByteArrayElements(nv21, nv21Data, 0);
env->ReleaseByteArrayElements(i420Array, i420Data, 0);
return i420Array;
}
- Java 层调用 JNI 方法
java
public class Nv21ConvertUtils {
static {
// 加载编译好的 libnv21convert.so
System.loadLibrary("nv21convert");
}
// 声明 JNI 方法
public native byte[] nv21ToI420(byte[] nv21, int width, int height);
// 调用示例
public static void testConvert() {
// 假设从摄像头获取的 NV21 数据
byte[] nv21Data = new byte[1920 * 1080 * 3 / 2]; // 1080P 数据
int width = 1920;
int height = 1080;
Nv21ConvertUtils utils = new Nv21ConvertUtils();
byte[] i420Data = utils.nv21ToI420(nv21Data, width, height);
}
}
- CMakeLists.txt 配置(编译 C++ 为 .so)
java
cmake_minimum_required(VERSION 3.22.1)
project("nv21convert")
add_library(
nv21convert
SHARED
src/main/cpp/nv21_convert.cpp)
find_library(
log-lib
log)
target_link_libraries(
nv21convert
${log-lib})
四、关键注意事项
数据长度校验 :NV21/YUV420 系列的总长度固定为 width * height * 3 / 2,转换前必须校验,避免数组越界;
性能选择:
小尺寸图像(如 640x480)用 Java 层即可,开发成本低;
大尺寸 / 实时场景(如 1080P 摄像头采集)必须用 C++ 层,效率提升 5~10 倍;
内存释放 :JNI 层操作字节数组后,必须调用 ReleaseByteArrayElements 释放资源,避免内存泄漏;
格式验证:转换后可通过「取像素点校验」验证正确性(比如 Y 分量值范围 0~255,UV 分量范围 -128~127)。
总结
1 NV21 转 YUV 核心是复用 Y 分量,拆分并重新排列 UV 分量;
2 Java 实现易理解但性能低,C++ 实现适合实时场景,是安卓音视频开发的主流方案;
3 转换前务必校验数据长度,JNI 层注意内存释放,避免崩溃 / 内存泄漏。
如果需要适配特定场景(比如摄像头预览回调、RTMP 推流前的格式转换),我可以给你完整的端到端代码。