ExoPlayer 支持各种播放分析需求。最终,分析就是收集、解读、汇总和总结播放数据。这些数据既可以在设备上使用(例如,用于日志记录、调试或为未来的播放决策提供信息),也可以报告给服务器,以监控所有设备上的播放情况。
分析系统通常需要先收集事件,然后进一步处理这些事件,使其具有意义:
- 事件收集:这可以通过在
ExoPlayer实例上注册AnalyticsListener来完成。已注册的分析监听器会在播放器使用期间接收发生的事件。每个事件都与播放列表中的相应媒体文件以及播放位置和时间戳元数据相关联。 - 事件处理:某些分析系统会将原始事件上传到服务器,所有事件处理都在服务器端执行。您也可以在设备上处理事件,这样做可能更简单,或者可以减少需要上传的信息量。ExoPlayer 提供了
PlaybackStatsListener,可让您执行以下处理步骤:- 事件解读:为了便于分析,需要在单次播放的情境中解读事件。例如,播放器状态更改为
STATE_BUFFERING的原始事件可能对应于初始缓冲、重新缓冲或跳转后发生的缓冲。 - 状态跟踪:此步骤会将事件转换为计数器。例如,状态更改事件可以转换为跟踪每个播放状态所用时间的计数器。结果是单次播放的基本分析数据值。
- 汇总:此步骤会合并多次播放的分析数据,通常是通过将计数器相加。
- 计算摘要指标:许多最实用的指标都是计算平均值或以其他方式组合基本分析数据值的指标。可以针对单次或多次播放计算摘要指标。
- 事件解读:为了便于分析,需要在单次播放的情境中解读事件。例如,播放器状态更改为
使用 AnalyticsListener 收集事件
播放器中的原始播放事件会报告给 AnalyticsListener 实现。您可以添加自己的监听器,并仅替换您感兴趣的方法:
Kotlin
exoPlayer.addAnalyticsListener( object : AnalyticsListener { override fun onPlaybackStateChanged(eventTime: EventTime, @Player.State state: Int) {} override fun onDroppedVideoFrames( eventTime: EventTime, droppedFrames: Int, elapsedMs: Long, ) {} } )
Java
exoPlayer.addAnalyticsListener( new AnalyticsListener() { @Override public void onPlaybackStateChanged(EventTime eventTime, @Player.State int state) {} @Override public void onDroppedVideoFrames( EventTime eventTime, int droppedFrames, long elapsedMs) {} });
传递给每个回调的 EventTime 会将事件与播放列表中的媒体文件以及播放位置和时间戳元数据相关联:
realtimeMs:事件的挂钟时间。timeline、windowIndex和mediaPeriodId:定义事件所属的播放列表以及播放列表中的内容。mediaPeriodId包含可选的其他信息,例如指明事件是否属于商品内的广告。eventPlaybackPositionMs:事件发生时项中的播放位置。currentTimeline、currentWindowIndex、currentMediaPeriodId和currentPlaybackPositionMs:如前所述,但针对的是当前播放的内容。当前播放的媒体项可能与事件所属的媒体项不同,例如,如果事件对应于要播放的下一个媒体项的预缓冲。
使用 PlaybackStatsListener 进行事件处理
PlaybackStatsListener 是一个实现设备端事件处理的 AnalyticsListener。它会计算 PlaybackStats,并提供计数器和派生指标,包括:
- 摘要指标,例如总播放时间。
- 自适应播放质量指标,例如平均视频分辨率。
- 渲染质量指标,例如丢帧率。
- 资源用量指标,例如通过网络读取的字节数。
您可以在 PlaybackStats Javadoc 中找到可用计数和派生指标的完整列表。
PlaybackStatsListener 会为播放列表中的每个媒体文件以及在这些内容中插入的每个客户端广告分别计算 PlaybackStats。您可以向 PlaybackStatsListener 提供回调,以便在播放完成时收到通知,并使用传递给回调的 EventTime 来识别哪个播放已完成。您可以汇总多次播放的分析数据。您还可以随时使用 PlaybackStatsListener.getPlaybackStats() 查询当前播放会话的 PlaybackStats。
Kotlin
exoPlayer.addAnalyticsListener( PlaybackStatsListener(/* keepHistory= */ true) { eventTime: EventTime?, playbackStats: PlaybackStats? -> // Analytics data for the session started at `eventTime` is ready. } )
Java
exoPlayer.addAnalyticsListener( new PlaybackStatsListener( /* keepHistory= */ true, (eventTime, playbackStats) -> { // Analytics data for the session started at `eventTime` is ready. }));
PlaybackStatsListener 的构造函数提供了保留已处理事件的完整历史记录的选项。请注意,这可能会产生未知的内存开销,具体取决于播放时长和事件数量。因此,只有在您需要访问已处理事件的完整历史记录(而不仅仅是最终分析数据)时,才应启用此功能。
请注意,PlaybackStats 使用一组扩展的状态,不仅可以指示媒体的状态,还可以指示用户的播放意图以及更详细的信息,例如播放中断或结束的原因:
| 播放状态 | 用户播放意图 | 无意播放 |
|---|---|---|
| 播放前 | JOINING_FOREGROUND |
NOT_STARTED、JOINING_BACKGROUND |
| 正在播放 | PLAYING |
|
| 播放中断 | BUFFERING、SEEKING |
PAUSED、PAUSED_BUFFERING、SUPPRESSED、SUPPRESSED_BUFFERING、INTERRUPTED_BY_AD |
| 结束状态 | ENDED、STOPPED、FAILED、ABANDONED |
用户的播放意图对于区分用户主动等待播放继续的时间与被动等待时间非常重要。例如,PlaybackStats.getTotalWaitTimeMs 会返回在 JOINING_FOREGROUND、BUFFERING 和 SEEKING 状态下花费的总时间,但不包括播放暂停的时间。同样,PlaybackStats.getTotalPlayAndWaitTimeMs 将返回用户意图播放的总时间,即总活跃等待时间和在 PLAYING 状态下花费的总时间。
已处理和已解读的事件
您可以使用 PlaybackStatsListener 和 keepHistory=true 记录已处理和已解读的事件。生成的 PlaybackStats 将包含以下事件列表:
playbackStateHistory:扩展播放状态的有序列表,其中包含开始应用这些状态的EventTime。您还可以使用PlaybackStats.getPlaybackStateAtTime查找给定挂钟时间的状态。mediaTimeHistory:挂钟时间和媒体时间对的历史记录,可让您重构媒体的哪些部分在什么时间播放。您还可以使用PlaybackStats.getMediaTimeMsAtRealtimeMs查找给定挂钟时间的播放位置。videoFormatHistory和audioFormatHistory:播放期间使用的视频和音频格式的有序列表,以及开始使用这些格式的EventTime。fatalErrorHistory和nonFatalErrorHistory:严重错误和非严重错误的有序列表,以及发生这些错误的EventTime。严重错误是指导致播放结束的错误,而非严重错误可能是可恢复的。
单次播放分析数据
如果您使用 PlaybackStatsListener,即使使用 keepHistory=false,系统也会自动收集这些数据。最终值是您可以在 PlaybackStats Javadoc 中找到的公共字段,以及 getPlaybackStateDurationMs 返回的播放状态时长。为方便起见,您还可以找到 getTotalPlayTimeMs 和 getTotalWaitTimeMs 等方法,这些方法会返回特定播放状态组合的时长。
Kotlin
Log.d( "DEBUG", "Playback summary: " + "play time = " + playbackStats.totalPlayTimeMs + ", rebuffers = " + playbackStats.totalRebufferCount, )
Java
Log.d( "DEBUG", "Playback summary: " + "play time = " + playbackStats.getTotalPlayTimeMs() + ", rebuffers = " + playbackStats.totalRebufferCount);
汇总多次播放的分析数据
您可以通过调用 PlaybackStats.merge 将多个 PlaybackStats 组合在一起。生成的 PlaybackStats 将包含所有合并播放的汇总数据。请注意,它不会包含单个播放事件的历史记录,因为这些事件无法汇总。
PlaybackStatsListener.getCombinedPlaybackStats 可用于获取在 PlaybackStatsListener 的生命周期内收集的所有分析数据的汇总视图。
计算摘要指标
除了基本分析数据之外,PlaybackStats 还提供了许多方法来计算摘要指标。
Kotlin
Log.d( "DEBUG", "Additional calculated summary metrics: " + "average video bitrate = " + playbackStats.meanVideoFormatBitrate + ", mean time between rebuffers = " + playbackStats.meanTimeBetweenRebuffers, )
Java
Log.d( "DEBUG", "Additional calculated summary metrics: " + "average video bitrate = " + playbackStats.getMeanVideoFormatBitrate() + ", mean time between rebuffers = " + playbackStats.getMeanTimeBetweenRebuffers());
高级主题
将分析数据与播放元数据相关联
在收集单个播放的分析数据时,您可能需要将播放分析数据与所播放媒体的元数据相关联。
建议使用 MediaItem.Builder.setTag 设置媒体专用元数据。
媒体标记是针对原始事件和 PlaybackStats 完成时报告的 EventTime 的一部分,因此可以在处理相应的分析数据时检索:
Kotlin
PlaybackStatsListener(/* keepHistory= */ false) { eventTime: EventTime, playbackStats: PlaybackStats -> val mediaTag = eventTime.timeline .getWindow(eventTime.windowIndex, Timeline.Window()) .mediaItem .localConfiguration ?.tag // Report playbackStats with mediaTag metadata. }
Java
new PlaybackStatsListener( /* keepHistory= */ false, (eventTime, playbackStats) -> { Object mediaTag = eventTime.timeline.getWindow(eventTime.windowIndex, new Timeline.Window()) .mediaItem .localConfiguration .tag; // Report playbackStats with mediaTag metadata. });
报告自定义分析事件
如果您需要向分析数据添加自定义事件,则需要将这些事件保存在您自己的数据结构中,并在稍后将其与报告的 PlaybackStats 结合使用。如果需要,您可以扩展 DefaultAnalyticsCollector,以便能够为自定义事件生成 EventTime 实例,并将其发送到已注册的监听器,如以下示例所示。
Kotlin
@OptIn(UnstableApi::class) private interface ExtendedListener : AnalyticsListener { fun onCustomEvent(eventTime: EventTime) } @OptIn(UnstableApi::class) private class ExtendedCollector : DefaultAnalyticsCollector(Clock.DEFAULT) { fun customEvent() { val eventTime = super.generateCurrentPlayerMediaPeriodEventTime() super.sendEvent(eventTime, CUSTOM_EVENT_ID) { listener: AnalyticsListener -> if (listener is ExtendedListener) { listener.onCustomEvent(eventTime) } } } } @OptIn(UnstableApi::class) fun useExtendedAnalyticsCollector(context: Context) { // Usage - Setup and listener registration. val player = ExoPlayer.Builder(context).setAnalyticsCollector(ExtendedCollector()).build() player.addAnalyticsListener( object : ExtendedListener { override fun onCustomEvent(eventTime: EventTime) { // Save custom event for analytics data. } } ) // Usage - Triggering the custom event. (player.analyticsCollector as ExtendedCollector).customEvent() }
Java
@OptIn(markerClass = UnstableApi.class) private interface ExtendedListener extends AnalyticsListener { void onCustomEvent(EventTime eventTime); } @OptIn(markerClass = UnstableApi.class) private static class ExtendedCollector extends DefaultAnalyticsCollector { public ExtendedCollector() { super(Clock.DEFAULT); } public void customEvent() { AnalyticsListener.EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); sendEvent( eventTime, CUSTOM_EVENT_ID, listener -> { if (listener instanceof ExtendedListener) { ((ExtendedListener) listener).onCustomEvent(eventTime); } }); } } @OptIn(markerClass = UnstableApi.class) public static void useExtendedAnalyticsCollector(Context context) { // Usage - Setup and listener registration. ExoPlayer player = new ExoPlayer.Builder(context).setAnalyticsCollector(new ExtendedCollector()).build(); player.addAnalyticsListener( (ExtendedListener) eventTime -> { // Save custom event for analytics data. }); // Usage - Triggering the custom event. ((ExtendedCollector) player.getAnalyticsCollector()).customEvent(); }