目录
学习课题:逐步构建开发播放器【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) {...//locksync->setAudioPts(ptsQueue->front());ptsQueue->pop();//read...
}void AudioOutPut::run() {...while (true) {SDL_LockMutex(mtx);if (av_audio_fifo_space(fifo) >= playSamples) {//保存ptspts = 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// audioaudioOutPut = new AudioOutPut(audioDecodeThread->dec_ctx, &audioFrameQueue);audioOutPut->init(1);audioOutPut->setSync(sync);audioOutPut->setStreamTimeBase(*demuxThread->getStreamTimeBase(MediaType::Audio));// videothis->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);
}
播放器开发(七):音视频同步实现