使用 SDL3 渲染 YUV 数据
1、Windows 安装 SDL3
- 下载库文件:Releases · libsdl-org/SDL (github.com)
- 官方文档:SDL3/APIByCategory - SDL Wiki (libsdl.org)
- 官方代码示例:SDL3 Examples
- 配置开发环境:以 CMake 为例,替换其中的路径变量即可
cmake_minimum_required(VERSION 3.20)
project(sdl3_test)
set(CMAKE_CXX_STANDARD 20)
set(SDL3_DIR "path/to/sdl3")
set(TARGET_NAME ${PROJECT_NAME})
add_executable(${TARGET_NAME} main.cpp)
target_include_directories(${TARGET_NAME} PRIVATE
${SDL3_DIR}/include
)
target_link_directories(${TARGET_NAME} PRIVATE
${SDL3_DIR}/lib/x64
)
target_link_libraries(${TARGET_NAME} PRIVATE
SDL3.lib
)
if (WIN32)
file(GLOB SDL3_DLLS "${SDL3_DIR}/lib/x64/*.dll")
add_custom_command(TARGET ${TARGET_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${SDL3_DLLS} $<TARGET_FILE_DIR:${TARGET_NAME}>
COMMENT "Copying all required DLLs to output directory"
)
endif ()
2、用到的 API
结构体
SDL_Window
:代表一个窗口,它是在屏幕上显示图形内容的容器。SDL_Renderer
:代表一个渲染器,它负责将图形元素(如纹理)绘制到窗口上。SDL_Texture
:代表一个纹理,它是存储在图形硬件中的图像数据。SDL_Event
:代表一个事件,如键盘输入、鼠标移动、窗口关闭等SDL_FRect
:代表一个浮点数矩形,有 x、y、w、h 四个属性
窗口 API
SDL_CreateWindow
:创建一个新的窗口。SDL_GetTicks
:获取自 SDL 初始化以来的毫秒数。SDL_PollEvent
:从事件队列中取出一个事件,事件队列为空直接返回。SDL_WaitEvent
:无限期等待下一个可用事件。SDL_Delay
:使当前线程暂停指定的毫秒数。
渲染 API
SDL_CreateRenderer
:为指定的窗口创建一个 2D 渲染器。SDL_CreateTexture
:创建一个纹理对象。SDL_UpdateTexture
:更新纹理的全部或部分内容。SDL_RenderClear
:用当前的渲染目标的清除颜色清除渲染目标。SDL_RenderTexture
:将纹理复制到渲染目标,可指定目标矩形大小。SDL_RenderPresent
:将当前渲染目标的内容呈现到屏幕上。
3、实战——播放 YUV420p 文件
-
需求:
- 能够启动一个窗口播放
.yuv
文件,格式为 YUV420p、640x360、25fps - 窗口支持缩放
- 视频画面比例始终不变,且水平和竖直方向都居中
- 能够启动一个窗口播放
-
实现思路:
- 子线程负责循环定时发送渲染事件给主线程
- 主线程负责处理事件循环:包括窗口缩放事件、窗口关闭事件、渲染事件、子线程退出事件
- 渲染流程:按照 YUV420p 格式读取一帧数据 => 更新纹理 => 根据窗口大小计算目标矩形区域 => 把纹理渲染到目标矩形区域
- 窗口关闭流程:主线程收到窗口关闭事件 => 子线程发送退出事件给主线程 => 子线程退出 => 主线程收到子线程退出事件并退出事件循环 => 释放 SDL 资源 => 主线程退出
-
通过 mp4 文件生成 yuv420p 文件,如果视频很长可以指定
-t
ffmpeg -hide_banner -i a.mp4 -pix_fmt yuv420p yuv420p_a.yuv
-
C++ 代码示例
#include <string>
#include <thread>
#include <atomic>
#include <fstream>
extern "C" {
#include <SDL3/SDL.h>
}
static constexpr int kVideoWidth = 640;
static constexpr int kVideoHeight = 360;
static constexpr uint64_t kTargetFrameTimeMs = 40; // 25 fps
static constexpr SDL_PixelFormat kPixelFormatYUV420p = SDL_PIXELFORMAT_IYUV;
static constexpr auto kUserFrameEvent = static_cast<uint32_t>(SDL_EVENT_USER + 1);
static constexpr auto kUserQuitEvent = static_cast<uint32_t>(SDL_EVENT_USER + 2);
// RAII
class SDLEntity {
public:
SDLEntity() : window_(nullptr), renderer_(nullptr), texture_(nullptr) {}
void SetWindow(SDL_Window *window) { window_ = window; }
void SetRenderer(SDL_Renderer *renderer) { renderer_ = renderer; }
void SetTexture(SDL_Texture *texture) { texture_ = texture; }
~SDLEntity() {
if (texture_) SDL_DestroyTexture(texture_);
if (renderer_) SDL_DestroyRenderer(renderer_);
if (window_) SDL_DestroyWindow(window_);
}
private:
SDL_Window *window_;
SDL_Renderer *renderer_;
SDL_Texture *texture_;
};
static void PlayYuvVideoInner(const std::string &yuv_file) {
SDLEntity entity;
SDL_Window *window = nullptr;
SDL_Renderer *renderer = nullptr;
SDL_Texture *texture = nullptr;
SDL_Event event;
SDL_FRect rect;
int window_width = kVideoWidth;
int window_height = kVideoHeight;
std::atomic_bool thread_quit{false}, poll_quit{false};
// create window and renderer
bool success = SDL_CreateWindowAndRenderer(
("YUV420p Player " + std::to_string(kVideoWidth) + "x" + std::to_string(kVideoHeight)).c_str(),
window_width,
window_height,
(SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE),
&window,
&renderer
);
entity.SetWindow(window);
entity.SetRenderer(renderer);
if (!success) {
SDL_Log("Couldn't create window/renderer: %s", SDL_GetError());
return;
}
// create yuv texture
texture = SDL_CreateTexture(renderer, kPixelFormatYUV420p, SDL_TEXTUREACCESS_STREAMING, kVideoWidth, kVideoHeight);
if (!texture) {
SDL_Log("Couldn't create texture: %s", SDL_GetError());
return;
}
entity.SetTexture(texture);
// allocate yuv420p frame buffer
constexpr int y_frame_len = kVideoWidth * kVideoHeight;
constexpr int u_frame_len = kVideoWidth * kVideoHeight / 4;
constexpr int v_frame_len = kVideoWidth * kVideoHeight / 4;
constexpr int yuv_frame_len = y_frame_len + u_frame_len + v_frame_len;
auto yuv_frame_buffer = std::make_unique<uint8_t[]>(yuv_frame_len);
// open yuv file
std::ifstream file(yuv_file, std::ios::binary);
if (!file.is_open()) {
SDL_Log("Couldn't open file: %s", yuv_file.c_str());
return;
}
// fps control thread
auto refresh_timer_thread = std::thread([&thread_quit]() -> void {
while (!thread_quit.load()) {
SDL_Event frame_event;
frame_event.type = kUserFrameEvent;
SDL_PushEvent(&frame_event);
SDL_Delay(kTargetFrameTimeMs);
}
SDL_Event quit_event;
quit_event.type = kUserQuitEvent;
SDL_PushEvent(&quit_event);
});
// event loop
while (!poll_quit.load()) {
SDL_WaitEvent(&event);
switch (event.type) {
case SDL_EVENT_QUIT:
thread_quit.store(true);
continue;
case SDL_EVENT_WINDOW_RESIZED:
SDL_GetWindowSize(window, &window_width, &window_height);
continue;
case kUserQuitEvent:
poll_quit.store(true);
continue;
case kUserFrameEvent:
break;
default:
continue;
}
if (thread_quit.load()) {
continue;
}
// read a yuv420p frame from file
file.read(reinterpret_cast<char *>(yuv_frame_buffer.get()), yuv_frame_len);
if (const uint64_t bytes_read = file.gcount(); bytes_read < yuv_frame_len) {
if (bytes_read == 0) {
SDL_Log("End of file reached. Stopping playback.");
} else {
SDL_Log("Couldn't read a complete frame. Stopping playback.");
}
thread_quit.store(true);
continue;
}
// update dst rect
constexpr float aspect_ratio = static_cast<float>(kVideoWidth) / kVideoHeight;
const auto target_video_width1 = static_cast<float>(window_width);
const auto target_video_height1 = static_cast<float>(window_height);
const float target_video_width2 = target_video_height1 * aspect_ratio;
const float target_video_height2 = target_video_width1 / aspect_ratio;
rect.w = std::min(target_video_width1, target_video_width2);
rect.h = std::min(target_video_height1, target_video_height2);
rect.x = (static_cast<float>(window_width) - rect.w) / 2;
rect.y = (static_cast<float>(window_height) - rect.h) / 2;
// render texture
SDL_UpdateTexture(texture, nullptr, yuv_frame_buffer.get(), kVideoWidth);
SDL_RenderClear(renderer);
SDL_RenderTexture(renderer, texture, nullptr, &rect);
SDL_RenderPresent(renderer);
}
file.close();
if (refresh_timer_thread.joinable()) {
refresh_timer_thread.join();
}
}
// this function can only be called once
void PlayYuvVideo(const std::string &yuv_file) {
if (!SDL_Init(SDL_INIT_VIDEO)) {
SDL_Log("Couldn't initialize SDL: %s", SDL_GetError());
return;
}
PlayYuvVideoInner(yuv_file);
SDL_Quit();
}