前面两章我们已经搞定了单路 H264 码流获取、H264/H265 双编码码流获取。 都是固定分辨率取流,虽然能满足基础录像需求,但在真实的 IPC 摄像头项目中远远不够。
实际项目里几乎全部是双分辨率方案: 一路高清 1080P做本地精细录像存档,一路低清 720P做网页预览、手机实时查看、低带宽推流。
所以这一章我们继续基于 RV1126 + RKMedia,实战实现:一帧摄像头画面,同时输出 1080P 高分辨率、720P 低分辨率两路 H264 码流,多线程独立获取并保存。
一、为什么要做双分辨率码流?
简单说就是各司其职:
- 高分辨率(1080P):画质清晰、细节完整,用于本地录像回放、取证存档;
- 低分辨率(720P):体积小、码率低、占用带宽少,专门用于网络预览、实时直播。
如果只存 1080P,带宽小的设备会卡、存储空间消耗巨大; 如果只用 720P,录像画质太差,达不到监控要求。
所以高低双分辨率并行是嵌入式摄像头的标配方案。
二、RKMedia 双分辨率实现原理
很多新手会踩坑:以为 VI 可以直接输出两路不同分辨率数据。
这里纠正一个关键知识点: VI 采集出来的原始 NV12 画面分辨率是固定的,无法直接编码出两种尺寸。
正确的硬件链路流程:VI 采集原图 → 一路直通高清 VENC 编码 → 一路经过 RGA 缩放 → 低清 VENC 编码
完整数据流:
- VI 模块采集 1920×1080 原始画面
- 原始画面分两路分发
- 第一路直接送入 VENC0,编码 1080P 高清 H264
- 第二路送入 RGA 硬件缩放为 1280×720
- 缩放后的画面送入 VENC1,编码 720P 低清 H264
- 开启两个独立线程分别获取两路码流,同时开启 RGA 转发线程处理缩放数据
三、为什么需要多线程?
和之前章节逻辑一致,再次强调工程规范:
RKMedia 的GetMediaBuffer是阻塞接口。 如果所有逻辑放主线程,会导致:
- 编码阻塞卡死
- 数据无法流转
- 严重丢帧、链路异常
所以本章我们开启3 个独立子线程分工处理:
- 高清取流线程:专门读取 1080P VENC 码流、保存文件
- 低清取流线程:专门读取 720P VENC 码流、保存文件
- RGA 转发线程:循环读取缩放后图像,转发给低清编码器
线程隔离,互不干扰,保证两路码流稳定、不丢帧、不卡顿。
四、核心开发步骤
1、VI 摄像头初始化
配置摄像头分辨率 1080P、像素格式 NV12、缓冲数量,开启正常采集模式。 保证输出原始画面完整、稳定。
2、RGA 缩放模块初始化
配置 RGA 输入尺寸为 1080P,输出尺寸缩放为 720P。 利用瑞芯微硬件 RGA 完成图像缩放,全程无 CPU 消耗。
3、两路 VENC 编码通道初始化
分别创建两个 VENC 通道:
- VENC0:高分辨率编码通道,1920×1080、H264、CBR 码率模式
- VENC1:低分辨率编码通道,1280×720、H264、CBR 码率模式
两路编码参数独立配置,互不影响。
4、模块绑定链路
搭建完整数据通路:
- VI 绑定 高清 VENC:原图直接编码
- VI 绑定 RGA:原图进入缩放处理
通过软件链路绑定,让硬件自动流转数据。
5、创建多线程取流
分别启动高清取流线程、低清取流线程、RGA 数据转发线程。 循环阻塞取帧、写入本地裸流文件。
五、代码
#include <assert.h> #include <fcntl.h> #include <getopt.h> #include <pthread.h> #include <signal.h> #include <stdbool.h> #include <stdio.h> #include <stdlib.h> #include <time.h> #include <unistd.h> // RKMedia 核心API头文件,提供VI、RGA、VENC、Bind等驱动接口 #include "rkmedia_api.h" // ===================== 宏定义:各个模块通道编号 ===================== #define PIPE_ID 0 // VI管道ID #define VI_CHN_ID 0 // 摄像头通道号 #define RGA_CHN_ID 0 // RGA硬件缩放通道号 #define HIGH_VENC_CHN 0 // 高清编码通道(1080P) #define LOW_VENC_CHN 1 // 低清编码通道(720P) // ===================== 高清码流获取线程 ===================== // 功能:从VENC高清通道获取1080P H264码流,保存到文件 void * get_high_venc_thread(void * args) { // 设置线程为分离模式,自动释放资源,不需要主线程join pthread_detach(pthread_self()); // 打开文件,保存高清H264码流 FILE * high_venc_file = fopen("test_high_venc.h264", "w+"); // 媒体缓冲区,用于接收VENC输出的一帧数据 MEDIA_BUFFER mb; // 循环持续取流 while(1) { // 阻塞获取高清VENC通道的码流数据 mb = RK_MPI_SYS_GetMediaBuffer(RK_ID_VENC, HIGH_VENC_CHN, -1); // 获取失败,退出线程 if(!mb) { printf("Get High_Venc Break...\n"); return NULL; } printf("Get High_Venc Succeed...\n"); // 将H264码流写入文件 fwrite(RK_MPI_MB_GetPtr(mb), RK_MPI_MB_GetSize(mb), 1, high_venc_file); // 必须释放缓冲区,否则编码器会卡死 RK_MPI_MB_ReleaseBuffer(mb); } return NULL; } // ===================== RGA图像数据转发线程 ===================== // 功能:从RGA获取缩放后的720P图像,转发给低清VENC进行编码 void * rga_handle_thread(void * args) { pthread_detach(pthread_self()); MEDIA_BUFFER mb; while(1) { // 阻塞获取RGA输出的缩放图像 mb = RK_MPI_SYS_GetMediaBuffer(RK_ID_RGA, RGA_CHN_ID, -1); // 获取失败,退出循环 if(!mb) { printf("Get RGA Break...\n"); break; } printf("Get RGA Succeed...\n"); // 将缩放后的图像数据发送给低清VENC编码通道 RK_MPI_SYS_SendMediaBuffer(RK_ID_VENC, LOW_VENC_CHN, mb); // 释放图像缓冲区 RK_MPI_MB_ReleaseBuffer(mb); } return NULL; } // ===================== 低清码流获取线程 ===================== // 功能:从VENC低清通道获取720P H264码流,保存到文件 void * get_low_venc_thread(void * args) { pthread_detach(pthread_self()); MEDIA_BUFFER mb; // 打开文件,保存低清H264码流 FILE * low_venc_file = fopen("test_low_venc.h264", "w+"); while(1) { // 阻塞获取低清VENC通道的码流数据 mb = RK_MPI_SYS_GetMediaBuffer(RK_ID_VENC, LOW_VENC_CHN, -1); if(!mb) { printf("Get Low_Venc Break...\n"); } printf("Get Low_Venc Succeed...\n"); // 将H264码流写入文件 fwrite(RK_MPI_MB_GetPtr(mb), RK_MPI_MB_GetSize(mb), 1, low_venc_file); // 释放缓冲区 RK_MPI_MB_ReleaseBuffer(mb); } return NULL; } // ===================== 主函数:模块初始化 + 绑定 + 启动线程 ===================== int main(int argc, char *argv[]) { int ret; // ===================== 1. 初始化VI摄像头采集模块 ===================== VI_CHN_ATTR_S vi_chn_attr; // 摄像头设备节点 vi_chn_attr.pcVideoNode = "rkispp_scale0"; // 输出分辨率 1920x1080 vi_chn_attr.u32Width = 1920; vi_chn_attr.u32Height = 1080; // 图像格式 NV12 vi_chn_attr.enPixFmt = IMAGE_TYPE_NV12; // 内存映射模式 vi_chn_attr.enBufType = VI_CHN_BUF_TYPE_MMAP; // 缓冲区数量 vi_chn_attr.u32BufCnt = 3; // 正常工作模式 vi_chn_attr.enWorkMode = VI_WORK_MODE_NORMAL; // 设置VI通道属性 ret = RK_MPI_VI_SetChnAttr(PIPE_ID, VI_CHN_ID, &vi_chn_attr); if(ret) { printf("VI_CHN_ATTR Set Failed...\n"); return 0; } else { printf("VI_CHN_ATTR Set Succeed...\n"); } // 启用VI通道,开始采集图像 ret |= RK_MPI_VI_EnableChn(PIPE_ID, VI_CHN_ID); if(ret) { printf("VI_CHN_ATTR Enable Attr Failed...\n"); return 0; } else { printf("VI_CHN_ATTR Enable Succeed...\n"); } // ===================== 2. 初始化RGA硬件缩放模块 ===================== RGA_ATTR_S rga_info; // RGA输入配置:与VI输出一致 1920x1080 NV12 rga_info.stImgIn.u32Width = 1920; rga_info.stImgIn.u32Height = 1080; rga_info.stImgIn.u32HorStride = 1920; rga_info.stImgIn.u32VirStride = 1080; rga_info.stImgIn.imgType = IMAGE_TYPE_NV12; rga_info.stImgIn.u32X = 0; rga_info.stImgIn.u32Y = 0; // RGA输出配置:缩放到 1280x720 NV12 rga_info.stImgOut.u32Width = 1280; rga_info.stImgOut.u32Height = 720; rga_info.stImgOut.u32HorStride = 1280; rga_info.stImgOut.u32VirStride = 720; rga_info.stImgOut.imgType = IMAGE_TYPE_NV12; rga_info.stImgOut.u32X = 0; rga_info.stImgOut.u32Y = 0; // RGA缓冲池数量 rga_info.u16BufPoolCnt = 3; // 旋转0度 rga_info.u16Rotaion = 0; // 不翻转 rga_info.enFlip = RGA_FLIP_NULL; // 使能缓冲池 rga_info.bEnBufPool = RK_TRUE; // 创建RGA通道 ret = RK_MPI_RGA_CreateChn(RGA_CHN_ID, &rga_info); if (ret) { printf("RGA Create Failed...\n"); return 0; } else { printf("RGA Create Succeed...\n"); } // ===================== 3. 初始化高清VENC编码通道(1080P H264) ===================== VENC_CHN_ATTR_S high_venc_attr; // 编码类型 H264 high_venc_attr.stVencAttr.enType = RK_CODEC_TYPE_H264; // 输入图像格式 NV12 high_venc_attr.stVencAttr.imageType = IMAGE_TYPE_NV12; // 编码分辨率 1920x1080 high_venc_attr.stVencAttr.u32PicWidth = 1920; high_venc_attr.stVencAttr.u32PicHeight = 1080; high_venc_attr.stVencAttr.u32VirWidth = 1920; high_venc_attr.stVencAttr.u32VirHeight = 1080; // H264 profile Baseline high_venc_attr.stVencAttr.u32Profile = 66; // 不旋转 high_venc_attr.stVencAttr.enRotation = VENC_ROTATION_0; // 码率控制模式:CBR恒定码率 high_venc_attr.stRcAttr.enRcMode = VENC_RC_MODE_H264CBR; // I帧间隔 high_venc_attr.stRcAttr.stH264Cbr.u32Gop = 25; // 源帧率 25fps high_venc_attr.stRcAttr.stH264Cbr.u32SrcFrameRateNum = 25; high_venc_attr.stRcAttr.stH264Cbr.u32SrcFrameRateDen = 1; // 目标帧率 25fps high_venc_attr.stRcAttr.stH264Cbr.fr32DstFrameRateNum = 25; high_venc_attr.stRcAttr.stH264Cbr.fr32DstFrameRateDen = 1; // 码率 8Mbps high_venc_attr.stRcAttr.stH264Cbr.u32BitRate = 8388608; // 创建高清VENC通道 ret = RK_MPI_VENC_CreateChn(HIGH_VENC_CHN, &high_venc_attr); if (ret) { printf("Set High_Venc_Attr Failed...\n"); return 0; } else { printf("Set High_Venc_Attr Succeed...\n"); } // ===================== 4. 初始化低清VENC编码通道(720P H264) ===================== VENC_CHN_ATTR_S low_venc_attr; // 编码类型 H264 low_venc_attr.stVencAttr.enType = RK_CODEC_TYPE_H264; low_venc_attr.stVencAttr.imageType = IMAGE_TYPE_NV12; // 编码分辨率 1280x720 low_venc_attr.stVencAttr.u32PicWidth = 1280; low_venc_attr.stVencAttr.u32PicHeight = 720; low_venc_attr.stVencAttr.u32VirWidth = 1280; low_venc_attr.stVencAttr.u32VirHeight = 720; low_venc_attr.stVencAttr.u32Profile = 66; low_venc_attr.stVencAttr.enRotation = VENC_ROTATION_0; // CBR恒定码率 low_venc_attr.stRcAttr.enRcMode = VENC_RC_MODE_H264CBR; low_venc_attr.stRcAttr.stH264Cbr.u32Gop = 25; low_venc_attr.stRcAttr.stH264Cbr.u32SrcFrameRateNum = 25; low_venc_attr.stRcAttr.stH264Cbr.u32SrcFrameRateDen = 1; low_venc_attr.stRcAttr.stH264Cbr.fr32DstFrameRateNum = 25; low_venc_attr.stRcAttr.stH264Cbr.fr32DstFrameRateDen = 1; // 码率 8Mbps(低清可以适当改小) low_venc_attr.stRcAttr.stH264Cbr.u32BitRate = 8388608; // 创建低清VENC通道 ret = RK_MPI_VENC_CreateChn(LOW_VENC_CHN, &low_venc_attr); if (ret) { printf("Set Low_Venc_Attr Failed...\n"); return 0; } else { printf("Set Low_Venc_Attr Succeed...\n"); } // ===================== 5. 模块绑定:建立数据链路 ===================== MPP_CHN_S vi_chn_s; vi_chn_s.enModId = RK_ID_VI; vi_chn_s.s32ChnId = VI_CHN_ID; MPP_CHN_S high_chn_s; high_chn_s.enModId = RK_ID_VENC; high_chn_s.s32ChnId = HIGH_VENC_CHN; // VI 绑定 高清VENC → 原图直接编码 ret = RK_MPI_SYS_Bind(&vi_chn_s, &high_chn_s); if(ret) { printf("Vi Bind High_Venc Failed...\n"); return -1; } else { printf("Vi Bind High_Venc Succeed...\n"); } MPP_CHN_S rga_chn_s; rga_chn_s.enModId = RK_ID_RGA; rga_chn_s.s32ChnId = RGA_CHN_ID; // VI 绑定 RGA → 图像进入缩放流程 ret = RK_MPI_SYS_Bind(&vi_chn_s, &rga_chn_s); if(ret) { printf("Vi Bind Rga Failed...\n"); return -1; } else { printf("Vi Bind Rga Succeed...\n"); } // ===================== 6. 创建三个工作线程 ===================== pthread_t high_venc_pid; pthread_t rga_pid; pthread_t low_venc_pid; // 高清取流线程 pthread_create(&high_venc_pid, NULL, get_high_venc_thread, NULL); // RGA数据转发线程 pthread_create(&rga_pid, NULL, rga_handle_thread, NULL); // 低清取流线程 pthread_create(&low_venc_pid, NULL, get_low_venc_thread, NULL); // 主线程保持运行 while(1) { sleep(1); } // ===================== 7. 程序退出时资源释放 ===================== RK_MPI_SYS_UnBind(&vi_chn_s, &high_chn_s); RK_MPI_SYS_UnBind(&vi_chn_s, &rga_chn_s); RK_MPI_RGA_DestroyChn(RGA_CHN_ID); RK_MPI_VENC_DestroyChn(HIGH_VENC_CHN); RK_MPI_VENC_DestroyChn(LOW_VENC_CHN); RK_MPI_VI_DisableChn(PIPE_ID, VI_CHN_ID); return 0; }将代码编译,移植进板子进行运行,这部分流程参考vi模块的博客
六、效果验证
程序正常运行后,会自动生成两个裸流文件:
- 高清:
test_high_venc.h264(1080P) - 低清:
test_low_venc.h264(720P)
继续使用 ffplay 播放验证:
# 播放1080P高清码流 ffplay -x 600 test_high_venc.h264 # 播放720P低清码流 ffplay -x 500 test_low_venc.h264两路画面均可正常播放、无花屏、无卡顿,代表双分辨率取流成功。
七、关键注意点
1、低清码流不能直接取 RGA 数据
RGA 只做图像缩放,不编码,必须把缩放后的 NV12 数据转发给 VEN1 编码,我们取流只能从 VENC 通道取。
2、Buffer 必须成对释放
无论是 RGA 取出的缓存,还是 VENC 编码后的缓存,必须 Release。 不释放会直接导致媒体池耗尽、编码器卡死、程序僵死。
3、单 VI 支持多绑定
RKMedia 支持一个 VI 同时绑定多个下游模块,这也是双分辨率实现的核心基础。
八、本章总结
- 掌握了嵌入式摄像头高低双分辨率的标准工程架构;
- 理解 RGA 硬件缩放 + 双 VENC 并行编码的核心原理;
- 通过多线程隔离,实现两路码流同时稳定获取、同时保存;
- 这套架构是后续双路 RTSP 推流、双路 MP4 录像、预览 + 录像分离的底层基础。