SDL3配置及基本使用(完整demo)

SDL3配置及基本使用

1.sdl3+QtCreator+cmake+MSVC

1、去sdl官网下载vc版本,这个是给msvc用的,解压后把路径添加到系统环境变量path

**例如我的:E:\SDL\SDL3-3.4.2**

2、创建cmake项目,编辑cmakelists.txt

  1. 添加find_package(SDL3 REQUIRED)
  2. 添加target_link_libraries(appsdlThree
    PRIVATE Qt6::Quick
    SDL3::SDL3
    )

第一步原因:

复制代码
之所以 SDL3 显得如此简单,而网上的 SDL2 教程又臭又长,主要归功于以下三个核心原因:

1. 从"找文件"进化到了"找配置" (Config Mode)
这是最根本的区别。

SDL2 的老办法 (Module Mode): 以前 SDL2 并不自带 CMake 的"说明书"。开发者必须自己写(或从网上抄)一个 FindSDL2.cmake 脚本。这个脚本满地打滚去搜 SDL.h 在哪、.lib 在哪。因为每个人的安装路径不一样,经常搜不到,所以你得手动配一大堆变量。

SDL3 的新办法 (Config Mode): SDL3 在安装时,会自动在目录下生成一个 SDL3Config.cmake 文件。这就像是 SDL3 自带了"官方身份证"和"位置指南"。当你执行 find_package(SDL3) 时,CMake 会直接去对接这个官方文件,而不是像无头苍蝇一样乱搜。

2. 引入了"目标" (Target-based CMake)
在旧式教程里,你找到 SDL2 后,还得写类似 include_directories(${SDL2_INCLUDE_DIR}) 和 target_link_libraries(mygame ${SDL2_LIBRARY})。

在 SDL3 中,所有信息都封装在一个 Target(即 SDL3::SDL3)里了:

它包含头文件路径。

它包含库文件路径。

它甚至包含编译时需要的宏定义。
你只需要把这个 Target 链接给你的程序,它就会把所有需要的"行李"一并带过来。

3. 智能的搜索路径
为什么 CMake 能"未卜先知"找到你的 SDL3?

当你运行 find_package 时,CMake 会按顺序扫描以下位置:

项目变量: 比如你在命令行指定的 -DSDL3_DIR=...。

系统环境变量: 比如 Path、SDL3_DIR 或者 CMAKE_PREFIX_PATH。

系统默认路径: Windows 下的 C:/Program Files/ 或 Linux 下的 /usr/local/lib/cmake/。

你之所以觉得简单,是因为你恰好把 SDL3 放在了它能找到的地方(或者是你之前的 PATH 设置帮了忙)。

第二步原因:

复制代码
现在的 SDL3::SDL3 和 Qt6::Quick 被称为 "导出目标"(Exported Targets)。当你链接它们时,它们会自动把以下三样东西传给你的 appsdlThree:

头文件路径 (Include Directories):你不需要写 include_directories,只要链接了,你就能在代码里 #include <SDL3/SDL.h>。

库文件 (Libraries):告诉链接器去找具体的 .lib 或 .a 文件。

编译选项 (Compile Definitions/Options):比如 SDL 需要的一些特殊系统宏(如 _REENTRANT),它会自动帮你加上。
------------------------------------
SDL3::SDL3:

让你的项目可以调用 SDL3 的底层 API,比如窗口创建、硬件加速绘图、手柄输入、音频等。

它会帮你处理好不同平台(Windows/Linux/Mac)下复杂的系统库依赖(比如 Windows 上的 winmm.lib 或 imm32.lib)。
总结:为什么要加这一句?
如果没有这一句,你的代码虽然能写出来,但:

编译时:编译器会报错"找不到 SDL.h"。

链接时:链接器会报错"无法解析的外部符号(Undefined reference)",因为它不知道具体的函数实现在哪个文件里。

现在的状态

你现在的配置就像是:

find_package:在系统里找到了这两家供应商。

target_link_libraries:正式签了合同,让这两家供应商把所有工具(头文件、库、参数)都搬进你的工作室(appsdlThree)。

最后在你的c或cpp文件使用时添加头文件即可

复制代码
// 包含 SDL3 头文件
#include <SDL3/SDL.h>

最后自己ai生成一份sdl窗口测试代码即可,或者复制

cpp 复制代码
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QTimer>

// 包含 SDL3 头文件
#include <SDL3/SDL.h>

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    // 1. 初始化 SDL
    if (!SDL_Init(SDL_INIT_VIDEO)) {
        return -1;
    }

    // 2. 创建窗口和渲染器
    SDL_Window* window = SDL_CreateWindow("SDL3 Window", 800, 600, SDL_WINDOW_RESIZABLE);
    SDL_Renderer* renderer = SDL_CreateRenderer(window, NULL);

    // 3. 使用定时器处理 SDL 事件和渲染
    QTimer timer;
    QObject::connect(&timer, &QTimer::timeout, [&]() {
        SDL_Event event;
        // 关键:轮询所有 SDL 事件,否则窗口会卡死
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_EVENT_QUIT) {
                // 如果 SDL 窗口关闭,我们就退出整个应用
                QCoreApplication::quit();
            }
        }

        // 简单的 SDL 渲染逻辑
        SDL_SetRenderDrawColor(renderer, 20, 20, 20, 255); // 深灰色背景
        SDL_RenderClear(renderer);

        // 这里以后可以放 FFmpeg 的渲染代码

        SDL_RenderPresent(renderer);
    });

    // 每秒刷新 60 次 (约 16ms)
    timer.start(16);

    QQmlApplicationEngine engine;
    const QUrl url(QStringLiteral("qrc:/sdlThree/Main.qml"));
    QObject::connect(
        &engine,
        &QQmlApplicationEngine::objectCreated,
        &app,
        [url](QObject *obj, const QUrl &objUrl) {
            if (!obj && url == objUrl)
                QCoreApplication::exit(-1);
        },
        Qt::QueuedConnection);
    engine.load(url);

    // 清理
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();
    return app.exec();
}

附赠cmakelists文件

cpp 复制代码
cmake_minimum_required(VERSION 3.16)

project(sdlThree VERSION 0.1 LANGUAGES C CXX)

set(CMAKE_AUTOMOC ON)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(Qt6 REQUIRED COMPONENTS Quick)
find_package(SDL3 REQUIRED)

qt_add_executable(appsdlThree
    main.cpp
)

qt_add_qml_module(appsdlThree
    URI sdlThree
    VERSION 1.0
    QML_FILES
        Main.qml
)

# Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1.
# If you are developing for iOS or macOS you should consider setting an
# explicit, fixed bundle identifier manually though.
set_target_properties(appsdlThree PROPERTIES
#    MACOSX_BUNDLE_GUI_IDENTIFIER com.example.appsdlThree
    MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
    MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
    MACOSX_BUNDLE TRUE
    WIN32_EXECUTABLE TRUE
)

target_link_libraries(appsdlThree
    PRIVATE Qt6::Quick
    SDL3::SDL3
)

include(GNUInstallDirs)
install(TARGETS appsdlThree
    BUNDLE DESTINATION .
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

2.ffmpeg+QtCreator+cmake+MSVC

  1. 下载对应的ffmpeg版本,我安装的为ffmpeg-8.0.1-full_build-shared

  2. 将ffmpeg文件夹复制到需要添加库的qt项目文件夹中,改名为ffmpeg

  3. 复制ffmpeg文件夹下bin文件夹下的dll文件,将其粘贴到类似下列文件夹E:\three\code\avideo\sdl3\sdlThree\build\Desktop_Qt_6_8_3_MSVC2022_64bit-Debug

  4. 修改cmakelists文件

    添加 ffmpeg 头文件路径

    target_include_directories(appsdlThree PRIVATE ${CMAKE_SOURCE_DIR}/ffmpeg/include)

    添加 ffmpeg 库路径

    target_link_directories(appsdlThree PRIVATE ${CMAKE_SOURCE_DIR}/ffmpeg/lib)

    target_link_libraries(appsdlThree
    PRIVATE Qt6::Quick
    SDL3::SDL3
    avcodec
    avdevice
    avfilter
    avformat
    avutil
    swresample
    swscale
    )

  5. 修改main.cpp文件,添加

    // 包含 FFmpeg 头文件(必须用 extern "C" 包裹)

    extern "C"

    {

    #include <libavcodec/avcodec.h>

    #include <libavformat/avformat.h>

    #include <libswscale/swscale.h>

    #include <libavdevice/avdevice.h>

    #include <libavformat/version.h>

    #include <libavutil/time.h>

    #include <libavutil/mathematics.h>

    }

    // 直接调用 FFmpeg 函数
    int version = avcodec_version();
    const char *license = avcodec_license();

    qDebug() << "FFmpeg avcodec version:" << version;
    qDebug() << "FFmpeg avcodec license:" << (license ? license : "unknown"); // 直接调用 FFmpeg 函数

    完整cmakelists文件

    复制代码
    cmake_minimum_required(VERSION 3.16)
    
    project(sdlThree VERSION 0.1 LANGUAGES C CXX)
    
    set(CMAKE_AUTOMOC ON)
    set(CMAKE_CXX_STANDARD_REQUIRED ON)
    
    find_package(Qt6 REQUIRED COMPONENTS Quick)
    find_package(SDL3 REQUIRED)
    
    qt_add_executable(appsdlThree
        main.cpp
    )
    
    # 添加 ffmpeg 头文件路径
    target_include_directories(appsdlThree PRIVATE ${CMAKE_SOURCE_DIR}/ffmpeg/include)
    
    # 添加 ffmpeg 库路径
    target_link_directories(appsdlThree PRIVATE ${CMAKE_SOURCE_DIR}/ffmpeg/lib)
    
    qt_add_qml_module(appsdlThree
        URI sdlThree
        VERSION 1.0
        QML_FILES
            Main.qml
    )
    
    # Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1.
    # If you are developing for iOS or macOS you should consider setting an
    # explicit, fixed bundle identifier manually though.
    set_target_properties(appsdlThree PROPERTIES
    #    MACOSX_BUNDLE_GUI_IDENTIFIER com.example.appsdlThree
        MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
        MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
        MACOSX_BUNDLE TRUE
        WIN32_EXECUTABLE TRUE
    )
    
    target_link_libraries(appsdlThree
        PRIVATE Qt6::Quick
        avcodec
        avdevice
        avfilter
        avformat
        avutil
        swresample
        swscale
        SDL3::SDL3
    )
    
    
    
    include(GNUInstallDirs)
    install(TARGETS appsdlThree
        BUNDLE DESTINATION .
        LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
        RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
    )

    完整cpp文件

    cpp 复制代码
    #include <QGuiApplication>
    #include <QQmlApplicationEngine>
    #include <QTimer>
    
    // 包含 SDL3 头文件
    #include <SDL3/SDL.h>
    
    // 包含 FFmpeg 头文件(必须用 extern "C" 包裹)
    extern "C"
    
    {
    
    #include <libavcodec/avcodec.h>
    
    #include <libavformat/avformat.h>
    
    #include <libswscale/swscale.h>
    
    #include <libavdevice/avdevice.h>
    
    #include <libavformat/version.h>
    
    #include <libavutil/time.h>
    
    #include <libavutil/mathematics.h>
    
    }
    
    
    int main(int argc, char *argv[])
    {
        QGuiApplication app(argc, argv);
    
        // 直接调用 FFmpeg 函数
        int version = avcodec_version();
        const char *license = avcodec_license();
    
        qDebug() << "FFmpeg avcodec version:" << version;
        qDebug() << "FFmpeg avcodec license:" << (license ? license : "unknown");    // 直接调用 FFmpeg 函数
    
        // 1. 初始化 SDL
        if (!SDL_Init(SDL_INIT_VIDEO)) {
            return -1;
        }
    
        // 2. 创建窗口和渲染器
        SDL_Window* window = SDL_CreateWindow("SDL3 Window", 800, 600, SDL_WINDOW_RESIZABLE);
        SDL_Renderer* renderer = SDL_CreateRenderer(window, NULL);
    
        // 3. 使用定时器处理 SDL 事件和渲染
        QTimer timer;
        QObject::connect(&timer, &QTimer::timeout, [&]() {
            SDL_Event event;
            // 关键:轮询所有 SDL 事件,否则窗口会卡死
            while (SDL_PollEvent(&event)) {
                if (event.type == SDL_EVENT_QUIT) {
                    // 如果 SDL 窗口关闭,我们就退出整个应用
                    QCoreApplication::quit();
                }
            }
    
            // 简单的 SDL 渲染逻辑
            SDL_SetRenderDrawColor(renderer, 20, 20, 20, 255); // 深灰色背景
            SDL_RenderClear(renderer);
    
            // 这里以后可以放 FFmpeg 的渲染代码
    
            SDL_RenderPresent(renderer);
        });
    
        // 每秒刷新 60 次 (约 16ms)
        timer.start(16);
    
        QQmlApplicationEngine engine;
        const QUrl url(QStringLiteral("qrc:/sdlThree/Main.qml"));
        QObject::connect(
            &engine,
            &QQmlApplicationEngine::objectCreated,
            &app,
            [url](QObject *obj, const QUrl &objUrl) {
                if (!obj && url == objUrl)
                    QCoreApplication::exit(-1);
            },
            Qt::QueuedConnection);
        engine.load(url);
    
        // 清理
        SDL_DestroyRenderer(renderer);
        SDL_DestroyWindow(window);
        SDL_Quit();
        return app.exec();
    }

总:下面案例的完整cpp代码

怕放在下面看不见,总的代码是下面这个,最后的时候把不同模块调整成了函数形式方便修改

cpp 复制代码
#include <SDL3/SDL.h>
#include <QFile>
#include <QByteArray>

//===================== 初始化 SDL =====================
bool initSDL()
{
    // SDL3 通过布尔值判断初始化是否成功(SDL2 是返回值小于0表示失败)
    if (!SDL_Init(SDL_INIT_AUDIO | SDL_INIT_VIDEO))
    {
        SDL_Log("SDL_Init failed: %s", SDL_GetError());
        return false;
    }
    return true;
}

//===================== 创建窗口 =====================
SDL_Window* createWindow(const char *title, int width, int height)
{
    // SDL3 不再在创建时指定窗口位置,由窗口管理器决定
    // 如需精细控制可使用 SDL_CreateWindowWithProperties
    SDL_Window *window = SDL_CreateWindow(title, width, height, 0);
    if (window == NULL)
    {
        SDL_Log("SDL_CreateWindow failed: %s", SDL_GetError());
        return NULL;
    }
    SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
    return window;
}

//===================== 创建渲染器 =====================
SDL_Renderer* createRenderer(SDL_Window *window)
{
    // NULL 表示自动选择最佳渲染驱动(SDL2 写法: SDL_CreateRenderer(window, -1, 0))
    SDL_Renderer *renderer = SDL_CreateRenderer(window, NULL);
    if (renderer == NULL)
    {
        SDL_Log("SDL_CreateRenderer failed: %s", SDL_GetError());
        return NULL;
    }
    return renderer;
}

//===================== 加载 BMP 图片并创建 Texture =====================
SDL_Texture* loadBMPTexture(SDL_Renderer *renderer, const char *filePath)
{
    // 1. 加载 BMP 文件
    SDL_Surface *img_surf = SDL_LoadBMP(filePath);
    if (img_surf == NULL)
    {
        SDL_Log("SDL_LoadBMP failed: %s", SDL_GetError());
        return NULL;
    }
    SDL_Log("BMP loaded: %dx%d, format=0x%08X", img_surf->w, img_surf->h, (unsigned int)img_surf->format);

    // 2. 转换为 RGBA32 格式(某些 BMP 是索引色格式,GPU 纹理不支持)
    SDL_Surface *converted_surf = SDL_ConvertSurface(img_surf, SDL_PIXELFORMAT_RGBA32);
    SDL_DestroySurface(img_surf);
    if (converted_surf == NULL)
    {
        SDL_Log("SDL_ConvertSurface failed: %s", SDL_GetError());
        return NULL;
    }

    // 3. 从 Surface 创建 Texture
    SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer, converted_surf);
    SDL_DestroySurface(converted_surf);
    if (texture == NULL)
    {
        SDL_Log("SDL_CreateTextureFromSurface failed: %s", SDL_GetError());
        return NULL;
    }

    float tw, th;
    SDL_GetTextureSize(texture, &tw, &th);
    SDL_Log("Texture created: %.0fx%.0f", tw, th);
    return texture;
}

//===================== 事件循环 + 渲染 =====================
void mainLoop(SDL_Renderer *renderer, SDL_Texture *texture)
{
    SDL_Event event;
    bool running = true;

    while (running)
    {
        // 处理事件
        while (SDL_PollEvent(&event))
        {
            switch (event.type)
            {
            //=============== 退出事件 ===============
            case SDL_EVENT_QUIT:
                SDL_Log("收到退出事件");
                running = false;
                break;

            //=============== 窗口事件 ===============
            case SDL_EVENT_WINDOW_MOVED:
                // 窗口被拖动
                SDL_Log("窗口移动到: (%d, %d)", (int)event.window.data1, (int)event.window.data2);
                break;

            case SDL_EVENT_WINDOW_RESIZED:
                // 窗口大小改变(用户拖动边框)
                SDL_Log("窗口大小改变: %dx%d", (int)event.window.data1, (int)event.window.data2);
                break;

            case SDL_EVENT_WINDOW_MAXIMIZED:
                SDL_Log("窗口最大化");
                break;

            case SDL_EVENT_WINDOW_RESTORED:
                SDL_Log("窗口恢复");
                break;

            case SDL_EVENT_WINDOW_EXPOSED:
                // 窗口需要重绘(被遮挡后恢复等)
                SDL_Log("窗口需要重绘");
                break;

            //=============== 鼠标移动事件 ===============
            case SDL_EVENT_MOUSE_MOTION:
                SDL_Log("鼠标移动: 位置(%0.f, %.0f), 偏移(%.0f, %.0f)",
                        event.motion.x, event.motion.y,
                        event.motion.xrel, event.motion.yrel);
                break;

            //=============== 鼠标点击事件 ===============
            case SDL_EVENT_MOUSE_BUTTON_DOWN:
                SDL_Log("鼠标按下: 按钮=%d, 位置(%.0f, %.0f), 点击次数=%d",
                        event.button.button, event.button.x, event.button.y,
                        (int)event.button.clicks);
                break;

            case SDL_EVENT_MOUSE_BUTTON_UP:
                SDL_Log("鼠标松开: 按钮=%d, 位置(%.0f, %.0f)",
                        event.button.button, event.button.x, event.button.y);
                break;

            //=============== 键盘事件 ===============
            case SDL_EVENT_KEY_DOWN:
                SDL_Log("键盘按下: 键=%s, scancode=%d, 重复=%s",
                        SDL_GetKeyName(event.key.key),
                        (int)event.key.scancode,
                        event.key.repeat ? "是" : "否");
                // ESC 键退出
                if (event.key.key == SDLK_ESCAPE)
                {
                    SDL_Log("按下 ESC,退出程序");
                    running = false;
                }
                break;

            case SDL_EVENT_KEY_UP:
                SDL_Log("键盘松开: 键=%s", SDL_GetKeyName(event.key.key));
                break;

            default:
                break;
            }
        }

        // 每帧重新绘制
        SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);
        SDL_RenderClear(renderer);
        SDL_RenderTexture(renderer, texture, NULL, NULL);
        SDL_RenderPresent(renderer);
    }
}

//===================== wav文件 =====================


//===================== wav文件播放 (SDL3 标准) =====================

/**
 * SDL3 音频回调函数
 * stream: 目标音频流
 * additional_amount: 需要提供的数据字节数
 * total_amount: 队列中总共需要的数据量
 */

// 全局变量(也可封装到结构体中)
static Uint8* audio_buf = NULL;
static Uint32 audio_len = 0;
static Uint32 audio_pos = 0;          // 当前播放位置
// SDL3 使用 SDL_AudioStream 管理音频,不再需要全局 DeviceID

static void SDLCALL audio_callback(void *userdata, SDL_AudioStream *stream, int additional_amount, int total_amount)
{
    if (additional_amount <= 0 || !audio_buf) return;

    int remain = (int)audio_len - (int)audio_pos;
    
    if (remain > 0) {
        int copy_len = (remain > additional_amount) ? additional_amount : remain;
        SDL_PutAudioStreamData(stream, audio_buf + audio_pos, copy_len);
        audio_pos += copy_len;
    } else {
        // 实现循环播放:重置位置
        audio_pos = 0;
        SDL_PutAudioStreamData(stream, audio_buf + audio_pos, additional_amount);
        audio_pos += additional_amount;
    }
}

/**
 * 播放 WAV 文件
 */
void play_wav()
{
    SDL_AudioSpec audio_spec;

    // 1. 加载 WAV 文件 (SDL3 返回 bool)
    if (!SDL_LoadWAV("E:/three/code/avideo/sdl3/sdlThree/test.wav",
                    &audio_spec, &audio_buf, &audio_len)) {
        SDL_Log("SDL_LoadWAV failed: %s", SDL_GetError());
        return;
    }

    // 2. 打开音频设备流并设置回调
    // SDL3 推荐使用这种方式处理带回调的音频
    SDL_AudioStream *stream = SDL_OpenAudioDeviceStream(
        SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, 
        &audio_spec, 
        audio_callback, 
        NULL
    );

    if (!stream) {
        SDL_Log("SDL_OpenAudioDeviceStream failed: %s", SDL_GetError());
        SDL_free(audio_buf);
        return;
    }

    // 3. 开始播放
    SDL_ResumeAudioStreamDevice(stream);

    // 4. 等待播放完成或至少让用户听到声音
    // 根据长度计算大约时间
    int bytes_per_sample = SDL_AUDIO_BYTESIZE(audio_spec.format);
    double total_seconds = (double)audio_len / (audio_spec.freq * audio_spec.channels * bytes_per_sample);
    SDL_Log("开始播放 WAV (时长约 %.2f 秒)...", total_seconds);
    
    // 注意:这里为了演示使用了 SDL_Delay,在实际应用中通常不需要阻塞,
    // 因为 audio_callback 会在后台线程持续运行。
    SDL_Delay((Uint32)(total_seconds * 1000) + 500);

    // 5. 清理资源
    SDL_DestroyAudioStream(stream);
    SDL_free(audio_buf);
    audio_buf = NULL;
}

//===================== yuv文件播放 =====================

/**
 * 播放 YUV 文件 (640x480, YUV420P, 1fps)
 */
void play_yuv(SDL_Renderer *renderer)
{
    const char *yuv_path = "E:/three/code/avideo/sdl3/sdlThree/output.yuv";
    int w = 640, h = 480;
    int y_size = w * h;
    int uv_size = (w / 2) * (h / 2);
    int frame_size = y_size + uv_size * 2;

    // 1. 打开文件
    FILE *fp = fopen(yuv_path, "rb");
    if (!fp) {
        SDL_Log("无法打开 YUV 文件: %s", yuv_path);
        return;
    }

    // 2. 创建 YUV 纹理 (IYUV 即 YUV420P)
    SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, w, h);
    if (!texture) {
        SDL_Log("创建 YUV 纹理失败: %s", SDL_GetError());
        fclose(fp);
        return;
    }

    // 3. 分配缓冲区
    uint8_t *y_plane = (uint8_t*)malloc(y_size);
    uint8_t *u_plane = (uint8_t*)malloc(uv_size);
    uint8_t *v_plane = (uint8_t*)malloc(uv_size);

    SDL_Log("开始播放 YUV 文件...");

    // 4. 循环读取并渲染 (5 帧)
    for (int i = 0; i < 5; i++) {
        if (fread(y_plane, 1, y_size, fp) != y_size ||
            fread(u_plane, 1, uv_size, fp) != uv_size ||
            fread(v_plane, 1, uv_size, fp) != uv_size) {
            break;
        }

        // 更新纹理数据
        SDL_UpdateYUVTexture(texture, NULL, 
                             y_plane, w, 
                             u_plane, w / 2, 
                             v_plane, w / 2);

        // 渲染到窗口
        SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
        SDL_RenderClear(renderer);
        SDL_RenderTexture(renderer, texture, NULL, NULL);
        SDL_RenderPresent(renderer);

        SDL_Log("播放 YUV 第 %d 帧", i + 1);
        SDL_Delay(1000); // 1 帧/秒
    }

    // 5. 清理
    free(y_plane);
    free(u_plane);
    free(v_plane);
    SDL_DestroyTexture(texture);
    fclose(fp);
    SDL_Log("YUV 播放完成");
}

//===================== pcm文件播放 =====================

/**
 * 播放 PCM 文件 (s16le, 44100Hz, Mono)
 */
void play_pcm()
{
    const char *pcm_path = "E:/three/code/avideo/sdl3/sdlThree/test2.pcm";
    
    // 1. 打开文件
    FILE *fp = fopen(pcm_path, "rb");
    if (!fp) {
        SDL_Log("无法打开 PCM 文件: %s", pcm_path);
        return;
    }

    // 2. 获取文件大小并读取数据
    fseek(fp, 0, SEEK_END);
    long pcm_len = ftell(fp);
    fseek(fp, 0, SEEK_SET);
    uint8_t *pcm_buf = (uint8_t*)malloc(pcm_len);
    fread(pcm_buf, 1, pcm_len, fp);
    fclose(fp);

    // 3. 设置音频格式
    SDL_AudioSpec spec;
    spec.freq = 44100;
    spec.format = SDL_AUDIO_S16LE;
    spec.channels = 1;

    // 4. 创建音频流 (Push 模式,不使用回调)
    SDL_AudioStream *stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, NULL, NULL);
    if (!stream) {
        SDL_Log("创建音频流失败: %s", SDL_GetError());
        free(pcm_buf);
        return;
    }

    // 5. 推送并播放
    SDL_Log("开始播放 PCM 文件...");
    SDL_PutAudioStreamData(stream, pcm_buf, (int)pcm_len);
    SDL_FlushAudioStream(stream); // 标记数据结束
    SDL_ResumeAudioStreamDevice(stream);

    // 6. 等待播放结束 (检查流中剩余数据)
    while (SDL_GetAudioStreamQueued(stream) > 0) {
        SDL_Delay(100);
    }

    // 7. 清理
    SDL_DestroyAudioStream(stream);
    free(pcm_buf);
    SDL_Log("PCM 播放完成");
}

//===================== 释放所有资源 =====================
void cleanup(SDL_Texture *texture, SDL_Renderer *renderer, SDL_Window *window)
{
    if (texture)  SDL_DestroyTexture(texture);
    if (renderer) SDL_DestroyRenderer(renderer);
    if (window)   SDL_DestroyWindow(window);
    SDL_Quit();
}

//===================== 主函数 =====================
int main(int argc, char *argv[])
{
    // 1. 初始化 SDL
    if (!initSDL()) return -1;

    // 2. 创建窗口
    SDL_Window *window = SDL_CreateWindow("多媒体播放演示", 800, 600, SDL_WINDOW_RESIZABLE);
    if (!window) { cleanup(NULL, NULL, NULL); return -1; }

    // 3. 创建渲染器
    SDL_Renderer *renderer = createRenderer(window);
    if (!renderer) { cleanup(NULL, NULL, window); return -1; }

    // --- 顺序播放示例 ---
    SDL_Log(">>> 步骤1: 播放 WAV 音频 (回调模式)");
    // play_wav();

    SDL_Log(">>> 步骤2: 播放 YUV 视频 (纹理渲染)");
    // play_yuv(renderer);

    SDL_Log(">>> 步骤3: 播放 PCM 音频 (Push模式)");
    // play_pcm();

    // 4. 加载图片
    SDL_Log(">>> 步骤4: 进入交互主循环 (BMP图片渲染)");
    SDL_Texture *texture = loadBMPTexture(renderer, "E:/three/code/avideo/sdl3/sdlThree/icon.bmp");
    if (!texture) { cleanup(NULL, renderer, window); return -1; }

    // 5. 进入主循环
    mainLoop(renderer, texture);

    // 6. 释放资源并退出
    cleanup(texture, renderer, window);
    return 0;
}

3.窗口创建

cpp 复制代码
#include <QGuiApplication>
#include <QQmlApplicationEngine>

// 1.包含 SDL3 头文件
#include <SDL3/SDL.h>


int main(int argc, char *argv[])
{
    //2.初始化
    //sdl2返回值小于0创建失败,判断是返回值小于0,但在sdl3通过布尔值判断了
    if(!SDL_Init(SDL_INIT_AUDIO | SDL_INIT_VIDEO))
    {
        SDL_Log("Init failed:%s",SDL_GetError());
        return -1;
    }

    //3.创建窗口
    //sdl2写法
    //SDL_Window *window = SDL_CreateWindow("Test",SDL_WINDOWPOS_CENTERED,SDL_WINDOWPOS_CENTERED,600,400,0);

    //sdl3写法:SDL3 认为窗口的初始位置通常由窗口管理器决定,因此不再在创建时直接指定
    //如果需要更精细的控制(如指定窗口位置、最小尺寸等),可以使用 SDL3 引入的 SDL_CreateWindowWithProperties
    SDL_Window *window = SDL_CreateWindow("Test", 600, 400, 0);
    SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
    if(window ==NULL)
    {
        SDL_Log("SDL_Window failed:%s",SDL_GetError());
        return -1;
    }

    //4.短暂窗口停留
    SDL_Delay(3000);

    //5.销毁窗口
    SDL_DestroyWindow(window);

    //6.退出
    SDL_Quit();
    return 0;
}

4.事件监听-关闭事件

cpp 复制代码
    //4.短暂窗口停留--->事件监听和响应
    // SDL_Delay(3000);
    SDL_Event event;
    while(true)
    {
        if(SDL_PollEvent(&event))
        {
            //判断返回值类型,如果是退出,也就是点窗口X时退出死循环,只是为了演示事件监听,如果没有窗口无法操作
            if(event.type == SDL_EVENT_QUIT)  //sdl2中是SDL_QUIT且返回值是int,sdl3中改为了bool
            {
                break;
            }
        }
    }

5.窗口上绘制矩形

cpp 复制代码
    //①获取与窗口关联的surface
    SDL_Surface *surf = SDL_GetWindowSurface(window);
    if(surf == NULL)
    {
        SDL_Log("SDL_GetWindowSurface failed:%s",SDL_GetError());
        return -1;
    }

    //②定义一个区域
    SDL_Rect rect = {100,200,100,100}; //位置和大小

    //③在surface上进行绘制
    SDL_FillSurfaceRect(surf,&rect,SDL_MapSurfaceRGB(surf, 255, 0, 0));//sdl2为SDL_FillRect(surf,&rect,SDL_MapSurfaceRGB(surf->format,255,0,0));

    //④将绘制的内容更新到屏幕上
    SDL_UpdateWindowSurface(window);
    
    //⑤释放surface指针
    SDL_DestroySurface(surf); //sdl2为SDL_FreeSurface

6.窗口上绘制bmp图片

cpp 复制代码
//一。获取与窗口关联的surface
    SDL_Surface *two_surf = SDL_GetWindowSurface(window);
    if(two_surf == NULL)
    {
        SDL_Log("SDL_GetWindowSurface failed:%s",SDL_GetError());
        return -1;
    }

    //二。导入bmp图片
    SDL_Surface *bmp_surf = SDL_LoadBMP("E:/three/code/avideo/sdl3/sdlThree/icon.bmp");
    if(bmp_surf == NULL)
    {
        SDL_Log("SDL_LoadBMP failed:%s",SDL_GetError());
        return -1;
    }

    //三。将图片的surface复制到窗口的surface上
    SDL_BlitSurface(bmp_surf,NULL,two_surf,NULL);

    //四。将绘制的内容更新到屏幕上
    SDL_UpdateWindowSurface(window);
    SDL_Delay(30000);

    //五。释放surface指针
    SDL_DestroySurface(two_surf); //sdl2为SDL_FreeSurface
    SDL_DestroySurface(bmp_surf); //sdl2为SDL_FreeSurface

7.渲染器+渲染图片

cpp 复制代码
    //1、创建与窗口关联的渲染器
    // NULL 表示自动选择最佳渲染驱动
    SDL_Renderer *render = SDL_CreateRenderer(window,NULL); //sdl2为SDL_Renderer *render = SDL_CreateRenderer(window, -1, 0);
    if(render == NULL)
    {
        SDL_Log("SDL_CreateRenderer failed:%s",SDL_GetError());
        return -1;
    }

    //①导入图片
    SDL_Surface *img_surf = SDL_LoadBMP("E:/three/code/avideo/sdl3/sdlThree/icon.bmp");
    if(img_surf == NULL)
    {
        SDL_Log("SDL_LoadBMP failed:%s",SDL_GetError());
        return -1;
    }
    SDL_Log("BMP loaded OK: width=%d, height=%d, format=0x%08X", img_surf->w, img_surf->h, (unsigned int)img_surf->format);

    //②将Surface转换为RGBA32格式(原始BMP是1位索引色,GPU纹理不支持索引色格式)
    SDL_Surface *converted_surf = SDL_ConvertSurface(img_surf, SDL_PIXELFORMAT_RGBA32);
    SDL_DestroySurface(img_surf); //原始surface不再需要
    if(converted_surf == NULL)
    {
        SDL_Log("SDL_ConvertSurface failed:%s",SDL_GetError());
        return -1;
    }
    SDL_Log("Converted surface: width=%d, height=%d, format=0x%08X", converted_surf->w, converted_surf->h, (unsigned int)converted_surf->format);

    //③创建texture
    SDL_Texture *texture = SDL_CreateTextureFromSurface(render,converted_surf);
    if(texture == NULL)
    {
        SDL_Log("SDL_CreateTextureFromSurface failed:%s",SDL_GetError());
        return -1;
    }
    float tw, th;
    SDL_GetTextureSize(texture, &tw, &th);
    SDL_Log("Texture created OK: width=%.0f, height=%.0f", tw, th);
    //④释放converted_surf,创建texture后就不再需要了
    SDL_DestroySurface(converted_surf);

    //4.事件循环 + 每帧重新渲染
    SDL_Event event;
    bool running = true;
    while(running)
    {
        while(SDL_PollEvent(&event))
        {
            if(event.type == SDL_EVENT_QUIT)
            {
                running = false;
            }
        }

        //每帧重新绘制
        SDL_SetRenderDrawColor(render,0,255,0,255);
        SDL_RenderClear(render);
        SDL_RenderTexture(render,texture,NULL,NULL);
        SDL_RenderPresent(render);
    }

    //5.释放资源
    SDL_DestroyTexture(texture);
    SDL_DestroyRenderer(render);

8.事件

cpp 复制代码
SDL_Event event;
    bool running = true;

    while (running)
    {
        // 处理事件
        while (SDL_PollEvent(&event))
        {
            switch (event.type)
            {
            //=============== 退出事件 ===============
            case SDL_EVENT_QUIT:
                SDL_Log("收到退出事件");
                running = false;
                break;

            //=============== 窗口事件 ===============
            case SDL_EVENT_WINDOW_MOVED:
                // 窗口被拖动
                SDL_Log("窗口移动到: (%d, %d)", (int)event.window.data1, (int)event.window.data2);
                break;

            case SDL_EVENT_WINDOW_RESIZED:
                // 窗口大小改变(用户拖动边框)
                SDL_Log("窗口大小改变: %dx%d", (int)event.window.data1, (int)event.window.data2);
                break;

            case SDL_EVENT_WINDOW_MAXIMIZED:
                SDL_Log("窗口最大化");
                break;

            case SDL_EVENT_WINDOW_RESTORED:
                SDL_Log("窗口恢复");
                break;

            case SDL_EVENT_WINDOW_EXPOSED:
                // 窗口需要重绘(被遮挡后恢复等)
                SDL_Log("窗口需要重绘");
                break;

            //=============== 鼠标移动事件 ===============
            case SDL_EVENT_MOUSE_MOTION:
                SDL_Log("鼠标移动: 位置(%0.f, %.0f), 偏移(%.0f, %.0f)",
                        event.motion.x, event.motion.y,
                        event.motion.xrel, event.motion.yrel);
                break;

            //=============== 鼠标点击事件 ===============
            case SDL_EVENT_MOUSE_BUTTON_DOWN:
                SDL_Log("鼠标按下: 按钮=%d, 位置(%.0f, %.0f), 点击次数=%d",
                        event.button.button, event.button.x, event.button.y,
                        (int)event.button.clicks);
                break;

            case SDL_EVENT_MOUSE_BUTTON_UP:
                SDL_Log("鼠标松开: 按钮=%d, 位置(%.0f, %.0f)",
                        event.button.button, event.button.x, event.button.y);
                break;

            //=============== 键盘事件 ===============
            case SDL_EVENT_KEY_DOWN:
                SDL_Log("键盘按下: 键=%s, scancode=%d, 重复=%s",
                        SDL_GetKeyName(event.key.key),
                        (int)event.key.scancode,
                        event.key.repeat ? "是" : "否");
                // ESC 键退出
                if (event.key.key == SDLK_ESCAPE)
                {
                    SDL_Log("按下 ESC,退出程序");
                    running = false;
                }
                break;

            case SDL_EVENT_KEY_UP:
                SDL_Log("键盘松开: 键=%s", SDL_GetKeyName(event.key.key));
                break;

            default:
                break;
            }
        }

        // 每帧重新绘制
        SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);
        SDL_RenderClear(renderer);
        SDL_RenderTexture(renderer, texture, NULL, NULL);
        SDL_RenderPresent(renderer);
    }

9.wav音频播放

cpp 复制代码
//===================== wav文件播放 (SDL3 标准) =====================

/**
 * SDL3 音频回调函数
 * stream: 目标音频流
 * additional_amount: 需要提供的数据字节数
 * total_amount: 队列中总共需要的数据量
 */

// 全局变量(也可封装到结构体中)
static Uint8* audio_buf = NULL;
static Uint32 audio_len = 0;
static Uint32 audio_pos = 0;          // 当前播放位置
static SDL_AudioDeviceID device_id = 0;

static void SDLCALL audio_callback(void *userdata, SDL_AudioStream *stream, int additional_amount, int total_amount)
{
    if (additional_amount <= 0 || !audio_buf) return;

    int remain = (int)audio_len - (int)audio_pos;
    
    if (remain > 0) {
        int copy_len = (remain > additional_amount) ? additional_amount : remain;
        SDL_PutAudioStreamData(stream, audio_buf + audio_pos, copy_len);
        audio_pos += copy_len;
    } else {
        // 实现循环播放:重置位置
        audio_pos = 0;
        SDL_PutAudioStreamData(stream, audio_buf + audio_pos, additional_amount);
        audio_pos += additional_amount;
    }
}

/**
 * 播放 WAV 文件
 */
void play_wav()
{
    SDL_AudioSpec audio_spec;

    // 1. 加载 WAV 文件 (SDL3 返回 bool)
    if (!SDL_LoadWAV("E:/three/code/avideo/sdl3/sdlThree/test.wav",
                    &audio_spec, &audio_buf, &audio_len)) {
        SDL_Log("SDL_LoadWAV failed: %s", SDL_GetError());
        return;
    }

    // 2. 打开音频设备流并设置回调
    // SDL3 推荐使用这种方式处理带回调的音频
    SDL_AudioStream *stream = SDL_OpenAudioDeviceStream(
        SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, 
        &audio_spec, 
        audio_callback, 
        NULL
    );

    if (!stream) {
        SDL_Log("SDL_OpenAudioDeviceStream failed: %s", SDL_GetError());
        SDL_free(audio_buf);
        return;
    }

    // 3. 开始播放
    SDL_ResumeAudioStreamDevice(stream);

    // 4. 等待播放完成或至少让用户听到声音
    // 根据长度计算大约时间
    int bytes_per_sample = SDL_AUDIO_BYTESIZE(audio_spec.format);
    double total_seconds = (double)audio_len / (audio_spec.freq * audio_spec.channels * bytes_per_sample);
    SDL_Log("开始播放 WAV (时长约 %.2f 秒)...", total_seconds);
    
    // 注意:这里为了演示使用了 SDL_Delay,在实际应用中通常不需要阻塞,
    // 因为 audio_callback 会在后台线程持续运行。
    SDL_Delay((Uint32)(total_seconds * 1000) + 500);

    // 5. 清理资源
    SDL_DestroyAudioStream(stream);
    SDL_free(audio_buf);
    audio_buf = NULL;
}

10.PCM声音播放

cpp 复制代码
//===================== pcm文件播放 =====================

/**
 * 播放 PCM 文件 (s16le, 44100Hz, Mono)
 */
void play_pcm()
{
    const char *pcm_path = "E:/three/code/avideo/sdl3/sdlThree/test2.pcm";
    
    // 1. 打开文件
    FILE *fp = fopen(pcm_path, "rb");
    if (!fp) {
        SDL_Log("无法打开 PCM 文件: %s", pcm_path);
        return;
    }

    // 2. 获取文件大小并读取数据
    fseek(fp, 0, SEEK_END);
    long pcm_len = ftell(fp);
    fseek(fp, 0, SEEK_SET);
    uint8_t *pcm_buf = (uint8_t*)malloc(pcm_len);
    fread(pcm_buf, 1, pcm_len, fp);
    fclose(fp);

    // 3. 设置音频格式
    SDL_AudioSpec spec;
    spec.freq = 44100;
    spec.format = SDL_AUDIO_S16LE;
    spec.channels = 1;

    // 4. 创建音频流 (Push 模式,不使用回调)
    SDL_AudioStream *stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, NULL, NULL);
    if (!stream) {
        SDL_Log("创建音频流失败: %s", SDL_GetError());
        free(pcm_buf);
        return;
    }

    // 5. 推送并播放
    SDL_Log("开始播放 PCM 文件...");
    SDL_PutAudioStreamData(stream, pcm_buf, (int)pcm_len);
    SDL_FlushAudioStream(stream); // 标记数据结束
    SDL_ResumeAudioStreamDevice(stream);

    // 6. 等待播放结束 (检查流中剩余数据)
    while (SDL_GetAudioStreamQueued(stream) > 0) {
        SDL_Delay(100);
    }

    // 7. 清理
    SDL_DestroyAudioStream(stream);
    free(pcm_buf);
    SDL_Log("PCM 播放完成");
}

11.YUV播放

cpp 复制代码
//===================== yuv文件播放 =====================

/**
 * 播放 YUV 文件 (640x480, YUV420P, 1fps)
 */
void play_yuv(SDL_Renderer *renderer)
{
    const char *yuv_path = "E:/three/code/avideo/sdl3/sdlThree/output.yuv";
    int w = 640, h = 480;
    int y_size = w * h;
    int uv_size = (w / 2) * (h / 2);
    int frame_size = y_size + uv_size * 2;

    // 1. 打开文件
    FILE *fp = fopen(yuv_path, "rb");
    if (!fp) {
        SDL_Log("无法打开 YUV 文件: %s", yuv_path);
        return;
    }

    // 2. 创建 YUV 纹理 (IYUV 即 YUV420P)
    SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, w, h);
    if (!texture) {
        SDL_Log("创建 YUV 纹理失败: %s", SDL_GetError());
        fclose(fp);
        return;
    }

    // 3. 分配缓冲区
    uint8_t *y_plane = (uint8_t*)malloc(y_size);
    uint8_t *u_plane = (uint8_t*)malloc(uv_size);
    uint8_t *v_plane = (uint8_t*)malloc(uv_size);

    SDL_Log("开始播放 YUV 文件...");

    // 4. 循环读取并渲染 (5 帧)
    for (int i = 0; i < 5; i++) {
        if (fread(y_plane, 1, y_size, fp) != y_size ||
            fread(u_plane, 1, uv_size, fp) != uv_size ||
            fread(v_plane, 1, uv_size, fp) != uv_size) {
            break;
        }

        // 更新纹理数据
        SDL_UpdateYUVTexture(texture, NULL, 
                             y_plane, w, 
                             u_plane, w / 2, 
                             v_plane, w / 2);

        // 渲染到窗口
        SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
        SDL_RenderClear(renderer);
        SDL_RenderTexture(renderer, texture, NULL, NULL);
        SDL_RenderPresent(renderer);

        SDL_Log("播放 YUV 第 %d 帧", i + 1);
        SDL_Delay(1000); // 1 帧/秒
    }

    // 5. 清理
    free(y_plane);
    free(u_plane);
    free(v_plane);
    SDL_DestroyTexture(texture);
    fclose(fp);
    SDL_Log("YUV 播放完成");
}

12.多线程

多线程实现方案(函数化风格):
1、视频读取线程 (VideoReadThread):

独立于主线程运行,每秒从 output.yuv 读取一帧数据。

使用 SDL_Mutex (互斥锁) 保护共享的 YUV 缓冲区,确保读取和渲染不会发生冲突。
2、音频播放回调 (AudioCallback):

在 SDL 内部的音频线程中运行。它会根据设备需求,自动从全局音频缓冲区提取 test2.pcm 的数据。

主线程渲染 (run_main_loop):

以约 60fps 运行,处理窗口事件。

当发现视频线程准备好新帧(new_frame_ready)时,才会锁定互斥锁并更新显卡纹理。
3、运行说明:

按 ESC 键:可以优雅地停止所有线程并关闭程序。

并发播放:你会发现视频每一秒切换一帧,同时背景音乐(PCM)持续流畅播放,互不干扰且不阻塞主界面的响应(如窗口缩放、拖动)。

cpp 复制代码
/**
 * SDL3 多线程音视频播放示例 (函数化风格)
 * 
 * 实现逻辑:
 * 1. 主线程:负责初始化、窗口渲染和事件循环。
 * 2. 视频读取线程:独立线程,负责定时读取 YUV 数据到共享缓冲区。
 * 3. 音频线程:由 SDL3 内部管理,通过回调函数持续消费 PCM 数据。
 * 
 * 同步:使用 SDL_Mutex 保护共享的 YUV 像素缓冲区。
 */

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <cstdio>
#include <cstdlib>

#ifdef _MSC_VER
#pragma execution_character_set("utf-8")
#endif

// ===================== 全局配置与状态 =====================
const char *YUV_PATH = "E:/three/code/avideo/sdl3/sdlThree/output.yuv";
const char *PCM_PATH = "E:/three/code/avideo/sdl3/sdlThree/test2.pcm";

const int WIDTH  = 640;
const int HEIGHT = 480;
const int Y_SIZE = WIDTH * HEIGHT;
const int UV_SIZE = (WIDTH / 2) * (HEIGHT / 2);

// 共享数据结构
struct SharedData {
    // 视频相关
    uint8_t *y_plane;
    uint8_t *u_plane;
    uint8_t *v_plane;
    SDL_Mutex *video_mutex;
    bool new_frame_ready;

    // 音频相关
    uint8_t *audio_buf;
    uint32_t audio_len;
    uint32_t audio_pos;

    // 状态
    bool is_running;
} g_data;

// ===================== 视频读取线程函数 =====================
static int SDLCALL VideoReadThread(void *ptr)
{
    FILE *fp = fopen(YUV_PATH, "rb");
    if (!fp) {
        SDL_Log("视频线程:无法打开 YUV 文件");
        return -1;
    }

    SDL_Log("视频线程:启动,开始读取数据...");

    while (g_data.is_running) {
        // 分配临时空间读取,减少对锁的占用时间
        uint8_t *ty = (uint8_t*)malloc(Y_SIZE);
        uint8_t *tu = (uint8_t*)malloc(UV_SIZE);
        uint8_t *tv = (uint8_t*)malloc(UV_SIZE);

        if (fread(ty, 1, Y_SIZE, fp) != Y_SIZE ||
            fread(tu, 1, UV_SIZE, fp) != UV_SIZE ||
            fread(tv, 1, UV_SIZE, fp) != UV_SIZE) {
            // 文件结束,重置到开头
            fseek(fp, 0, SEEK_SET);
            free(ty); free(tu); free(tv);
            continue;
        }

        // 写入共享缓冲区
        SDL_LockMutex(g_data.video_mutex);
        memcpy(g_data.y_plane, ty, Y_SIZE);
        memcpy(g_data.u_plane, tu, UV_SIZE);
        memcpy(g_data.v_plane, tv, UV_SIZE);
        g_data.new_frame_ready = true;
        SDL_UnlockMutex(g_data.video_mutex);

        free(ty); free(tu); free(tv);

        // 控制帧率 (1fps)
        SDL_Delay(1000);
    }

    fclose(fp);
    // SDL_Log("视频线程:退出");
    return 0;
}

// ===================== 音频回调函数 =====================
static void SDLCALL AudioCallback(void *userdata, SDL_AudioStream *stream, int additional_amount, int total_amount)
{
    if (additional_amount <= 0 || !g_data.audio_buf) return;

    uint32_t remain = g_data.audio_len - g_data.audio_pos;
    
    if (remain > 0) {
        uint32_t copy_len = (remain > (uint32_t)additional_amount) ? (uint32_t)additional_amount : remain;
        SDL_PutAudioStreamData(stream, g_data.audio_buf + g_data.audio_pos, copy_len);
        g_data.audio_pos += copy_len;
    } else {
        // 循环播放
        g_data.audio_pos = 0;
        SDL_PutAudioStreamData(stream, g_data.audio_buf, additional_amount);
        g_data.audio_pos += additional_amount;
    }
}

// ===================== 初始化与资源加载 =====================
bool init_resources()
{
    // 初始化 SDL
    if (!SDL_Init(SDL_INIT_AUDIO | SDL_INIT_VIDEO)) return false;

    // 初始化共享数据
    g_data.y_plane = (uint8_t*)malloc(Y_SIZE);
    g_data.u_plane = (uint8_t*)malloc(UV_SIZE);
    g_data.v_plane = (uint8_t*)malloc(UV_SIZE);
    g_data.video_mutex = SDL_CreateMutex();
    g_data.new_frame_ready = false;
    g_data.is_running = true;

    // 加载 PCM 到内存
    FILE *fp = fopen(PCM_PATH, "rb");
    if (fp) {
        fseek(fp, 0, SEEK_END);
        g_data.audio_len = ftell(fp);
        fseek(fp, 0, SEEK_SET);
        g_data.audio_buf = (uint8_t*)malloc(g_data.audio_len);
        fread(g_data.audio_buf, 1, g_data.audio_len, fp);
        fclose(fp);
        g_data.audio_pos = 0;
    } else {
        SDL_Log("无法加载 PCM 文件: %s", PCM_PATH);
        return false;
    }

    return true;
}

// ===================== 主循环与渲染 =====================
void run_main_loop(SDL_Renderer *renderer, SDL_Texture *texture)
{
    SDL_Event event;
    while (g_data.is_running) {
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_EVENT_QUIT || 
               (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_ESCAPE)) {
                g_data.is_running = false;
            }
        }

        // 只有当有新帧时才更新纹理
        SDL_LockMutex(g_data.video_mutex);
        if (g_data.new_frame_ready) {
            SDL_UpdateYUVTexture(texture, NULL, 
                                 g_data.y_plane, WIDTH, 
                                 g_data.u_plane, WIDTH / 2, 
                                 g_data.v_plane, WIDTH / 2);
            g_data.new_frame_ready = false;
        }
        SDL_UnlockMutex(g_data.video_mutex);

        // 渲染
        SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
        SDL_RenderClear(renderer);
        SDL_RenderTexture(renderer, texture, NULL, NULL);
        SDL_RenderPresent(renderer);

        SDL_Delay(16); // ~60fps 刷新界面
    }
}

// ===================== 资源清理 =====================
void cleanup(SDL_Window *window, SDL_Renderer *renderer, SDL_Texture *texture, SDL_AudioStream *stream)
{
    if (stream) SDL_DestroyAudioStream(stream);
    if (texture) SDL_DestroyTexture(texture);
    if (renderer) SDL_DestroyRenderer(renderer);
    if (window) SDL_DestroyWindow(window);

    if (g_data.y_plane) free(g_data.y_plane);
    if (g_data.u_plane) free(g_data.u_plane);
    if (g_data.v_plane) free(g_data.v_plane);
    if (g_data.audio_buf) free(g_data.audio_buf);
    if (g_data.video_mutex) SDL_DestroyMutex(g_data.video_mutex);

    SDL_Quit();
}

// ===================== Main =====================
int main(int argc, char *argv[])
{
    if (!init_resources()) return -1;

    // 创建窗口与渲染器
    SDL_Window *window = SDL_CreateWindow("SDL3 多线程音视频播放示例", WIDTH, HEIGHT, 0);
    SDL_Renderer *renderer = SDL_CreateRenderer(window, NULL);
    SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, WIDTH, HEIGHT);

    // 启动音频
    SDL_AudioSpec spec = { SDL_AUDIO_S16LE, 1, 44100 };
    SDL_AudioStream *stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, AudioCallback, NULL);
    if (stream) {
        SDL_ResumeAudioStreamDevice(stream);
        SDL_Log("音频已启动 (回调模式)");
    }

    // 启动视频读取线程
    SDL_Thread *videoThread = SDL_CreateThread(VideoReadThread, "VideoReader", NULL);
    if (!videoThread) {
        SDL_Log("无法创建视频线程: %s", SDL_GetError());
        g_data.is_running = false;
    }

    // 主循环
    run_main_loop(renderer, texture);

    // 等待线程结束
    SDL_WaitThread(videoThread, NULL);

    // 清理
    cleanup(window, renderer, texture, stream);

    return 0;
}
相关推荐
谢尔登1 小时前
深入React19任务调度器Scheduler
开发语言·前端·javascript
hoiii1871 小时前
MATLAB中LSSVM工具包及简单例程详解
开发语言·matlab
李可以量化2 小时前
【Python 量化入门】AKshare 保姆级使用教程:零成本获取股票 / 基金 / 期货全市场金融数据
开发语言·python·金融·qmt·miniqmt·量化 qmt ptrade
众创岛2 小时前
使用IIS运行php程序,处理put和delete请求出现405错误
开发语言·php
sycmancia2 小时前
C++——完善的复数类
开发语言·c++
金刚狼882 小时前
在qt creator中创建helloworld程序并构建
开发语言·qt
小二·2 小时前
Go 语言系统编程与云原生开发实战(第21篇)
开发语言·云原生·golang
小二·2 小时前
Go 语言系统编程与云原生开发实战(第20篇)
开发语言·云原生·golang
女王大人万岁2 小时前
Golang实战Eclipse Paho MQTT库:MQTT通信全解析
服务器·开发语言·后端·golang