Analytics

ExoPlayer 可满足各种播放分析需求。最终,分析的目的是收集、解读、汇总和总结播放数据。这些数据可在设备上使用(例如用于记录、调试或为未来的播放决策提供信息),也可报告给服务器以监控所有设备上的播放情况。

分析系统通常需要先收集事件,然后对其进行进一步处理,使其具有意义:

  • 事件收集:可以通过在 ExoPlayer 实例上注册 AnalyticsListener 来实现。注册的 Analytics 监听器会在播放器使用期间接收事件。每个事件都与播放列表中的相应媒体项以及播放位置和时间戳元数据相关联。
  • 事件处理:某些分析系统会将原始事件上传到服务器,所有事件处理都在服务器端进行。还可以在设备上处理事件,这样做可能更简单,或者可以减少需要上传的信息量。ExoPlayer 提供了 PlaybackStatsListener,可让您执行以下处理步骤:
    1. 事件解读:为了便于进行分析,需要根据单次播放的情境来解读事件。例如,播放器状态更改为 STATE_BUFFERING 的原始事件可能对应于初始缓冲、重新缓冲或在搜索后发生的缓冲。
    2. 状态跟踪:此步骤将事件转换为计数器。例如,状态更改事件可以转换为用于跟踪每种播放状态所用时间的计数器。结果是单次播放的基本分析数据值。
    3. 汇总:此步骤会汇总多次播放的分析数据,通常是通过将计数器相加来实现。
    4. 汇总指标的计算:许多最有用的指标都是通过计算平均值或以其他方式组合基本分析数据值来获得的。可以针对单次或多次播放计算汇总指标。

使用 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:事件的挂钟时间。
  • timelinewindowIndexmediaPeriodId:用于定义事件所属的播放列表以及播放列表中的相应项。mediaPeriodId 包含可选的其他信息,例如指示相应事件是否属于商品内的广告。
  • eventPlaybackPositionMs:发生相应事件时,项的播放位置。
  • currentTimelinecurrentWindowIndexcurrentMediaPeriodIdcurrentPlaybackPositionMs:与上述相同,但适用于当前播放的内容。当前播放的媒体项可能与事件所属的媒体项不同,例如,如果事件对应于要播放的下一个媒体项的预缓冲。

使用 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_STARTEDJOINING_BACKGROUND
有效播放 PLAYING
播放中断 BUFFERINGSEEKING PAUSEDPAUSED_BUFFERINGSUPPRESSEDSUPPRESSED_BUFFERINGINTERRUPTED_BY_AD
结束状态 ENDEDSTOPPEDFAILEDABANDONED

用户播放意图对于区分用户主动等待播放继续的时间和被动等待时间非常重要。例如,PlaybackStats.getTotalWaitTimeMs 会返回在 JOINING_FOREGROUNDBUFFERINGSEEKING 状态下花费的总时间,但不会返回播放暂停时的时间。同样,PlaybackStats.getTotalPlayAndWaitTimeMs 将返回用户有意播放的总时间,即总主动等待时间和在 PLAYING 状态下花费的总时间。

已处理和解读的事件

您可以使用 PlaybackStatsListenerkeepHistory=true 记录已处理和已解读的事件。生成的 PlaybackStats 将包含以下事件列表:

  • playbackStateHistory:一个有序的扩展播放状态列表,其中包含开始应用这些状态的 EventTime。您还可以使用 PlaybackStats.getPlaybackStateAtTime 在给定的挂钟时间查找状态。
  • mediaTimeHistory:包含挂钟时间和媒体时间对的历史记录,可用于重建媒体的哪些部分是在何时播放的。您还可以使用 PlaybackStats.getMediaTimeMsAtRealtimeMs 查找指定挂钟时间的播放位置。
  • videoFormatHistoryaudioFormatHistory:播放期间使用的视频和音频格式的有序列表,以及开始使用这些格式的 EventTime
  • fatalErrorHistorynonFatalErrorHistory:按时间顺序排列的严重错误和非严重错误列表,其中包含发生这些错误的时间点 EventTime。严重错误是指导致播放结束的错误,而非严重错误可能可以恢复。

单次播放分析数据

如果您使用 PlaybackStatsListener,即使使用 keepHistory=false,系统也会自动收集这些数据。最终值是您可以在 PlaybackStats Javadoc 中找到的公共字段,以及由 getPlaybackStateDurationMs 返回的播放状态时长。为方便起见,您还可以找到 getTotalPlayTimeMsgetTotalWaitTimeMs 等方法,这些方法会返回特定播放状态组合的持续时间。

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 设置特定于媒体的元数据。媒体标记是针对原始事件报告的 EventTime 的一部分,并在 PlaybackStats 完成时报告,因此在处理相应的分析数据时可以轻松检索:

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();
}