目录
学习课题:逐步构建开发播放器【QT5 + FFmpeg6 + SDL2】
原理
简单分析:
下图简单描述了在一个播放过程中,假设我们先播放音频,对比一个公共时间轴,视频就会始终比音频慢0.003s。
我们在日常中用一些播放器播放视频资源时,可能会遇见“画面中人先说话,声音后面才听到” 或者是“声音先出来了,画面中人物嘴巴还没动”的情况,这些情况的发生就是“音频播放当前帧的时间轴位置>视频播放当前帧时间轴的位置 或者是<”。所以需要进行音视频同步,这里说的同步并不是完全同步,只是在“音频”>"视频"时,让"视频"加速,追上"音频";在“音频”<"视频"时,让"视频"减速,等待"音频"
就像是有两个叫”音频“和”视频“的同学在操场上跑一千米,两个人的差距非常小”音频“超过了”视频“,”视频“就会加速追上去,两人几乎保持全程”同步“最后冲刺终点时两个人同时冲线。
如何实现:
通过分析,我们现在知道的就是要做【在“音频”>"视频"时,让"视频"加速,追上"音频"】【在“音频”<"视频"时,让"视频"减速,等待"音频"】 这个事情,因为我们使用了FFmpeg框架,在AVFrame[帧结构体]中,定义了字段“pts”
解释:Presentation timestamp in time_base units (time when frame should be shown to user)
就是指这一帧需要显示给用户的“时间点”。
即在“音频pts”>"视频pts"时,让"视频"加速,追上"音频"
在“音频pts”<"视频pts"时,让"视频"减速,等待"音频"。
步骤
MediaSync模块
1、在audio写入实际播放数据之前记录对应当前帧数据的pts
2、在video读取帧数据开始进行缩放渲染之前判断“视频pts”是否小于"音频pts",根据结果进行加速或者是等待。
AudioOutPut模块
添加代码在合适的位置设置pts
VideoOutPut模块
添加代码在帧读取后判断“视频pts”是否小于"音频pts"
添加的部分
AudioOutPut
1、添加了一个存放音频pts的队列
2、在SDL回调中把pts设置进MediaSync,提供给video获取进行“加速” or “等待”
为什么使用队列:
我看过很多的文章,他们在实现音视频同步的时候都不会用到队列去存放pts,而是使用一些样本计算公式去算出pts,然后设置进时钟。
例如:
“时长=音频数据长度(bytes)/(声道数∗采样率∗位深度/8)”
”时长=采样数/采样率“
这两条公式在理论上是可行的,但是在一些特殊情况下就会变得不同步,比如根据公式计算出来的帧时长与实际帧的时长不同,即使是非常细微的差距也会导致音视频同步出现异常。
我在做音视频同步的时候就刚好遇到了这种特殊情况,根据公式计算出来的pts一直是固定的0.02322,而实际帧(AVFrame)结构体下pts字段所表示的每一帧的pts差距并不固定是0.02322,有时候会小于,有时候会大于,我的理解是这个“帧差距”代表的就是这一音频帧实际的持续时间,如果我们用公式计算出来的值与实际的不符,就会导致在video进行同步时获得了错误的音频pts,导致同步出现异常。
//AudioOutPut.h
std::queue<double>*ptsQueue;//音频帧pts队列
MediaSync *sync;
AVRational streamTimeBase;
// 设置音频流的TimeBase
void setStreamTimeBase(AVRational &streamTimeBase);
// 添加同步对象
void setSync(MediaSync *sync);
//AudioOutPut.cpp
int AudioOutPut::init(int mode) {
...
fifo = av_audio_fifo_alloc(playSampleFmt, playChannels, spec.samples * 5);
ptsQueue = new std::queue<double>();
...
}
void AudioOutPut::AudioCallBackFromQueue(Uint8 *stream, int len) {
...
//lock
sync->setAudioPts(ptsQueue->front());
ptsQueue->pop();
//read
...
}
void AudioOutPut::run() {
...
while (true) {
SDL_LockMutex(mtx);
if (av_audio_fifo_space(fifo) >= playSamples) {
//保存pts
pts = frame->pts * av_q2d(streamTimeBase);
ptsQueue->push(pts);
av_audio_fifo_write(fifo, (void **) &audioBuffer, playSamples);
SDL_UnlockMutex(mtx);
av_frame_unref(frame);
break;
}
SDL_UnlockMutex(mtx);
//队列可用空间不足则延时等待
SDL_Delay((double) playSamples / playSampleRate);
}
...
}
void AudioOutPut::setSync(MediaSync *sync) {
this->sync = sync;
}
void AudioOutPut::setStreamTimeBase(AVRational &streamTimeBase) {
this->streamTimeBase = streamTimeBase;
}
VideoOutPut
获取从audio中拿到的pts,计算vidio_pts与audio_pts的差距,判断进行“加速“还是”等待“
//VideoOutPut.h
MediaSync *sync;
AVRational streamTimeBase;
// 设置视频流的streamTimeBase
void setStreamTimeBase(AVRational &streamTimeBase);
// 添加同步对象
void setSync(MediaSync *sync);
//VideoOutPut.cpp
void VideoOutPut::run() {
AVFrame *frame;
double pts;
double diff;
double audio_pts;
while (!isStopped) {
frame = frameQueue->pop(10);
if (frame) {
//同步
pts = frame->pts * av_q2d(streamTimeBase);
audio_pts = sync->getAudioPts();
diff = pts - audio_pts;
if (diff > 0) {
av_usleep(diff * 1000000.0);
}
//图像缩放、颜色空间转换
sws_scale(swsContext, (const uint8_t *const *) frame->data, frame->linesize, 0, decCtx->height, playFrame->data, playFrame->linesize);
av_frame_unref(frame);
//视频区域
SDL_Rect sdlRect;
sdlRect.x = 0;
sdlRect.y = 0;
sdlRect.w = decCtx->width;
sdlRect.h = decCtx->height;
//渲染到sdl窗口
emit refreshImage(sdlRect, playFrame);
}
}
}
void VideoOutPut::setStreamTimeBase(AVRational &streamTimeBase) {
this->streamTimeBase = streamTimeBase;
}
void VideoOutPut::setSync(MediaSync *sync) {
this->sync = sync;
}
完整代码
MediaSync
直接添加get set函数即可,单独新建类存放,后续可能进行优化拓展
//MediaSync.h
#include <mutex>
/**
* 用于进行音视频同步
*/
class MediaSync {
private:
std::mutex m_mutex; // 互斥锁
double m_audioPts=0;
public:
/**
* 设置音频pts
* @param pts 经过frame->pts * av_q2d(time_base)的pts
*/
void setAudioPts(double pts);
/**
* 获取音频经过frame->pts * av_q2d(time_base)的pts
* @return m_audioPts
*/
double getAudioPts();
};
//MediaSync.cpp
#include "MediaSync.h"
void MediaSync::setAudioPts(double pts) {
m_audioPts = pts;
}
double MediaSync::getAudioPts() {
return m_audioPts;
}
测试运行结果
PlayerMain
//PlayerMain.h
MediaSync *sync;
//PlayerMain.cpp
PlayerMain::PlayerMain(QWidget *parent)
: QWidget(parent), ui(new Ui::PlayerMain) {
ui->setupUi(this);
sync = new MediaSync();
// 解复用
demuxThread = new DemuxThread(&audioPacketQueue, &videoPacketQueue);
demuxThread->setUrl("/Users/mac/Downloads/0911超前派对:于文文孟佳爆笑猜词 王源欧阳靖脑洞大开.mp4");
// demuxThread->setUrl("/Users/mac/Downloads/23.mp4");
demuxThread->start();
int ret;
// 解码-音频
audioDecodeThread = new DecodeThread(
demuxThread->getCodec(MediaType::Audio),
demuxThread->getCodecParameters(MediaType::Audio),
&audioPacketQueue,
&audioFrameQueue);
audioDecodeThread->init();
audioDecodeThread->start();
// 解码-视频
videoDecodeThread = new DecodeThread(
demuxThread->getCodec(MediaType::Video),
demuxThread->getCodecParameters(MediaType::Video),
&videoPacketQueue,
&videoFrameQueue);
videoDecodeThread->init();
videoDecodeThread->start();
//output
// audio
audioOutPut = new AudioOutPut(audioDecodeThread->dec_ctx, &audioFrameQueue);
audioOutPut->init(1);
audioOutPut->setSync(sync);
audioOutPut->setStreamTimeBase(*demuxThread->getStreamTimeBase(MediaType::Audio));
// video
this->resize(1920 / 2, 1080 / 2);
videoOutPut = new VideoOutPut(videoDecodeThread->dec_ctx, &videoFrameQueue);
videoOutPut->init();
videoOutPut->setSync(sync);
videoOutPut->setStreamTimeBase(*demuxThread->getStreamTimeBase(MediaType::Video));
VideoWidget *videoWidget = new VideoWidget(this);
connect(videoOutPut, &VideoOutPut::refreshImage, videoWidget, &VideoWidget::updateImage);
videoWidget->show();
videoWidget->initSDL();
audioOutPut->start();
videoOutPut->start();
// videoWidget->setParent(this);
}
播放器开发(七):音视频同步实现