基于FFmpeg开发视频播放器,音视频同步(四)
为什么需要音视频同步?
从前面的代码可以看到,播放的过程有解码线程不断的把解码好的AVFrame数据放入队列,然后播放线程从队列中取出解码后的数据,经过格式转换,分别送给ANativeWindow去绘制,送给OpenSlES去播放声音,这个过程如果不去控制,播放的速度就取决与解码线程,播放线程的处理速度,及系统的性能.这样播放的效果,肯定是不流畅的.
为了让播放尽可能流畅,就要把视频播放的帧率考虑进来,比如希望fps是30,那么就在绘制时的间隔控制在1/30.
加入绘制间隔的控制,虽然视频播放比流畅了,但是画面跟音频没有保持一致.音频与视频各播各的,由于机器运行速度,解码效率等种种造成时间差异的因素影响,即使最初音视频是基本同步的,也会随着时间的流逝逐渐失去同步。所以,必须要采用一定的同步策略,不断对音视频的时间差作校正,使图像显示与声音播放总体保持一致。所以需要做音视频的同步.
音视频的同步,有三种方式:
1、参考一个外部时钟,将音频与视频同步至此时间;
2、以视频为基准,音频去同步视频的时间;
3、以音频为基准,视频去同步音频的时间。
由于人对声音的变化相对于视觉更加敏感。所以频繁的去调整声音的播放会感觉刺耳或杂音影响用户体验。所以一般情况下,播放器使用第三种同步方式。
在音视频同步的处理中,有一个音视频时钟的概念,通过AVFrame->pts来获取,所以先说下PTS想关几个概念:
视频中的I P B帧:
I 帧:帧内编码帧 ,一个图像经过压缩后的产物,包含一幅完整的图像信息;
P 帧: 前向预测编码帧,利用之前的I帧或P帧进行预测编码
B 帧: 双向预测内插编码帧 ,利用之前和之后的I帧或P帧进行双向预测编码。
IDR帧:一个序列的第一个图像叫做IDR帧(立即刷新图像),IDR 帧都是I帧帧。H.264引入IDR帧是为了解码的重同步,当解码器解码到IDR帧时,立即将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找参数集,开始一个新的序列。这样,如果前一个序列出现重大错误,在这里获得重新同步的机会。IDR帧之后的图像永远不会使用IDR之前的图像数据来解码。
音视频中时间戳:
PTS:Presentation Time Stamp。显示时间戳,表示显示顺序。
DTS:Decode Time Stamp。解码时间戳,表示解码顺序
在没有B帧存在的情况下DTS的顺序和PTS的顺序应该是一样的。
音频中DTS和PTS是相同的,视频中由于可能存在B帧,含B帧的视频PTS与DTS不同。
显示顺序,解码顺序可以借助下图理解:
在视频编码序列中,GOP即Group of picture(图像组),指两个I帧之间的距离.
假如得到一段视频数据,他的帧类型是I B B P B B...
首先 是把I 帧送给解码器,所以他的 解码顺序 , 显示顺序 , DTS, PTS 都是1,
然后是第一个 B帧,虽然这个B帧显示顺序是2, 但是解码顺序时3,因为他要参考后面的P帧,要等P帧解码了,才能解码这个B帧,
第二个B帧也是一个道理,虽然这个B帧显示顺序是3, 但是解码顺序是4,因为他要参考后面的P帧,要等P帧解码了,才能解码这个B帧,
第一个P帧, 虽然这个P帧显示顺序是4, 但是解码顺序时2,这样才能解码I 和P帧之间的B帧.
其中DTS,在这里不需要关注,把AVPacket送给解码器,解码器会按照DTS的顺序去解码.
绘制视频时,需要考虑PTS,他决定了两张图片之间的显示间隔,也即是说,当要显示下一张图片时,需要休眠多少,除了考虑帧率,还要考虑PTSD的值.
理解了这几个概念,下面看代码:
首先是获取音频的时钟:
这个函数是从拿到AVFrame数据,转换成OpenSLES需要的格式时调用的.只关注其中clock属性:
//使用转换器,把frame_queue中的数据,转成我们需要的。把转换后的数据放入buffer,返回值表示转换数据的大小。
int AudioChannel::_getData() {
AVFrame *frame = 0;
while (isPlaying) {
//获取这段音频的时刻,pts表示这一帧的时间戳,以time_base为单位的时间戳,time_base是AVRational结构体类型,
// 也就是pts的单位是 (AVRational.Numerator / AVRational.Denominator),这样下面得出的时间单位是秒。
clock = frame->pts * av_q2d(time_base);
}
}
然后,视频播放这边的处理:
1, 根据帧率,再参考额外延迟时间repeat_pict,让视频播放更流畅。delay是要让视频以正常的速度播放,
2, 根据一个阈值范围,
#define AV_SYNC_THRESHOLD_MIN 0.04
#define AV_SYNC_THRESHOLD_MAX 0.1
调整音视频的时间差.
代码很容易看的懂,以音频的时钟为基准,视频快了,多休眠一会,视频慢了,少休眠一会,让他们的时间差保持一个合理的范围(0.04 ~ 0.1).
void VideoChannel::_play() {
AVFrame *frame = 0;
double frame_delay = 1.0 / fps;
while (isPlaying) {
//根据帧率,再参考额外延迟时间repeat_pict,让视频播放更流畅。delay是要让视频以正常的速度播放,
double extra_delay = frame->repeat_pict / (2*fps);
double delay = extra_delay + frame_delay;
if (audioChannel) {
//处理音视频同步,best_effort_timestamp跟pts通常是一致的,
// 区别是best_effort_timestamp经过了一些参考,得到一个最优的时间
clock = frame->best_effort_timestamp * av_q2d(time_base); //视频的时钟,
double diff = clock - audioChannel->clock;
//音频,视频的时间戳 的差,这个差有一个允许的范围(0.04 ~ 0.1)
double sync = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay));
if (diff <= -sync) {
delay = FFMAX(0, delay + diff); //视频慢了
} else if (diff > sync) {
delay = delay + diff;
}
LOGE("clock ,video:%1f ,audio:%1f, delay:%1f, V-A = %1f ",clock, audioChannel->clock, delay, diff);
}
av_usleep(delay * 1000000);
}
}
上一篇: find方法怎么使用
下一篇: P2P之Noise代码分析