该文章主要参考:https://www.cnblogs.com/leisure_chn/p/10284653.html
对于音视频播放来说,如果不进行同步的话,即使在视频开头是同步的,但是播放到后面肯定会出现不同步的现象。
视频是按帧播放,图像显示设备每次显示一帧画面,视频播放速度由帧率确定,帧率指示每秒显示多少帧;音频按采样点播放,声音播放设备每次播放一个采样点,声音播放速度由采样率确定,采样率指示每秒播放多少个采样点。如果仅仅是视频按帧率播放,音频按采样率播放,二者没有同步机制,即使最初音视频是基本同步的,随着时间的流逝,音视频会逐渐失去同步,并且不同步的现象会越来越严重。这是因为:一、播放时间难以精确控制,二、异常及误差会随时间累积。所以,必须要采用一定的同步策略,不断对音视频的时间差作校正,使图像显示与声音播放总体保持一致。
音视频同步的方式基本是确定一个时钟(音频时钟、视频时钟、外部时钟)作为主时钟,非主时钟的音频或视频时钟为从时钟。在播放过程中,主时钟作为同步基准,不断判断从时钟与主时钟的差异,调节从时钟,使从时钟追赶(落后时)或等待(超前时)主时钟。按照主时钟的不同种类,可以将音视频同步模式分为如下三种:
音频同步到视频,视频时钟作为主时钟。
视频同步到音频,音频时钟作为主时钟。
音视频同步到外部时钟,外部时钟作为主时钟。
模仿ffpaly的音视频同步机制,在这里实现最简单的音视频同步机制,可以基本的实现音视频的同步播放:
实现主时钟的结构体
typedef struct
{
double m_Pts; // 当前帧(待播放)显示时间戳,播放后,当前帧变成上一帧
double m_PtsDrift; // 当前帧显示时间戳与当前系统时钟时间的差值
double m_LastUpdated; // 当前时钟(如视频时钟)最后一次更新时间,也可称当前时钟时间
double m_Speed; // 时钟速度控制,用于控制播放速度
int m_Serial; // 播放序列,所谓播放序列就是一段连续的播放动作,一个seek操作会启动一段新的播放序列
int m_Paused; // 暂停标志
}PALY_CLOCK_S;
以下的主时钟操作函数是从ffplay中拷贝得来
// 返回值:返回上一帧的pts更新值(上一帧pts+流逝的时间)
double get_clock(PALY_CLOCK_S *c)
{
if (c->m_Paused)
{
return c->m_Pts;
}
else
{
double time = av_gettime_relative() / 1000000.0;
double ret = c->m_PtsDrift + time; // 展开得: c->pts + (time - c->last_updated)
return ret;
}
}
void set_clock_at(PALY_CLOCK_S *c, double pts, int serial, double time)
{
c->m_Pts = pts;
c->m_LastUpdated = time;
c->m_PtsDrift = c->m_Pts - time;
c->m_Serial = serial;
}
void set_clock(PALY_CLOCK_S *c, double pts, int serial)
{
double time = av_gettime_relative() / 1000000.0;
set_clock_at(c, pts, serial, time);
}
void init_clock(PALY_CLOCK_S *c)
{
c->m_Speed = 1.0;
c->m_Paused = 0;
set_clock(c, NAN, -1);
}
修改AudioThread和VideoThread的run函数:
void AudioThread::run(void)
{
char AudioDataOut[10000] = {0};
int data_count = 0;
qDebug()<<"Start Audio Thread";
while(m_IsRuning)
{
if( g_MedieInfo.m_AudioPacketQueue.empty())
{
msleep(1);
continue;
}
#ifndef _USE_SDL_
if (GetFree() < 10000)
{
msleep(1);
continue;
}
#endif
AVPacket avPacket;
g_MedieInfo.m_AudioPacketQueue.wait_and_pop(avPacket);
//按解码顺序发送packet,将视频文件中的packet序列依次发送给解码器。发送packet的顺序如IPBBPBB
int ret = avcodec_send_packet(g_MedieInfo.avFormatContext->streams[g_MedieInfo.audioStreamIndex]->codec, &avPacket);
if (ret != 0)
{
qDebug()<<__func__<<__LINE__<<"send packet error";
continue;
}
ret = avcodec_receive_frame(g_MedieInfo.avFormatContext->streams[g_MedieInfo.audioStreamIndex]->codec,audioFrame);
if (ret < 0)
{
if (ret == AVERROR(EAGAIN))
{
continue;
}
if (ret == AVERROR_EOF)
{
qDebug()<<__func__<<__LINE__<<"receive frame error";
}
}
double audioPts = audioFrame->pts * av_q2d(g_MedieInfo.avFormatContext->streams[g_MedieInfo.audioStreamIndex]->time_base);
//将音频的时钟设置为主时钟
set_clock(&g_MedieInfo.m_PalyClock,audioPts,0);
qDebug()<<"Audio Time "<streams[g_MedieInfo.audioStreamIndex]->codec,AudioDataOut,audioFrame);
#ifndef _USE_SDL_
Get()->Write(out, len);
#else
data_count += len;
//Set audio buffer (PCM data)
audio_chunk = (Uint8 *) AudioDataOut;
//Audio buffer length
audio_len = len;
audio_pos = audio_chunk;
while(audio_len>0)//Wait until finish
SDL_Delay(1);
#endif
av_packet_unref(&avPacket);
av_freep(&avPacket);
}
if (m_aCtx)
{
swr_free(&m_aCtx);
m_aCtx = NULL;
}
SDL_Quit();
qDebug()<<"Stop Audio Thread";
}
void VideoThread::run()
{
qDebug()<<"Start Video Thread";
while(m_IsRuning)
{
if( g_MedieInfo.m_VideoPacketQueue.empty())
{
msleep(1);
continue;
}
AVPacket avPacket;
g_MedieInfo.m_VideoPacketQueue.wait_and_pop(avPacket);
//按解码顺序发送packet,将视频文件中的packet序列依次发送给解码器。发送packet的顺序如IPBBPBB
int ret = avcodec_send_packet(g_MedieInfo.avFormatContext->streams[g_MedieInfo.videoStreamIndex]->codec, &avPacket);
if (ret != 0)
{
qDebug()<<__func__<<__LINE__<<"send packet error";
continue;
}
ret = avcodec_receive_frame(g_MedieInfo.avFormatContext->streams[g_MedieInfo.videoStreamIndex]->codec,vidioFrameRaw);
if (ret < 0)
{
if (ret == AVERROR(EAGAIN))
{
continue;
}
if (ret == AVERROR_EOF)
{
qDebug()<<__func__<<__LINE__<<"receive frame error";
}
}
double videoPts = 0;
double audioTime = 0;
videoPts = vidioFrameRaw->pts * av_q2d(g_MedieInfo.avFormatContext->streams[g_MedieInfo.videoStreamIndex]->time_base);
audioTime = get_clock(&g_MedieInfo.m_PalyClock);
//如果读取到无效的时钟时间的话则continue
if(isnan(audioTime))
{
usleep(1000*1000/g_MedieInfo.m_FrameRate);
av_packet_unref(&avPacket);
av_freep(&avPacket);
continue;
}
//qDebug()<<"Video Time "<= 0)//视频时钟比主时钟快,则休眠
{
msleep(DiffTimeMs);
}
else//视频时钟比主时钟慢,则丢弃该帧
{
msleep(1);
av_packet_unref(&avPacket);
av_freep(&avPacket);
continue;
}
sws_scale(g_MedieInfo.swsContext, (const uint8_t *const *)vidioFrameRaw->data, vidioFrameRaw->linesize, 0, videoHeight, vidioFrameConvert->data, vidioFrameConvert->linesize);
QImage image(vidioFrameConvert->data[0], videoWidth, videoHeight, QImage::Format_RGB32);
if (!image.isNull())
{
emit receiveImage(image);
}
usleep(1000*1000/g_MedieInfo.m_FrameRate);
av_packet_unref(&avPacket);
av_freep(&avPacket);
}
qDebug()<<"Stop Video Thread";
}
通过以上视频播放线程中对时钟进行误差校验的话,可以初步实现音视频同步的功能。但是这只是最简单的实现方法,在ffplay中音视频同步的实现十分的复杂,等有空再仔细的阅读源码。