播放器事件

播放器状态的变化(例如开始播放、缓冲或错误)会触发事件,这些事件会发送到已注册的 Player.Listener 实例。这些事件由整数常量表示,并由 Player.Event 和 Player.Events 定义。

注册 Player.Listener

播放器事件会报告给已注册的 Player.Listener 实例。如需注册监听器以接收此类事件,请执行以下操作:

Kotlin

// Add a listener to receive events from the player.
player.addListener(listener)

Java

// Add a listener to receive events from the player.
player.addListener(listener);

如果您使用的是 Kotlin,还可以使用 media3-common-ktx 模块提供的挂起扩展函数,通过协程监听事件。在这种情况下,您无需明确注册或取消注册 Player.Listener。

使用 Player.Listener 监听播放事件

Player.Listener 具有空的默认方法,因此您只需实现您感兴趣的方法。如需查看这些方法及其调用时间的完整说明,请参阅 Javadoc。下文将更详细地介绍一些最重要的方法。

监听器可以选择实现单个事件回调,也可以选择实现一个或多个事件一起发生后调用的通用 onEvents 回调。如需了解在不同使用场景下应首选哪种方法,请参阅 Individual callbacks vs onEvents。

播放状态更改

您可以通过在已注册的 Player.Listener 中实现 onPlaybackStateChanged(@State int state) 来接收玩家状态的变化。播放器可能会处于以下 4 种播放状态之一:

  • Player.STATE_IDLE:这是初始状态,即播放器停止时以及播放失败时的状态。在此状态下,播放器只会保留有限的资源。
  • Player.STATE_BUFFERING:播放器无法立即从当前位置开始播放。这主要是因为需要加载更多数据。
  • Player.STATE_READY:播放器可以立即从当前位置开始播放。
  • Player.STATE_ENDED:播放器已播放完所有媒体。

除了这些状态之外,播放器还有一个 playWhenReady 标志,用于指示用户的播放意图。您可以通过实现 onPlayWhenReadyChanged(playWhenReady, @PlayWhenReadyChangeReason int reason) 来接收此标志中的更改。

当满足以下所有三个条件时,播放器正在播放(即其位置正在前进,并且正在向用户呈现媒体):

  • 播放器处于 Player.STATE_READY 状态
  • playWhenReady 为 true
  • 播放未因 Player.getPlaybackSuppressionReason 返回的原因而停止

您可以调用 Player.isPlaying,而无需单独检查这些属性。您可以通过实现 onIsPlayingChanged(boolean isPlaying) 来接收对此状态的更改:

Kotlin

player.addListener(
  object : Player.Listener {
    override fun onIsPlayingChanged(isPlaying: Boolean) {
      if (isPlaying) {
        // Active playback.
      } else {
        // Not playing because playback is paused, ended, suppressed, or the player
        // is buffering, stopped or failed. Check player.playWhenReady,
        // player.playbackState, player.playbackSuppressionReason and
        // player.playerError for details.
      }
    }
  }
)

Java

player.addListener(
    new Player.Listener() {
      @Override
      public void onIsPlayingChanged(boolean isPlaying) {
        if (isPlaying) {
          // Active playback.
        } else {
          // Not playing because playback is paused, ended, suppressed, or the player
          // is buffering, stopped or failed. Check player.getPlayWhenReady,
          // player.getPlaybackState, player.getPlaybackSuppressionReason and
          // player.getPlaybackError for details.
        }
      }
    });

播放错误

通过在已注册的 Player.Listener 中实现 onPlayerError(PlaybackException error),可以接收导致播放失败的错误。发生故障时,系统会在播放状态转换为 Player.STATE_IDLE 之前立即调用此方法。可以通过调用 ExoPlayer.prepare 重试失败或停止的播放。

请注意,某些 Player 实现会传递 PlaybackException 子类的实例,以提供有关失败的其他信息。例如,ExoPlayer 会传递 ExoPlaybackException,其中包含 type、rendererIndex 和其他 ExoPlayer 专用字段。

以下示例展示了如何检测播放因 HTTP 网络问题而失败的情况:

Kotlin

player.addListener(
  object : Player.Listener {
    override fun onPlayerError(error: PlaybackException) {
      val cause = error.cause
      if (cause is HttpDataSourceException) {
        // An HTTP error occurred.
        val httpError = cause
        // It's possible to find out more about the error both by casting and by querying
        // the cause.
        if (httpError is InvalidResponseCodeException) {
          // Cast to InvalidResponseCodeException and retrieve the response code, message
          // and headers.
        } else {
          // Try calling httpError.getCause() to retrieve the underlying cause, although
          // note that it may be null.
        }
      }
    }
  }
)

Java

player.addListener(
    new Player.Listener() {
      @Override
      public void onPlayerError(PlaybackException error) {
        @Nullable Throwable cause = error.getCause();
        if (cause instanceof HttpDataSourceException) {
          // An HTTP error occurred.
          HttpDataSourceException httpError = (HttpDataSourceException) cause;
          // It's possible to find out more about the error both by casting and by querying
          // the cause.
          if (httpError instanceof HttpDataSource.InvalidResponseCodeException) {
            // Cast to InvalidResponseCodeException and retrieve the response code, message
            // and headers.
          } else {
            // Try calling httpError.getCause() to retrieve the underlying cause, although
            // note that it may be null.
          }
        }
      }
    });

播放列表过渡

每当播放器切换到播放列表中的新媒体文件时,系统都会对已注册的 Player.Listener 对象调用 onMediaItemTransition(MediaItem mediaItem, @MediaItemTransitionReason int reason)。原因会指明这是自动转换、跳转(例如在调用 player.next() 后)、同一内容的重复播放,还是由播放列表更改(例如,当前播放的内容被移除)所致。

元数据

从 player.getCurrentMediaMetadata() 返回的元数据可能会因多种原因而发生变化:播放列表转换、流内元数据更新或在播放过程中更新当前 MediaItem。

如果您对元数据更改感兴趣(例如,为了更新显示当前标题的界面),可以监听 onMediaMetadataChanged。

正在指定播放时间点

调用 Player.seekTo 方法会导致对已注册的 Player.Listener 实例进行一系列回调:

  1. onPositionDiscontinuity 是 reason=DISCONTINUITY_REASON_SEEK。这是调用 Player.seekTo 的直接结果。该回调具有 PositionInfo 字段,用于表示跳转前后的位置。
  2. onPlaybackStateChanged,并立即更改与跳转相关的状态。请注意,可能没有此类更改。

如果您通过 ExoPlayer.setScrubbingModeEnabled 启用拖动模式以实现互动式进度条拖动,ExoPlayer 会抑制播放并使用 Player.PLAYBACK_SUPPRESSION_REASON_SCRUBBING 调用 onPlaybackSuppressionReasonChanged。每次 Player.seekTo 调用仍会触发 onPositionDiscontinuity,即使 ExoPlayer 丢弃了被取代的中间跳转并将其报告给 AnalyticsListener.onDroppedSeeksWhileScrubbing 也是如此。

单独回调与 onEvents

监听器可以选择实现 onIsPlayingChanged(boolean isPlaying) 等单个回调,也可以选择实现通用 onEvents(Player player, Events events) 回调。通用回调可提供对 Player 对象的访问权限,并指定同时发生的一组 events。此回调始终在与各个事件对应的回调之后调用。

Kotlin

override fun onEvents(player: Player, events: Player.Events) {
  if (
    events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED) ||
      events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)
  ) {
    uiModule.updateUi(player)
  }
}

Java

@Override
public void onEvents(Player player, Events events) {
  if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)
      || events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)) {
    uiModule.updateUi(player);
  }
}

在以下情况下,应首选单个事件:

  • 监听器关注的是变更原因。例如,为 onPlayWhenReadyChanged 或 onMediaItemTransition 提供的理由。
  • 监听器仅对通过回调参数提供的新值执行操作,或触发不依赖于回调参数的其他操作。
  • 监听器实现更倾向于在方法名称中清晰可读地指明触发事件的内容。
  • 监听器会向需要了解所有单个事件和状态变化的分析系统报告。

在以下情况下,应首选通用 onEvents(Player player, Events events):

  • 监听器希望针对多个事件触发相同的逻辑。例如,同时更新 onPlaybackStateChanged 和 onPlayWhenReadyChanged 的界面。
  • 监听器需要访问 Player 对象才能触发进一步的事件,例如在媒体文件转换后进行跳转。
  • 监听器打算将通过单独的回调报告的多个状态值一起使用,或与 Player getter 方法结合使用。 例如,只有在 onEvents 回调中,将 Player.getCurrentWindowIndex() 与 onTimelineChanged 中提供的 Timeline 搭配使用才是安全的。
  • 监听器关注的是事件是否在逻辑上同时发生。 例如,由于媒体文件转换,从 onPlaybackStateChanged 转换为 STATE_BUFFERING。

在某些情况下,监听器可能需要将个别回调与通用 onEvents 回调结合使用,例如使用 onMediaItemTransition 记录媒体文件更改原因,但仅在所有状态更改都可以在 onEvents 中一起使用时才执行操作。

使用协程监听播放事件

或者,您也可以使用 Player.listenTo 启动 Kotlin 协程,并指定相关的 Player.Event:

请注意,Player.listen 和 Player.listenTo 可以从任何线程调用,而回调 lambda 始终在与 Player.getApplicationLooper 关联的线程上调用。因此,即使协程是在其他线程上启动的,也可以安全地访问回调 lambda 中的 Player 方法和状态属性。

播放状态更改

coroutineScope.launch {
  player.listenTo(Player.EVENT_IS_PLAYING_CHANGED) {
    // `Player` is a receiver scope for this trailing lambda
    if (isPlaying) {
      // Active playback.
    } else {
      // Not playing.
    }
  }
}

播放错误

coroutineScope.launch {
  player.listenTo(Player.EVENT_PLAYER_ERROR) {
    val error = playerError ?: return@listenTo
    val cause = error.cause
    if (cause is HttpDataSourceException) {
      // An HTTP error occurred.
      if (cause is InvalidResponseCodeException) {
        // Retrieve the response code, message and headers
      } else {
        // Try calling cause.cause to retrieve the underlying cause
      }
    }
  }
}

单独回调与 onEvents

在协程内监听玩家事件时,您始终需要提供 onEvents 回调的实现,而不是单个回调。您可以选择 Player.listen 或 Player.listenTo,具体取决于哪些事件应触发 Lambda 调用。但这些函数在其他方面是等效的:

听发音

coroutineScope.launch {
  player.listen { events ->
    // `Player` is a receiver scope for this trailing lambda
    if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)) {
      // Access the player state directly from the receiver
      updateUi(playbackState)
    }

    if (events.contains(Player.EVENT_PLAYER_ERROR)) {
      // Access the error directly from the player
      handleError(playerError)
    }
  }
}

listenTo

coroutineScope.launch {
  player.listenTo(Player.EVENT_PLAYBACK_STATE_CHANGED, Player.EVENT_PLAYER_ERROR) { events ->
    // `Player` is a receiver scope for this trailing lambda
    if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)) {
      // Access the player state directly from the receiver
      updateUi(playbackState)
    }

    if (events.contains(Player.EVENT_PLAYER_ERROR)) {
      // Access the error directly from the player
      handleError(playerError)
    }
  }
}

如果您对多种事件类型感兴趣,可以将事件列表传递给 Player.listenTo。每当发生上述任何事件时,系统都会调用您的 lambda,您可以检查 Events 参数,以查看实际触发了哪些事件:

coroutineScope.launch {
  player.listenTo(Player.EVENT_PLAYBACK_STATE_CHANGED, Player.EVENT_PLAYER_ERROR) { events ->
    // Unclear which event got triggered without querying `events` parameter
    // The following function will fire whenever either one is caught
    updateUiAndHandleError(playbackState, playerError)
  }
}

由于这些函数在 onEvents 上运行,因此它们不提供对传递给各个回调的瞬时参数的访问权限,例如 onMediaItemTransition(..., int reason) 中的原因或 onPositionDiscontinuity(...) 中的 oldPosition。如果您的逻辑依赖于这些特定参数(并且它们在 Player 上不作为状态属性提供),则应改用标准 Player.Listener 接口。

使用 AnalyticsListener

使用 ExoPlayer 时,可以通过调用 addAnalyticsListener 向播放器注册 AnalyticsListener。AnalyticsListener 实现能够监听可能对分析和日志记录有用的详细事件。如需了解详情,请参阅 Google Analytics 页面。

使用 EventLogger

EventLogger 是由库直接提供的 AnalyticsListener,用于日志记录。将 EventLogger 添加到 ExoPlayer,只需一行代码即可启用有用的额外日志记录:

Kotlin

player.addAnalyticsListener(EventLogger())

Java

player.addAnalyticsListener(new EventLogger());

如需了解详情,请参阅调试日志记录页面。

在指定的播放位置触发事件

在某些用例中,需要在指定的播放位置触发事件。这可以通过使用 PlayerMessage 来实现。您可以使用 ExoPlayer.createMessage 创建 PlayerMessage。可以使用 PlayerMessage.setPosition 设置应执行该操作的播放位置。默认情况下,消息会在播放线程上执行,但可以使用 PlayerMessage.setLooper 进行自定义。PlayerMessage.setDeleteAfterDelivery 可用于控制每次遇到指定的播放位置时(由于跳转和循环播放模式,这可能会发生多次)还是仅在第一次遇到时执行消息。配置 PlayerMessage 后,可以使用 PlayerMessage.send 对其进行调度。

Kotlin

player
  .createMessage { messageType: Int, payload: Any? -> }
  .setLooper(Looper.getMainLooper())
  .setPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 120000)
  .setPayload(customPayloadData)
  .setDeleteAfterDelivery(false)
  .send()

Java

player
    .createMessage((messageType, payload) -> {
      // Do something at the specified playback position.
    })
    .setLooper(Looper.getMainLooper())
    .setPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 120_000)
    .setPayload(customPayloadData)
    .setDeleteAfterDelivery(false)
    .send();