rk3568抓图推流卡顿延时优化实践

前言:

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测试动态码率还是固定码率效果更好

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容