在前几章中,我们依次讲解了 OSD 基础原理、RV1126 RKMedia 的两大核心结构体、OSD 全套 API,同时完成了zlib、FreeType、SDL、SDL_TTF 整套依赖库的交叉编译。
本章将整合所有知识点,从零实现完整工程 :借助 SDL_TTF 渲染文字位图,通过 RV1126 硬件 OSD(RGN 模块)叠加到视频画面,最终由 VENC 模块编码生成 H264 视频文件。代码融合视频采集 (VI)、硬件编码 (VENC)、图层叠加 (OSD/RGN)、SDL 文字渲染、多线程开发五大核心技能,是 RV1126 嵌入式音视频开发的经典综合实战案例。
一、整体方案与技术架构
1.1 业务目标
基于 RV1126 开发板,实现功能闭环:
- 摄像头采集 1080P NV12 原始视频流;
- 使用 SDL+SDL_TTF 渲染自定义时间文字,生成标准位图;
- 调用 RKMedia OSD 接口,将文字图层叠加到原始视频帧;
- 叠加后的视频送入 VENC 编码器,压缩为 H264 码流并本地保存;
- 采用多线程分离文字渲染 & OSD 推送 、H264 码流读取 & 保存两大任务,保证视频流畅无丢帧。
1.2 整体数据流链路
摄像头(VI) → 视频流水线 → OSD硬件图层叠加(RGN) → H264编码(VENC) → 本地文件保存
- VI 模块:负责视频图像采集,输出 NV12 格式原始帧;
- RGN (OSD) 模块:RV1126 硬件图层单元,接收外部位图,硬件完成画面叠加,不占用 CPU;
- VENC 模块:视频编码器,将叠加完成的画面压缩为标准 H264 码流;
- SDL_TTF:纯软件文字渲染工具,负责把字符串转为硬件可识别的像素位图;
- 多线程:解耦渲染、编码、IO 任务,避免单线程阻塞导致视频卡顿。
1.3 前置环境校验
在运行代码前,请确保环境全部就绪:
- 已完成
zlib → FreeType → SDL → SDL_TTF交叉编译; - 字体文件
fzlth.ttf与可执行文件放在同一目录; - RV1126 视频节点
rkispp_scale0正常,摄像头可正常出图; - 开发板系统时间可正常使用(避免水印显示 1970 初始时间戳)。
二、流程

用RV1126多线程输出OSD叠加需要经过上面几个重要步骤,分别是VI模块初始化、VENC模块初始化、RGN模块初始化、多线程进行OSD字库的叠加(里面主要是进行字库的渲染和RV1126的OSD模块叠加)、多线程获取每一帧OSD处理过后的编码数据
2.1. VI 模块的初始化
VI模块的初始化实际上就是对VI_CHN_ATTR_S的参数进行设置、然后调用RK_MPI_VI_SetChnAttr 设置VI模块并使能RK_MPI_VI_EnableChn **,**伪代码如下:
VI_CHN_ATTR_S vi_chn_attr;
。。。。。。。。。。。。。。。(这里是设置VI的属性)
ret = RK_MPI_VI_SetChnAttr(CAMERA_ID, 0, &vi_chn_attr);
ret |= RK_MPI_VI_EnableChn(CAMERA_ID, 0);
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. 初始化 RGN 模块:
RGN是RV1126图层区域功能管理的功能,比方说OSD图层、Bitmap位图都属于RGN模块。所以开发者要用到OSD功能都需要初始化RGN,具体的API描述如下:

第一个传参数:VENC 编码通道号
第二个传参数:VENC_COLOR_TBL_S 结构体指针,默认是NULL
RK_MPI_VENC_RGN_Init(0, NULL);
2.4. 绑定 VI 模块和 VENC 模块,
伪代码如下
MPP_CHN_S vi_chn_s;
MPP_CHN_S venc_chn_s;
ret = RK_MPI_SYS_Bind(&vi_chn_s, &venc_chn_s);
2.5. 创建多线程进行 OSD 字库的叠加:
创建OSD叠加线程主要是对VENC的数据进行字库的创建和OSD的数据叠加,下面是伪代码:
char *pstr = "2019-11-21 15:40:29";
if(TTF_Init() < 0)
{
fprintf(stderr, "Couldn't initialize TTF: %s\n", SDL_GetError());
}
ttf_font = TTF_OpenFont("./fzlth.ttf", 48);
if(ttf_font == NULL)
{
fprintf(stderr, "Couldn't load %d pt font from %s: %s\n", 48, "ptsize", SDL_GetError());
}
text = TTF_RenderText_Solid(ttf_font, pstr, sdl_color);
............................................................
SDL_PixelFormat *pixel_format = (SDL_PixelFormat *)malloc(sizeof(SDL_PixelFormat));
........................
temp = SDL_ConvertSurface(text, pixel_format, 0);
static int get_align16_value(int input_value, int align)
{
int handle_value = 0;
if (align && (input_value % align))
handle_value = (input_value/ align + 1) * align;
return handle_value;
}
{
......................
bitmap_width = get_align16_value(temp->w, 16);
bitmap_height = get_align16_value(temp->h, 16);
if (bitmap_width < 64)
bitmap_width = 64;
if (bitmap_height < 64)
bitmap_height = 64;
BITMAP_S BitMap;
BitMap.enPixelFormat = PIXEL_FORMAT_ARGB_8888;
BitMap.u32Width = bitmap_width;
BitMap.u32Height = bitmap_height;
BitMap.pData = malloc(wxh_size * fmt->BytesPerPixel);
if (!BitMap.pData)
{
printf("ERROR: no mem left for argb8888(%d)!\n", wxh_size * fmt->BytesPerPixel);
break;
}
memcpy(BitMap.pData, temp->pixels, (temp->w) * (temp->h) * fmt->BytesPerPixel);
OSD_REGION_INFO_S RngInfo;
RngInfo.enRegionId = REGION_ID_7;
RngInfo.u32PosX = 0;
RngInfo.u32PosY = 0;
RngInfo.u32Width = bitmap_width;
RngInfo.u32Height = bitmap_height;
RngInfo.u8Enable = 1;
RngInfo.u8Inverse = 0;
ret = RK_MPI_VENC_RGN_SetBitMap(0, &RngInfo, &BitMap);
...........
}
上面是OSD叠加的代码,主要是通过RV1126的RNG模块对VENC进行OSD进行字库叠加。其中划黑色粗体 是这个程序的核心,它需要把每个width和height进行16位对齐,因为OSD需要处理16位对齐的数据(所谓十六位对齐可以理解为,width和height能够被16位整除)。这里面用到16位对齐的方法是get_align16_value , input_value 指的是输入的数值,align是要对齐的数值。
2.6 获取每一帧处理过后的 VENC 数据:
开启一个线程去采集每一帧VENC模块的数据,使用的API是RK_MPI_SYS_GetMediaBuffer , 模块ID是RK_ID_VENC,通道号ID是VENC创建的ID号**。** 这个API的具体作用已我们直接上伪代码:
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);
............................
}
2.7 两大核心结构体
OSD_REGION_INFO_S:OSD 图层属性结构体 作用:定义图层编号、显示坐标、宽高、使能状态。硬性规则:宽高、坐标必须 16 像素对齐,否则硬件解析异常、出现花屏噪点。BITMAP_S:位图像素结构体 作用:承载 SDL_TTF 渲染后的文字像素数据。RV1126 对外声明格式为PIXEL_FORMAT_ARGB_8888,底层硬件实际兼容BGRA8888,直接拷贝 SDL 原生数据会出现颜色错乱。
2.8 核心 API 调用顺序
- 初始化 VI 采集通道并使能;
- 初始化 VENC 编码通道,配置 H264 参数;
- 初始化 OSD (RGN) 模块(OSD 功能入口);
- 绑定 VI 与 VENC 数据流;
- 启动业务线程,循环推送位图、读取码流;
- 程序退出时,按顺序解绑、销毁通道,释放硬件资源。
2.9 SDL_TTF 渲染要点
- 必须先初始化 SDL,再初始化 TTF;
- 手动补全
SDL_PixelFormat所有掩码、偏移字段,保证格式转换正常; - 渲染完成后及时释放
SDL_Surface,避免内存泄漏。
三、完整实战代码
#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>
#include <string.h> // 补充 memset 头文件
// RKMedia 多媒体接口头文件
#include "rkmedia_api.h"
// SDL 图形渲染 + 字体渲染库(SDL1.2 版本)
#include "SDL.h"
#include "SDL_ttf.h"
#include <time.h>
/* 宏定义:摄像头、编码通道编号 */
#define CAMERA_PATH "rkispp_scale0" // 摄像头设备节点名
#define CAMERA_ID 0 // VI 视频输入模块ID
#define CAMERA_CHN 0 // VI 通道号
#define VENC_CHN 0 // VENC 编码通道号
/**
* @brief 数值按指定字节对齐(RK OSD 位图要求宽高16字节对齐)
* @param input_value 原始数值
* @param align 对齐字节数
* @return 对齐后的数值
*/
static int get_align16_value(int input_value, int align)
{
int handle_value = 0;
// 非0对齐值 + 原始值不能被对齐数整除时,向上对齐
if (align && (input_value % align))
handle_value = (input_value / align + 1) * align;
return handle_value;
}
/**
* @brief OSD水印绘制线程:使用SDL+TTF生成文字位图,叠加到VENC编码通道
* @param args 线程入参(未使用)
* @return NULL
*/
void *bitmap_osd_handle_thread(void * args)
{
// 线程分离:线程退出后自动回收资源,无需主线程pthread_join
pthread_detach(pthread_self());
int ret;
TTF_Font *ttf_font; // TTF字体句柄
const char *pstr = "2025-6-11 23:23:23"; // 待绘制的水印文字
SDL_Surface * text_surface; // SDL原始文字画布
SDL_Surface *convert_text_surface; // 格式转换后的ARGB8888画布
SDL_PixelFormat *pixel_format; // 自定义像素格式结构体
// ===================== 1. 初始化TTF字体库 =====================
ret = TTF_Init();
if (ret < 0)
{
printf("TTF_Init Failed...\n");
}
// 打开本地字体文件,字号48
ttf_font = TTF_OpenFont("./fzlth.ttf", 48);
if (ttf_font == NULL)
{
printf("TTF_Open Failed...\n");
}
// ===================== 2. 设置文字颜色并渲染文字到SDL画布 =====================
SDL_Color sdl_color; // SDL颜色结构体(SDL1.2仅包含R/G/B)
sdl_color.r = 0; // 红色分量 0
sdl_color.g = 0; // 绿色分量 0
sdl_color.b = 0; // 蓝色分量 0 → 最终文字为黑色
// 实心模式渲染文字,生成原始SDL Surface画布
text_surface = TTF_RenderText_Solid(ttf_font, pstr, sdl_color);
// ===================== 3. 手动构造ARGB8888像素格式 =====================
// 动态分配像素格式内存,适配RKMedia要求的ARGB8888格式
pixel_format = (SDL_PixelFormat *)malloc(sizeof(SDL_PixelFormat));
pixel_format->BitsPerPixel = 32; // 每个像素32bit
pixel_format->BytesPerPixel = 4; // 每个像素4字节
pixel_format->Amask = 0XFF000000; // Alpha通道掩码(最高8位)
pixel_format->Rmask = 0X00FF0000; // Red通道掩码
pixel_format->Gmask = 0X0000FF00; // Green通道掩码
pixel_format->Bmask = 0X000000FF; // Blue通道掩码
// 将原始文字画布 转换为 ARGB8888 格式画布
convert_text_surface = SDL_ConvertSurface(text_surface, pixel_format, 0);
if (convert_text_surface == NULL)
{
printf("convert_text_surface Failed...\n");
}
// ===================== 4. 构造RKMedia OSD位图结构体 BITMAP_S =====================
BITMAP_S bitmap;
// 画布宽度向上16字节对齐(RK OSD强制要求)
bitmap.u32Width = get_align16_value(convert_text_surface->w, 16);
// 画布高度向上16字节对齐
bitmap.u32Height = get_align16_value(convert_text_surface->h, 16);
// 指定位图像素格式为 ARGB8888
bitmap.enPixelFormat = PIXEL_FORMAT_ARGB_8888;
// 分配对齐后的位图内存,calloc自动清零,避免残留垃圾数据
bitmap.pData = calloc(1, bitmap.u32Width * bitmap.u32Height * 4);
// ===================== 5. 逐行拷贝SDL画布数据到RK位图(核心:解决行跨度pitch错位) =====================
int src_w = convert_text_surface->w; // SDL画布实际宽度(像素)
int src_h = convert_text_surface->h; // SDL画布实际高度(像素)
int src_pitch = convert_text_surface->pitch;// SDL单行真实字节数(行跨度,SDL内部行对齐)
// 指针强制转换:适配C语言void*隐式转换,兼容编译
uint8_t *src_buf = (uint8_t*)convert_text_surface->pixels;
uint8_t *dst_buf = (uint8_t*)bitmap.pData;
// 逐行拷贝:跳过SDL行对齐填充字节,只拷贝有效像素,根治花屏/歪斜
for (int y = 0; y < src_h; y++)
{
memcpy(dst_buf + y * bitmap.u32Width * 4,
src_buf + y * src_pitch,
src_w * 4);
}
// ===================== 6. 配置OSD区域信息,设置水印位置与使能 =====================
OSD_REGION_INFO_S rgn_info;
rgn_info.enRegionId = REGION_ID_0; // OSD区域编号0
rgn_info.u32Width = bitmap.u32Width;// OSD区域宽度
rgn_info.u32Height = bitmap.u32Height;// OSD区域高度
rgn_info.u32PosX = 128; // 水印左上角X坐标
rgn_info.u32PosY = 128; // 水印左上角Y坐标
rgn_info.u8Enable = 1; // 使能OSD叠加
rgn_info.u8Inverse = 0; // 不做颜色翻转
// ===================== 7. 将位图设置到VENC编码通道,完成水印叠加 =====================
ret = RK_MPI_VENC_RGN_SetBitMap(VENC_CHN, &rgn_info, &bitmap);
if (ret)
{
printf("RK_MPI_VENC_RGN_SetBitMap Failed...\n");
}
else
{
printf("RK_MPI_VENC_RGN_SetBitMap Success...\n");
}
// ===================== 8. 统一释放所有动态内存与SDL资源(防止内存泄漏) =====================
free(bitmap.pData); // 释放RK位图内存
free(pixel_format); // 释放自定义像素格式
SDL_FreeSurface(text_surface); // 释放原始SDL画布
SDL_FreeSurface(convert_text_surface); // 释放转换后SDL画布
TTF_CloseFont(ttf_font); // 关闭字体文件
TTF_Quit(); // 退出TTF字体库
return NULL;
}
/**
* @brief H264码流获取线程:从编码通道取码流并写入本地文件
* @param args 线程入参(未使用)
* @return NULL
*/
void *get_h264_data_thread(void *args)
{
pthread_detach(pthread_self());
// 以读写模式创建本地h264文件
FILE *h264_file = fopen("test_osd_venc.h264", "w+");
MEDIA_BUFFER mb; // RK媒体缓冲区句柄
while (1)
{
// 阻塞获取VENC编码后的码流缓冲区
mb = RK_MPI_SYS_GetMediaBuffer(RK_ID_VENC, VENC_CHN, -1);
if (!mb)
{
printf("get media_buffer failed...\n");
break;
}
printf("get OSD_buffer success...\n");
// 将码流数据写入文件
fwrite(RK_MPI_MB_GetPtr(mb), RK_MPI_MB_GetSize(mb), 1, h264_file);
// 释放媒体缓冲区(必须释放,否则内存溢出)
RK_MPI_MB_ReleaseBuffer(mb);
}
return NULL;
}
int main()
{
int ret;
VI_CHN_ATTR_S vi_chn_attr;
// ===================== 1. 配置并初始化VI视频输入(摄像头) =====================
vi_chn_attr.pcVideoNode = CAMERA_PATH;
vi_chn_attr.u32Width = 1920;
vi_chn_attr.u32Height = 1080;
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;
ret = RK_MPI_VI_SetChnAttr(CAMERA_ID, CAMERA_CHN, &vi_chn_attr);
if (ret)
{
printf("Vi Set Attr Failed.....\n");
return 0;
}
printf("Vi Set Attr Success.....\n");
ret = RK_MPI_VI_EnableChn(CAMERA_ID, CAMERA_CHN);
if (ret)
{
printf("Vi Enable Attr Failed.....\n");
return 0;
}
printf("Vi Enable Attr Success.....\n");
// ===================== 2. 配置并初始化VENC视频编码(H264) =====================
VENC_CHN_ATTR_S venc_chn_attr;
memset(&venc_chn_attr, 0, sizeof(venc_chn_attr));
// 编码基础属性
venc_chn_attr.stVencAttr.enType = RK_CODEC_TYPE_H264;
venc_chn_attr.stVencAttr.imageType = IMAGE_TYPE_NV12;
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 = 77;
// 码率控制:CBR恒定码率
venc_chn_attr.stRcAttr.enRcMode = VENC_RC_MODE_H264CBR;
venc_chn_attr.stRcAttr.stH264Cbr.u32Gop = 25;
venc_chn_attr.stRcAttr.stH264Cbr.u32BitRate = 2000000; // 2Mbps码率
venc_chn_attr.stRcAttr.stH264Cbr.fr32DstFrameRateDen = 1;
venc_chn_attr.stRcAttr.stH264Cbr.fr32DstFrameRateNum = 25;
venc_chn_attr.stRcAttr.stH264Cbr.u32SrcFrameRateDen = 1;
venc_chn_attr.stRcAttr.stH264Cbr.u32SrcFrameRateNum = 25;
ret = RK_MPI_VENC_CreateChn(0, &venc_chn_attr);
if (ret)
{
printf("ERROR: create VENC[0] error! ret=%d\n", ret);
return 0;
}
printf("VENC SUCCESS\n");
// ===================== 3. 初始化VENC OSD区域模块 =====================
ret = RK_MPI_VENC_RGN_Init(VENC_CHN, NULL);
if (ret)
{
printf("ERROR: create VENC_RGN error! ret=%d\n", ret);
return 0;
}
printf("VENC SUCCESS\n");
// ===================== 4. 绑定VI(摄像头) → VENC(编码器) 数据流 =====================
MPP_CHN_S vi_chn_s;
MPP_CHN_S venc_chn_s;
vi_chn_s.enModId = RK_ID_VI;
vi_chn_s.s32ChnId = CAMERA_CHN;
venc_chn_s.enModId = RK_ID_VENC;
venc_chn_s.s32ChnId = VENC_CHN;
ret = RK_MPI_SYS_Bind(&vi_chn_s, &venc_chn_s);
if (ret)
{
printf("RK_MPI_SYS_Bind Failed...\n");
return 0;
}
printf("RK_MPI_SYS_Bind SUCCESS...\n");
// ===================== 5. 启动两个工作线程 =====================
pthread_t bitmap_pid, venc_pid;
pthread_create(&bitmap_pid, NULL, bitmap_osd_handle_thread, NULL); // OSD水印线程
pthread_create(&venc_pid, NULL, get_h264_data_thread, NULL); // 码流保存线程
// 主线程死循环阻塞,维持程序运行
while (1)
{
sleep(2);
}
// 以下代码实际永远不会执行
RK_MPI_SYS_UnBind(&vi_chn_s, &venc_chn_s);
RK_MPI_VENC_DestroyChn(VENC_CHN);
RK_MPI_VI_DisableChn(CAMERA_ID, CAMERA_CHN);
return 0;
}
四、代码分模块深度解析
整体分为 5 大模块 ,重点详解 OSD 水印线程,其余模块简略说明。
4.1 模块 1:基础头文件 + 工具函数
- 引入系统库、线程库、SDL/TTF 图形库、RKMedia 多媒体库。
get_align16_value:RK 平台 OSD 位图强制要求宽 / 高 16 字节对齐,该函数实现向上对齐,是 RK OSD 的前置要求。
4.2 模块 2:核心重点 ------ bitmap_osd_handle_thread 水印线程
4.2.1 线程基础设置
pthread_detach(pthread_self());
- 作用:分离线程 。线程退出后系统自动回收栈 / 资源,主线程不需要调用
pthread_join等待,嵌入式多线程标准写法。
4.2.2 TTF 字体库初始化 & 打开字体文件
TTF_Init();
TTF_OpenFont("./fzlth.ttf", 48);
- 依赖:本地目录必须存在
fzlth.ttf字体文件,否则文字渲染失败。 - 逻辑:SDL_TTF 专门用于在 SDL 画布上绘制矢量文字,必须先初始化库、再打开字体。
4.2.3 文字颜色 + 渲染原始 SDL 画布
SDL_Color sdl_color;
text_surface = TTF_RenderText_Solid(ttf_font, pstr, sdl_color);
SDL_Color:SDL1.2 仅支持 R/G/B 三通道,无 Alpha。TTF_RenderText_Solid:实心渲染模式,生成一张纯文字的 SDL 画布(原始格式非 ARGB8888)。
4.2.4 手动构造 ARGB8888 像素格式
pixel_format = malloc(...);
pixel_format->Amask/Rmask/Gmask/Bmask...
convert_text_surface = SDL_ConvertSurface(...)
- 背景:RKMedia OSD 只识别 ARGB8888 位图格式,原始文字画布格式不匹配,必须转换。
- 做法:手动定义 32 位 ARGB 掩码规则,调用
SDL_ConvertSurface完成格式转换。 - 坑点:SDL1.2 没有现成 ARGB8888 接口,只能手动构造格式结构体。
4.2.5 构建 RK 标准位图 BITMAP_S
BITMAP_S bitmap;
bitmap.u32Width = 16对齐宽度;
bitmap.u32Height = 16对齐高度;
bitmap.enPixelFormat = PIXEL_FORMAT_ARGB_8888;
bitmap.pData = calloc(...);
BITMAP_S:RKMedia 定义的标准位图结构体,给 OSD 模块使用。- 关键点:
- 宽高必须 16 字节对齐,否则驱动解析异常、花屏;
calloc分配内存并清零,避免随机垃圾像素;- 格式严格指定为
PIXEL_FORMAT_ARGB_8888。
4.2.6 【最核心】逐行拷贝数据(解决歪斜 / 花屏)
int src_pitch = convert_text_surface->pitch;
for (int y = 0; y < src_h; y++)
{
memcpy(..., src_buf + y * src_pitch, src_w * 4);
}
关键概念解释:pitch(行跨度)
SDL 为了内存访问效率,每行像素末尾会额外填充字节做对齐:
w * 4= 单行有效像素字节数;pitch= 单行实际占用总字节数(有效像素 + 填充字节)。
为什么会歪 / 花?
- 旧代码:
memcpy(整块内存),会把 每行末尾的填充垃圾字节 也拷贝进去 → 像素串行、错位、花屏。 - 现在写法:逐行拷贝,只复制有效像素,跳过 SDL 填充字节 → 图像正常。
4.2.7 配置 OSD 区域并叠加水印
OSD_REGION_INFO_S rgn_info;
RK_MPI_VENC_RGN_SetBitMap(VENC_CHN, &rgn_info, &bitmap);
1.OSD_REGION_INFO_S:配置水印位置、大小、开关。
u32PosX/u32PosY:水印在画面上的左上角坐标;u8Enable = 1:开启 OSD 叠加。
2.RK_MPI_VENC_RGN_SetBitMap:RKMedia 核心接口,将位图绑定到编码通道,摄像头画面编码时自动叠加水印。
4.2.8 资源释放(嵌入式必做)
依次释放:位图内存 → 格式结构体 → SDL 画布 → 字体 → 退出 TTF 库。 不释放会造成内存泄漏,长时间运行程序崩溃。
4.3 模块 3:H264 码流获取线程
逻辑很简单:
- 打开本地文件用于保存码流;
- 循环调用
RK_MPI_SYS_GetMediaBuffer阻塞读取编码后的 H264 数据; fwrite写入文件,RK_MPI_MB_ReleaseBuffer释放缓冲区(必须释放)。
4.4 模块 4:VI 视频输入初始化
标准 RKMedia 摄像头初始化流程:
- 配置分辨率、像素格式 (NV12)、缓冲数;
RK_MPI_VI_SetChnAttr设置属性 →RK_MPI_VI_EnableChn开启摄像头。
4.5 模块 5:VENC 编码 + OSD 初始化 + 数据流绑定
- VENC 配置:编码格式 H264、分辨率、CBR 恒定码率、帧率、GOP;
RK_MPI_VENC_RGN_Init:初始化编码通道的 OSD 水印模块(必须先初始化才能叠加水印);RK_MPI_SYS_Bind:绑定 VI → VENC,摄像头画面自动流向编码器;- 创建两个子线程,主线程死循环保活。
4.6 核心流程总梳理
摄像头(VI) → 数据流绑定 → 编码器(VENC)
↓
提前初始化OSD模块
↓
子线程1:SDL+TTF生成文字位图 → 设置到OSD → 画面叠加水印
子线程2:循环取出H264码流 → 写入本地文件
四、补充关键注意点(踩坑总结)
- 字体文件
fzlth.ttf必须放在程序同级目录,否则字体打开失败; - SDL 1.2 无 Alpha 通道、无新版格式接口,只能手动构造
pixel_format; pitch行跨度 是水印花屏的核心根源,必须逐行拷贝;- RK OSD 位图强制 16 字节对齐,对齐函数不可省略;
- 所有
malloc/calloc、SDL 资源、媒体 Buffer 都必须手动释放。
五、编译配置与板卡部署
-
使用
make编译代码,生成可执行文件osd_h264_demo; -
将 可执行文件、fzlth.ttf 字体文件 拷贝到 RV1126 开发板同一目录;
-
若编译为动态库,需将
libSDL.so、libSDL_ttf.so等依赖库拷贝至板卡/usr/lib;静态库则无需额外拷贝; -
添加执行权限并运行:
chmod +x osd_h264_demo ./osd_h264_demo -
运行一段时间后
Ctrl+C终止程序,使用播放器打开test_osd_venc.h264查看带时间水印的视频。
六、常见问题与排错方案(实战踩坑总结)
结合调试过程中高频问题,汇总原因与解决方案:
-
问题:水印区域出现彩色噪点 / 花屏 原因:宽高、坐标未做 16 对齐;结构体未
memset清零产生脏数据。 解决:统一使用get_align16_value函数,所有局部结构体强制清零。 -
问题:文字颜色错乱、重影、变色 原因:未做
ARGB → BGRA像素转换,SDL 格式与 RV1126 硬件格式不匹配。 解决:补全逐像素格式转换逻辑。 -
问题:SDL/TTF 初始化、字体加载失败 原因:库文件缺失、字体文件路径错误、交叉编译版本不匹配。 解决:检查动态库部署,确保
fzlth.ttf与程序同目录。 -
问题:程序运行一段时间后卡死 原因:媒体缓冲区
MEDIA_BUFFER未释放,或 SDL Surface 未释放导致内存泄漏。 解决:严格执行资源释放逻辑。 -
问题:水印只显示一帧,后续消失 原因:
RK_MPI_VENC_RGN_SetBitMap仅执行一次,未放入死循环。 解决:在循环中持续推送位图数据。
七、本章总结与知识点汇总
7.1 核心知识点复盘
- 架构层面 :掌握 RV1126 音视频流水线
VI → OSD → VENC,理解硬件模块分工与数据流绑定逻辑; - OSD 核心 :吃透
OSD_REGION_INFO_S、BITMAP_S两大结构体,牢记16 像素对齐、像素格式转换两大硬件约束; - SDL_TTF 应用:掌握 SDL 初始化、字体加载、文字渲染、Surface 资源管理全套流程;
- 工程思想:使用多线程解耦任务,区分硬件资源与内存资源的释放规则,培养嵌入式代码健壮性思维。
7.2 拓展学习方向
- 实现多行水印:创建多个 OSD 图层,叠加时间 + 设备编号;
- 优化性能:降低文字渲染频率(1 秒渲染 1 次即可,无需 25 帧全渲染),减少 CPU 占用;
- 自定义水印:修改字体、颜色、位置、透明度,适配不同场景需求;
- 视频推流:将 H264 码流改为网络推流,替代本地文件保存。
本案例是 RV1126 嵌入式音视频开发的综合入门项目,吃透本代码后,可延伸学习视频截图、动态贴纸、多路 OSD 叠加等进阶功能。