播放器事件

播放器状态(例如播放开始、缓冲或错误) 发生变化时,会触发发送到已注册 Player.Listener 实例的事件。这些 事件由整数常量表示,并由 Player.EventPlayer.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 状态
  • playWhenReadytrue
  • 播放不会因 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,其中包含 typerendererIndex 和其他特定于 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);
  }
}

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

  • 监听器对变化的原因感兴趣。例如,为 onPlayWhenReadyChangedonMediaItemTransition 提供的原因。
  • 监听器仅对通过回调参数提供的新值执行操作,或触发不依赖于回调参数的其他操作。
  • 监听器实现更喜欢在方法名称中清晰易懂地指明触发事件的原因。
  • 监听器会向需要了解所有单个事件和状态变化的分析系统报告。

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

  • 监听器希望为多个事件触发相同的逻辑。例如,同时为 onPlaybackStateChangedonPlayWhenReadyChanged 更新界面。
  • 监听器需要访问 Player 对象以触发更多事件,例如在媒体项过渡后进行搜索。
  • 监听器打算一起使用通过单独回调报告的多个状态值,或将这些状态值与 Player getter 方法结合使用。 例如,只有在 onEvents 回调中,使用 Player.getCurrentWindowIndex()Timeline 提供的 onTimelineChanged 才是安全的。
  • 监听器对事件是否在逻辑上同时发生感兴趣。 例如,因媒体项过渡而将 onPlaybackStateChanged 更改为 STATE_BUFFERING

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

使用协程监听播放事件

或者,您可以使用 Player.listenTo 并 指定相关的 Player.Event

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

使用 AnalyticsListener

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

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