当前位置: 首页 > news >正文

【C++实战(72)】解锁C++音视频开发新姿势:SDL基础实战攻略

目录

  • 一、SDL 的基础概念与环境搭建
    • 1.1 SDL 的作用
    • 1.2 SDL 开发环境搭建
    • 1.3 SDL 的核心模块与初始化
  • 二、SDL 视频渲染实战
    • 2.1 窗口创建与渲染器初始化
    • 2.2 纹理创建与 YUV 数据渲染
    • 2.3 渲染循环与事件处理
  • 三、SDL 音频播放实战
    • 3.1 音频设备打开与参数设置
    • 3.2 音频回调函数的实现
    • 3.3 音视频同步的简单实现
  • 四、实战项目:FFmpeg+SDL 视频播放器
    • 4.1 项目需求
    • 4.2 FFmpeg 解码与 SDL 渲染 / 播放的结合代码
    • 4.3 播放器功能测试


一、SDL 的基础概念与环境搭建

1.1 SDL 的作用

SDL(Simple DirectMedia Layer)是一个跨平台的多媒体开发库,它在音视频开发领域扮演着至关重要的角色,为开发者提供了一套统一的接口,使得在不同操作系统(如 Windows、Linux、macOS 等)上进行多媒体应用开发变得更加便捷。

在音视频播放方面,SDL 支持多种音频和视频格式,能够轻松实现音频的播放与录制,以及视频的解码与播放。例如,通过 SDL 可以快速搭建一个简单的视频播放器,播放常见的 MP4、AVI 等格式的视频文件。它内部封装了复杂的音视频处理逻辑,开发者无需深入了解底层的编解码细节,就能够实现基本的音视频播放功能。

在图形渲染方面,SDL 提供了 2D 图形渲染的接口,支持图像的加载、绘制、变换等操作。利用这些接口,开发者可以创建精美的游戏界面、动画效果等。同时,SDL 还可以与 OpenGL、Vulkan 等 3D 图形库结合使用,进一步拓展其图形渲染能力,为开发更复杂的 3D 应用提供支持。

1.2 SDL 开发环境搭建

  1. 库安装
    • Windows 平台:可以从 SDL 官方网站(https://www.libsdl.org/download-2.0.php )下载预编译的开发库。例如,下载SDL2-devel-2.0.16-VC.zip文件,解压后会得到包含头文件、库文件和动态链接库的目录。
    • Linux 平台:在基于 Debian 或 Ubuntu 的系统上,可以使用包管理器进行安装,执行命令sudo apt - get install libsdl2 - dev,系统会自动下载并安装 SDL 开发所需的库文件和头文件。在基于 Red Hat 或 CentOS 的系统上,可能需要使用yum命令进行安装,如sudo yum install SDL2 - devel。
    • macOS 平台:可以使用 Homebrew 包管理器,执行命令brew install sdl2,Homebrew 会自动处理依赖关系并完成安装。
  2. 头文件配置
    • 在项目中,需要将 SDL 头文件所在的目录添加到编译器的包含路径中。例如,在使用 GCC 编译的 C++ 项目中,可以在编译命令中添加-I/path/to/SDL2/include,其中/path/to/SDL2/include是 SDL 头文件所在的实际路径。在集成开发环境(IDE)中,如 Visual Studio,可以在项目属性的 “C/C++ -> 常规 -> 附加包含目录” 中添加 SDL 头文件目录。
  3. 链接设置
    需要将 SDL 的库文件链接到项目中。在 Windows 平台上,如果使用 Visual Studio,在项目属性的 “链接器 -> 常规 -> 附加库目录” 中添加 SDL 库文件所在的目录,如SDL2 - 2.0.16/lib/x86,然后在 “链接器 -> 输入 -> 附加依赖项” 中添加SDL2.lib和SDL2main.lib。在 Linux 平台上,编译时使用-L/path/to/SDL2/lib - lSDL2,其中/path/to/SDL2/lib是 SDL 库文件所在的路径,-lSDL2表示链接 SDL2 库。

1.3 SDL 的核心模块与初始化

  1. 核心模块

    • SDL_Video:负责视频相关的操作,如窗口创建、图形渲染、视频模式设置等。通过SDL_Video模块,可以创建一个窗口,并在窗口中进行图形绘制,支持多种显示模式和分辨率。
    • SDL_Audio:主要用于音频处理,包括音频设备的打开、关闭,音频数据的播放、录制等。开发者可以使用该模块播放背景音乐、音效,以及进行音频的实时处理。
    • SDL_Render:提供了高效的渲染功能,支持硬件加速渲染,能够将纹理数据渲染到窗口上。它简化了图形渲染的过程,提高了渲染效率,使得开发者可以更方便地实现复杂的图形效果。
  2. 初始化:在使用 SDL 之前,需要对相关模块进行初始化。例如,要初始化SDL_Video、SDL_Audio和SDL_Render模块,可以使用以下代码:

#include <SDL2/SDL.h>
#include <iostream>int main(int argc, char* argv[]) {// 初始化SDL,同时初始化视频、音频和事件子系统if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_EVENTS) < 0) {std::cerr << "SDL初始化失败: " << SDL_GetError() << std::endl;return 1;}// 这里可以进行后续的SDL操作,如创建窗口、渲染器等// 退出SDLSDL_Quit();return 0;
}

在上述代码中,SDL_Init函数用于初始化 SDL,参数SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_EVENTS表示同时初始化视频、音频和事件子系统。如果初始化失败,SDL_Init函数会返回一个小于 0 的值,通过SDL_GetError函数可以获取具体的错误信息。

二、SDL 视频渲染实战

2.1 窗口创建与渲染器初始化

在 SDL 视频渲染中,首先需要创建一个窗口来显示视频内容,然后初始化渲染器,用于将视频图像绘制到窗口上。

#include <SDL2/SDL.h>
#include <iostream>int main(int argc, char* argv[]) {// 初始化SDLif (SDL_Init(SDL_INIT_VIDEO) < 0) {std::cerr << "SDL初始化失败: " << SDL_GetError() << std::endl;return 1;}// 创建窗口SDL_Window* window = SDL_CreateWindow("SDL视频渲染示例",SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,800, 600,SDL_WINDOW_SHOWN);if (window == nullptr) {std::cerr << "窗口创建失败: " << SDL_GetError() << std::endl;SDL_Quit();return 1;}// 创建渲染器SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);if (renderer == nullptr) {std::cerr << "渲染器创建失败: " << SDL_GetError() << std::endl;SDL_DestroyWindow(window);SDL_Quit();return 1;}// 这里可以进行后续的渲染操作// 清理资源SDL_DestroyRenderer(renderer);SDL_DestroyWindow(window);SDL_Quit();return 0;
}

在上述代码中,SDL_CreateWindow函数用于创建一个窗口,参数依次为窗口标题、窗口在屏幕上的初始 x 坐标、初始 y 坐标、窗口宽度、窗口高度以及窗口标志。SDL_WINDOWPOS_CENTERED表示窗口在屏幕上居中显示,SDL_WINDOW_SHOWN表示创建的窗口是可见的。SDL_CreateRenderer函数用于创建渲染器,第一个参数是关联的窗口,第二个参数-1表示让 SDL 自动选择合适的渲染驱动,SDL_RENDERER_ACCELERATED表示使用硬件加速渲染。

2.2 纹理创建与 YUV 数据渲染

在创建好窗口和渲染器后,需要创建纹理来存储视频图像数据,并将 YUV 格式的视频数据渲染到纹理上。

#include <SDL2/SDL.h>
#include <iostream>
#include <fstream>// 假设视频分辨率为800x600
const int WIDTH = 800;
const int HEIGHT = 600;int main(int argc, char* argv[]) {// 初始化SDLif (SDL_Init(SDL_INIT_VIDEO) < 0) {std::cerr << "SDL初始化失败: " << SDL_GetError() << std::endl;return 1;}// 创建窗口SDL_Window* window = SDL_CreateWindow("SDL视频渲染示例",SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,WIDTH, HEIGHT,SDL_WINDOW_SHOWN);if (window == nullptr) {std::cerr << "窗口创建失败: " << SDL_GetError() << std::endl;SDL_Quit();return 1;}// 创建渲染器SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);if (renderer == nullptr) {std::cerr << "渲染器创建失败: " << SDL_GetError() << std::endl;SDL_DestroyWindow(window);SDL_Quit();return 1;}// 创建纹理,用于存储YUV数据SDL_Texture* texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, SDL_TEXTUREACCESS_STREAMING, WIDTH, HEIGHT);if (texture == nullptr) {std::cerr << "纹理创建失败: " << SDL_GetError() << std::endl;SDL_DestroyRenderer(renderer);SDL_DestroyWindow(window);SDL_Quit();return 1;}// 读取YUV文件数据std::ifstream yuvFile("test.yuv", std::ios::binary);if (!yuvFile.is_open()) {std::cerr << "无法打开YUV文件" << std::endl;SDL_DestroyTexture(texture);SDL_DestroyRenderer(renderer);SDL_DestroyWindow(window);SDL_Quit();return 1;}// 读取一帧YUV数据const int frameSize = WIDTH * HEIGHT * 3 / 2;char* yuvBuffer = new char[frameSize];yuvFile.read(yuvBuffer, frameSize);// 更新纹理数据SDL_UpdateTexture(texture, nullptr, yuvBuffer, WIDTH);// 渲染纹理到窗口SDL_RenderClear(renderer);SDL_RenderCopy(renderer, texture, nullptr, nullptr);SDL_RenderPresent(renderer);// 清理资源delete[] yuvBuffer;yuvFile.close();SDL_DestroyTexture(texture);SDL_DestroyRenderer(renderer);SDL_DestroyWindow(window);SDL_Quit();return 0;
}

在这段代码中,SDL_CreateTexture函数创建了一个用于存储 YUV 数据的纹理,SDL_PIXELFORMAT_YV12指定了纹理的像素格式为 YV12。SDL_UpdateTexture函数用于将读取到的 YUV 数据更新到纹理中。最后,通过SDL_RenderCopy函数将纹理渲染到窗口上,并使用SDL_RenderPresent函数将渲染结果显示出来。

2.3 渲染循环与事件处理

为了实现视频的连续播放,需要创建一个渲染循环,并在循环中处理窗口关闭、键盘事件等。

#include <SDL2/SDL.h>
#include <iostream>
#include <fstream>// 假设视频分辨率为800x600
const int WIDTH = 800;
const int HEIGHT = 600;int main(int argc, char* argv[]) {// 初始化SDLif (SDL_Init(SDL_INIT_VIDEO) < 0) {std::cerr << "SDL初始化失败: " << SDL_GetError() << std::endl;return 1;}// 创建窗口SDL_Window* window = SDL_CreateWindow("SDL视频渲染示例",SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,WIDTH, HEIGHT,SDL_WINDOW_SHOWN);if (window == nullptr) {std::cerr << "窗口创建失败: " << SDL_GetError() << std::endl;SDL_Quit();return 1;}// 创建渲染器SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);if (renderer == nullptr) {std::cerr << "渲染器创建失败: " << SDL_GetError() << std::endl;SDL_DestroyWindow(window);SDL_Quit();return 1;}// 创建纹理,用于存储YUV数据SDL_Texture* texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, SDL_TEXTUREACCESS_STREAMING, WIDTH, HEIGHT);if (texture == nullptr) {std::cerr << "纹理创建失败: " << SDL_GetError() << std::endl;SDL_DestroyRenderer(renderer);SDL_DestroyWindow(window);SDL_Quit();return 1;}// 读取YUV文件数据std::ifstream yuvFile("test.yuv", std::ios::binary);if (!yuvFile.is_open()) {std::cerr << "无法打开YUV文件" << std::endl;SDL_DestroyTexture(texture);SDL_DestroyRenderer(renderer);SDL_DestroyWindow(window);SDL_Quit();return 1;}bool running = true;SDL_Event event;while (running) {while (SDL_PollEvent(&event)) {if (event.type == SDL_QUIT) {running = false;} else if (event.type == SDL_KEYDOWN) {switch (event.key.keysym.sym) {case SDLK_ESCAPE:running = false;break;// 可以添加更多键盘事件处理}}}// 读取一帧YUV数据const int frameSize = WIDTH * HEIGHT * 3 / 2;char* yuvBuffer = new char[frameSize];if (yuvFile.read(yuvBuffer, frameSize).gcount() != frameSize) {// 到达文件末尾,重新回到文件开头yuvFile.clear();yuvFile.seekg(0, std::ios::beg);yuvFile.read(yuvBuffer, frameSize);}// 更新纹理数据SDL_UpdateTexture(texture, nullptr, yuvBuffer, WIDTH);// 渲染纹理到窗口SDL_RenderClear(renderer);SDL_RenderCopy(renderer, texture, nullptr, nullptr);SDL_RenderPresent(renderer);delete[] yuvBuffer;}// 清理资源yuvFile.close();SDL_DestroyTexture(texture);SDL_DestroyRenderer(renderer);SDL_DestroyWindow(window);SDL_Quit();return 0;
}

在上述代码中,通过SDL_PollEvent函数不断从事件队列中获取事件。当接收到SDL_QUIT事件(如用户点击窗口关闭按钮)或按下ESC键时,将running标志设置为false,从而退出渲染循环。在每次循环中,读取一帧 YUV 数据,更新纹理并渲染到窗口上,实现视频的连续播放效果。

三、SDL 音频播放实战

3.1 音频设备打开与参数设置

在 SDL 音频播放中,打开音频设备并正确设置参数是实现音频播放的基础。首先,需要定义一个SDL_AudioSpec结构体来描述音频的相关参数,如采样率、声道数、样本格式等。

#include <SDL2/SDL.h>
#include <iostream>int main(int argc, char* argv[]) {// 初始化SDL音频子系统if (SDL_Init(SDL_INIT_AUDIO) < 0) {std::cerr << "SDL音频初始化失败: " << SDL_GetError() << std::endl;return 1;}// 定义音频参数SDL_AudioSpec desiredSpec, obtainedSpec;desiredSpec.freq = 44100; // 采样率,常见值有44100Hz、48000Hz等desiredSpec.format = AUDIO_S16SYS; // 样本格式,AUDIO_S16SYS表示16位有符号整数,系统字节序desiredSpec.channels = 2; // 声道数,2表示立体声desiredSpec.silence = 0; // 静音样本值desiredSpec.samples = 1024; // 音频缓冲区大小,以样本数为单位desiredSpec.callback = nullptr; // 音频回调函数,暂时设为nullptr// 打开音频设备SDL_AudioDeviceID deviceID = SDL_OpenAudioDevice(nullptr, 0, &desiredSpec, &obtainedSpec, 0);if (deviceID == 0) {std::cerr << "音频设备打开失败: " << SDL_GetError() << std::endl;SDL_Quit();return 1;}// 打印实际获取的音频参数std::cout << "实际采样率: " << obtainedSpec.freq << std::endl;std::cout << "实际样本格式: " << obtainedSpec.format << std::endl;std::cout << "实际声道数: " << obtainedSpec.channels << std::endl;// 这里可以进行后续的音频播放操作// 关闭音频设备SDL_CloseAudioDevice(deviceID);SDL_Quit();return 0;
}

在上述代码中,SDL_OpenAudioDevice函数用于打开音频设备。第一个参数nullptr表示使用默认音频设备,第二个参数0表示打开输出设备(如果为 1 则表示打开输入设备)。desiredSpec是期望的音频参数,obtainedSpec用于返回实际获取的音频参数。如果设备打开成功,deviceID将是一个非零值,否则为 0。通过比较desiredSpec和obtainedSpec,可以了解实际使用的音频参数与期望参数是否一致。

3.2 音频回调函数的实现

音频回调函数在音频播放过程中起着关键作用,它负责将 PCM 音频数据填充到音频缓冲区中,以实现音频的持续播放。下面是一个简单的音频回调函数示例:

#include <SDL2/SDL.h>
#include <iostream>
#include <fstream>// 假设音频文件为16位PCM格式,单声道,采样率为44100Hz
const int SAMPLE_RATE = 44100;
const int BITS_PER_SAMPLE = 16;
const int CHANNELS = 1;
const int BUFFER_SIZE = 1024;std::ifstream audioFile("test.pcm", std::ios::binary);void audioCallback(void* userdata, Uint8* stream, int len) {// 读取音频数据到缓冲区audioFile.read(reinterpret_cast<char*>(stream), len);// 如果读取到文件末尾,重新回到文件开头if (audioFile.eof()) {audioFile.clear();audioFile.seekg(0, std::ios::beg);audioFile.read(reinterpret_cast<char*>(stream), len);}
}int main(int argc, char* argv[]) {// 初始化SDL音频子系统if (SDL_Init(SDL_INIT_AUDIO) < 0) {std::cerr << "SDL音频初始化失败: " << SDL_GetError() << std::endl;return 1;}// 定义音频参数SDL_AudioSpec desiredSpec, obtainedSpec;desiredSpec.freq = SAMPLE_RATE;desiredSpec.format = AUDIO_S16SYS;desiredSpec.channels = CHANNELS;desiredSpec.silence = 0;desiredSpec.samples = BUFFER_SIZE;desiredSpec.callback = audioCallback;desiredSpec.userdata = nullptr;// 打开音频设备SDL_AudioDeviceID deviceID = SDL_OpenAudioDevice(nullptr, 0, &desiredSpec, &obtainedSpec, 0);if (deviceID == 0) {std::cerr << "音频设备打开失败: " << SDL_GetError() << std::endl;SDL_Quit();return 1;}// 启动音频播放SDL_PauseAudioDevice(deviceID, 0);// 保持程序运行,让音频持续播放while (true) {SDL_Delay(100);}// 关闭音频设备和文件SDL_CloseAudioDevice(deviceID);audioFile.close();SDL_Quit();return 0;
}

在这个示例中,audioCallback函数是音频回调函数,它会在音频缓冲区需要数据时被 SDL 调用。函数参数userdata是传递给回调函数的用户数据,这里设为nullptr;stream是指向音频缓冲区的指针,len是缓冲区的大小。在函数内部,从 PCM 音频文件中读取数据填充到stream缓冲区中。如果读取到文件末尾,则重新回到文件开头继续读取,以实现循环播放。在main函数中,将desiredSpec.callback设置为audioCallback,并在打开音频设备后,通过SDL_PauseAudioDevice(deviceID, 0)启动音频播放。

3.3 音视频同步的简单实现

实现音视频同步的关键在于基于时间戳对音频和视频的播放进行控制。首先,在解码音视频数据时,需要获取每个音频帧和视频帧的时间戳。

#include <SDL2/SDL.h>
#include <iostream>
#include <fstream>
#include <cmath>// 假设视频分辨率为800x600,音频为16位PCM格式,单声道,采样率为44100Hz
const int WIDTH = 800;
const int HEIGHT = 600;
const int SAMPLE_RATE = 44100;
const int BITS_PER_SAMPLE = 16;
const int CHANNELS = 1;
const int BUFFER_SIZE = 1024;std::ifstream videoFile("test.yuv", std::ios::binary);
std::ifstream audioFile("test.pcm", std::ios::binary);// 用于存储视频和音频的时间戳
double videoTimestamp = 0.0;
double audioTimestamp = 0.0;// 音频回调函数
void audioCallback(void* userdata, Uint8* stream, int len) {// 读取音频数据到缓冲区audioFile.read(reinterpret_cast<char*>(stream), len);// 更新音频时间戳,这里简单假设每个音频样本的时间间隔audioTimestamp += static_cast<double>(len) / (SAMPLE_RATE * BITS_PER_SAMPLE / 8 * CHANNELS);// 如果读取到文件末尾,重新回到文件开头if (audioFile.eof()) {audioFile.clear();audioFile.seekg(0, std::ios::beg);audioTimestamp = 0.0;}
}int main(int argc, char* argv[]) {// 初始化SDLif (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0) {std::cerr << "SDL初始化失败: " << SDL_GetError() << std::endl;return 1;}// 创建窗口和渲染器SDL_Window* window = SDL_CreateWindow("音视频同步示例",SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,WIDTH, HEIGHT,SDL_WINDOW_SHOWN);if (window == nullptr) {std::cerr << "窗口创建失败: " << SDL_GetError() << std::endl;SDL_Quit();return 1;}SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);if (renderer == nullptr) {std::cerr << "渲染器创建失败: " << SDL_GetError() << std::endl;SDL_DestroyWindow(window);SDL_Quit();return 1;}// 创建纹理SDL_Texture* texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, SDL_TEXTUREACCESS_STREAMING, WIDTH, HEIGHT);if (texture == nullptr) {std::cerr << "纹理创建失败: " << SDL_GetError() << std::endl;SDL_DestroyRenderer(renderer);SDL_DestroyWindow(window);SDL_Quit();return 1;}// 定义音频参数SDL_AudioSpec desiredSpec, obtainedSpec;desiredSpec.freq = SAMPLE_RATE;desiredSpec.format = AUDIO_S16SYS;desiredSpec.channels = CHANNELS;desiredSpec.silence = 0;desiredSpec.samples = BUFFER_SIZE;desiredSpec.callback = audioCallback;desiredSpec.userdata = nullptr;// 打开音频设备SDL_AudioDeviceID deviceID = SDL_OpenAudioDevice(nullptr, 0, &desiredSpec, &obtainedSpec, 0);if (deviceID == 0) {std::cerr << "音频设备打开失败: " << SDL_GetError() << std::endl;SDL_DestroyTexture(texture);SDL_DestroyRenderer(renderer);SDL_DestroyWindow(window);SDL_Quit();return 1;}// 启动音频播放SDL_PauseAudioDevice(deviceID, 0);bool running = true;SDL_Event event;while (running) {while (SDL_PollEvent(&event)) {if (event.type == SDL_QUIT) {running = false;}}// 读取一帧YUV视频数据const int frameSize = WIDTH * HEIGHT * 3 / 2;char* yuvBuffer = new char[frameSize];if (videoFile.read(yuvBuffer, frameSize).gcount() != frameSize) {// 到达文件末尾,重新回到文件开头videoFile.clear();videoFile.seekg(0, std::ios::beg);videoFile.read(yuvBuffer, frameSize);videoTimestamp = 0.0;}// 更新视频时间戳,这里简单假设每个视频帧的时间间隔videoTimestamp += 0.04; // 假设每帧时间间隔为40毫秒// 根据时间戳进行音视频同步if (std::fabs(videoTimestamp - audioTimestamp) > 0.05) {// 时间差大于50毫秒,进行调整if (videoTimestamp > audioTimestamp) {// 视频播放快了,等待音频SDL_Delay(static_cast<Uint32>((videoTimestamp - audioTimestamp) * 1000));} else {// 音频播放快了,跳过一些音频数据(这里简单处理,实际可更复杂)audioFile.seekg(BUFFER_SIZE, std::ios::cur);audioTimestamp += static_cast<double>(BUFFER_SIZE) / (SAMPLE_RATE * BITS_PER_SAMPLE / 8 * CHANNELS);}}// 更新纹理数据SDL_UpdateTexture(texture, nullptr, yuvBuffer, WIDTH);// 渲染纹理到窗口SDL_RenderClear(renderer);SDL_RenderCopy(renderer, texture, nullptr, nullptr);SDL_RenderPresent(renderer);delete[] yuvBuffer;}// 清理资源videoFile.close();audioFile.close();SDL_DestroyTexture(texture);SDL_DestroyRenderer(renderer);SDL_DestroyWindow(window);SDL_CloseAudioDevice(deviceID);SDL_Quit();return 0;
}

在上述代码中,分别为音频和视频维护了时间戳audioTimestamp和videoTimestamp。在音频回调函数audioCallback中,每次读取音频数据时更新音频时间戳。在视频播放循环中,每次读取视频帧时更新视频时间戳。通过比较音频和视频的时间戳,当时间差大于一定阈值(这里设为 50 毫秒)时,进行相应的调整。如果视频时间戳大于音频时间戳,说明视频播放快了,通过SDL_Delay函数等待音频;如果音频时间戳大于视频时间戳,说明音频播放快了,这里简单地跳过一些音频数据来调整,实际应用中可以采用更复杂的算法,如动态调整音频播放速度等。

四、实战项目:FFmpeg+SDL 视频播放器

4.1 项目需求

本项目旨在打造一个具备基础功能的视频播放器,通过结合 FFmpeg 强大的音视频解码能力与 SDL 优秀的多媒体渲染和播放功能,实现对 MP4 格式视频文件的流畅播放。具体需求如下:

  • 解码 MP4 视频:利用 FFmpeg 库,准确解析 MP4 文件的封装格式,将其中的视频和音频压缩编码数据分离出来,并进一步解码为非压缩的原始数据,如视频的 YUV 格式数据和音频的 PCM 格式数据。
  • SDL 渲染视频:借助 SDL 的视频渲染模块,将解码后的 YUV 视频数据渲染到窗口上,呈现出清晰的视频画面。需要创建窗口、初始化渲染器和纹理,并实现高效的渲染循环,确保视频播放的流畅性。
  • SDL 播放音频:使用 SDL 的音频模块,打开音频设备,设置合适的音频参数,如采样率、声道数、样本格式等。通过实现音频回调函数,将解码后的 PCM 音频数据填充到音频缓冲区中,实现音频的流畅播放。
  • 音视频同步:基于时间戳机制,对音频和视频的播放进行精确控制,使音频和视频能够保持同步播放,避免出现音画不同步的现象,为用户提供良好的观看体验。

4.2 FFmpeg 解码与 SDL 渲染 / 播放的结合代码

#include <iostream>
#include <SDL2/SDL.h>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
}// 视频窗口的宽度和高度
const int WIDTH = 800;
const int HEIGHT = 600;// 音频参数
const int SAMPLE_RATE = 44100;
const int CHANNELS = 2;
const int BITS_PER_SAMPLE = 16;// 音频回调函数
void audioCallback(void* userdata, Uint8* stream, int len) {// 从用户数据中获取音频帧队列等信息(这里简化未实现完整队列管理)// 假设userdata是一个包含音频相关信息的结构体指针// AudioInfo* audioInfo = (AudioInfo*)userdata;// 从队列中取出音频帧数据填充到stream中// 这里简单模拟读取音频数据,实际应从解码后的音频帧队列获取static std::ifstream audioFile("test.pcm", std::ios::binary);audioFile.read(reinterpret_cast<char*>(stream), len);if (audioFile.eof()) {audioFile.clear();audioFile.seekg(0, std::ios::beg);}
}int main(int argc, char* argv[]) {if (argc < 2) {std::cerr << "Usage: " << argv[0] << " <mp4 file>" << std::endl;return 1;}// 初始化SDLif (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0) {std::cerr << "SDL初始化失败: " << SDL_GetError() << std::endl;return 1;}// 创建窗口SDL_Window* window = SDL_CreateWindow("FFmpeg+SDL视频播放器",SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,WIDTH, HEIGHT,SDL_WINDOW_SHOWN);if (window == nullptr) {std::cerr << "窗口创建失败: " << SDL_GetError() << std::endl;SDL_Quit();return 1;}// 创建渲染器SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);if (renderer == nullptr) {std::cerr << "渲染器创建失败: " << SDL_GetError() << std::endl;SDL_DestroyWindow(window);SDL_Quit();return 1;}// 创建纹理SDL_Texture* texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, SDL_TEXTUREACCESS_STREAMING, WIDTH, HEIGHT);if (texture == nullptr) {std::cerr << "纹理创建失败: " << SDL_GetError() << std::endl;SDL_DestroyRenderer(renderer);SDL_DestroyWindow(window);SDL_Quit();return 1;}// 初始化FFmpegav_register_all();AVFormatContext* formatCtx = nullptr;if (avformat_open_input(&formatCtx, argv[1], nullptr, nullptr) != 0) {std::cerr << "无法打开视频文件: " << argv[1] << std::endl;SDL_DestroyTexture(texture);SDL_DestroyRenderer(renderer);SDL_DestroyWindow(window);SDL_Quit();return 1;}if (avformat_find_stream_info(formatCtx, nullptr) < 0) {std::cerr << "无法获取视频流信息" << std::endl;avformat_close_input(&formatCtx);SDL_DestroyTexture(texture);SDL_DestroyRenderer(renderer);SDL_DestroyWindow(window);SDL_Quit();return 1;}int videoStreamIndex = -1;int audioStreamIndex = -1;for (unsigned int i = 0; i < formatCtx->nb_streams; ++i) {if (formatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {videoStreamIndex = i;} else if (formatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {audioStreamIndex = i;}}if (videoStreamIndex == -1) {std::cerr << "未找到视频流" << std::endl;avformat_close_input(&formatCtx);SDL_DestroyTexture(texture);SDL_DestroyRenderer(renderer);SDL_DestroyWindow(window);SDL_Quit();return 1;}if (audioStreamIndex == -1) {std::cerr << "未找到音频流" << std::endl;}AVCodecParameters* videoCodecParams = formatCtx->streams[videoStreamIndex]->codecpar;AVCodec* videoCodec = avcodec_find_decoder(videoCodecParams->codec_id);if (!videoCodec) {std::cerr << "不支持的视频编解码器" << std::endl;avformat_close_input(&formatCtx);SDL_DestroyTexture(texture);SDL_DestroyRenderer(renderer);SDL_DestroyWindow(window);SDL_Quit();return 1;}AVCodecContext* videoCodecCtx = avcodec_alloc_context3(videoCodec);if (avcodec_parameters_to_context(videoCodecCtx, videoCodecParams) < 0 || avcodec_open2(videoCodecCtx, videoCodec, nullptr) < 0) {std::cerr << "无法打开视频编解码器" << std::endl;avformat_close_input(&formatCtx);avcodec_free_context(&videoCodecCtx);SDL_DestroyTexture(texture);SDL_DestroyRenderer(renderer);SDL_DestroyWindow(window);SDL_Quit();return 1;}AVFrame* videoFrame = av_frame_alloc();AVPacket packet;struct SwsContext* sws_ctx = sws_getContext(videoCodecCtx->width,videoCodecCtx->height,videoCodecCtx->pix_fmt,WIDTH, HEIGHT,AV_PIX_FMT_YV12,SWS_BILINEAR, nullptr, nullptr, nullptr);// 音频部分if (audioStreamIndex != -1) {AVCodecParameters* audioCodecParams = formatCtx->streams[audioStreamIndex]->codecpar;AVCodec* audioCodec = avcodec_find_decoder(audioCodecParams->codec_id);if (!audioCodec) {std::cerr << "不支持的音频编解码器" << std::endl;} else {AVCodecContext* audioCodecCtx = avcodec_alloc_context3(audioCodec);if (avcodec_parameters_to_context(audioCodecCtx, audioCodecParams) < 0 || avcodec_open2(audioCodecCtx, audioCodec, nullptr) < 0) {std::cerr << "无法打开音频编解码器" << std::endl;} else {// 配置音频设备和参数SDL_AudioSpec desiredSpec, obtainedSpec;desiredSpec.freq = SAMPLE_RATE;desiredSpec.format = AUDIO_S16SYS;desiredSpec.channels = CHANNELS;desiredSpec.silence = 0;desiredSpec.samples = 1024;desiredSpec.callback = audioCallback;desiredSpec.userdata = nullptr;SDL_AudioDeviceID deviceID = SDL_OpenAudioDevice(nullptr, 0, &desiredSpec, &obtainedSpec, 0);if (deviceID == 0) {std::cerr << "音频设备打开失败: " << SDL_GetError() << std::endl;} else {SDL_PauseAudioDevice(deviceID, 0);}}}}bool running = true;SDL_Event event;while (running) {while (SDL_PollEvent(&event)) {if (event.type == SDL_QUIT) {running = false;}}while (av_read_frame(formatCtx, &packet) >= 0) {if (packet.stream_index == videoStreamIndex) {if (avcodec_send_packet(videoCodecCtx, &packet) == 0) {while (avcodec_receive_frame(videoCodecCtx, videoFrame) == 0) {// 分配内存用于转换后的YUV数据uint8_t* yuvBuffer = new uint8_t[av_image_get_buffer_size(AV_PIX_FMT_YV12, WIDTH, HEIGHT, 1)];uint8_t* yuvData[AV_NUM_DATA_POINTERS];int yuvLinesize[AV_NUM_DATA_POINTERS];av_image_fill_arrays(yuvData, yuvLinesize, yuvBuffer, AV_PIX_FMT_YV12, WIDTH, HEIGHT, 1);sws_scale(sws_ctx, videoFrame->data, videoFrame->linesize, 0, videoCodecCtx->height, yuvData, yuvLinesize);SDL_UpdateTexture(texture, nullptr, yuvData[0], yuvLinesize[0]);SDL_RenderClear(renderer);SDL_RenderCopy(renderer, texture, nullptr, nullptr);SDL_RenderPresent(renderer);delete[] yuvBuffer;}}} else if (packet.stream_index == audioStreamIndex) {// 音频解码和播放控制(这里简化未实现完整流程)// 实际应将解码后的音频数据送入音频缓冲区}av_packet_unref(&packet);}// 回到文件开头重新播放av_seek_frame(formatCtx, -1, 0, AVSEEK_FLAG_BACKWARD);}// 清理资源sws_freeContext(sws_ctx);av_frame_free(&videoFrame);avcodec_free_context(&videoCodecCtx);avformat_close_input(&formatCtx);SDL_DestroyTexture(texture);SDL_DestroyRenderer(renderer);SDL_DestroyWindow(window);SDL_Quit();return 0;
}

4.3 播放器功能测试

  1. 进度控制:在上述代码基础上,可以添加进度控制功能。例如,通过获取用户输入的时间点,使用av_seek_frame函数将视频播放位置定位到指定时间点。在while (running)循环中,处理用户输入的进度控制指令,如按下J键将播放位置往前移动 10 秒,按下L键将播放位置往后移动 10 秒。
while (running) {while (SDL_PollEvent(&event)) {if (event.type == SDL_QUIT) {running = false;} else if (event.type == SDL_KEYDOWN) {switch (event.key.keysym.sym) {case SDLK_j:// 往前移动10秒int64_t seekTime = av_rescale_q(10 * AV_TIME_BASE, AV_TIME_BASE_Q, formatCtx->streams[videoStreamIndex]->time_base);av_seek_frame(formatCtx, videoStreamIndex, seekTime, AVSEEK_FLAG_BACKWARD);break;case SDLK_l:// 往后移动10秒seekTime = av_rescale_q(10 * AV_TIME_BASE, AV_TIME_BASE_Q, formatCtx->streams[videoStreamIndex]->time_base);av_seek_frame(formatCtx, videoStreamIndex, seekTime, AVSEEK_FLAG_FORWARD);break;}}}// 播放循环代码...
}
  1. 音量调节:对于音量调节,可以使用 SDL 提供的音量控制函数SDL_SetAudioDeviceVolume。在打开音频设备后,定义一个音量变量volume,初始值设为 1.0(表示正常音量)。通过处理用户输入,如按下UP键增加音量,按下DOWN键减小音量,然后调用SDL_SetAudioDeviceVolume函数设置音量。
float volume = 1.0f;
// 打开音频设备代码...
SDL_AudioDeviceID deviceID = SDL_OpenAudioDevice(nullptr, 0, &desiredSpec, &obtainedSpec, 0);
if (deviceID != 0) {SDL_PauseAudioDevice(deviceID, 0);SDL_SetAudioDeviceVolume(deviceID, static_cast<int>(volume * SDL_MIX_MAXVOLUME));
}while (running) {while (SDL_PollEvent(&event)) {if (event.type == SDL_QUIT) {running = false;} else if (event.type == SDL_KEYDOWN) {switch (event.key.keysym.sym) {case SDLK_UP:if (volume < 2.0f) {volume += 0.1f;SDL_SetAudioDeviceVolume(deviceID, static_cast<int>(volume * SDL_MIX_MAXVOLUME));}break;case SDLK_DOWN:if (volume > 0.1f) {volume -= 0.1f;SDL_SetAudioDeviceVolume(deviceID, static_cast<int>(volume * SDL_MIX_MAXVOLUME));}break;}}}// 播放循环代码...
}

通过上述测试,可以验证播放器的进度控制和音量调节功能是否正常工作。在实际应用中,还可以进一步完善这些功能,如显示当前播放进度、音量百分比等信息,以提升用户体验。

http://www.dtcms.com/a/447238.html

相关文章:

  • 红外与可见光图像融合的战略前沿:高影响力论文发表指南
  • 网站建设与管理试题答案做易经网站
  • 网站开发协助方案搜狗搜索引擎网页
  • 上海的设计网站建筑设计毕业设计作品
  • wps上怎么做网站点击分析表优秀品牌企业网站建设案例
  • 【数据结构与算法-Day 40】深入理解分治算法:从归并排序到快速排序的思想基石
  • 重庆长寿网站设计公司推荐安卓app开发实验报告
  • 连云港市海州区建设局网站互联网制作网站
  • 塘沽手机网站建设0基础怎么学服装设计
  • 建设银行互联网网站网站前端模板
  • 北京做网站公司有哪些金华网站建设公司哪个好
  • MTK调试-创建新工程
  • 网站平台定制开发建站快车管理
  • 怎么样建设一个电影网站如何用自己的电脑建网站
  • 基于Binder的4种RPC调用
  • WordPress设置二级域名石家庄seo代理商
  • 做美剧盗版网站广州市中智软件开发有限公司
  • 威联通NAS部署umami
  • 做游戏出租的网站合肥聚名网络科技有限公司
  • 网站后台更新为什么前台不现实免费推广软件平台seo博客
  • 论企业网站建设的好处的文献如何检测网站死链
  • 如何做网站支付接口免费源码资源源码站在线
  • 微信建设网站郑州做网站熊掌号
  • 做指甲的网站叫什么名字来着湖北立方建设工程有限公司网站
  • ps怎么网站首页seo网络推广公司
  • 自助建站源码下载电脑租赁平台哪个好
  • 西宁房地产网站建设页面设计的怎么样
  • 申请备案网站首页网站的建设有什么好处
  • 网站搭建平台demo免费做购票系统网站
  • 增城百度做网站多少钱网站的营销推广