播放器事件

播放器状态(例如播放开始、缓冲或错误) 发生变化时,会触发发送到已注册 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)。 播放器可以处于以下四种播放状态之一:

  • 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.
          }
        }
      }
    });

播放列表过渡

每当播放器更改为播放列表中的新媒体项时 onMediaItemTransition(MediaItem mediaItem, @MediaItemTransitionReason int reason),系统都会在已注册的 Player.Listener 对象上调用。该原因表示这是自动过渡、搜索(例如在调用 player.next() 之后)、同一项的重复,还是由播放列表更改(例如,如果移除了当前播放的项)引起的。

元数据

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

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

正在指定播放时间点

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

  1. onPositionDiscontinuity,其中 reason=DISCONTINUITY_REASON_SEEK。这是调用 Player.seekTo 的直接结果。该回调具有用于搜索前后的位置的 PositionInfo 字段。
  2. onPlaybackStateChanged ,其中包含与搜索相关的任何即时状态变化。请注意,可能没有此类变化。

单个回调与 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() 和 Timeline 提供的 onTimelineChanged 才是安全的。
  • 监听器对事件是否在逻辑上同时发生感兴趣。 例如,因媒体项过渡而将 onPlaybackStateChanged 更改为 STATE_BUFFERING。

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

使用协程监听播放事件

或者,您可以使用 Player.listenTo 并 指定相关的 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 调用。但这些函数在其他方面是等效的:

listen

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) 中的原因或 oldPosition 在 onPositionDiscontinuity(...) 中。如果您的逻辑依赖于这些特定参数 (并且这些参数在 Player 上不可用作 状态属性),则应 改用标准 Player.Listener 接口。

使用 AnalyticsListener

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

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