ffmpeg开发播放器学习笔记 - 音视频同步

该节是ffmpeg开发播放器学习笔记的第六节《音视频同步》

一般来说,视频同步指的是视频和音频同步,也就是说播放的声音要和当前显示的画面保持一致。想象以下,看一部电影的时候只看到人物嘴动没有声音传出;或者画面是激烈的战斗场景,而声音不是枪炮声却是人物说话的声音,这是非常差的一种体验。


image.png

✅ 第一节 - Hello FFmpeg
✅ 第二节 - 软解视频流,渲染 RGB24
✅ 第三节 - 认识YUV
✅ 第四节 - 硬解码,OpenGL渲染YUV
✅ 第五节 - Metal 渲染YUV
✅ 第六节 - 解码音频,使用AudioQueue 播放
🔔 第七节 - 音视频同步
📗 第八节 - 完善播放控制
📗 第九节 - 倍速播放
📗 第十节 - 增加视频过滤效果
📗 第十一节 - 音频变声

该节 Demo 地址: https://github.com/czqasngit/ffmpeg-player/releases/tag/Audio-And-Video-Sync

实例代码提供了Objective-CSwift两种实现,为了方便说明,文章引用的是Objective-C代码,因为Swift代码指针看着不简洁。

该节最终效果如下图:

image

目标

  • 音视频同步的背景以及产生不同步的原因
  • 音视频同步的处理方案及选择
  • 编码实现音视频同步

音视频同步的背景以及产生不同步的原因

在视频流和音频流中已包含了其以怎样的速度播放的相关数据,视频的帧率(Frame Rate)指示视频一秒显示的帧数(图像数);音频的采样率(Sample Rate)表示音频一秒播放的样本(Sample)的个数。可以使用以上数据通过简单的计算得到其在某一Frame(Sample)的播放时间,以这样的速度音频和视频各自播放互不影响,在理想条件下,其应该是同步的,不会出现偏差。如果用上面那种简单的计算方式,慢慢的就可能 会出现音视频不同步的情况。要不是视频播放快了,要么是音频播放快了。这就需要一种随着时间会线性增长的量,视频和音频的播放速度都以该量为标准,播放快了就减慢播放速度;播放快了就加快播放的速度。所以,视频和音频的同步实际上是一个动态的过程,同步是暂时的,不同步则是常态。以选择的播放速度量为标准,快的等待慢的,慢的则加快速度,是一个你等我赶的过程。

音视频同步的处理方案及选择

处理音视频同步的方案通常有以下三种:

1.视频时钟同步到音频时钟

以音频时钟为标准时钟,音频自然播放。视频帧播放时判断当前视频帧播放结束后的时间与当前的音频时钟时间对比,如果视频当前帧播放完时间比音频时钟时间早,则让当前视频播放线程暂时时间差,以保证播放完后与音频时钟同步。如果当前视频帧播放完时间比音频时间晚,则丢弃当前视频帧读取下一帧再判断,以保证播放完后与音频时钟同步。

2.音频时钟同步到视频时钟

以视频时钟为标准,视频自然播放。同步逻辑则与第1点的同步逻辑一致:即音频快了就暂停音频播放线程等待时间差,慢了则丢弃当前音频帧。以保证当前音频帧播放完与视频帧时钟同步。

3.以外部时钟为准,音频与视频时钟同时同步到外部时钟。

同步逻辑与1、2点一致。需要注意的是外部时钟应尽量使用毫秒时钟以确保同步的精准。

同步方案选择

以上3种方案都可以实现音频与视频的同步处理,但怎么选择更适合的方案呢?

  • 人的眼睛与耳朵对图像与声音的敏感程度不一样,当画面偶尔缺少一帧或者几帧时人的眼睛可能不太容易察觉。这是因为画面的连贯性比较强,两帧画面之前的差异有时候很小,眼睛比耳机敏感度更低。当声音发生一变化,比如缺失了一点声音或者声音异常的,人的耳朵马上就察觉到了。
  • 在大多数平台上声音的播放开销都比渲染画面小。声音的数据处理过程更简单,数量量也更小。声音线程播放声音卡顿的概率很小。
  • 在macOS/iOS平台上,以AudioQueue播放为例,声音的播放缓存对象是重复利用的,而这个利用则是由实际播放声音的具体线程来回调的。不同于视频每一帧的渲染,声音的暂停与丢弃相比视频实现成本更高。

综合上以的三点,本文选择第1点同步方案<strong>视频时钟同步到音频时钟</strong>

编码实现音视频同步

音频视频同步基础

FFmpeg里有两种时间戳:DTS(Decoding Time Stamp)和PTS(Presentation Time Stamp)。 顾名思义,前者是解码的时间,后者是显示的时间。要仔细理解这两个概念,需要先了解FFmpeg中的packet和frame的概念。

FFmpeg中用AVPacket结构体来描述解码前或编码后的压缩包,用AVFrame结构体来描述解码后或编码前的信号帧。 对于视频来说,AVFrame就是视频的一帧图像。这帧图像什么时候显示给用户,就取决于它的PTS。DTS是AVPacket里的一个成员,表示这个压缩包应该什么时候被解码。 如果视频里各帧的编码是按输入顺序(也就是显示顺序)依次进行的,那么解码和显示时间应该是一致的。可事实上,在大多数编解码标准(如H.264或HEVC)中,编码顺序和输入顺序并不一致。 于是才会需要PTS和DTS这两种不同的时间戳。

基本概念:

I帧 :帧内编码帧 又称intra picture,I 帧通常是每个 GOP(MPEG 所使用的一种视频压缩技术)的第一个帧,经过适度地压缩,做为随机访问的参考点,可以当成图象。I帧可以看成是一个图像经过压缩后的产物。

P帧: 前向预测编码帧 又称predictive-frame,通过充分将低于图像序列中前面已编码帧的时间冗余信息来压缩传输数据量的编码图像,也叫预测帧;

B帧: 双向预测内插编码帧 又称bi-directional interpolated prediction frame,既考虑与源图像序列前面已编码帧,也顾及源图像序列后面已编码帧之间的时间冗余信息来压缩传输数据量的编码图像,也叫双向预测帧;

PTS:Presentation Time Stamp。PTS主要用于度量解码后的视频帧什么时候被显示出来

DTS:Decode Time Stamp。DTS主要是标识读入内存中的bit流在什么时候开始送入解码器中进行解码。

在没有B帧存在的情况下DTS的顺序和PTS的顺序应该是一样的。

IPB帧的不同:

I帧:自身可以通过视频解压算法解压成一张单独的完整的图片。

P帧:需要参考其前面的一个I frame 或者B frame来生成一张完整的图片。

B帧:则要参考其前一个I或者P帧及其后面的一个P帧来生成一张完整的图片。

两个I帧之间形成一个GOP,在x264中同时可以通过参数来设定bf的大小,即:I 和p或者两个P之间B的数量。
通过上述基本可以说明如果有B frame 存在的情况下一个GOP的最后一个frame一定是P。

DTS和PTS的不同:

DTS主要用于视频的解码,在解码阶段使用.PTS主要用于视频的同步和输出.在display的时候使用.在没有B frame的情况下.DTS和PTS的输出顺序是一样的.

2.音频与视频数据缓冲对象增加时钟数据

@interface FFQueueAudioObject : NSObject
@property (nonatomic, assign, readonly)float pts;
@property (nonatomic, assign, readonly)float duration;
- (instancetype)initWithLength:(int64_t)length pts:(float)pts duration:(float)duration;
- (uint8_t *)data;
- (int64_t)length;
- (void)updateLength:(int64_t)length;
@end
@interface FFQueueVideoObject : NSObject
@property (nonatomic, assign)double unit;
@property (nonatomic, assign)double pts;
@property (nonatomic, assign)double duration;
- (instancetype)init;
- (AVFrame *)frame;
@end

分别在上面的音频与视频缓冲对象上增加变量ptsduration

  • pts: 当前帧播放或显示的时间。
  • duration: 当前帧播放或显示持续的时长。音频帧内包括多个音频数据包,而视频则可以通过FPS计算得到每一帧的显示持续时长。

3.视频时钟同步到音频时钟

pthread_mutex_lock(&(self->mutex));
/// 读取当前的音频时钟时间
double ac = self->audio_clock;
pthread_mutex_unlock(&(self->mutex));
FFQueueVideoObject *obj = NULL;
/// 统计路过的视频帧数量
int readCount = 0;
/// 首先读取一帧视频数据
obj = [self.videoFrameCacheQueue dequeue];
readCount ++;
/// 计算当前视频帖播放结束时的时间点
double vc = obj.pts + obj.duration;
if(ac - vc > self->tolerance_scope) {
    /// 视频太慢,丢弃当前帧继续读取下一帧
    /// 这里认为读取下一帧或者更下一帧不会造成视频缓冲队列枯竭,所以未做等待处理
    /// 因为时时同步能形成的时间差比较有限
    while (ac - vc > self->tolerance_scope) {
        FFQueueVideoObject *_nextObj = [self.videoFrameCacheQueue dequeue];
        if(!_nextObj) break;
        obj = _nextObj;
        vc = obj.pts + obj.duration;
        readCount ++;
    }
} else if (vc - ac > self->tolerance_scope) {
  /// 视频太快,暂停一下再接着渲染显示当前视频帧
    float sleep_time = vc - ac;
    usleep(sleep_time * 1000 * 1000);
} else {
  /// 在误差范围之后, 不需要处理
}

tolerance_scope为可允许的误差值,即音频与视频时间差小于这个数据则认为是同步的。这是因为要达到绝对的时间一致性是不可能的,在计算时间的过程中有精度的丢失。

  • 获取当前音频时钟的时间(该时间为当前音频帧播放结束后的时间)
  • 读取一帧视频帧,计算出该视频帧播放完之后的时间
  • 判断音频时间与视频时间的差值,进行同步处理

到此,音视频同步的完成了,现在再去看视频就不会发现嘴巴与声音不一致的问题了🏄🏄🏄🏄🏄🏄。

总结

  • 了解了音视频同步的背景以及产生不同步的原因
  • 了解音视频同步的处理方案及合理的选择了视频时钟同步到音频时钟的方案
  • 编码实现音视频同步

更多内容请关注微信公众号<<程序猿搬砖>>

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269