概述
本篇博客主要记录如何使用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,解析完成之后,就可以得到这一帧的图片原始数据了。图片原始数据的格式可以通过AVFrame
的format
参数得到,宽高信息也在其中。
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的swresample
和swscale
来对音频和图片数据进行格式转换,可以在例子的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做对比,如果视频帧时间到了,就进行绘制