8、SDL2入门与视频渲染:SDL窗口创建、事件处理、纹理渲染,将FFmpeg解码后的YUV数据渲染到屏幕
好,我们终于走到了这一步。前面几章我们把FFmpeg解码流程跑通了,拿到了YUV数据。但数据拿在手里,看不见摸不着,总感觉少了点什么。这一章,我们就来解决「怎么看」的问题。
说到跨平台窗口和渲染,SDL2绝对是首选。我最早接触SDL2是在做一个播放器项目,当时想在Windows和Linux上统一渲染接口,试了一圈,最后还是选了SDL2。为什么?因为它够轻量,API设计得也干净,跟FFmpeg配合起来特别顺手。
8.1 SDL2到底是什么?
SDL2,全称Simple DirectMedia Layer 2。说白了,它就是一个跨平台的多媒体库。你可以在Windows、Linux、macOS、Android甚至iOS上用同一套代码创建窗口、处理输入、播放音频、渲染图形。
我个人习惯把SDL2看作「操作系统的图形抽象层」。你不需要关心底层是X11还是Wayland,是DirectX还是OpenGL,SDL2帮你全包了。
核心能力:
- 窗口创建与管理
- 事件处理(键盘、鼠标、窗口事件)
- 2D纹理渲染(支持YUV直接渲染)
- 音频播放
- 线程与定时器
我们这一章只用到前三项。音频部分后面会单独讲。
8.2 从零搭建SDL2环境
先别急着写代码,环境得先配好。我踩过这个坑——编译时找不到库,运行时又报版本不匹配,折腾了大半天。
8.2.1 安装SDL2
Ubuntu/Debian:
sudo apt-get install libsdl2-dev libsdl2-image-dev
macOS(Homebrew):
brew install sdl2 sdl2_image
Windows:
去SDL官网下载开发库,解压后把include和lib路径配置到你的项目里。我个人建议用vcpkg,省心很多:
vcpkg install sdl2:x64-windows
小提示:编译时记得链接SDL2库。CMake里加一句 target_link_libraries(your_target SDL2::SDL2) 就行。
8.3 SDL2核心流程:窗口、渲染器、纹理
SDL2渲染视频的流程,其实就三步:
- 创建窗口(SDL_Window)
- 创建渲染器(SDL_Renderer)
- 创建纹理(SDL_Texture)并更新数据
嗯,这里要注意:纹理的像素格式必须跟你解码出来的YUV格式一致。否则渲染出来颜色会怪怪的。我曾经犯过这个错,解码出来是YUV420P,纹理格式设成了YV12,结果画面偏绿,排查了半天才发现是格式不匹配。
8.3.1 创建窗口
SDL_Window* window = SDL_CreateWindow(
"视频播放器 - SDL2渲染",
SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
1280, 720,
SDL_WINDOW_SHOWN
);
if (!window) {
// 错误处理
SDL_Log("创建窗口失败: %s", SDL_GetError());
return -1;
}
窗口位置我习惯用 SDL_WINDOWPOS_CENTERED,让窗口居中显示。尺寸可以根据视频分辨率动态调整,后面会讲到。
8.3.2 创建渲染器
SDL_Renderer* renderer = SDL_CreateRenderer(
window,
-1,
SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC
);
if (!renderer) {
SDL_Log("创建渲染器失败: %s", SDL_GetError());
return -1;
}
标志位我推荐加上 SDL_RENDERER_ACCELERATED,启用硬件加速。SDL_RENDERER_PRESENTVSYNC 是垂直同步,能防止画面撕裂。你想想看,如果不要垂直同步,渲染速度跟显示器刷新率不匹配,画面就会像被撕开一样,很难看。
8.3.3 创建纹理
纹理是渲染的关键。我们要创建一个YUV格式的纹理,用来承载解码后的数据。
SDL_Texture* texture = SDL_CreateTexture(
renderer,
SDL_PIXELFORMAT_IYUV, // 对应YUV420P
SDL_TEXTUREACCESS_STREAMING,
width, height
);
if (!texture) {
SDL_Log("创建纹理失败: %s", SDL_GetError());
return -1;
}
注意:纹理访问模式必须用 SDL_TEXTUREACCESS_STREAMING。因为我们要频繁更新纹理数据(每一帧都更新),静态纹理模式不适合。
8.4 事件处理:让窗口活起来
窗口创建好了,但如果你不处理事件,窗口会立刻卡死。为什么?因为SDL2的事件循环是阻塞的,你不去取事件,窗口系统会认为程序无响应。
事件处理的核心就是一个while循环:
SDL_Event event;
bool quit = false;
while (!quit) {
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) {
quit = true;
}
if (event.type == SDL_KEYDOWN) {
if (event.key.keysym.sym == SDLK_ESCAPE) {
quit = true;
}
}
}
// 渲染视频帧...
}
我个人习惯把事件处理和渲染分开。事件循环只负责收集事件,渲染逻辑放在主循环的另一部分。这样代码结构清晰,也方便后面加暂停、快进等功能。
8.5 将YUV数据渲染到屏幕
好,重头戏来了。假设我们已经用FFmpeg解码了一帧YUV420P数据,存在三个缓冲区里:data[0](Y)、data[1](U)、data[2](V)。怎么把它显示到窗口上?
8.5.1 更新纹理
SDL_UpdateYUVTexture(
texture,
NULL, // 更新整个纹理
frame->data[0], frame->linesize[0], // Y平面
frame->data[1], frame->linesize[1], // U平面
frame->data[2], frame->linesize[2] // V平面
);
这里有个细节:linesize 不一定等于宽度。因为内存对齐的原因,linesize可能比宽度大几个字节。如果你直接用宽度去算,画面会偏移。我刚开始做的时候就被这个坑过,画面总是斜的,后来打印linesize才发现问题。
8.5.2 渲染纹理
SDL_RenderClear(renderer); // 清空渲染器
SDL_RenderCopy(renderer, texture, NULL, NULL); // 复制纹理到渲染器
SDL_RenderPresent(renderer); // 显示到屏幕
SDL_RenderCopy 的第三个参数是源矩形,第四个是目标矩形。都传NULL表示全尺寸渲染。如果你想缩放画面,可以指定目标矩形的大小。
8.6 完整渲染循环
把上面所有代码串起来,就是一个完整的渲染循环:
// 伪代码,展示核心逻辑
while (!quit) {
// 1. 处理事件
SDL_PollEvent(&event);
// 2. 从FFmpeg解码器取一帧
if (avcodec_receive_frame(dec_ctx, frame) == 0) {
// 3. 更新纹理
SDL_UpdateYUVTexture(texture, NULL,
frame->data[0], frame->linesize[0],
frame->data[1], frame->linesize[1],
frame->data[2], frame->linesize[2]);
// 4. 渲染
SDL_RenderClear(renderer);
SDL_RenderCopy(renderer, texture, NULL, NULL);
SDL_RenderPresent(renderer);
}
// 控制帧率,别跑太快
SDL_Delay(40); // 约25fps
}
经验之谈:实际项目中不要用 SDL_Delay 来控制帧率,精度不够。建议用SDL2的定时器或者基于时间戳的同步方式。后面讲音视频同步时会详细说。
8.7 核心流程总览
为了让你对整个流程有个直观的认识,我画了一张图:
这张图把整个流程串起来了。你从左往右看:FFmpeg解码出YUV数据,创建纹理,更新纹理数据,然后通过渲染器复制并显示。左侧的事件循环贯穿始终,保证窗口能响应用户操作。
8.8 常见问题与避坑指南
做这个环节,有几个坑我不得不提:
- 纹理格式不匹配:FFmpeg解码出来的YUV格式跟SDL2纹理格式必须一致。YUV420P对应
SDL_PIXELFORMAT_IYUV。如果格式不对,颜色会完全错乱。 - linesize问题:前面说过了,别直接用宽度,要用
frame->linesize[i]。 - 内存泄漏:SDL2创建的窗口、渲染器、纹理,记得在退出时释放。顺序是:先销毁纹理,再销毁渲染器,最后销毁窗口。
- 线程安全:SDL2的渲染函数不是线程安全的。如果你用多线程解码,记得把渲染操作放在主线程。
我曾经踩过的坑:有一次在嵌入式设备上跑,画面渲染出来是花的。排查了半天,发现是纹理的宽高没有对齐到16的倍数。SDL2底层对纹理尺寸有对齐要求,不是所有尺寸都支持。解决办法是创建纹理时用 SDL_QueryTexture 查询实际支持的尺寸。
8.9 小结
这一章我们走通了SDL2渲染YUV数据的完整流程。从窗口创建到事件处理,从纹理创建到数据更新,每一步都有它的道理。你想想看,其实核心逻辑并不复杂:解码 -> 填纹理 -> 渲染 -> 显示。但每个环节的细节处理,决定了最终画面能不能正常显示。
下一章我们会在这个基础上,加入音频播放,让画面和声音同步起来。不过那是后话了,先把这一章的代码跑通,看到画面动起来,你会有成就感的。
公众号:蓝海资料掘金营,微信deep3321