上一章我们把视频编码原理讲透了:原始 YUV 数据太大,必须通过 H.264 编码压缩,才能真正用于存储、直播、网络传输。我们也认识了 SPS、PPS、I 帧、码率这些核心概念。
理论落地才有价值,这一章我们直接上实战代码 : 在 RV1126 平台上,通过RKMedia + 多线程 ,从 VENC 硬件编码模块中,实时获取标准 H.264 码流数据 ,并保存成可直接播放的 .h264 文件。
这是做摄像头录像、RTSP 推流、网络传输的最基础、最核心一步。
前言
在 RKMedia 架构里,VENC 是负责硬件编码的模块。它会把 VI 采集、RGA 缩放后的图像,自动压缩成 H.264 码流。
但有一个关键点: 编码出来的 H.264 数据,不能直接在主线程里读,必须开一个独立线程去取。
原因很简单:
- 主线程要负责模块初始化、链路管理;
- 取码流是一个阻塞等待的过程;
- 如果不放线程里,会导致整个程序卡住,甚至丢帧。
所以这一章的核心就是: 初始化 VENC → 绑定 RGA 与 VENC → 开线程循环取 H.264 数据 → 保存文件。
一、整体流程梳理(和前面的章节 VI+RGA 衔接)
我们的整体数据流是这样的:
摄像头(VI) → RGA硬件缩放 → VENC硬件编码 → 多线程获取H.264码流 → 保存文件
对应代码执行步骤:

- 初始化系统(RK_MPI_SYS_Init)
- 初始化 VI(摄像头采集)
- 初始化 VENC(H.264 编码配置)
- 绑定模块:VI → VENC
- 创建线程,专门获取 VENC 输出的 H.264 数据
- 启动所有模块数据流
- 线程中循环取码流、写文件
- 程序退出时释放资源
二、VI模块初始化
2.1 初始化 VI 模块:
在取码流之前,必须先告诉 VENC 怎么编码:分辨率多大、编码格式 H.264、码率多少、I 帧间隔多大、是否开启 SPS/PPS 等。
这些参数都在 VENC_ATTR_S 结构体里配置。
关键配置项
- 编码类型:H.264
- 输入分辨率:和 RGA 输出保持一致(比如 1280×720)
- 码率设置:CBR/VBR,比如设置 2Mbps
- GOP(I 帧间隔):一般设 30~60 帧一个 I 帧
- 帧率:25fps
- 开启编码缓冲池
这些参数直接决定画质、流畅度、文件大小。
VI模块的初始化实际上就是对VI_CHN_ATTR_S的参数进行设置、然后调用RK_MPI_VI_SetChnAttr 设置VI模块并使能RK_MPI_VI_EnableChn **,**代码如下:
2.2 初始化 VENC 模块:
VENC模块的初始化实际上就是对VENC_CHN_ATTR_S的参数进行设置、然后调用RK_MPI_VENC_CreateChn 创建编码器**,**代码如下:
VENC_CHN_ATTR_S venc_chn_attr;
venc_chn_attr.stVencAttr.enType = RK_CODEC_TYPE_H264;
。。。。。。。。。。。。。。。。(这里是设置VENC的属性)
ret = RK_MPI_VENC_CreateChn(0, &venc_chn_attr);
2.3 绑定 VI 节点和 VENC 节点:
绑定VI节点和VENC节点,使其两个模块能够关联起来,使用的API是RK_MPI_SYS_Bind 。 关于这个API的定义,之前说过了,这里不在阐述。代码如下:
MPP_CHN_S vi_chn_s;
vi_chn_s.enModId = RK_ID_VI;
vi_chn_s.s32ChnId = 0;
MPP_CHN_S venc_chn_s;
venc_chn_s.enModId = RK_ID_VENC;
venc_chn_s.s32ChnId = 0;
ret = RK_MPI_SYS_Bind(&vi_chn_s, &venc_chn_s);
2.4 开启多线程采集 VENC 的数据
为什么必须用多线程取码流?
很多新手直接把取码流的逻辑写在主线程,结果一运行就出现:
- 画面卡顿
- 码流丢失
- 程序卡死
- 编码器报 timeout
原因: RK_MPI_SYS_GetMediaBuffer 这个接口是阻塞式的 ------ 没有数据时,它会一直等。
如果放在主线程,整个程序都会被卡住,其他模块无法正常运行。
标准做法:开一个独立线程,死循环取码流。 线程只做三件事:
- 从 VENC 模块取一帧 H.264 数据
- 把数据写入文件
- 释放码流 buffer(必须释放!)
使用的API是RK_MPI_SYS_GetMediaBuffer , 模块ID是RK_ID_VENC,通道号ID是VENC创建的ID号**。** 这个API的具体作用已经在之前的获取VI数据的博客已经讲解过,我们直接上代码:
while(1)
{
.........................
mb = RK_MPI_SYS_GetMediaBuffer(RK_ID_VENC, s32_chn_id, -1);
fwrite(RK_MPI_MB_GetPtr(mb), RK_MPI_MB_GetSize(mb), 1, h264_file);
.......................
}
三、代码
#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、VENC、Bind等所有接口
#include "rkmedia_api.h"
// ===================== 宏定义 =====================
#define CAMERA_PATH "rkispp_scale0" // 摄像头V4L2设备节点
#define CAMERA_ID 0 // 摄像头管道ID,默认0
#define CAMERA_CHN 0 // 摄像头通道号,默认0
#define VENC_CHN 0 // 硬件编码通道号,默认0
// ===================== 线程函数 =====================
/**
* @brief 从VENC获取H264码流的线程函数
* @param args 线程参数,未使用
* @return 线程退出指针
*/
void * get_h264_venc_thread(void *args)
{
// 将线程设置为分离模式,自动释放资源,无需主线程pthread_join回收
pthread_detach(pthread_self());
// 以读写方式打开文件,保存H264裸码流
FILE * h264_file = fopen("test_camre.h264","w+");
// 定义媒体缓存结构体,用于接收编码后的数据
MEDIA_BUFFER mb;
// 无限循环,持续从VENC取码流
while(1)
{
// 从VENC模块阻塞获取一帧H264数据
// RK_ID_VENC:指定从编码模块取流
// VENC_CHN:编码通道号
// -1:永久阻塞等待,直到有数据
mb = RK_MPI_SYS_GetMediaBuffer(RK_ID_VENC, VENC_CHN, -1);
// 没有获取到缓存,退出循环
if (!mb)
{
printf("Get Venc Mb Break...\n");
break;
}
printf("Get H264 One Frame...\n");
// 将获取到的H264数据写入文件
// RK_MPI_MB_GetPtr(mb):获取数据缓冲区指针
// RK_MPI_MB_GetSize(mb):获取数据长度
fwrite(RK_MPI_MB_GetPtr(mb), RK_MPI_MB_GetSize(mb), 1, h264_file);
// 必须释放缓存,归还给VENC缓冲池,否则会导致缓存耗尽
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通道属性配置结构体
vi_chn_attr.pcVideoNode = CAMERA_PATH; // 绑定摄像头设备节点
vi_chn_attr.u32Width = 1920; // 摄像头输出宽度 1920
vi_chn_attr.u32Height = 1080; // 摄像头输出高度 1080
vi_chn_attr.enPixFmt = IMAGE_TYPE_NV12; // 图像格式 NV12
vi_chn_attr.enBufType = VI_CHN_BUF_TYPE_MMAP; // 内存映射模式
vi_chn_attr.u32BufCnt = 3; // VI缓存帧数
vi_chn_attr.enWorkMode = VI_WORK_MODE_NORMAL; // 正常工作模式
// 设置VI通道属性
ret = RK_MPI_VI_SetChnAttr(CAMERA_ID, CAMERA_CHN, &vi_chn_attr);
if(ret)
{
printf("Vi Set Attr Failed...\n");
return 0;
}
else
{
printf("Vi Set Attr Succeed...\n");
}
// 使能VI通道,开启摄像头硬件
ret = RK_MPI_VI_EnableChn(CAMERA_ID, CAMERA_CHN);
if(ret)
{
printf("Vi Enable Attr Failed...\n");
return 0;
}
else
{
printf("Vi Enable Attr Succeed...\n");
}
// ===================== 2. 初始化VENC硬件编码模块 =====================
VENC_CHN_ATTR_S venc_chn_attr; // VENC通道配置结构体
// 基础编码配置
venc_chn_attr.stVencAttr.enType = RK_CODEC_TYPE_H264; // 编码类型 H264
venc_chn_attr.stVencAttr.u32PicWidth = 1920; // 编码图像宽度
venc_chn_attr.stVencAttr.u32PicHeight = 1080; // 编码图像高度
venc_chn_attr.stVencAttr.u32VirWidth = 1920; // 虚拟宽度(与输入一致)
venc_chn_attr.stVencAttr.u32VirHeight = 1080; // 虚拟高度(与输入一致)
venc_chn_attr.stVencAttr.u32Profile = 66; // H264 profile 66=Baseline
venc_chn_attr.stVencAttr.imageType = IMAGE_TYPE_NV12; // 输入图像格式NV12
venc_chn_attr.stVencAttr.enRotation = VENC_ROTATION_0; // 不旋转
// 码率控制配置:H264 CBR 恒定码率模式
venc_chn_attr.stRcAttr.enRcMode = VENC_RC_MODE_H264CBR;
venc_chn_attr.stRcAttr.stH264Cbr.u32Gop = 25; // I帧间隔,25帧一个I帧
venc_chn_attr.stRcAttr.stH264Cbr.u32SrcFrameRateNum = 25; // 源帧率 分子
venc_chn_attr.stRcAttr.stH264Cbr.u32SrcFrameRateDen = 1; // 源帧率 分母
venc_chn_attr.stRcAttr.stH264Cbr.fr32DstFrameRateNum = 25;// 目标帧率 分子
venc_chn_attr.stRcAttr.stH264Cbr.fr32DstFrameRateDen = 1; // 目标帧率 分母
venc_chn_attr.stRcAttr.stH264Cbr.u32BitRate = 8388608; // 码率 8Mbps
// 创建VENC编码通道
ret = RK_MPI_VENC_CreateChn(VENC_CHN, &venc_chn_attr);
if(ret)
{
printf("Venc Create Failed...\n");
return 0;
}
else
{
printf("Venc create Succeed...\n");
}
// ===================== 3. 绑定VI和VENC模块 =====================
// 定义源模块:VI
MPP_CHN_S vi_chn_s;
vi_chn_s.enModId = RK_ID_VI;
vi_chn_s.s32ChnId = CAMERA_CHN;
// 定义目标模块:VENC
MPP_CHN_S venc_chn_s;
venc_chn_s.enModId = RK_ID_VENC;
venc_chn_s.s32ChnId = VENC_CHN;
// 执行绑定:VI输出 → VENC输入,自动流转数据
ret = RK_MPI_SYS_Bind(&vi_chn_s, &venc_chn_s);
if(ret)
{
printf("RK_MPI_SYS_Bind Failed...\n");
return 0;
}
else
{
printf("RK_MPI_SYS_Bind Succeed...\n");
}
// ===================== 4. 创建取流线程 =====================
pthread_t pid;
// 创建线程,执行get_h264_venc_thread函数,从VENC取码流
pthread_create(&pid, NULL, get_h264_venc_thread, NULL);
// 主线程休眠,保持程序运行
while(1)
{
sleep(1);
}
// ===================== 5. 资源释放(正常退出时执行) =====================
RK_MPI_VI_DisableChn(CAMERA_ID, CAMERA_CHN); // 关闭VI通道
RK_MPI_VENC_DestroyChn(VENC_CHN); // 销毁VENC通道
RK_MPI_SYS_UnBind(&vi_chn_s, &venc_chn_s); // 解绑模块
return 0;
}
四、运行效果与验证
程序移植进板子并运行,具体流程前面的章节讲的很清楚了,这里不再赘述,程序运行成功后,ssh会出现这样一个画面

这就意味着采集成功了。
会在当前目录生成一个 test_camre.h264 文件。
这个文件可以直接用 ffmpeg播放(播放的具体流程也在前面的博客):
ffplay.exe test_canre.h264
出现这个画面:

说明能正常播放、画面清晰不花屏,说明码流获取成功。
五、本章小结
这一章我们完成了从 "理论编码" 到 "实战取流" 的关键一步:
- 学会了 VENC 初始化配置
- 理解了 为什么必须用多线程取码流
- 掌握了 RKMedia 取 H.264 标准码流 的标准写法
- 实现了 保存可播放 H.264 文件