欢迎光临
我们一直在努力

RV1126——OSD模块和SDL_TTF结合输出H264文件

 在前几章中,我们依次讲解了 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_valueinput_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 叠加等进阶功能。 

    赞(0)
    未经允许不得转载:171主机测评 » RV1126——OSD模块和SDL_TTF结合输出H264文件
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址