Enhance ad experiences with HLS interstitials
System Frameworks 进阶 20m

用 HLS Interstitials 和 Integrated Timeline 构建更好的广告体验

Enhance ad experiences with HLS interstitials

2024年6月10日

在 Apple 官方观看视频

一句话判断

如果你在用 HLS 做视频播放并且有广告插入需求,AVFoundation 新出的 Integrated Timeline API 让你不用再手动计算广告和正片的交替时间,直接用 snapshot 对象就能画出完整的播放进度条。

这场 Session 讲了什么

HLS Interstitials 是 Apple 提出的在主内容时间线上插入独立广告片段的方案。相比直接把广告拼接进主内容流,Interstitials 支持延迟绑定广告决策、广告内容无需与正片一起处理,灵活性更高。

今年的重点是 Integrated Timeline API。之前的难点在于:当时间线上既有正片又有广告片段时,进度条怎么画?快进/快退怎么处理?拖动到广告还没加载的位置怎么办?新 API 通过 AVPlayerItemIntegratedTimelineAVPlayerItemSegment 解决了这些问题。每个时间段(正片或广告)都是一个 Segment,带有起止时间、类型、occupancy 等属性。Snapshot 机制保证你在读取时间线状态时拿到的是一致的数据。

Interstitials 在时间线上的展示方式有三种:作为单个点标记(Point),作为带时长范围的标记(Fill),以及与正片完全融合不可区分(Supplements Primary Content)。这三种模式分别适用于不同场景。

值得深挖的点

三种 Timeline Occupancy 模式的选择逻辑

singlePoint 模式下,广告不会占用进度条时长。播放到该点时进度条停住,广告播完后继续前进。这是 VOD 内容插入广告最常见的方式——用户看到的总时长就是正片时长,广告只是”暂停”。

fill 模式下,广告的时长被算进总进度条时长。适用于直播流中用广告替换原有内容,或者需要展示完整时间线(包括广告时段)的场景。

supplementsPrimaryContentfill 的变体,广告虽然在时间线上占范围,但在 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 行为
WWDC 2024