Discover media performance metrics in AVFoundation
Audio & Video 进阶 20m

探索 AVFoundation 中的媒体性能指标

Discover media performance metrics in AVFoundation

2024年6月10日

在 Apple 官方观看视频

一句话判断

iOS 18 的 AVMetrics API 让你能以事件流的方式精确追踪 HLS 播放的每一个环节——从 playlist 请求到 content key 获取到 stall 发生——终于可以在客户端层面系统性地诊断播放质量问题了。

这场 Session 讲了什么

媒体播放最让人头疼的两个问题:启动太慢、播放中途卡顿。在 iOS 18 之前,你能拿到的客户端指标很有限——access log、error log 和 AVPlayer 的各种通知。这些数据不够细,很难定位问题的根因。

iOS 18 引入了 AVMetrics,一个基于事件流的性能指标系统。AVPlayerItem 现在可以作为事件发布者,把播放过程中发生的每一件有意义的事情都变成一个事件:multi-variant playlist 请求、media playlist 请求、segment 下载、content key 获取、stall 发生、variant 切换、播放结束时的 summary。每个事件都带有时间戳和详细上下文。

Session 用两个真实场景演示了 AVMetrics 的价值。第一个场景:播放启动耗时 2 秒,通过回溯事件链发现大部分时间花在了 content key 请求上——解决方案是预加载 key 或优化 key server。第二个场景:播放到第 7 分钟时发生 stall,通过查看 segment 事件发现服务器返回了 HTTP 404,导致 variant 从 20Mbps 降到 15Mbps,新 variant 又是 404,buffer 耗尽后卡顿。这种问题在以前几乎不可能从客户端数据中定位到。

值得深挖的点

事件链分析:从症状到根因

AVMetrics 最强大的能力不是单个事件的数值,而是事件之间的因果关系链。以”likely to keep up”事件为例,它告诉你播放器何时达到可以开始播放的状态,并给出了 startup time。但更重要的是,它关联了前面的所有事件——哪个 playlist 请求、哪个 segment 下载、哪个 content key 获取影响了启动时间。

这意味着你不需要猜测。以前发现”播放启动慢”,你可能在服务端查 CDN 日志、查 manifest 配置、查 key server 响应时间,却不知道客户端实际经历了什么。现在客户端可以直接告诉你:启动花了 2 秒,其中 1.5 秒在等 content key。定位问题从”到处查”变成了”看一条链路”。

对于规模化监控,AVPlayerItem 在播放结束时还会生成一个 summary 事件,包含 stall count、switch count 等 KPI。你可以把这些 KPI 上报到后端分析系统,用聚合数据监控整体播放健康度,发现异常后再下钻到具体的事件链。

publisher/subscriber 模型的设计取舍

AVMetrics 选择了 async sequence + publisher/subscriber 模型,而不是传统的 callback 或 notification。这个选择很合理:事件是时序性的,async sequence 天然保持顺序;你可以只订阅感兴趣的事件类型,不需要接收所有事件再过滤;Swift 的 chronologicalMerge 可以把多个事件流按时间顺序合并成一个流。

对于 Objective-C 项目,API 形态不同但功能等价:创建 AVMetricEventStream 实例,设置 subscriber,订阅感兴趣的事件类型,添加 publisher。虽然不如 Swift 版本优雅,但核心能力没有阉割。

代码片段

订阅 “likely to keep up” 和 summary 事件

Swift 版本的核心用法。

import AVFoundation

let playerItem: AVPlayerItem = // 你的 playerItem

// 获取两种事件流
let ltkuMetrics = playerItem.metrics(forType: AVMetricPlayerItemLikelyToKeepUpEvent.self)
let summaryMetrics = playerItem.metrics(forType: AVMetricPlayerItemPlaybackSummaryEvent.self)

// 按时间顺序合并两个流
for await (metricEvent, publisher) in ltkuMetrics.chronologicalMerge(with: summaryMetrics) {
    switch metricEvent {
    case let ltku as AVMetricPlayerItemLikelyToKeepUpEvent:
        print("启动耗时: \(ltku.startupTime)秒")
        // 上报到分析后端
        reportToBackend(event: ltku)
    case let summary as AVMetricPlayerItemPlaybackSummaryEvent:
        print("播放结束 - 卡顿次数: \(summary.stallCount), 切换次数: \(summary.switchCount)")
        reportToBackend(event: summary)
    default:
        break
    }
}

坑:chronologicalMerge 返回的元组中 publisher 参数可以用来区分事件来自哪个流,当你的处理逻辑需要知道事件类型时很有用。

订阅 media segment 事件诊断网络问题

// 监控 segment 下载情况,捕获网络异常
let segmentMetrics = playerItem.metrics(forType: AVMetricMediaSegmentRequestEvent.self)

for await (metricEvent, _) in segmentMetrics {
    if let httpResponse = metricEvent.response as? HTTPURLResponse,
       httpResponse.statusCode != 200 {
        // segment 下载失败,记录详细信息
        print("Segment 请求失败: HTTP \(httpResponse.statusCode)")
        print("URL: \(metricEvent.request.url?.absoluteString ?? "unknown")")
        print("响应头: \(httpResponse.allHeaderFields)")
        // 可以拿到 NSURLSessionTaskMetrics 做更深入的网络分析
        if let taskMetrics = metricEvent.taskMetrics {
            print("DNS 解析: \(taskMetrics.domainLookupDuration)")
            print("TCP 连接: \(taskMetrics.connectDuration)")
            print("TLS 握手: \(taskMetrics.secureConnectionDuration)")
        }
    }
}

Objective-C 版本的事件订阅

AVPlayerItem *item = /* 你的 playerItem */;

// 创建事件流
AVMetricEventStream *eventStream = [AVMetricEventStream eventStream];

// 创建并设置 subscriber
id<AVMetricEventStreamSubscriber> subscriber = [[MyMetricSubscriber alloc] init];
[eventStream setSubscriber:subscriber queue:mySerialQueue];

// 订阅感兴趣的事件类型
[eventStream subscribeToMetricEvent:[AVMetricPlayerItemLikelyToKeepUpEvent class]];
[eventStream subscribeToMetricEvent:[AVMetricPlayerItemPlaybackSummaryEvent class]];

// 添加 publisher
[eventStream addPublisher:item];

坑:Objective-C 的 subscriber 需要自己处理序列化,建议在 subscriber 回调中直接把事件数据转成 JSON 发送到后端,不要在客户端做复杂分析。

最佳实践

新项目: 从第一天就接入 AVMetrics。播放质量问题越早监控越好,等到用户投诉再查就晚了。最低限度:订阅 summary 事件,把 stall count 和 switch count 上报到你的分析系统。这只需要十几行代码。

已有项目: 如果你正在用 access log 和 error log 做播放质量监控,AVMetrics 是它们的升级替代品,而不是补充。新 API 提供的信息粒度远超旧方案。建议先在一个小比例用户上灰度 AVMetrics,对比和旧方案的数据一致性,确认没问题后全量切换。

与 CDN 厂商协作: media segment 事件中的 NSURLSessionTaskMetrics 包含完整的网络事务详情(DNS、TCP、TLS、TTFB 等)。当你发现某个 region 的用户频繁遇到 segment 下载失败时,把这些数据(特别是 session identifier 和 response headers)发给 CDN 厂商,他们能快速定位边缘节点的问题。

还有什么值得关注

  • 除了 Session 提到的事件,AVPlayerItem 还会生成 rate change 事件(播放速率变化)、seek 事件(用户拖动进度条)和 error 事件——完整的播放行为都可以被追踪
  • 所有事件都带有时间戳,你可以把客户端事件和服务端 CDN 日志按时间对齐,做端到端的性能分析
  • summary 事件中的 KPI 适合做聚合监控:设定 stall count 的阈值,超过就触发告警
WWDC 2024