FFmpeg学习笔记 - 简易视频播放器

240 阅读3分钟

概述

本篇博客主要记录如何使用ffmpeg和SDL2制作一个简易的视频播放器,主要包括三个部分:

  • 解析视频文件
  • SDL2播放视频和声音
  • 音视频同步

代码示例

例子的地址

例子为CMake项目,目前只在MacOS上运行。运行项目中的Target MediaFileViewer,可查看效果

解析视频文件

可以在例子的MediaFileReader中找到完整代码

打开文件

AVFormatContext *formatCtx = avformat_alloc_context();
avformat_open_input(&formatCtx, filePath.c_str(), NULL, NULL);
avformat_find_stream_info(formatCtx, NULL);

avformat_open_input打开文件并给AVFormatContext赋值,AVFormatContext中包含了媒体文件的所有信息。但是如果这个文件没有头部,则需要再次调用avformat_find_stream_info来猜测更多信息,它的内部会读取部分数据,来进行信息的猜测。

准备解码器

解码器是读取文件过程中很重要的部分,不同编码格式的数据需要使用不同的解码器解码。在AVFormatContext中,ffmpeg已经为我们准备好了视频和音频的编码格式,我们只需要遍历AVFormatContext中的stream,找到视频(音频)的编码信息即可。

for (int i = 0; i < formatCtx->nb_streams; ++i) {
    if (formatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
        videoStreamIdx = i;
    } else if (formatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO) {
        audioStreamIdx = i;
    }

    avcodec_open2(formatCtx->streams[i]->codec, avcodec_find_decoder((AVCodecID)formatCtx->streams[i]->codec->codec_id), NULL);
}

找到信息后,使用avcodec_open2打开解码器,这里我将视频和音频流的索引值保存,以便后续再次访问。

解析数据

视频和音频的原始数据在ffmpeg中都是由AVPacket表示,我们可以通过av_read_frame轻松的读取一个AVPacket,类似于文件读写的API,ffmpeg会自动往后移动读取数据的游标。读取到一个AVPacket后,可以通过它的stream_index属性判断是哪个流的数据,如果是视频流,则使用视频解码器进一步解码数据,如果是音频流,则使用音频解码器解码。

  if (av_read_frame(formatCtx, pkt) >= 0) {
    if (pkt->stream_index == videoStreamIdx) {
      // decode video
    } else if (pkt->stream_index == audioStreamIdx) {
      // decode audio 
    }
  }

解码视频数据

我们得到视频的AVPacket后,可以使用avcodec_decode_video2将AVPacket解析成AVFrame,解析完成之后,就可以得到这一帧的图片原始数据了。图片原始数据的格式可以通过AVFrameformat参数得到,宽高信息也在其中。

AVFrame *rawFrame = av_frame_alloc();
int gotFrame;
avcodec_decode_video2(formatCtx->streams[videoStreamIdx]->codec, rawFrame, &gotFrame, pkt);

解码音频数据

音频的AVPacket是通过avcodec_decode_audio4解析成AVFrame,解析出来的数据信息也在AVFrame中,比如采样率,通道数,数据格式等等

AVFrame *rawFrame = av_frame_alloc();
int gotFrame;
avcodec_decode_audio4(formatCtx->streams[audioStreamIdx]->codec, rawFrame, &gotFrame, pkt);

SDL2播放音视频

格式转换

由于SDL2不一定直接支持ffmpeg解码出来的数据格式,我们可以通过ffmepg的swresampleswscale来对音频和图片数据进行格式转换,可以在例子的OCFFFormatConverter中找到完整代码。例子中我将图片转成了YUV420格式,音频转成了SDL支持的交错格式。

展示视频

其实展示视频就是显示图片,通过动态更新Texture可以方便的完成这项工作,在例子的SDLPresenter中可以找到完整实现

window = SDL_CreateWindow(title.c_str(), SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, w, h, SDL_WINDOW_OPENGL);
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
...


if (!dynamicTex) {
    dynamicTex = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, w, h);
}
SDL_Rect rect = {0, 0, w, h};
SDL_UpdateYUVTexture(dynamicTex, &rect, (Uint8 *)pixels[0], w, (Uint8 *)pixels[1], w / 2, (Uint8 *)pixels[2], w / 2);

SDL_RenderClear(renderer);
SDL_RenderCopy(renderer, dynamicTex, NULL, NULL);
SDL_RenderPresent(renderer);

展示音频

音频可以通过SDL_QueueAudio直接播放解码出来的PCM数据,不过要保证输入的PCM数据和SDL初始化时约定的数据格式是一致的

音视频同步

例子中采用的同步方式是,音频为同步主线。该方案的核心就是计算当前音频播放的PTS,计算方法如下

  • 每次SDL_QueueAudio时记录一下时间(AVFrame的PTS + AVFrame的Duration),也就是这个声音播完的PTS
  • 通过SDL_GetQueuedAudioSize计算出当前等待播放的音频Buffer有多长时间
  • 第一步算出来的时间点减去第二步的时长,就可以认为是当前音频播放到的PTS了 有了音频播放的PTS,就可以拿来和视频帧的PTS做对比,如果视频帧时间到了,就进行绘制