基于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);
    }
}

  

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值