【音视频开发】3. 使用 SDL3 渲染 YUV 数据

168 阅读4分钟

使用 SDL3 渲染 YUV 数据

1、Windows 安装 SDL3

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();
}