前面已经介绍过视频的解码与显示,和音频的解码与播放了。但这里会有一个问题,那就是视频和音频的同步。
不同步有什么后果?
后果就是要么视频播放太快了,音频没有跟上;或者音频播放太快了,视频没有跟上;严重影响整体的观看体验。
就好比小姐姐当面问你联系方式,小姐姐你看到了,但人家说的啥你还没听到,之后人家都走了,你才听到原来是问电话号码的,多惨。
那怎么解决呢?
方法一:以音频的解码流为主参照,视频流的解码向音频的解码时间靠拢。
方法二:以视频的解码流为主参照,音频流的解码视频的解码时间靠拢。
方法三:以手机系统时间为主参照,视频流和音频流的解码都向系统时间靠拢。
哪一种方法最好?或者说,每种方法的使用场景是什么?
我的理解是:以谁为主参照,就是看重谁。
如果声音断开一下下,我们的耳朵是很容易感觉出来的,相反如果声音连续,视频帧偶尔卡一下下,一般都影响不大。当需求是极度要求声音的连续性的,那就方法一。
相反,当需求是极度要求视频的连续性的,那就方法二。
至于方法三,就是折中的方法,感觉啥时候都可以用,我自己也大多数用方法三。
如何实现?
在这之前,先介绍几个属性。
I帧:关键帧,帧内编码帧 又称intra picture,I 帧通常是每个 GOP(MPEG 所使用的一种视频压缩技术)的第一个帧,经过适度地压缩,做为随机访问的参考点,可以当成图象。I帧可以看成是一个图像经过压缩后的产物。可独立解码。
B帧:双向预测内插编码帧 又称bi-directional interpolated prediction frame,可以大大提高压缩倍数。(与I帧相似度95%以上)
P帧:前向预测编码帧 又称predictive-frame,P 帧图像只采用前向时间预测,可以提高压缩效率和图像质量。(与I帧相似度70%以上)
DTS:帧数据的编码时间戳,这个时间戳的意义在于告诉播放器该在什么时候解码这一帧的数据。
PTS:帧数据的显示时间戳,这个时间戳用来告诉播放器该在什么时候显示这一帧的数据。
怎么理解这些东西?直接复制网络上的一张图比较直观和方便。
上图就是一个GOP内的帧数据,通过PTS和DTS,播放器可以知道在某一个时间,解码哪一帧,显示哪一帧。
同样的,当有2个流(视频流和音频流),我们需要他们的进度保持相对的一致,那么只要保证他们的PTS或者DTS都相对一致,就可以了。
具体做法
/**
* 解码一帧数据
* @return 0 if OK, < 0 on error or end of file
*/
int BaseDecoder::DecodeOnePacket() {
if (m_SeekPosition > 0) {//拖动进度条
}
//读取一帧数据到 m_Packet 中
int result = av_read_frame(m_AVFormatContext, m_Packet);
while (result == 0) {
//匹配帧的index
if (m_Packet->stream_index == m_StreamIndex) {
if (avcodec_send_packet(m_AVCodecContext, m_Packet) == AVERROR_EOF) {
//解码结束
result = -1;
goto __EXIT;
}
int frameCount = 0;
while (avcodec_receive_frame(m_AVCodecContext, m_Frame) == 0) {
//更新时间戳
UpdateTimeStamp();
//同步
AVSync();
//渲染视频
OnFrameAvailable(m_Frame);
frameCount++;
}
//判断一个 packet 是否解码完成
if (frameCount > 0) {
result = 0;
goto __EXIT;
}
}
av_packet_unref(m_Packet);
result = av_read_frame(m_AVFormatContext, m_Packet);
}
__EXIT:
av_packet_unref(m_Packet);
return result;
}
这段代码的主要功能,就是解码一帧数据(视频帧、音频帧都可以),然后交给对应的模块去显示和播放声音。其中的UpdateTimeStamp()和AVSync()就是同步的主要方法了。
void BaseDecoder::UpdateTimeStamp() {
LOGE("DecoderBase::UpdateTimeStamp");
std::unique_lock<std::mutex> lock(m_Mutex);
if(m_Frame->pkt_dts != AV_NOPTS_VALUE) {
m_CurTimeStamp = m_Frame->pkt_dts;
} else if (m_Frame->pts != AV_NOPTS_VALUE) {
m_CurTimeStamp = m_Frame->pts;
} else {
m_CurTimeStamp = 0;
}
m_CurTimeStamp = (int64_t)((m_CurTimeStamp * av_q2d(m_AVFormatContext->streams[m_StreamIndex]->time_base)) * 1000);
if(m_SeekPosition > 0 && m_SeekSuccess)
{
m_StartTimeStamp = GetSysCurrentTime() - m_CurTimeStamp;
m_SeekPosition = 0;
m_SeekSuccess = false;
}
}
long BaseDecoder::AVSync() {
LOGD("BaseDecoder::AVSync");
long curSysTime = GetSysCurrentTime();
//基于系统时钟计算从开始播放流逝的时间
long elapsedTime = curSysTime - m_StartTimeStamp;
long delay = 0;
//向系统时钟同步
if(m_CurTimeStamp > elapsedTime) {
//休眠时间
auto sleepTime = static_cast<unsigned int>(m_CurTimeStamp - elapsedTime);//ms
//限制休眠时间不能过长
sleepTime = sleepTime > DELAY_THRESHOLD ? DELAY_THRESHOLD : sleepTime;
av_usleep(sleepTime * 1000);
}
delay = elapsedTime - m_CurTimeStamp;
return delay;
}
每次解码都更新一下时间,然后跟系统时间做对比,根据时间差进行判断,要么睡眠等待,要么继续执行解码。
还有需要注意的不?
首先,睡眠等待的时间,需要一个阈值,不然很影响体验。
然后,如果输入流是网络,输入数据是不稳定的,有可能会丢帧之类的情况出现,这些情况需要根据具体需求去建立方案解决,这里就不写了。