结合项目实战需求,想要在 RV1126 视频 OSD 中实现中文、数字、时间等自定义文字水印 ,单纯依靠芯片硬件 OSD 模块无法完成文字绘制工作。行业内主流方案是使用 SDL + SDL_TTF 组合库实现矢量文字渲染,再将渲染后的位图数据对接 RKMedia OSD 接口,最终完成视频画面水印叠加。
SDL_TTF 是专门搭配 SDL 使用的字体渲染库,底层依赖 FreeType2 字体引擎解析字体文件,能够高效渲染 TrueType 矢量字体,输出高清文字画面。由于 RV1126 是 ARM 架构嵌入式 Linux 板卡,PC 端原生库无法直接运行,必须在 Ubuntu 编译主机上完成交叉编译,生成适配 ARM 架构的库文件。
本文将参考标准编译流程,依次完成 zlib、FreeType、SDL、SDL_TTF 四层依赖库交叉编译,全程基于 RV1126 官方 SDK 工具链,步骤完整、注释详细,新手也可一步步跟着操作。
一、SDL_TTF 库整体介绍
1.1 库功能说明
SDL_TTF 是一款轻量级、跨平台的 TrueType 字体渲染开源库,必须依赖 SDL 基础图形库运行 。它底层调用 FreeType2 内核解析 .ttf、.ttc 等主流字体文件,开发者无需手动编写字体点阵、轮廓绘制逻辑,仅调用简单 API 就能快速渲染出不同字号、颜色的文字。
结合我们的 OSD 水印场景:
- 程序调用 SDL_TTF 加载自定义字体文件;
- 渲染时间、设备编号、通道名称等文本,生成标准位图数据;
- 将位图数据传入 RV1126 RKMedia OSD 接口,由硬件完成画面叠加。
该组合方案兼容性强、渲染效果清晰,是嵌入式视频设备实现自定义文字水印的通用方案。
1.2 依赖层级关系(核心重点)
SDL_TTF 并非独立库,存在多层依赖,编译顺序绝对不能颠倒 ,正确编译链路如下: zlib(压缩库) → FreeType(字体解析内核) → SDL(基础图形库) → SDL_TTF(文字渲染库)
简单理解:
- zlib:为 FreeType 提供文件压缩、解压支持;
- FreeType:真正解析字体文件的核心引擎;
- SDL:提供像素画布、内存管理等基础图形能力;
- SDL_TTF:封装 FreeType,对接 SDL,对外提供简易文字渲染接口。
二、编译环境整体说明
2.1 软硬件环境
- 编译主机:Ubuntu 系统(Ubuntu 16.04/18.04/20.04 均可)
- 目标板卡:瑞芯微 RV1126(ARM32 架构 Linux 系统)
- 交叉工具链:RV1126 SDK 自带
arm-linux-gnueabihf编译器 - 编译产物:ARM 架构静态 / 动态库,最终部署到 RV1126 开发板使用
2.2 工具链路径(本文统一路径)
RV1126 官方工具链固定路径,下文所有编译命令均以此为准:
/opt/rv1126_rv1109_linux_sdk_v1.8.0_20210224/prebuilts/gcc/linux-x86/arm/gcc-arm-8.3-2019.03-x86_64-arm-linux-gnueabihf/bin/arm-linux-gnueabihf-gcc
2.3 源码与安装目录规划
为方便统一管理库文件,我们约定所有库的源码随意存放,编译产物统一安装到 /opt/ 目录下:
| 库名称 | 源码版本 | 最终安装目录 |
|---|---|---|
| zlib | zlib-1.2.11 | /opt/arm_zlib |
| FreeType | freetype-2.4.10 | /opt/arm_freetype |
| SDL | SDL-1.2.15 | /opt/arm_sdl |
| SDL_TTF | SDL_TTF 对应版本 | /opt/arm_sdl_ttf_install |
操作前置:将
zlib-1.2.11、freetype-2.4.10、SDL-1.2.15、SDL_TTF 源码包解压到 Ubuntu 主机任意目录。
三、分层交叉编译完整流程
3.1 第一层:zlib 库交叉编译
zlib 是通用压缩解压缩库,FreeType 字体库依赖它完成字体文件读取与解压,是整个编译链路的最底层依赖。
1.进入 zlib 源码根目录
cd $(PWD)/zlib-1.2.11
2.指定交叉编译器并执行配置 直接指定 RV1126 对应的 ARM 交叉编译器,执行配置脚本:
export CC=/opt/rv1126_rv1109_linux_sdk_v1.8.0_20210224/prebuilts/gcc/linux-x86/arm/gcc-arm-8.3-2019.03-x86_64-arm-linux-gnueabihf/bin/arm-linux-gnueabihf-gcc
./configure --prefix=/opt/arm_zlib
--prefix:指定库文件、头文件的安装目录,此处设置为/opt/arm_zlib。
3.多线程编译 -j8 代表使用 8 线程编译,根据自己 CPU 核心数调整,加速编译过程:
make -j8
4.安装编译产物 将编译生成的头文件、库文件拷贝到 --prefix 指定目录:
make install
执行完成后,/opt/arm_zlib 目录下会生成 include(头文件)、lib(库文件)两个文件夹,zlib 编译完成。
3.2 第二层:FreeType 字体引擎交叉编译
FreeType 是 SDL_TTF 依赖的字体解析核心,负责解析 .ttf 字体文件、提取字体轮廓,必须在 zlib 编译完成后再操作。
1.进入 FreeType 源码目录
cd $(PWD)/freetype-2.4.10
2.执行交叉编译配置 指定编译器、依赖 zlib 路径、目标架构、安装目录:
./configure CC=/opt/rv1126_rv1109_linux_sdk_v1.8.0_20210224/prebuilts/gcc/linux-x86/arm/gcc-arm-8.3-2019.03-x86_64-arm-linux-gnueabihf/bin/arm-linux-gnueabihf-gcc \
--with-zlib=/opt/arm_zlib/lib \
--host=arm-linux \
--prefix=/opt/arm_freetype
参数说明:
CC:指定 ARM 交叉编译器;--with-zlib:指定上一步编译好的 zlib 库路径,解决依赖关联;--host=arm-linux:指定编译目标为 ARM 嵌入式平台;--prefix:指定 FreeType 最终安装目录。
3.编译 + 安装
make -j8
make install
安装完成后,/opt/arm_freetype 目录生成对应头文件与库文件,FreeType 编译结束。
3.3 第三层:SDL 基础图形库交叉编译
SDL 是一套跨平台多媒体基础库,提供画布创建、像素管理、内存操作等基础能力,是 SDL_TTF 的直接依赖。
1.进入 SDL 源码目录
cd $(PWD)/SDL-1.2.15
2.交叉编译配置
./configure CC=/opt/rv1126_rv1109_linux_sdk_v1.8.0_20210224/prebuilts/gcc/linux-x86/arm/gcc-arm-8.3-2019.03-x86_64-arm-linux-gnueabihf/bin/arm-linux-gnueabihf-gcc \
--host=arm-linux-gnueabihf \
--prefix=/opt/arm_sdl
参数说明:
--host=arm-linux-gnueabihf:精准匹配 RV1126 的 ARMhf 软浮点架构;--prefix:SDL 库安装目录。
3.编译 + 安装
make -j8
make install
执行完毕,/opt/arm_sdl 目录生成 SDL 头文件与库文件。
3.4 第四层:SDL_TTF 文字库交叉编译
前三层依赖全部编译完成后,最后编译我们最终需要的 SDL_TTF 文字渲染库,这也是整个编译链路的目标库。
1.进入 SDL_TTF 源码根目录
2.执行完整配置命令 关联前面所有已编译好的 SDL、FreeType 库,指定编译架构与安装路径:
./configure CC=/opt/rv1126_rv1109_linux_sdk_v1.8.0_20210224/prebuilts/gcc/linux-x86/arm/gcc-arm-8.3-2019.03-x86_64-arm-linux-gnueabihf/bin/arm-linux-gnueabihf-gcc \
--host=arm-linux-gnueabihf \
--prefix=/opt/arm_sdl_ttf_install \
--with-sdl-prefix=/opt/arm_sdl \
--with-freetype-prefix=/opt/arm_freetype
参数重点解读:
--with-sdl-prefix:指定已编译完成的 SDL 库路径,让配置脚本自动识别 SDL 依赖;--with-freetype-prefix:指定已编译完成的 FreeType 库路径,关联字体解析引擎;- 其余参数与上层库保持一致,保证架构统一。
3.编译 + 安装
make -j8
make install
四、查看所有编译产物
全部库编译安装完成后,进入 /opt 目录查看所有产物文件夹:
cd /opt
ls
正常会看到以下目录,代表四层库全部编译成功:
arm_zlib:zlib 压缩库arm_freetype:FreeType 字体引擎库arm_sdl:SDL 基础图形库arm_sdl_ttf_install:SDL_TTF 文字渲染库
每个目录内部都包含 include(头文件,代码编译时引用)和 lib(库文件,链接阶段使用)两个子目录,是后续 OSD 工程开发的核心文件。
五、编译后工程使用说明
5.1 代码引用头文件
在我们的 OSD 水印代码中,需要引入 SDL 和 SDL_TTF 头文件,对应路径:
- SDL 头文件:
/opt/arm_sdl/include - SDL_TTF 头文件:
/opt/arm_sdl_ttf_install/include
5.2 Makefile 配置(交叉编译工程必备)
在项目 Makefile 中添加头文件路径、库文件路径,并链接所有依赖库,示例配置:
makefile
# 交叉编译器
CROSS_COMPILE = /opt/rv1126_rv1109_linux_sdk_v1.8.0_20210224/prebuilts/gcc/linux-x86/arm/gcc-arm-8.3-2019.03-x86_64-arm-linux-gnueabihf/bin/arm-linux-gnueabihf-
CC = $(CROSS_COMPILE)gcc
# 头文件路径
INC += -I/opt/arm_zlib/include
INC += -I/opt/arm_freetype/include
INC += -I/opt/arm_sdl/include
INC += -I/opt/arm_sdl_ttf_install/include
# 库文件路径
LIB_PATH += -L/opt/arm_zlib/lib
LIB_PATH += -L/opt/arm_freetype/lib
LIB_PATH += -L/opt/arm_sdl/lib
LIB_PATH += -L/opt/arm_sdl_ttf_install/lib
# 链接依赖库
LIBS += -lz -lfreetype -lSDL -lSDL_ttf -lpthread -lm
5.3 板卡运行注意事项
- 若编译为动态库(.so) :需要将所有
.so文件拷贝到 RV1126 开发板/usr/lib目录,否则程序运行会提示库找不到; - 若编译为静态库(.a):无需拷贝库文件,可直接运行,嵌入式开发推荐使用静态库,部署更简单。
六、常见编译问题与排查思路
-
configure 提示
freetype not found原因:FreeType 未编译成功,或--with-freetype-prefix路径填写错误。 解决:重新编译 FreeType,核对路径与配置参数。 -
configure 提示
sdl not found原因:SDL 库未安装、--with-sdl-prefix路径错误。 解决:检查 SDL 编译产物,修正路径参数。 -
编译过程报大量语法错误 原因:误用 PC 端 gcc 编译器,未正确指定 ARM 交叉编译器。 解决:严格使用文档中指定的
arm-linux-gnueabihf-gcc。 -
开发板运行程序提示
error while loading shared libraries原因:使用动态库编译,板卡缺失对应.so文件。 解决:将所有依赖动态库拷贝至板卡系统库目录。
七、渲染文字并保存
进行代码实战将sdl_ttf的库渲染出文字库并以bmp的图像的方式保存
7.1 渲染文字流程

上面是SDL_TTF输出文字图层的几个重点步骤,分别是ttf_init初始化、TTF_OpenFont打开字符库、TTF_RenderUTF8_Solid渲染字体、填充SDL_PixelFormat信息、SDL_ConvertSurface进行像素转换
7.2 SDL_TTF 库每个步骤的讲解
下面我们对每一个步骤的操作进行讲解
1. TTF_Init初始化TTF的全局环境
它的作用是初始化TTF的库,是做SDL_TTF渲染的第一步

2. TTF_OpenFont打开字符库

它的作用是打开字体库,同时设置字体的大小,我们来看看它的传参
TTF_Font * SDLCALL TTF_OpenFont(const char *file, int ptsize);
第一个参数:需要加载的字符库的名称,我们这里选择的是方正字库fzlth.ttf。
第二个参数:设置字体的大小,方正字库48效果最好。
3. TTF_RenderUTF8_Solid
对输入的字符串进行渲染

SDL_Surface * SDLCALL TTF_RenderUTF8_Solid(TTF_Font *font, const char *text, SDL_Color fg);
第一个参数:TTF_Font结构体指针
第二个参数:需要渲染的字符串
第三个参数:字体颜色的设置,用SDL_Color进行设置,这个结构体分别是r、g、b三种颜色进行设置,这里的rgb都是用16进制表示。在本实例中我用三个0xff代表的是三个255,也就是字体是白色。
4. 填充 SDL_PixelFormat 信息

SDL_PixelFormat主要是填充一些像素信息,其中重点设置的参数就上面那几个。
- BitsPerPixel **:**SDL的每个像素位
- BytesPerPixel **:**SDL的每个像素占的字节数
5. SDL_ConvertSurface
SDL_ConvertSurface对渲染的字体进行像素转换,转换成位图
SDL_Surface * SDLCALL SDL_ConvertSurface (SDL_Surface *src, SDL_PixelFormat *fmt, Uint32 flags);
第一个参数:SDL_Surface 结构体指针
第二个参数:SDL_PixelFormat 结构体指针
第三个参数:标识符,默认为0
7.3 代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "SDL.h"
#include "SDL_ttf.h"
#include <time.h>
int main()
{
// -------------------------- 1. 变量定义 --------------------------
// 待渲染的文字内容(这里是固定时间,可改为动态时间)
char *pstr = "2019-11-21 15:40:29";
// 自定义像素格式(适配 OSD 需求的 32 位 BGRA 格式)
SDL_PixelFormat *fmt = NULL;
// 字体文件句柄
TTF_Font *font = NULL;
// 渲染后的原始文字表面、格式转换后的目标表面
SDL_Surface *text_surface = NULL, *convert_surface = NULL;
// -------------------------- 2. 初始化 SDL 与 SDL_ttf --------------------------
// 注意:SDL_ttf 依赖 SDL,必须先初始化 SDL
if (SDL_Init(SDL_INIT_VIDEO) < 0)
{
fprintf(stderr, "SDL 初始化失败: %s\n", SDL_GetError());
return -1;
}
// 初始化 SDL_ttf 库
if (TTF_Init() < 0)
{
fprintf(stderr, "SDL_ttf 初始化失败: %s\n", SDL_GetError());
SDL_Quit();
return -1;
}
// -------------------------- 3. 加载字体文件 --------------------------
// 加载指定路径的 TTF 字体文件,设置字体大小为 48
font = TTF_OpenFont("./fzlth.ttf", 48);
if (font == NULL)
{
fprintf(stderr, "加载字体文件失败: %s\n", SDL_GetError());
TTF_Quit();
SDL_Quit();
return -1;
}
// -------------------------- 4. 定义文字颜色并渲染 --------------------------
// SDL_Color 格式:{R, G, B, A}(红、绿、蓝、透明度)
// 这里设置为白色不透明:R=255, G=255, B=255, A=255
SDL_Color text_color = {0xFF, 0xFF, 0xFF, 0xFF};
// 背景颜色(仅 TTF_RenderText_Shaded 需要,设置为黑色)
SDL_Color bg_color = {0x00, 0x00, 0x00, 0xFF};
// 方式1:Solid 模式(透明背景,适合叠加)
text_surface = TTF_RenderUTF8_Solid(font, pstr, text_color);
// 方式2:Shaded 模式(带背景色,适合需要黑底白字的场景)
// text_surface = TTF_RenderUTF8_Shaded(font, pstr, text_color, bg_color);
if (text_surface == NULL)
{
fprintf(stderr, "文字渲染失败: %s\n", SDL_GetError());
TTF_CloseFont(font);
TTF_Quit();
SDL_Quit();
return -1;
}
// -------------------------- 5. 自定义 32 位 BGRA 像素格式(适配 OSD) --------------------------
// 为了后续对接 RV1126 OSD,必须使用 32 位 BGRA 格式(B-G-R-A 顺序)
fmt = (SDL_PixelFormat *)malloc(sizeof(SDL_PixelFormat));
if (fmt == NULL)
{
fprintf(stderr, "内存分配失败\n");
SDL_FreeSurface(text_surface);
TTF_CloseFont(font);
TTF_Quit();
SDL_Quit();
return -1;
}
memset(fmt, 0, sizeof(SDL_PixelFormat));
// 32 位像素格式配置
fmt->BitsPerPixel = 32; // 每个像素 32 位
fmt->BytesPerPixel = 4; // 每个像素占 4 字节
// 颜色掩码(定义每个颜色分量在 32 位中的位置)
fmt->Rmask = 0x00FF0000; // 红色分量:第 16-23 位
fmt->Gmask = 0x0000FF00; // 绿色分量:第 8-15 位
fmt->Bmask = 0x000000FF; // 蓝色分量:第 0-7 位
fmt->Amask = 0xFF000000; // 透明分量:第 24-31 位
// 位移量(每个颜色分量的起始偏移)
fmt->Rshift = 16;
fmt->Gshift = 8;
fmt->Bshift = 0;
fmt->Ashift = 24;
// -------------------------- 6. 格式转换 --------------------------
// 将原始渲染表面转换为自定义的 32 位格式
convert_surface = SDL_ConvertSurface(text_surface, fmt, 0);
if (convert_surface == NULL)
{
fprintf(stderr, "格式转换失败: %s\n", SDL_GetError());
free(fmt);
SDL_FreeSurface(text_surface);
TTF_CloseFont(font);
TTF_Quit();
SDL_Quit();
return -1;
}
// -------------------------- 7. 保存 BMP 调试(可选) --------------------------
// 保存转换后的表面为 BMP 图片,方便查看渲染效果
SDL_SaveBMP(convert_surface, "text.bmp");
printf("文字渲染成功,已保存为 text.bmp\n");
// -------------------------- 8. 资源释放 --------------------------
free(fmt); // 释放自定义格式结构体
SDL_FreeSurface(text_surface); // 释放原始渲染表面
SDL_FreeSurface(convert_surface); // 释放转换后的表面
TTF_CloseFont(font); // 关闭字体文件句柄
TTF_Quit(); // 退出 SDL_ttf
SDL_Quit(); // 退出 SDL
return 0;
}
1. 头文件引入
#include <stdio.h>
#include "SDL.h"
#include "SDL_ttf.h"
#include <time.h>
stdio.h:标准输入输出,用于printf/fprintf。SDL.h:SDL 基础图形库头文件,提供SDL_Surface、SDL_PixelFormat等核心类型。SDL_ttf.h:SDL_ttf 字体渲染库头文件,提供TTF_Init、TTF_RenderText等接口。time.h:代码中未实际使用,推测是后续动态时间渲染的预留头文件。
2. 主函数与变量定义
int main()
{
char *pstr = "2019-11-21 15:40:29";
SDL_PixelFormat *fmt;
TTF_Font *font;
SDL_Surface *text, *temp;
pstr:待渲染的文字字符串,这里是固定时间文本。fmt:自定义像素格式结构体,用于指定渲染后的位图格式。font:字体文件句柄,用于后续加载.ttf字体。text:SDL 渲染生成的原始文字表面(Surface)。temp:转换格式后的目标表面,用于适配后续 OSD 需求。
3. SDL_ttf 初始化
//初始化SDL_TTF的全局参数
if (TTF_Init() < 0)
{
fprintf(stderr, "Couldn't initialize TTF: %s\n", SDL_GetError());
}
SDL_Quit();
TTF_Init():初始化 SDL_ttf 库,返回 0 表示成功,-1 表示失败。SDL_Quit():这里存在严重错误 :如果初始化失败,直接调用SDL_Quit(),但 SDL 本身还未初始化;如果初始化成功,也会直接调用SDL_Quit(),导致后续代码完全无法执行。
4. 加载字体文件
//使用了fzlth.ttf字库,字体大小最合适设置为48
font = TTF_OpenFont("./fzlth.ttf", 48);
if (font == NULL)
{
fprintf(stderr, "Couldn't load %d pt font from %s: %s\n", 48, "ptsize", SDL_GetError());
}
TTF_OpenFont(const char *file, int ptsize):加载指定路径的.ttf字体文件,并设置字体大小为 48。- 错误处理:如果字体加载失败(文件不存在 / 权限不足),打印错误信息。
- 问题:
printf中的%s格式符错误地使用了"ptsize"字符串,会导致日志信息混乱。
5. 定义文字颜色并渲染
//字体颜色是黑色,0xff=255, 0xff=255,0xff=255,rgb={255,255,255}
SDL_Color forecol = {0xff, 0xff, 0xff, 0xff};
//SDL_Color forecol = {0x00, 0x00, 0x00, 0xff};
text = TTF_RenderUTF8_Solid(font, pstr, forecol); //对输入的字符串进行渲染
SDL_Color:定义文字颜色,格式为{R, G, B, A}(红、绿、蓝、透明度)。- 当前代码中
forecol = {0xff, 0xff, 0xff, 0xff}表示白色不透明。 - 注释掉的
forecol = {0x00, 0x00, 0x00, 0xff}表示黑色不透明。
- 当前代码中
TTF_RenderUTF8_Solid(TTF_Font *font, const char *text, SDL_Color fg):以实心模式渲染文字,生成SDL_Surface位图,背景为透明(0x00000000)。
6. 自定义像素格式与格式转换
fmt = (SDL_PixelFormat *)malloc(sizeof(SDL_PixelFormat));
memset(fmt, 0, sizeof(SDL_PixelFormat));
fmt->BitsPerPixel = 16; //SDL的每个像素位
fmt->BytesPerPixel = 2; //SDL的每个像素占的字节数
temp = SDL_ConvertSurface(text, fmt, 0); //SDL_ConvertSurface对渲染的字体进行像素转换,转换成位图
- 手动
malloc并初始化SDL_PixelFormat,目标格式为 16 位色(BitsPerPixel=16,BytesPerPixel=2)。 SDL_ConvertSurface(SDL_Surface *src, SDL_PixelFormat *fmt, Uint32 flags):将原始text表面转换为指定格式的temp表面。- 问题:
- 16 位色格式不完整,缺少
Rmask/Gmask/Bmask/Amask等关键掩码定义,转换后数据会乱序。 - 没有对
malloc结果做 NULL 检查,可能导致空指针崩溃。 - 16 位色不支持透明通道,不适合后续 OSD 叠加(需要 32 位 BGRA 格式)。
- 16 位色格式不完整,缺少
7. 保存位图与资源释放
SDL_SaveBMP(temp, "save.bmp");//保存位图
SDL_FreeSurface(text);
SDL_FreeSurface(temp);
TTF_CloseFont(font);
TTF_Quit();
return 0;
}
SDL_SaveBMP(SDL_Surface *surface, const char *file):将temp表面保存为 BMP 图片,方便调试查看渲染效果。SDL_FreeSurface():释放SDL_Surface占用的内存。TTF_CloseFont(TTF_Font *font):关闭字体文件句柄。TTF_Quit():退出 SDL_ttf 库。
编译与运行说明
-
确保
fzlth.ttf字体文件和代码在同一目录下。 -
交叉编译时,需要链接 SDL 和 SDL_ttf 库:
arm-linux-gnueabihf-gcc text_render.c -o text_render -lSDL -lSDL_ttf -
运行程序后,会生成
text.bmp文件,打开即可看到渲染好的文字。
7.4 运行效果

上面是这个程序运行的效果,保存了一张bmp的位图。这个位图就是2019-11-21 15:40:29分的字符串。
7.5 运行时报的错误

上面这个错误是缺少了libSDL_ttf-2.0.so.0的库,此时要把libSDL_ttf-2.0.so.0放在/usr/lib下面

上面这个错误是缺少了libSDL-1.2.so.0的库,此时要把libSDL-1.2.so.0放在/usr/lib下面
八、本章总结
- SDL_TTF 是 RV1126 OSD 实现文字水印的核心工具库,整体依赖链:
zlib → FreeType → SDL → SDL_TTF,编译顺序不可打乱; - 全套库均采用 RV1126 官方 ARM 交叉工具链编译,所有产物统一存放在
/opt目录,便于工程统一管理; - 编译核心命令为
configure+make+make install,交叉编译的关键是指定CC编译器、--host目标架构、依赖库路径; - 库编译完成后,在项目 Makefile 中配置头文件、库路径并链接对应库,即可在代码中调用 SDL_TTF 接口渲染文字,对接 OSD 模块。
至此,SDL 文字渲染环境全部搭建完成。下一章我们将结合前文讲解的 OSD 结构体、API,使用 SDL_TTF 渲染文字,完成文字水印 + 硬件 OSD 叠加的完整代码实战。