搞音视频开发好些年,分享过许多博客文章,比如:前几年发布的《FFmpeg Tips》系列,《Android 音频开发》系列,《直播疑难杂症排查》系列等等。最近想把多年来开发和优化播放器的经验也分享出来,同时也考虑把自己业余时间开发的基于 ffmpeg 的播放器内核开源出来,希望能帮助到音视频领域的初学者。第一期文章要推出的内容主要涉及到播放器比较核心的几个技术点,大概的目录如下:

1. 播放器技术分享(1):架构设计
2. 播放器技术分享(2):缓冲区管理
3. 播放器技术分享(3):音画同步
4. 播放器技术分享(4):首开时间
5. 播放器技术分享(5):延时优化

本篇是系列文章的第二篇,主要聊一聊播放器的缓冲区管理。

1 概述

在上一篇文章中,我们有提到利用缓冲区把单线程模型的数据流改造为多线程模型,从而可以有效抵抗网络和解码的抖动,防止频繁卡顿,同时也能充分利用多核 CPU 的计算能力,如下图所示:

播放器内核架构 播放器技术_缓冲池


播放器的读线程,将 IO 和 Parser 模块输出的”未解码“的音视频数据包放到”帧缓冲区“队列中,将解码后的数据,存放到”显示缓冲区“队列中。

2 缓冲区的作用

我们深挖一下,这个 “帧缓冲区” 和 “显示缓冲区” 究竟起到了一个什么作用 ?

2.1 帧缓冲区
帧缓冲区,作为“读线程”和“解码线程”之间的缓冲池,它主要起到了三个作用:

1. 抵抗网络抖动
 2. 抵抗解码抖动
 3. 避免被动丢帧导致花屏

假设没有“帧缓冲区”,即:IO -> Parser -> Decoder 整个流程是串行的,那么会有如下潜在问题:

1. IO 网络抖动的时候(比如:短暂拥塞,无法读到数据),那么整个数据链条都会被卡住,Decoder 只能干等着 IO 恢复
 2.Decoder 同样会出现“抖动”,因为解码某些复杂的视频帧,会耗时比较久,如果 Decoder 卡住,同样 IO 模块也只能干等着
 3. 因为整个流程是串行的,每一帧都必须 IO -> Parser -> Decoder走完才会读取和处理下一帧,那么,当网络抖动的时候,会出现服务端的 TCP  协议栈缓存了较多的数据,在网络恢复的时候,下发到客户端的时候,因为接收不及时,导致 TCP 发送队列爆满而产生被动丢帧,从而使得后续因为数据不完整导致解码花屏

2.2 显示缓冲区

显示缓冲区,作为“解码线程”和“显示线程”之间的缓冲池,它主要起到了三个作用:

1. 实现 “音画同步” 的必要条件
 2. 抵抗渲染抖动

假设没有“显示缓冲区”,即:Decoder -> Renderer 整个流程是串行的,那么会有如下潜在问题:

1. 无论是视频帧还是音频数据,都是解码完了就立马送入了渲染模块,无法添加音画同步的逻辑处理
 2. 如果渲染模块出现“抖动”,会直接阻塞×××,无法异步去解码帧缓冲区中的数据,降低了效率
3 “主动缓冲” 与 “被动缓冲”

了解了缓冲区的作用,我们再看看缓冲区的数据是怎么被填充的 ?

缓冲区的数据填充,主要分为 2 种情况,第一种叫 “主动缓冲”,另一种叫做 “被动缓冲”

主动缓冲:是指播放器主动暂停缓冲区的数据消费,等待数据生产者逐渐填充数据,直到达到某种条件再恢复

被动缓冲:是指数据的消费速度赶不上生产速度,从而被动滞留了数据在缓冲区中

主动缓冲,多用于点播场景,为了降低频繁卡顿,在开始播放视频之前,会主动 buffering 一段时间(比如:10s)的数据,再开始播放。当缓冲区内的数据因为网络抖动等原因消耗完了,会再次启动 buffering,如此循环。

播放器内核架构 播放器技术_播放器内核架构_02


如图所示,假设播放器缓冲区内的数据低于 Low 这个水位点后,会主动暂停播放,启动 buffering 过程直到缓冲区中的数据达到 M 水位值。

被动缓冲,多出现在直播场景,可能有 2 种原因:

1. 手机等设备的解码性能不足,比如软解 1080P 的高清视频,导致视频的解码和渲染的速度赶不上视频的读取速度,导致数据堆积在“帧缓冲区”
 2. 网络的频繁抖动,导致客户端无法及时拿到数据进行解码渲染,当网络恢复后,数据会迅速下发下来,但播放器已没有办法再快速消费掉(因为播放的速率是固定的,除非添加追帧的逻辑,后续文章会详细介绍)
4 缓冲区的大小怎么定 ?

理解了缓冲区的作用,那这两个缓冲区的大小如何制定呢 ?首先,我们需要知道这两个缓冲区大小究竟影响或者决定了什么 ?

1. 缓冲区越大 -> 抗抖动能力越强
 2. 缓冲区越大 -> 内存占用越高
 3. 缓冲区越大 -> 播放延时越大

由此可见,缓冲区也不是越大越好,需要根据实际的使用场景来决定。

“显示缓冲区” 其实是 解码线程 和 渲染线程 之间的桥梁,由于解码和渲染的抖动并不频繁,所以并不需要特别大的缓冲区,最低 3 帧左右即可,一帧在生产,一帧在消费,还有一帧在缓冲区中待命。

而 “帧缓冲区” 是用来抵抗网络抖动的,网络抖动往往是比较频繁的,抖动的时间也有时会比较久一些,所以 “帧缓冲区” 相对要设置得大一点,但以不过于影响内存和播放延时为前提。

对于直播场景,为了防止 “被动丢帧”,往往 “帧缓冲区” 默认是设置为 “无限大” 的,当检测到缓冲区达到一定阈值后,启动一些诸如主动丢帧或者倍数播放的方式,来快速消耗掉缓冲的内容,从而降低内存和延时。

5 缓冲区何时会主动清空 ?

有如下几种场景,播放器会主动清空缓冲区内的数据:

1. 播放器重置
 2. 播放进度条被拖动
 3. 消除累积延时
 4. 系统内存告警
6 IJKPlayer 的缓冲区管理

播放器内核架构 播放器技术_播放器内核架构_03


ijkplayer 使用非常广泛,这里以它为例看看播放器缓冲区的真实案例是怎么样的 ?

1. 播放器打开后,缓冲 100ms,再开始播放
 2. 如果遇到了卡顿(缓冲区为空),则暂停播放,缓冲到 1000ms,再开始播放
 3. 如果再次遇到卡顿,则缓冲到 2000ms 再播,依次类推,直到 5000ms

可以看出,它的缓冲区的最大阈值是逐步递增上去的,这是一个非常棒的用户体验优化,因为如果用户网络不是那么差的话,不用第一次缓冲就等 5s 了

7 总结

播放器的缓冲区管理,就分享到这里了。

转载于:https://blog.51cto.com/ticktick/2326207