播放器状态的变化(例如播放开始、缓冲或错误)会触发发送到已注册 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 实例进行一系列回调:
onPositionDiscontinuity是reason=DISCONTINUITY_REASON_SEEK。这是调用Player.seekTo的直接结果。该回调具有PositionInfo字段,用于表示跳转前后的位置。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对象才能触发进一步的事件,例如在媒体文件转换后进行跳转。 - 监听器打算将通过单独的回调报告的多个状态值一起使用,或与
Playergetter 方法结合使用。 例如,只有在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 实现能够监听可能对分析和日志记录有用的详细事件。如需了解详情,请参阅“分析”页面。
使用“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();