音视频同步原理:播放器开发的关键步骤

发表时间: 2023-07-29 20:27

一、音视频同步概念

在多媒体领域音频与视频是两个独立的概念,它们有各自的编解码算法。视频常见的编解码算法有H264/H265/VP8/VP9;音频有AAC。编码算法的核心逻辑就是一个压缩的过程而播放=解码+渲染。播放过程中音频与视频有各自的解码算法与渲染机制,是一个独立的过程。音频视频同步则是为了使播放的声音和显示的画面保持一致,即画面内容与声音保持一致。当音频与视频播放时出现不一致时是非常影响体验的。下面有一个音视频不一致的例子(注意观看字幕与声音的同步情况):

音视频不同步的有两种情况,要么视频播放的时间比音频快,要么是音频播放的时间比视频快。而音视频同步就是让它俩在保持相对同步。

二、音视频不同步的根本原因

要这个问题得先找到问题原因,这样才能对症下药。我们以一个44.1KHz的AAC音频流和25FPS的H264视频流为例,来看一下理想情况下音视频的同步过程:
一个AAC音频frame每个声道包含1024个采样点(也可能是2048,参“
FFmpeg关于nb_smples,frame_size以及profile的解释”),则一个frame的播放时长(duration)为:(1024/44100)×1000ms = 23.22ms;一个H264视频frame播放时长(duration)为:1000ms/25 = 40ms。声卡虽然是以音频采样点为播放单位,但通常我们每次往声卡缓冲区送一个音频frame,每送一个音频frame更新一下音频的播放时刻,即每隔一个音频frame时长更新一下音频时钟,实际上ffplay就是这么做的。我们暂且把一个音频时钟更新点记作其播放点,理想情况下,音视频完全同步,音视频播放过程如下图所示:


在播放过程中视频按帧播放,图像显示设备每次显示一帧画面,视频播放速度由帧率确定,帧率指示每秒显示多少帧;音频按采样点播放,声音播放设备每次播放一个采样点,声音播放速度由采样率确定,采样率指示每秒播放多少个采样点。如果仅仅是视频按帧率播放,音频按采样率播放,二者没有同步机制,即使最初音视频是基本同步的,随着时间的流逝,音视频会逐渐失去同步(上图的虚线也能看出来),并且不同步的现象会越来越严重。这是因为:一、播放时间难以精确控制,二、异常及误差会随时间累积。

三、音视频同步的策略

知道问题的原因之后就好办了,视频和音频的同步实际上是一个动态的过程,同步是暂时的,不同步则是常态。以选择的播放速度量为标准,快的等待慢的,慢的则加快速度,是一个你等我赶的过程。同步的方式通常有以下三种:

  • 将视频同步到音频上,就是以音频的播放速度为基准来同步视频。视频比音频播放慢了,加快其播放速度;快了,则延迟播放。
  • 将音频同步到视频上,就是以视频的播放速度为基准来同步音频。
  • 将视频和音频同步外部的时钟上,选择一个外部时钟为基准,视频和音频的播放速度都以该时钟为标准。

3.1 PTS 与DTS 、time_base 介绍

知道了策略之后,怎么让它快慢播放呢?上面提到,视频和音频的同步过程是一个你等我赶的过程,快了则等待,慢了就加快速度。这就需要一个量来判断(和选择基准比较),到底是播放的快了还是慢了,或者正以同步的速度播放。而DTS 与PTS就是这样的量(准确来说是PTS)。在流媒体中每一帧的数据包中都含有DTS和PTS,这两个参数是在编码的时候的时候就确定好的。播放时无需关心是从哪来的。。

  • DTS:Decoding Time Stamp,解码时间戳,告诉解码器packet的解码顺序;
  • PTS:Presentation Time Stamp,显示时间戳,指示从packet中解码出来的数据的显示顺序。

音视频数据有了PTS 之后就能确定每一帧在何时播放了,也即确定了顺序。FFMPEG 计算PTS 的的方法为可参看源码:ffmpeg.c:

  • 视频时间戳计算
pts = count++ *(1000/fps);  //其中count初始值为0,每次打完时间戳count加1.//在ffmpeg,中的代码为pkt.pts= count++ * (Ctx->time_base.num * 1000 / Ctx->time_base.den);
  • 音频时间戳
pts = count++ * (frame_size * 1000 / sample_rate)//在ffmpeg中的代码为pkt.pts= count++ * (Ctx->frame_size * 1000 / Ctx->sample_rate);

有了PTS 之后就可以计算媒体文件的播放总时长与当前进度了。在计算某一帧的显示时间之前,先介绍FFmpeg中一个重量的时间单位:时间基(TIME BASE)

时间基也称之为时间基准,它代表的是每个刻度是多少秒,是一个相对的概念。比方说:视频帧率是30FPS,那它的时间刻度是{1,30}。相当于1s内划分出30个等分,也就是每隔1/30秒后显示一帧视频数据。具体的如下图所示:在FFmpeg中存在这多个不同的时间基,对应着视频处理的不同的阶段(分布于不同的结构体中)。在本文中使用的是AVStream的时间基,来指示Frame显示时的时间戳(timestamp)。

/**    * This is the fundamental unit of time (in seconds) in terms    * of which frame timestamps are represented.    *    */AVRational time_base;

上面的注释说明AVStream中的time_base是以秒为单位,表示frame显示的时间,其类型为AVRationalAVRational是一个分数,其声明如下:

/** * rational number numerator/denominator */typedef struct AVRational{    int num; ///< numerator    int den; ///< denominator} AVRational;

num为分子,den为分母。PTS为一个uint64_t的整型,其单位就是time_base。表示视频长度的duration也是一个uint64_t,那么使用如下方法就可以计算出一个视频流的时间长度:

time(second) = st->duration * av_q2d(st->time_base)

st为一个AVStream的指针,av_q2d将一个AVRational转换为双精度浮点数。同样的方法也可以得到视频中某帧的显示时间

timestamp(second) = pts * av_q2d(st->time_base)

得到了Frame的PTS后,就可以得到该frame显示的时间戳即当前播放的时刻。下面的代码展示了在从packet中解码出frame后,如何得到frame的PTS

ret = avcodec_receive_frame(video->video_ctx, frame);if (ret < 0 && ret != AVERROR_EOF)    continue;if ((pts = av_frame_get_best_effort_timestamp(frame)) == AV_NOPTS_VALUE)    pts = 0;pts *= av_q2d(video->stream->time_base);pts = video->synchronize(frame, pts);frame->opaque = &pts;


有了PTS 这个重量的衡量工具之后,是如何控制音频或者视频的播放快慢以达到人眼舒服的观看体验呢?我们以FPS =25 的视频文件为例。它的时间基={1,25}即1s 内需要渲染25帧图像。以现在解码芯片性能而言解码一帧图像并渲染出来的总时间在几ms 之内就可以完成这个工作。如果不加以控制这25帧图片根本不需要花费1s 的时间。那么造成的现象就是瞬间将图像帧渲染完了,根本看不清图像的内容,就会造成有大部分时间处于等待状态。


在视觉领域人眼的区分度有限的,简单来说就是无法区域高分辨率下的图像,通常FPS 大于30,人眼就很难分辨了,低15帧能明显感受到卡顿。为了消除这个等待区域,可以将这个时间平摊给前面的25帧内容,每帧播放完之后给它增加一个【休息】时间,这样就可以解决播放快同时又出现等待的情况。示意图如下:


音频也是同样的原理。

四、音视频同步具体实现

上面提到了同步的三种策略,下面以音频时钟为参考时钟实现音视频同步为例进行分析是如何达到“音视频同步”的。

以音频时钟为参考做音画同步,音频的解码播放的过程中认为是正常播放的不做处理。在播放的过程中,需要不断地将视频的播放时间戳和音频时间戳作比较,如果两者的差值超过了某个阈值,则认为视频播放太快或者太慢了,需要将视频播放调慢或者调快甚至丢帧,如果它们的差值在允许的阈值范围内,则代表播放正常,不用调整。如下图所示(图片来源于网络)

需要同步的处理的情况通常有两种:

  • 如果视频出现播放早于音频,那么这种情况就需要进行丢帧处理将当前未播放的帧全部丢弃直到与音频时间戳在合理的阈值内。同时要增加播放视频线程的休眠时间,让它工作的慢点。示意图如下:



  • 如果视频播放滞后音频,就需要加快视频的播放,忽略当前帧,立即显示下一帧,加速视频追赶。这种加快的处理其实就是减少视频播放线程的休眠时间。它的示意图如下:

最终实现的效果见视频: