前言:
Camera做图像采集端,可以通过ImageReader采集Yuv格式的图像数据。但是考虑兼容性在应用中通常会转化为NV12数据格式用于编码。然后通过rtspserver推流,vlc拉流显示。以下是实践中的一些优化方法及说明
一:Yuv转nv12数据优化
优化效果:修改后预览卡顿明显下降。
实测在rk3588平台java转化一帧耗时约20ms,用native方式转化一帧耗时约12ms。
Native方式实现代码:
extern "C" {
JNIEXPORT void JNICALL
Java_com_media_imageToNV12(JNIEnv *env, jobject thiz, jobject image, jbyteArray nv12_output) {
// 获取 Image 类和方法
jclass image_class = env->GetObjectClass(image);
jmethodID get_width = env->GetMethodID(image_class, "getWidth", "()I");
jmethodID get_height = env->GetMethodID(image_class, "getHeight", "()I");
jmethodID get_planes = env->GetMethodID(image_class, "getPlanes", "()[Landroid/media/Image$Plane;");
// 获取图像尺寸
jint width = env->CallIntMethod(image, get_width);
jint height = env->CallIntMethod(image, get_height);
// 获取平面数组
jobjectArray planes = (jobjectArray)env->CallObjectMethod(image, get_planes);
if (env->GetArrayLength(planes) < 3) {
LOGE("Invalid YUV_420_888 image, planes < 3");
return;
}
// 获取三个平面
jobject y_plane = env->GetObjectArrayElement(planes, 0);
jobject u_plane = env->GetObjectArrayElement(planes, 1);
jobject v_plane = env->GetObjectArrayElement(planes, 2);
// 获取 Plane 类和方法
jclass plane_class = env->GetObjectClass(y_plane);
jmethodID get_buffer = env->GetMethodID(plane_class, "getBuffer", "()Ljava/nio/ByteBuffer;");
jmethodID get_row_stride = env->GetMethodID(plane_class, "getRowStride", "()I");
jmethodID get_pixel_stride = env->GetMethodID(plane_class, "getPixelStride", "()I");
// 获取每个平面的缓冲区
jobject y_buffer = env->CallObjectMethod(y_plane, get_buffer);
jobject u_buffer = env->CallObjectMethod(u_plane, get_buffer);
jobject v_buffer = env->CallObjectMethod(v_plane, get_buffer);
// 获取行跨度和像素跨度
jint y_row_stride = env->CallIntMethod(y_plane, get_row_stride);
jint u_row_stride = env->CallIntMethod(u_plane, get_row_stride);
jint v_row_stride = env->CallIntMethod(v_plane, get_row_stride);
jint u_pixel_stride = env->CallIntMethod(u_plane, get_pixel_stride);
jint v_pixel_stride = env->CallIntMethod(v_plane, get_pixel_stride);
// 获取直接缓冲区指针
uint8_t* y_data = (uint8_t*)env->GetDirectBufferAddress(y_buffer);
uint8_t* u_data = (uint8_t*)env->GetDirectBufferAddress(u_buffer);
uint8_t* v_data = (uint8_t*)env->GetDirectBufferAddress(v_buffer);
if (!y_data || !u_data || !v_data) {
LOGE("Failed to get direct buffer address");
return;
}
// 获取输出数组指针
jbyte* nv12_data = env->GetByteArrayElements(nv12_output, NULL);
uint8_t* nv12 = (uint8_t*)nv12_data;
// 1. 复制 Y 平面
if (y_row_stride == width) {
// 没有填充,可以直接复制
memcpy(nv12, y_data, width * height);
} else {
// 有行填充,需要逐行复制
uint8_t* y_dst = nv12;
uint8_t* y_src = y_data;
for (int i = 0; i < height; ++i) {
memcpy(y_dst, y_src, width);
y_dst += width;
y_src += y_row_stride;
}
}
// 2. 处理 UV 平面 (NV12 是 U 和 V 交错排列: U0, V0, U1, V1...)
uint8_t* uv_dst = nv12 + width * height;
// 检查 UV 平面是否是交错的
if (u_pixel_stride == 2 && v_pixel_stride == 2 &&
u_row_stride == v_row_stride &&
u_data + 1 == v_data) {
// UV 已经是交错的,可能是 NV21 格式
// 这里需要转换为 NV12 (UV 顺序交换)
for (int row = 0; row < height / 2; ++row) {
for (int col = 0; col < width / 2; ++col) {
int uv_src_index = row * u_row_stride + col * 2;
int uv_dst_index = row * width + col * 2;
// NV12: U 在前,V 在后
uv_dst[uv_dst_index] = u_data[uv_src_index]; // U
uv_dst[uv_dst_index + 1] = v_data[uv_src_index]; // V
}
}
} else {
// UV 是分开的平面,需要手动交错
for (int row = 0; row < height / 2; ++row) {
for (int col = 0; col < width / 2; ++col) {
int u_index = row * u_row_stride + col * u_pixel_stride;
int v_index = row * v_row_stride + col * v_pixel_stride;
int uv_dst_index = row * width + col * 2;
// NV12: U 在前,V 在后
uv_dst[uv_dst_index] = u_data[u_index]; // U
uv_dst[uv_dst_index + 1] = v_data[v_index]; // V
}
}
}
// 释放资源
env->ReleaseByteArrayElements(nv12_output, nv12_data, 0);
}
} // extern "C"
二:midiacodec 编码方式从同步改为异步
实测入帧帧率60/s的情况下,出帧帧率从43/s提升到了71/s
三:推流jni去掉在rk3568的feature方式的并发
同时进行的tcp/udp拉流方式引发的必然延时10S问题解决。
四:送入mediacodec的数据由YUV-NV12改为Yuv
RK3568平台直接支持Yuv数据直接编码,不需要转nv12,取图耗时同等条件下由20~60之间波动降低到了12ms,大大减轻了CPU的使用,长时间测试的卡顿和延时等问题减轻了很多
五:编码关键帧频率可以从1~5S/次之间做调试
实测频率降低到5S/次可以在1080P下消除明显的延时。
六:帧率控制,将原有的帧率60帧出图控制到只使用其中30帧
实测长时间的跑,存在的延时问题,卡顿问题消失,app被kill(广播无法送达)问题没有复现
七:camera视频流停止
实测是camera视频流停止,
查看日志发现系抓图超时导致,6911读取视频信号没有返回,进行了断流操作
07-16 16:41:26.188 E/RkCamera( 279): <HAL> V4L2DevBase: @pollDevices: Device[0] /dev/video0 poll failed (3000ms timeout)
串口日志查看,会出现和拔出HDMI视频线同样的log,分析系传输线不稳定导致,更换视频线后恢复正常
[16:41:24.272]收←◆
Goto PCR
RxState = 10
[INFO ] RX Set Event:04
[INFO ] TxEvent-4
[DEBUG] TxState = 0x03
[INFO ] DPRX_VIDEO_OFF_EVENT
[INFO ] MIPIVidInfoClear!!!!!!
八:编码方式由H264改为H265
实际数据传输量理论值会下降一半,减轻贷款和数据处理压力
九:送入编码的缓存帧改为空帧
onInputBufferAvailable回调时必须送入帧数据,不然mediacodec的编码会停止。
由送入的缓存帧改为null帧,保证mediacocec的编码持续但是负载减轻。
十:推流的帧pts时间由本地时间改为编码时计算的pts时间
Pts时间是用于渲染显示帧的时间,不同的pts时间会影响播放的画面流畅,跳动。根据实际情况看pts时间的计算时的波动,动态选择使用pts时间,可以让画面的流程跳动减少。
十一:拉流播放端缓存几帧,减少播放的画面跳动
参考vlc缓存优化,画面更流畅
十二:调整mediacocec编码的码率,
CBR,VBR测试动态码率还是固定码率效果更好