如何开发一个音视频播放器(ffmpeg3.2+SDL2.0)
前言
上一篇讲了如何开发一个音频播放器,其实很简单。总结起来无非就是从文件中获取音频帧,再把音频中的数据传递给系统回调系统直接读取并播放音频的过程。
这次我们更近一步开发一个“视频+音频“”的媒体播放器,本篇教程与上一篇有着很强的连续性,请先阅读上一篇后再来看这一篇。同样这次使用的仍然是ffmpeg3.2+SDL2.0开发的,在阅读时请注意自身使用的版本。本篇的源码已提交在github上:https://github.com/XP-online/media-player
创建一个音视频播放器的步骤
如何创建一个音视频的播放器是一个类似于把大象放冰箱里的问题。它总共分三步,即:播放音频 —— 播放视频 —— 音视频同步。
一、播放音频
这一部分 我们上篇讲过。
二、播放视频
第二步播放视频它大概分为两个部分:
- 读取文件中的视频帧
- 根据获取到的图像信息播放视频
第一部分与读取音频帧的方式几乎没有任何区别。但第二步播放视频的方式却与播放音频完全不同。这也很好理解因为视频的图像信息已经在这里全部获取完毕了,而现实图像的方式有很多种。
实际上我们这里完全可以在获取完图像信息后不适用SDL来播放,换成其他GUI库根据图像信息自行渲染视频完全没有问题。但在本篇种我们的重点是为了介绍播放媒体文件的核心步骤,秉着尽量减少使用其他库的原则我们仍使用SDL2.0来渲染视频。
三、音视频同步
因为视频与音频都是各自独立解码,播放。由于它们各自的解码,渲染速率的不同很可能会产生视频与音频不同步的情况。所以为了解决这个问题快的那一方往往需要等待慢的一方。这就是音视频同步的问题。幸运的是每个通过ffmpeg所获取的包(* AVPacket* )中都封存着当前包的时间信息: pts 和 dts 。它们的具体含义我们后文会介绍,目前只要知道通过这两个成员我们就可以拿到当前包的时间戳就可以。
常见的音视频同步方式主要有三种即:
- 以音频时间戳为基准。视频根据音频时间戳决定当前包播放还是舍弃或等待。
- 以视频时间戳为基准。音频根据视频时间戳决定当前包播放还是舍弃或是等待。
- 以系统时间戳为基准。音频视频根据系统时间决定当前包播放还是舍弃或是等待。
在本篇教程中采用第一种方式,即以音频时间戳为准,视频同步音频。
源码分析
根据我们上文所说,在播放时音频和视频的播放应该是分开相互独立,所以我们需要开辟两个线程分别进行音频和视频的播放。同时与我们之前只播放音频的情况不同,之前视频包是被舍弃的现在我们显然不能这样。所以我们还需要去创建一个音频和一个视频的缓冲队列去保存已经读取到的包再通过队列把包发送给视频和音频的播放线程。此处是多线程的操作,所以这两个队列还应该是线程安全的。
一、正式开始前的准备工作
在正式分析源码前先要创建一个线程安全的队列
#include <vector>
#include <mutex>
template<typename T>
// 线程安全的队列
class Queue
{
public:
Queue() {
q.clear();
}
// push 入列
void push(T val) {
m.lock();
q.push_back(val);
m.unlock();
}
// pull 出列,返回false说明队列为空
bool pull(T &val) {
m.lock();
if (q.empty()) {
m.unlock();
return false;
} else {
val = q.front();
q.erase(q.begin());
m.unlock();
return true;
}
}
// empty 返回队列是否为空
bool empty() {
m.lock();
bool isEmpty = q.empty();
m.unlock();
return isEmpty;
}
// size 返回队列的大小
int size() {