如何设计不会让ABR算法抓狂的转码阶梯
如果你曾在实际环境中观察过HLS播放,你一定见过这种现象:流媒体在两个码率档位之间不断跳来跳去。上去,下来,上去,下来。观众每隔几秒就看到画质不断变化。这比始终保持在较低档位还要糟糕。至少稳定的720p流给人一种是刻意选择的感觉。但一个每10秒在720p和1080p之间来回切换的流,则让人觉得出了故障。
根本原因几乎总是相同的:编码阶梯中相邻档位的码率太接近,或者档位之间的画质差异不足以证明带宽的增加是值得的。客户端的ABR算法无法做出决定,因为"负担得起"和"负担不起"之间的差距极其微小。
让我们来谈谈如何正确解决这个问题。
每像素比特数(BPP)健全性检查
在做任何事情之前,你需要了解每像素比特数(BPP)对你的阶梯意味着什么。这是评估给定分辨率下特定码率是否合理的最简单指标。
公式很简单:
BPP = bitrate / (width × height × framerate)
例如,一个1920×1080的流,码率4500 kbps,帧率30 fps:
BPP = 4,500,000 / (1920 × 1080 × 30) = 0.072
为什么这很重要?因为BPP告诉你每个档位的压缩密度。如果阶梯中相邻两个档位的BPP值非常接近,观众不会看到有意义的画质差异——但ABR算法仍然会尝试在它们之间切换。这就是产生乒乓现象的原因。
一个设计良好的阶梯应该呈现出随着分辨率增加BPP曲线下降的趋势。这反映了视频编解码器的一个真实特性:在更高的分辨率下效率更高。在1080p下,你需要更少的每像素比特数就能达到与480p相同的感知质量。如果你的BPP在各档位之间平坦或不一致,那就有问题了。
"0.70法则"在这里是一个实用的参考。其思想是,当你将像素数翻倍时(例如从720p到1080p),应该大约应用低分辨率BPP的0.70倍。这是经验法则,不是定律,但它为你提供了一种快速发现异常值的方法。如果你绘制阶梯的BPP值,某个档位与其相邻档位相比明显过高或过低——那个档位就会造成问题。
要点:不要仅仅选择看起来是整数的码率。计算每个档位的BPP,确保曲线合理。如果相邻两个档位的BPP在15-20%以内,观众无法区分,但ABR启发式算法会浪费时间在它们之间切换。
码率间距:1.5倍经验法则
没有通用标准,但一个常见的工程准则是在相邻码率档位之间保持至少1.5倍的比率。一些实现将此提高到2倍。
为什么?因为ABR算法使用带宽估计(BWE)来决定选择哪个档位。估计有一个置信区间——它永远不是精确的。如果两个档位分别是2.5 Mbps和3.0 Mbps,在稍有波动的连接上,BWE可以轻易地在3.0 Mbps上下振荡,导致不断切换。如果跳跃是从2.0 Mbps到4.0 Mbps,算法需要更显著的带宽变化才能触发切换。结果:更稳定的播放。
这里有一个具体的例子。假设你有一个干净的四档阶梯:
| 分辨率 | 码率 | BPP (30fps) | 与前一档的比率 |
|---|---|---|---|
| 480×270 | 400 kbps | 0.103 | — |
| 960×540 | 2000 kbps | 0.129 | 5.0× |
| 1280×720 | 2800 kbps | 0.101 | 1.4× |
| 1920×1080 | 4500 kbps | 0.072 | 1.6× |
乍一看似乎合理——四个分辨率,递增的码率。但看看540p到720p的跳跃:2000 kbps到2800 kbps。这只有1.4倍的比率。在2.5-3 Mbps附近徘徊的连接上(大多数移动连接都是如此),BWE会不断跨越这个阈值。上去,下来,上去,下来。观众每隔几个片段就看到分辨率切换。
还有另一个问题:BPP实际上从540p的0.129下降到720p的0.101。也就是说,观众得到了更多像素,但每个像素的数据量更少了。根据内容的不同,720p档位可能看起来并不比540p明显更好——你增加了分辨率,但失去了压缩余量。ABR算法在做无谓的切换。
这个阶梯的改进版本应该提高720p的码率并降低540p的码率:
| 分辨率 | 码率 | BPP (30fps) | 与前一档的比率 |
|---|---|---|---|
| 480×270 | 400 kbps | 0.103 | — |
| 960×540 | 1500 kbps | 0.096 | 3.75× |
| 1280×720 | 3000 kbps | 0.109 | 2.0× |
| 1920×1080 | 5800 kbps | 0.093 | 1.93× |
现在540p到720p的跳跃是一个干净的2倍比率。BWE需要翻倍,播放器才会考虑向上切换。而且BPP实际上从0.096增加到0.109,意味着720p档位同时提供了更多像素和更好的压缩质量——观众能看到真正的改善。720p到1080p的跳跃1.93倍同样稳固,BPP仅略微下降到0.093,这反映了更高分辨率下的预期效率提升。
逐档检查:单一档位播放测试
这是我很少看到团队做的事情,但它至关重要:单独播放每个档位并完整观看。
这听起来很显而易见,但大多数人只在完整的多码率流中测试ABR。他们从不隔离单个档位并从头到尾播放。当你这样做时,你会发现ABR行为所隐藏的问题:
- 即使在其自身声明的码率下也会缓冲的档位(因为播放列表中声明的BANDWIDTH比实际峰值码率低太多)
- 编码器在某些场景中苦苦挣扎并产生明显伪影的档位
- 由于分辨率/码率组合对目标设备的解码器要求过高而导致帧率下降或卡顿的档位
要进行测试,你可以通过几种方式强制单一档位播放:
使用hls.js演示页面:加载你的多码率流,然后在画质选择器下拉菜单中,逐个手动锁定每个级别。hlsjs.video-dev.org/demo/上的hls.js演示会显示所有画质级别,并允许你覆盖ABR。至少播放几分钟有代表性的内容。在"Buffer & Statistics"标签中查看丢帧情况。
在Apple平台上使用AVPlayer:使用AVPlayerItem上的preferredPeakBitRate和preferredMaximumResolution将播放限制在单一档位。或者更简单的方法:创建一个只包含一个变体的测试播放列表。
使用ffprobe或mediainfo:在播放之前,检查每个档位的实际码率统计数据。你的主播放列表中的BANDWIDTH值必须考虑峰值,而不仅仅是平均值。如果你的VBR编码的峰值比平均值高出40%,你声明的BANDWIDTH需要反映这一点。
如果单个档位在其声明的码率下都无法流畅播放,那么在ABR模式下肯定会出问题。ABR算法认为有足够的带宽而选择了该档位,然后遇到VBR峰值,卡顿,降回低档位,恢复后又选择该档位——乒乓效应就此产生。
片段时长:ABR切换时钟
还有一个人们容易忽视的因素:片段时长直接控制ABR算法做出决策的频率。每个片段边界都是一个潜在的切换点。因此,如果使用2秒片段,播放器每分钟最多可以重新评估并切换30次。使用6秒片段,降至每分钟10次。使用10秒片段,仅6次。
当你的阶梯本身码率间距就很紧凑时,这一点非常重要。短片段加上接近的码率档位是最糟糕的组合——你给了ABR算法最大的机会去做观众不需要看到的微小切换。
相反,较长的片段充当振荡的天然阻尼器。即使带宽估计波动,播放器也必须在它刚获取的片段持续时间内保持当前档位。到下一个决策点到来时,BWE可能已经稳定了。
HLS规范没有强制规定特定的时长,但Apple的创作指南建议以6秒为目标。在实际应用中:
- 2秒片段适用于快速启动和快速适应至关重要的低延迟直播。但你需要间距充分的阶梯来避免持续切换。
- 6秒片段是VOD和标准直播的良好默认值。它为ABR算法在决策之间建立可靠的带宽估计提供了充足的时间。
- 10秒片段非常稳定但适应缓慢。如果带宽急剧下降,播放器会陷入下载无法及时完成的高码率片段。
VBR编码还有一个微妙之处:片段内的码率变化随片段时长而增大。一个场景转换的10秒片段——从静态镜头到动作序列——内部可能有巨大的码率波动。如果播放列表中声明的BANDWIDTH与整体平均值匹配但与每片段峰值不匹配,ABR算法就会措手不及。较短的片段往往每片段码率更一致,使BWE更准确。
结论:如果你看到振荡而且阶梯间距看起来没问题,请检查你的片段时长。从4秒改为6秒可能就足以平息振荡。
使用Network Link Conditioner模拟真实条件
在稳定的100 Mbps光纤连接上测试ABR毫无意义。你需要模拟真实条件,在macOS上Apple的Network Link Conditioner是最佳工具。
你可以从Apple的Additional Tools for Xcode包中获取:在Xcode中,进入Xcode > Open Developer Tool > More Developer Tools,会跳转到Apple开发者下载页面。搜索"Additional Tools for Xcode",下载对应Xcode版本的DMG,打开后在Hardware文件夹中找到Network Link Conditioner.prefPane。双击安装后,它会出现在系统设置(旧版macOS中为系统偏好设置)中的一个面板。你可以定义具有特定吞吐量、延迟、丢包率和DNS延迟的带宽配置文件。
创建以下重要的自定义配置文件:
- 一般4G:下行8 Mbps / 上行2 Mbps,80ms RTT,1%丢包率
- 差WiFi:下行3 Mbps / 上行1 Mbps,150ms RTT,3%丢包率
- 过渡测试:从15 Mbps开始,播放过程中手动切换到2 Mbps
最后一个测试是关键。如果你的阶梯设计良好,播放器应该在几秒内平滑降至较低档位并保持不变。如果它开始在两个档位之间振荡,说明在带宽临界点处档位太近了。
在iOS设备上,可以通过开发者设置使用Network Link Conditioner(将设备连接到Xcode后,在设置 > 开发者中启用)。
这些测试的目标不仅仅是"会不会缓冲?"——而是"流媒体是否会稳定在一个档位并保持不变?"良好的ABR体验是切换稀少且果断的。观众只看到一次画质变化,然后就稳定下来。
使用Apple的AVMetrics监控生产环境中的切换
从iOS 18开始,Apple在AVFoundation中引入了AVMetrics API。这是在实际环境中监控ABR行为的革命性工具。
对我们来说关键的事件类型是变体切换事件。每当AVPlayer在HLS变体之间切换时,你都会收到一个指标事件,告诉你从哪个变体切换到哪个变体,以及媒体档位的详细信息。当播放器重新缓冲时,你还会收到卡顿事件,以及在会话结束时带有总体KPI的摘要事件。
Swift的模式如下:
let playerItem: AVPlayerItem = // your configured item
let switchMetrics = playerItem.metrics(
forType: AVMetricPlayerItemVariantSwitchEvent.self
)
let stallMetrics = playerItem.metrics(
forType: AVMetricPlayerItemStallEvent.self
)
for await (event, _) in switchMetrics.chronologicalMerge(with: stallMetrics) {
switch event {
case let switchEvent as AVMetricPlayerItemVariantSwitchEvent:
// Log: from variant, to variant, timestamp
await analytics.logSwitch(switchEvent)
case let stallEvent as AVMetricPlayerItemStallEvent:
// Log: stall duration, variant at time of stall
await analytics.logStall(stallEvent)
default:
break
}
}
在分析数据中需要关注的:
- 切换频率:如果平均每个会话每分钟切换超过3-4次,你的阶梯有问题。健康的流通常只在启动时切换一两次。
- 振荡模式:两个档位不断交替出现——这是档位太近的典型标志。
- 与向上切换相关的卡顿:如果卡顿发生在切换到更高档位之后,说明播放列表中的BANDWIDTH声明太低了。
在WWDC 2025上,Apple扩展了AVMetrics,在变体切换事件中包含了媒体档位信息,使得更容易查看切换期间哪些音频/视频/字幕轨道处于活动状态。
如果你不在Apple平台上,可以通过hls.js的LEVEL_SWITCHED和FRAG_BUFFERED事件收集类似数据。相同的原则适用。
使用hls.js进行实验:调整ABR行为
hls.js演示页面(hlsjs.video-dev.org/demo/)是在Web上实验ABR切换行为的最佳免费工具。加载你的HLS流,使用"Real-time metrics"和"Buffer & Statistics"面板观察发生了什么。
需要实验的关键hls.js配置参数:
abrEwmaFastVoD和abrEwmaSlowVoD:这些控制带宽估计的EWMA(指数加权移动平均)。较低的值使播放器对带宽变化反应更快(更激进的切换)。较高的值使其更保守(切换更慢,更稳定)。如果你看到过多切换,试着增大这些值。abrBandWidthFactor(默认值:0.95)和abrBandWidthUpFactor(默认值:0.7):这些是安全裕度。播放器选择码率低于estimatedBandwidth × factor的档位。向上切换系数故意设得较低——播放器对向上切换比向下切换更保守。如果你的阶梯间距紧凑,可以考虑将abrBandWidthUpFactor降至0.6以减少振荡。abrMaxWithRealBitrate(默认值:false):启用后,ABR控制器使用获取的片段的实际测量码率,而不是播放列表中声明的BANDWIDTH。当你声明的码率不准确时特别有用。
但关键是:如果你需要大幅调整这些参数才能获得稳定播放,那你的阶梯可能有问题。这些参数是用于微调的。编码阶梯本身才是基础。一个间距合理的阶梯在任何播放器上使用默认ABR设置都能良好运行。
实用建议
如果要总结为一个检查清单:
- 为阶梯中的每个档位计算BPP。确保随着分辨率的增加BPP递减。删除或调整BPP与相邻档位在15%以内的任何档位。
- 保持相邻档位之间至少1.5倍的码率比率。尽可能使用2倍,尤其是在带宽波动影响最大的阶梯下半部分。
- 单独测试每个档位。强制单一变体播放,从头到尾观看有代表性的内容。如果单独无法流畅播放,在ABR中也不会流畅。
- 使用Network Link Conditioner模拟带宽下降。流媒体应该快速稳定在一个档位并保持不变。
- 检查你的片段时长。如果你使用2-4秒并看到振荡,试试6秒。较长的片段通过在决策之间给BWE更多稳定时间,自然减少切换频率。
- 检测你的播放器。在Apple上使用AVMetrics,在Web上使用hls.js事件。跟踪每个会话的切换频率。如果启动阶段之外的平均切换次数超过几次,请进行调查。
- 不要只信任声明的BANDWIDTH。在hls.js中使用
abrMaxWithRealBitrate,或用ffprobe验证你的VBR峰值没有超过声明值。 - 更少的档位通常更好。一个间距合理的5档阶梯胜过一半档位太近的10档阶梯。观众不需要12个画质级别。他们需要4到5个看起来有明显差异且能可靠播放的档位。
ABR的全部意义在于它应该是不可见的。观众不应该注意到它在工作。如果他们注意到了,说明出了问题——而且十有八九,问题出在阶梯上。
参考文献: