用 HLS Interstitials 和 Integrated Timeline 构建更好的广告体验
Enhance ad experiences with HLS interstitials
2024年6月10日
一句话判断
如果你在用 HLS 做视频播放并且有广告插入需求,AVFoundation 新出的 Integrated Timeline API 让你不用再手动计算广告和正片的交替时间,直接用 snapshot 对象就能画出完整的播放进度条。
这场 Session 讲了什么
HLS Interstitials 是 Apple 提出的在主内容时间线上插入独立广告片段的方案。相比直接把广告拼接进主内容流,Interstitials 支持延迟绑定广告决策、广告内容无需与正片一起处理,灵活性更高。
今年的重点是 Integrated Timeline API。之前的难点在于:当时间线上既有正片又有广告片段时,进度条怎么画?快进/快退怎么处理?拖动到广告还没加载的位置怎么办?新 API 通过 AVPlayerItemIntegratedTimeline 和 AVPlayerItemSegment 解决了这些问题。每个时间段(正片或广告)都是一个 Segment,带有起止时间、类型、occupancy 等属性。Snapshot 机制保证你在读取时间线状态时拿到的是一致的数据。
Interstitials 在时间线上的展示方式有三种:作为单个点标记(Point),作为带时长范围的标记(Fill),以及与正片完全融合不可区分(Supplements Primary Content)。这三种模式分别适用于不同场景。
值得深挖的点
三种 Timeline Occupancy 模式的选择逻辑
singlePoint 模式下,广告不会占用进度条时长。播放到该点时进度条停住,广告播完后继续前进。这是 VOD 内容插入广告最常见的方式——用户看到的总时长就是正片时长,广告只是”暂停”。
fill 模式下,广告的时长被算进总进度条时长。适用于直播流中用广告替换原有内容,或者需要展示完整时间线(包括广告时段)的场景。
supplementsPrimaryContent 是 fill 的变体,广告虽然在时间线上占范围,但在 UI 上与正片不可区分。适合放节目回顾、片头片尾、过场等内容——这些内容与正片密切相关,不应该被用户感知为”广告”。
选择错误会导致进度条行为异常:该停的不停,该走的不走,或者用户无法区分广告和正片。
Snapshot 机制解决的时间线一致性问题
时间线在播放过程中是动态变化的——广告可能还没加载,播放位置可能跨越多个段。如果直接读取各 Segment 的属性来绘制 UI,可能在读取过程中状态就变了。Snapshot 是某个时间点的完整时间线快照,所有属性保证一致性。这比用 KVO 监听一堆属性然后自己组装要可靠得多。
代码片段
获取 Integrated Timeline 并绘制进度条
import AVFoundation
// 创建主内容 AVPlayerItem
let playerItem = AVPlayerItem(url: contentURL)
// 获取 Integrated Timeline
let integratedTimeline = playerItem.integratedTimeline
// 获取当前快照(一致的状态)
let snapshot = integratedTimeline.snapshot()
// 绘制进度条
let startTime: Double = 0
let totalDuration = snapshot.duration.seconds
let currentPosition = snapshot.currentTime.seconds
// slider 设置
progressSlider.minimumValue = 0
progressSlider.maximumValue = Float(totalDuration)
progressSlider.value = Float(currentPosition)
场景:视频播放器需要绘制一个包含广告标记的进度条。通过 snapshot 一次性拿到所有需要的数据。
坑点:snapshot() 返回的是调用时刻的快照,不会自动更新。播放过程中需要定期获取新快照或用通知机制刷新。
标记广告点在时间线上
// 遍历 snapshot 中的 segments,找到 singlePoint 类型的广告
let pointEvents = snapshot.segments.filter { segment in
segment.type == .interstitial &&
segment.timelineOccupancy == .singlePoint
}
// 在进度条上标记广告位置
for event in pointEvents {
let position = event.startTimeInSeconds
// 在 slider 上 position 处添加一个小标记点
addMarker(at: Float(position))
}
场景:在进度条上用黄色小标记显示广告出现的位置,让用户提前知道哪里有广告。
创建 Interstitial Event 并设置 Occupancy
// 创建一个点式广告事件
let pointEvent = AVPlayerInterstitialEvent(
primaryItem: playerItem,
identifier: "ad-break-1",
time: CMTime(seconds: 300, preferredTimescale: 600), // 5 分钟处
templateItems: [adPlayerItem]
)
// 广告不占用进度条时长
pointEvent.timelineOccupancy = .singlePoint
// 创建一个范围式广告事件(直播流替换)
let fillEvent = AVPlayerInterstitialEvent(
primaryItem: playerItem,
identifier: "live-ad-1",
time: CMTime(seconds: 1200, preferredTimescale: 600),
templateItems: [adPlayerItem]
)
fillEvent.timelineOccupancy = .fill
fillEvent.plannedDuration = CMTime(seconds: 30, preferredTimescale: 600) // 预估 30 秒
场景:VOD 内容中 5 分钟处插入一个广告点,直播流中 20 分钟处用 30 秒广告替换原内容。
坑点:Fill 类型的 interstitial 在广告还没加载时不知道实际时长,必须设置 plannedDuration 作为占位值,否则时间线计算会出错。
最佳实践
- 迁移到 Integrated Timeline API 时,先确保你的 HLS Interstitials 配置正确,再接入新 API 做 UI
- Fill 类型 Interstitial 务必设置
plannedDuration,即使实际时长可能不同 - 用 Snapshot 机制绘制 UI,不要直接读取 timeline 的属性拼凑状态
supplementsPrimaryContent适合片头回顾、过场等内容,不适合真正的广告
还有什么值得关注
- Interstitials 的 SharePlay 支持也在今年有了更新,多人同步观看时广告体验更一致
- 建议结合 “Explore dynamic pre-rolls and mid-rolls in HLS” Session 了解 HLS Interstitials 的完整语法
- 新的 HLS 语法可以直接在 m3u8 中描述 Interstitial 的 timeline occupancy 行为