浅析音视频同步原理

音视频同步

今天我们来讲解一下音视频同步吧。讲完这篇,结合之前几篇博客,我们对音视频开发也算有一个入门级别的理解了。

首先思考几个问题:

1、为什么需要音视频同步?

因为视频和音频是两个独立的任务在运行,视频和音频的解码速度也不一样,解码出来的数据也不一定马上就可以显示出来。

  • 视频:帧率,表示视频一秒显示的帧数。
  • 音频:采样率,表示音频一秒播放的样本的个数。

从帧率及采样率,即可知道视频/音频播放速度。声卡和显卡均是以一帧数据来作为播放单位,如果单纯依赖帧率及采样率来进行播放,在理想条件下,应该是同步的,不会出现偏差。以一个44.1KHz的AAC音频流和24FPS的视频流为例:一个AAC音频frame每个声道包含1024个采样点,则一个frame的播放时长(duration)为:(1024/44100)×1000ms = 23.22ms;一个视频frame播放时长(duration)为:1000ms/24 = 41.67ms。理想情况下,音视频完全同步,音视频播放过程如下图所示:

在这里插入图片描述

但实际情况下,如果用上面那种简单的方式,慢慢的就会出现音视频不同步的情况,要不是视频播放快了,要么是音频播放快了。可能的原因如下:

  1. 一帧的播放时间,难以精准控制。音视频解码及渲染的耗时不同,可能造成每一帧输出有一点细微差距,长久累计,不同步便越来越明显。(例如受限于性能,42ms才能输出一帧)
  2. 音频输出是线性的,而视频输出可能是非线性,从而导致有偏差。
  3. 媒体流本身音视频有差距。(特别是TS实时流,音视频能播放的第一个帧起点不同)
2、那要怎么做音视频同步?

为了解决音视频同步问题,引入了时间戳:首先选择一个参考时钟(要求参考时钟上的时间是线性递增的);编码时依据参考时钟上的给每个音视频数据块都打上时间戳;播放时,根据音视频时间戳及参考时钟,来调整播放。所以,视频和音频的同步实际上是一个动态的过程,同步是暂时的,不同步则是常态。以参考时钟为标准,放快了就减慢播放速度;播放快了就加快播放的速度。

3、那么参考时钟都有哪些?

前面已经说了,实现音视频同步,在播放时,需要选定一个参考时钟,读取帧上的时间戳,同时根据的参考时钟来动态调节播放。现在已经知道时间戳就是PTS,那么参考时钟的选择一般来说有以下三种:

  1. 将视频同步到音频上:就是以音频的播放速度为基准来同步视频。
  2. 将音频同步到视频上:就是以视频的播放速度为基准来同步音频。
  3. 将视频和音频同步外部的时钟上:选择一个外部时钟为基准,视频和音频的播放速度都以该时钟为标准。

当播放源比参考时钟慢,则加快其播放速度,或者丢弃;快了,则延迟播放。

这三种是最基本的策略,**考虑到人对声音的敏感度要强于视频,频繁调节音频会带来较差的观感体验,且音频的播放时钟为线性增长,所以一般会以音频时钟为参考时钟,视频同步到音频上。**在实际使用基于这三种策略做一些优化调整,例如:

  • 调整策略可以尽量采用渐进的方式,因为音视频同步是一个动态调节的过程,一次调整让音视频PTS完全一致,没有必要,且可能导致播放异常较为明显。
  • 调整策略仅仅对早到的或晚到的数据块进行延迟或加快处理,有时候是不够的。如果想要更加主动并且有效地调节播放性能,需要引入一个反馈机制,也就是要将当前数据流速度太快或太慢的状态反馈给“源”,让源去放慢或加快数据流的速度。
  • 对于起播阶段,特别是TS实时流,由于视频解码需要依赖第一个I帧,而音频是可以实时输出,可能出现的情况是视频PTS超前音频PTS较多,这种情况下进行同步,势必造成较为明显的慢同步。处理这种情况的较好方法是将较为多余的音频数据丢弃,尽量减少起播阶段的音视频差距。

以上就是关于音视频同步的理论知识了。有了理论知识之后,我们就可以去实践了,但是我们实践的是以外部时钟为方法的。为什么呢?因为这种比较简单,也容易理解。这里我会参考网上的一个例子去讲解,重点也只讲解音视频同步的那一部分,其他的解码部分,可以自己去阅读一下源码。github传送门

final override fun run() {
    if (mState == DecodeState.STOP) {
        mState = DecodeState.START
    }
    mStateListener?.decoderPrepare(this)

    //【解码步骤:1. 初始化,并启动解码器】
    if (!init()) return

    Log.i(TAG, "开始解码")
    try {
        while (mIsRunning) {
            if (mState != DecodeState.START &&
                mState != DecodeState.DECODING &&
                mState != DecodeState.SEEKING) {
                Log.i(TAG, "进入等待:$mState")

                waitDecode()

                // ---------【同步时间矫正】-------------
                //恢复同步的起始时间,即去除等待流失的时间
                mStartTimeForSync = System.currentTimeMillis() - getCurTimeStamp()
            }

            if (!mIsRunning ||
                mState == DecodeState.STOP) {
                mIsRunning = false
                break
            }
			//记录开始解码的时间
            if (mStartTimeForSync == -1L) {
                mStartTimeForSync = System.currentTimeMillis()
            }

            //如果数据没有解码完毕,将数据推入解码器解码
            if (!mIsEOS) {
                //【解码步骤:2. 见数据压入解码器输入缓冲】
                mIsEOS = pushBufferToDecoder()
            }

            //【解码步骤:3. 将解码好的数据从缓冲区拉取出来】
            val index = pullBufferFromDecoder()
            if (index >= 0) {
                // ---------【音视频同步】-------------
                if (mSyncRender && mState == DecodeState.DECODING) {
                    sleepRender()
                }
                //【解码步骤:4. 渲染】
                if (mSyncRender) {// 如果只是用于编码合成新视频,无需渲染
                    render(mOutputBuffers!![index], mBufferInfo)
                }

                //将解码数据传递出去
                val frame = Frame()
                frame.buffer = mOutputBuffers!![index]
                frame.setBufferInfo(mBufferInfo)
                mStateListener?.decodeOneFrame(this, frame)

                //【解码步骤:5. 释放输出缓冲】
                mCodec!!.releaseOutputBuffer(index, true)

                if (mState == DecodeState.START) {
                    mState = DecodeState.PAUSE
                }
            }
            //【解码步骤:6. 判断解码是否完成】
            if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
                Log.i(TAG, "解码结束")
                mState = DecodeState.FINISH
                mStateListener?.decoderFinish(this)
            }
        }
    } catch (e: Exception) {
        e.printStackTrace()
    } finally {
        doneDecode()
        release()
    }
}

//通过线程sleep去做同步
private fun sleepRender() {
    //获取从开始解码到这一次解码,一共流失的时间
    val passTime = System.currentTimeMillis() - mStartTimeForSync
    //如果当前的帧的时间戳比这个流失的时间要大的话,那么就需要休眠等待,否则就直接渲染。这里的同步方案确实有点问题,并没有考虑到很多的场景,但是不影响对音视频同步的这个理解,如果后面有机会的话,我还会和大家讲讲ffpaly的音视频同步的代码。那里的场景是以音频时钟作为同步时钟的
    val curTime = getCurTimeStamp()
    if (curTime > passTime) {
        Thread.sleep(curTime - passTime)
    }
}

override fun getCurTimeStamp(): Long {
    return mBufferInfo.presentationTimeUs / 1000
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值