ExoPlayer 支持各种播放分析需求。归根结底,分析就是收集、解读、汇总和总结播放数据。这些数据可在设备上使用(例如用于日志记录、调试或为日后做出播放决策提供依据),也可以报告给服务器以监控所有设备上的播放情况。
分析系统通常需要先收集事件,然后再进一步处理这些事件,使其具有意义:
- 事件收集:这可以通过在
ExoPlayer
实例上注册AnalyticsListener
来实现。注册的分析监听器会在使用播放器期间发生事件时接收事件。每个事件都与播放列表中的相应媒体内容以及播放位置和时间戳元数据相关联。 - 事件处理:某些分析系统会将原始事件上传到服务器,并在服务器端执行所有事件处理。您也可以在设备上处理事件,这样可能会更简单或减少需要上传的信息量。ExoPlayer 提供了
PlaybackStatsListener
,可让您执行以下处理步骤:- 事件解释:为了便于分析,事件需要在单次播放的上下文中加以解释。例如,播放器状态更改为
STATE_BUFFERING
的原始事件可能与初始缓冲、重新缓冲或在跳转后发生的缓冲相对应。 - 状态跟踪:此步骤会将事件转换为计数器。例如,状态更改事件可转换为计数器,用于跟踪在每个播放状态下所花费的时间。结果是一组有关单次播放的基本分析数据值。
- 汇总:此步骤会合并来自多个播放的分析数据,通常是通过对计数器进行求和来实现。
- 汇总指标的计算:许多最实用的指标都是通过计算平均值或以其他方式组合基本分析数据值得出的。系统可以针对单次或多次播放计算摘要指标。
- 事件解释:为了便于分析,事件需要在单次播放的上下文中加以解释。例如,播放器状态更改为
使用 AnalyticsListener 收集事件
来自播放器的原始播放事件会报告给 AnalyticsListener
实现。您可以轻松添加自己的监听器,并仅替换您感兴趣的方法:
exoPlayer.addAnalyticsListener(
object : AnalyticsListener {
override fun onPlaybackStateChanged(
eventTime: EventTime, @Player.State state: Int
) {}
override fun onDroppedVideoFrames(
eventTime: EventTime,
droppedFrames: Int,
elapsedMs: Long,
) {}
}
)
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
。
exoPlayer.addAnalyticsListener(
PlaybackStatsListener(/* keepHistory= */ true) {
eventTime: EventTime?,
playbackStats: PlaybackStats?,
-> // Analytics data for the session started at `eventTime` is ready.
}
)
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
等方法,它们会返回特定播放状态组合的时长。
Log.d(
"DEBUG",
"Playback summary: " +
"play time = " +
playbackStats.totalPlayTimeMs +
", rebuffers = " +
playbackStats.totalRebufferCount
)
Log.d(
"DEBUG",
"Playback summary: "
+ "play time = "
+ playbackStats.getTotalPlayTimeMs()
+ ", rebuffers = "
+ playbackStats.totalRebufferCount);
汇总多个播放的分析数据
您可以通过调用 PlaybackStats.merge
将多个 PlaybackStats
组合在一起。生成的 PlaybackStats
将包含所有合并播放的汇总数据。请注意,该报告不会包含各个播放事件的历史记录,因为这些事件无法汇总。
PlaybackStatsListener.getCombinedPlaybackStats
可用于获取 PlaybackStatsListener
生命周期内收集的所有分析数据的汇总视图。
计算摘要指标
除了基本分析数据之外,PlaybackStats
还提供了许多用于计算汇总指标的方法。
Log.d(
"DEBUG",
"Additional calculated summary metrics: " +
"average video bitrate = " +
playbackStats.meanVideoFormatBitrate +
", mean time between rebuffers = " +
playbackStats.meanTimeBetweenRebuffers
)
Log.d(
"DEBUG",
"Additional calculated summary metrics: "
+ "average video bitrate = "
+ playbackStats.getMeanVideoFormatBitrate()
+ ", mean time between rebuffers = "
+ playbackStats.getMeanTimeBetweenRebuffers());
高级主题
将分析数据与播放元数据相关联
收集各次播放的分析数据时,您可能需要将播放分析数据与所播放媒体的元数据相关联。
建议使用 MediaItem.Builder.setTag
设置媒体专用元数据。媒体代码是报告的原始事件和完成 PlaybackStats
时报告的 EventTime
的一部分,因此在处理相应的分析数据时可以轻松检索:
PlaybackStatsListener(/* keepHistory= */ false) {
eventTime: EventTime,
playbackStats: PlaybackStats ->
val mediaTag =
eventTime.timeline
.getWindow(eventTime.windowIndex, Timeline.Window())
.mediaItem
.localConfiguration
?.tag
// Report playbackStats with mediaTag metadata.
}
new PlaybackStatsListener(
/* keepHistory= */ false,
(eventTime, playbackStats) -> {
Object mediaTag =
eventTime.timeline.getWindow(eventTime.windowIndex, new Timeline.Window())
.mediaItem
.localConfiguration
.tag;
// Report playbackStats with mediaTag metadata.
});
报告自定义 Google Analytics 事件
如果您需要向分析数据添加自定义事件,则需要在您自己的数据结构中保存这些事件,并稍后将其与报告的 PlaybackStats
合并。如果有帮助,您可以扩展 DefaultAnalyticsCollector
,以便为自定义事件生成 EventTime
实例,并将其发送到已注册的监听器,如以下示例所示。
private interface ExtendedListener : AnalyticsListener {
fun onCustomEvent(eventTime: EventTime)
}
private class ExtendedCollector : DefaultAnalyticsCollector(Clock.DEFAULT) {
fun customEvent() {
val eventTime = generateCurrentPlayerMediaPeriodEventTime()
sendEvent(eventTime, CUSTOM_EVENT_ID) { listener: AnalyticsListener ->
if (listener is ExtendedListener) {
listener.onCustomEvent(eventTime)
}
}
}
}
// 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()
private interface ExtendedListener extends AnalyticsListener {
void onCustomEvent(EventTime eventTime);
}
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);
}
});
}
}
// 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();