分析

ExoPlayer 支持广泛的播放分析需求。最终,分析就是收集、解释、聚合和总结播放数据。这些数据可以在设备上使用——例如用于日志记录、调试或为将来的播放决策提供信息——或者报告到服务器以监控所有设备上的播放情况。

分析系统通常需要首先收集事件,然后进一步处理以使其有意义。

  • 事件收集:这可以通过在 ExoPlayer 实例上注册 AnalyticsListener 来完成。注册的分析侦听器在播放器使用过程中收到事件时会接收事件。每个事件都与播放列表中的相应媒体项目以及播放位置和时间戳元数据相关联。
  • 事件处理:一些分析系统将原始事件上传到服务器,所有事件处理都在服务器端执行。也可以在设备上处理事件,这样做可能更简单或减少需要上传的信息量。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

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

Java

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