SDL3配置及基本使用
1.sdl3+QtCreator+cmake+MSVC
1、去sdl官网下载vc版本,这个是给msvc用的,解压后把路径添加到系统环境变量path
**例如我的:E:\SDL\SDL3-3.4.2**
2、创建cmake项目,编辑cmakelists.txt
- 添加
find_package(SDL3 REQUIRED) - 添加
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
-
下载对应的ffmpeg版本,我安装的为ffmpeg-8.0.1-full_build-shared
-
将ffmpeg文件夹复制到需要添加库的qt项目文件夹中,改名为ffmpeg
-
复制ffmpeg文件夹下bin文件夹下的dll文件,将其粘贴到类似下列文件夹
E:\three\code\avideo\sdl3\sdlThree\build\Desktop_Qt_6_8_3_MSVC2022_64bit-Debug -
修改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
) -
修改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;
}