基于 FFmpeg 的跨平台视频播放器简明教程(五):使用 SDL 播放视频

系列文章目录

  1. 基于 FFmpeg 的跨平台视频播放器简明教程(一):FFMPEG + Conan 环境集成
  2. 基于 FFmpeg 的跨平台视频播放器简明教程(二):基础知识和解封装(demux)
  3. 基于 FFmpeg 的跨平台视频播放器简明教程(三):视频解码
  4. 基于 FFmpeg 的跨平台视频播放器简明教程(四):像素格式与格式转换

文章目录

  • 系列文章目录
  • 前言
  • SDL2,跨平台多媒体开发库
  • SDL 显示图像
  • SDL 显示 YUV 图像
  • SDL 播放视频
  • 总结
  • 参考


前言

经过前面四章的学习,现在我们已经掌握了如何使用 FFmpeg 进行视频解码,中间穿插了很多音视频相关的知识点,例如容器、编解码器、解封装、像素格式、格式转换等等。现在回看,音视频的入门门槛还是比较高的,一个最简单的任务就已经涉及到大量的知识点。但问题不大,本人希望通过一系列的文章来带你入门,通过完成一个播放器项目来不断地学习音视频内容。

在开始新的旅程前,重新审视下现有代码,发现有些模块可以被封装成更为内聚的类,具体的包括:

  1. FFmpegDemuxer,用于解封装相关的任务
  2. FFmpegCodec,用于解码相关的任务
  3. FFmpegImageConverter,用于 AVFrame 格式转换

这些类的使用方式,你可以在单元测试中找到示例,此处不再赘述。

抽象封装成一些类的好处主要有几点:

  1. 资源管理。FFmpeg 是 C 接口,很多资源需要手动的管理,这样的代码写多了难免会出现内存泄漏的问题。因此用 C++ 的 ”资源获取即初始化“ 理念来管理这些资源。
  2. 减少代码冗余。将某些任务封装成更为简便的接口,减少代码冗余。

好的,准备就绪,让我们进入今天的主题:播放视频。本文参考文章来自 An ffmpeg and SDL Tutorial - Tutorial 02: Outputting to the Screen。这个系列对新手较为友好,但 2015 后就不再更新了,以至于文章中的 ffmpeg api 已经被弃用了。幸运的是,有人对该教程的代码进行重写,使用了较新的 api,你可以在 rambodrahmani/ffmpeg-video-player 找到这些代码。

本文的代码在 ffmpeg_video_player_tutorial-my_tutorial02。

SDL2,跨平台多媒体开发库

SDL2 是 Simple DirectMedia Layer(简单直接媒体层)的缩写,它是一个跨平台的 C/C++ 库,专为游戏和多媒体应用程序提供硬件抽象层的支持。SDL2 提供了对音频、键盘、鼠标、操纵杆和图形硬件的底层访问能力,它在 Windows、macOS、Linux、iOS 和 Android 等平台上都有广泛的应用。

FFmpeg 提供的命令行工具 FFplay 中使用了 SDL2 作为渲染图像的依赖库。FFplay 是 FFmpeg 项目中的一个简单的媒体播放器,它依赖于 FFmpeg 库来解码、解复用和处理媒体数据。FFmpeg 本身专注于视频和音频的编解码,以及其他媒体处理功能,但不涉及与硬件交互的部分,例如音视频的渲染和播放。

FFplay 使用 SDL 主要是因为 SDL 提供了易于使用的跨平台 API,用于访问音频、视频、键盘、鼠标和操纵杆等硬件设备。通过使用 SDL,FFplay 能在各种操作环境下实现音视频的同步播放,以及用户交互(如键盘和鼠标操作)。

SDL 提供了以下特性,使其成为 FFplay 使用的理想选择:

  1. 跨平台支持:SDL 可在多个平台(如 Windows、macOS、Linux 等)上运行,这意味着基于 SDL 的 FFplay 可以很容易地移植到其他环境。
  2. 音频和视频渲染:SDL 提供了对音频和视频的播放支持,使得 FFplay 可以正确地渲染音频和视频数据。
  3. 事件处理:SDL 提供了对各类输入设备事件(如键盘、鼠标等)的处理,这使得 FFplay 可以对用户的操作做出响应,例如暂停、快进等。

关于 SDL 的使用,我之前写过一些文章,供大家参考,此处不再赘述:

  • SDL2 简明教程(一):使用 Cmake 和 Conan 构建 SDL2 编程环境
  • SDL2 简明教程(二):创建一个空的窗口
  • SDL2 简明教程(三):显示图片
  • SDL2 简明教程(四):用 SDL_IMAGE 库导入图片
  • SDL2 简明教程(五):OpenGL 绘制

SDL 显示图像

首先让我们快速过一下 SDL 显示图片的过程。使用SDL来显示一帧图像的步骤如下:

  1. 初始化SDL:调用SDL_Init函数来初始化SDL库。
  2. 创建一个窗口和渲染器:调用SDL_CreateWindow和SDL_CreateRenderer函数来创建一个窗口和渲染器。
  3. 加载图像:使用SDL_image库中的函数(如IMG_Load)加载需要显示的图像。
  4. 创建一个纹理:使用加载的图像创建一个纹理,使用SDL_CreateTextureFromSurface函数。
  5. 将纹理渲染到屏幕:使用SDL_RenderCopy函数将纹理渲染到屏幕上。
  6. 刷新屏幕:使用SDL_RenderPresent函数来刷新屏幕。
  7. 释放资源:使用SDL_DestroyTexture、SDL_DestroyRenderer、SDL_DestroyWindow等函数释放分配的资源。

下面是C++代码示例,显示一张图像:

#include 
#include 

int main(int argc, char const *argv[])
{
    SDL_Init(SDL_INIT_VIDEO);

    SDL_Window* window = SDL_CreateWindow("SDL Example", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 640, 480, 0);
    SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);

    SDL_Surface* image = IMG_Load("example.png");
    SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, image);

    SDL_RenderCopy(renderer, texture, NULL, NULL);
    SDL_RenderPresent(renderer);

    SDL_Delay(5000);    //等待5秒

    SDL_DestroyTexture(texture);
    SDL_FreeSurface(image);
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);

    IMG_Quit();
    SDL_Quit();
    
    return 0;
}

在SDL中,window、renderer、surface和texture是四个关键的图形和图像处理对象,它们之间有一定的联系。让我们一一分析它们的作用和关系。

  • SDL_Window: 窗口对象。用于在屏幕上创建、管理和显示一个简单的矩形窗口。窗口可以由操作系统进行管理,提供了标题栏、边框和其他窗口功能。SDL_Window代表了应用程序中使用的这个窗口实例。

  • SDL_Renderer: 渲染器对象。负责将多个图像(通常由SDL_Texture表示)绘制到屏幕上。渲染器可以使用不同的后端实现(如OpenGL,Direct3D等),并将其集成到SDL_Window中。与SDL_Window对象关联的渲染器用于将图像和图形绘制到窗口中。

  • SDL_Surface: 表面对象。表示原始的位图图像,每个像素由色彩值定义。表面可以包括一个或多个图层,用于绘制2D图像。然而,它们在处理上较慢,因为渲染操作通常在CPU端执行。表面通常用于加载、处理和创建图像资源,然后将它们转换成纹理用于高效渲染。

  • SDL_Texture: 纹理对象。代表了可以被硬件加速的图形。纹理被GPU管理,可以更高效地使用SDL_Renderer进行渲染。它们通常由SDL_Surface转换而来,在纹理上传到GPU之后,关联的表面对象可以被释放以节省内存。

这四个对象之间的关系如下:

  • 一个SDL_Window与一个SDL_Renderer关联,将图形和图像绘制到窗口上。
  • SDL_Renderer负责处理和绘制SDL_Texture对象。
  • SDL_Surface用于创建、处理和存储原始的位图图像,然后将它们转换成硬件加速的SDL_Texture以供渲染器绘制。

简而言之,SDL_Window负责显示,SDL_Renderer负责绘制,SDL_Surface负责处理原始位图,SDL_Texture则可以高效地被渲染器绘制到窗口。在实际应用中,通常需要处理并将多个表面转换成纹理,然后使用渲染器将它们绘制到窗口中。

SDL 显示 YUV 图像

视频多数情况下使用 YUV 格式来存放像素数据,主要原因是压缩效率更高,同时还能保持较好的图像质量。具体来说,有以下几点原因:

  1. 符合人眼特性: YUV格式将图像的亮度信息(Y分量)和色度信息(U和V分量)分开存储。而人眼对亮度信息(黑白图像)的敏感度要高于色度信息(彩色信息)。在进行压缩时,可以减少色度信息的分辨率,从而降低数据量,这符合人类视觉系统的特性,不会明显降低观感质量。这种削减色度分辨率的方式叫色度子采样(如4:2:0、4:2:2、4:4:4)。
  2. 节省存储空间和带宽: 相比于像RGB这种直接存储色彩值的格式,YUV格式可以利用色度子采样来有效地减少数据量,在同样的图像质量下,YUV格式需要的存储空间和传输带宽更小。这在视频传输、压缩、存储等场景中十分重要。
  3. 兼容性和广泛应用: YUV格式已经被广泛应用于许多视频压缩标准,如H.264(AVC)、H.265(HEVC)等,以及各种视频设备和传输系统。这意味着使用YUV格式可以显著提高视频系统的通用性和兼容性。
  4. 易于处理: YUV格式把亮度信息与色度信息分开存储,方便视频图像处理和编辑,如调整亮度、对比度、色彩平衡等。

在SDL中显示一张YUV图像,可以使用 SDL_Texture的SDL_PIXELFORMAT_YV12 或SDL_PIXELFORMAT_IYUV像素格式。首先,需要创建一个相应格式的纹理,然后通过SDL_UpdateYUVTexture()函数更新纹理数据。下面是一个简单的示例:

#include 

int main(int argc, char *argv[])
{
    // 初始化SDL
    if (SDL_Init(SDL_INIT_VIDEO) != 0)
    {
        SDL_Log("Unable to initialize SDL: %s", SDL_GetError());
        return -1;
    }

    // 创建窗口和渲染器
    SDL_Window* window = SDL_CreateWindow("YUV Example", SDL_WINDOWPOS_CENTERED,
        SDL_WINDOWPOS_CENTERED, 640, 480, 0);
    SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);

    // 创建纹理
    SDL_Texture* texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12,
        SDL_TEXTUREACCESS_STREAMING, 640, 480);

    // 加载YUV数据
    Uint8* yuvData = loadYUVData();


    // 假设 yPlane, uPlane 和 vPlane 分别是Y,U和V分量的指针
	Uint8 *yPlane;
	Uint8 *uPlane;
	Uint8 *vPlane;

	int dataSize = width * height;

	// 设置三个分量数据的跨度
	int yPitch = width;
	int uPitch = width / 2;
	int vPitch = width / 2;

	// 更新纹理数据
	SDL_UpdateYUVTexture(texture,
                     	nullptr, // 更新整个纹理
                     	yPlane, yPitch,
                     	uPlane, uPitch,
                     	vPlane, vPitch);

    // 渲染纹理
    SDL_RenderClear(renderer);
    SDL_RenderCopy(renderer, texture, NULL, NULL);
    SDL_RenderPresent(renderer);

    SDL_Delay(5000);

    // 释放资源
    SDL_DestroyTexture(texture);
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();

    return 0;
}

在文章 YUV 文件读取、显示、缩放、裁剪等操作教程
有更详细的说明,包括 SDL YUV 格式与 ffmpeg 像素格式之间的对应,Chroma subsampling 是什么,YUV Packed 与 Planar 的区别,如何正确读取一张 YUV 图片等等,强烈推荐先看这篇文章后再继续我们的教程。此外,你也可以直接看项目 simple_yuv_viewer 的 源码来学习 。

SDL 播放视频

现在,我们的目标是取代上一个教程中的 saveFrame 函数,而直接将视频帧输出到屏幕。为了让视频解码与视频播放的逻辑独立开,这里引入一个类叫 SDLApp,它负责 SDL 环境的创建、将 AVFrame 更新至纹理、处理事件等等。具体实现你可以在 ffmpeg_video_player_tutorial-my_tutorial02 中看到。

这里先简单介绍下 SDLApp 的主要方法:

  • SDLApp::onInit(int video_width, int video_height),负责 SDL 资源的初始化,包括窗口、纹理以及 Render。注意,在创建纹理时使用的格式 SDL_PIXELFORMAT_IYUV,对应 FFmpeg 中的 yuv420p 格式。这是最为常用的格式。
  • SDLApp::onLoop(AVFrame *pict),负责将 YUV 数据更新至纹理。
  • SDLApp::onRender(double sleep_time_s),负责渲染纹理,即将纹理显示在窗口中。其中 sleep_time_s 是一个等待时间,用于控制播放的速率。
  • void onEvent(const SDL_Event &event) 负责对 SDL 窗口事件进行响应。在这里,这个函数其实没有起任何作用。可以忽略。
  • SDLApp::onCleanup,负责释放 SDL 资源。

SDLApp 负责渲染图像,FFmpeg 负责解码视频,整体流程大致是这样的:

// 使用 demuxer 打开文件
FFmpegDmuxer demuxer;
demuxer.openFile(infile);

// 创建视频解码器
AVStream *video_stream = demuxer.getStream(video_stream_index);
FFmpegCodec video_codec;
auto codec_id = video_stream->codecpar->codec_id;
auto par = video_stream->codecpar;
video_codec.prepare(codec_id, par);

// 创建 ImageConverter,负责将解码后的图像转换为 yuv420p 格式。与 SDL 中的纹理格式匹配
auto dst_format = AVPixelFormat::AV_PIX_FMT_YUV420P;
auto codec_context = video_codec.getCodecContext();
FFMPEGImageConverter img_conv;
img_conv.prepare(codec_context->width, codec_context->height,
                 codec_context->pix_fmt, codec_context->width,
                 codec_context->height, dst_format, SWS_BILINEAR, nullptr,
                 nullptr, nullptr);

//  sdl 环境初始化
auto video_width = video_codec.getCodecContext()->width;
auto video_height = video_codec.getCodecContext()->height;
SDLApp app;
app.onInit(video_width, video_height);

// 获取视频的 fps
double fps = av_q2d(video_stream->r_frame_rate);
double sleep_time = 1.0 / fps;

// 一直解码,直到满足某种条件退出
for(;!finished;)
{
	AVFrame* frame = decodeNextFrame();
	AVFRame* yuv_frame = convertToYUV(frame);

	// 显示图片
	app.onLoop(pict);
	app.onRender(sleep_time);
}

app.onCleanup();

好的,以上就是使用 SDL 播放视频的所有逻辑。详细代码请参考 ffmpeg_video_player_tutorial-my_tutorial02。

总结

本文介绍了 SDL 框架,使用 SDL 框架显示图片的流程,以及如何结合 FFmpeg 的解码能力使用 SDL 来播放视频。在我们的实现中,只显示显示了画面,但没有声音,这是没有灵魂的。在下一章中,我们将介绍如何使用 SDL 同时播放视频与音频。

参考

  • SDL - github
  • SDL2 简明教程(一):使用 Cmake 和 Conan 构建 SDL2 编程环境
  • SDL2 简明教程(二):创建一个空的窗口
  • SDL2 简明教程(三):显示图片
  • SDL2 简明教程(四):用 SDL_IMAGE 库导入图片
  • SDL2 简明教程(五):OpenGL 绘制
  • YUV 文件读取、显示、缩放、裁剪等操作教程
  • simple_yuv_viewer
  • ffmpeg_video_player_tutorial-my_tutorial02

你可能感兴趣的:(音视频,ffmpeg,ffmpeg,音视频)