Изменения состояния проигрывателя (например, начало воспроизведения, буферизация или ошибки) вызывают события, которые отправляются зарегистрированным экземплярам 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.
Изменение состояния воспроизведения
Чтобы получать информацию об изменении состояния проигрывателя, реализуйте метод onPlaybackStateChanged(@State int state) в зарегистрированном объекте Player.Listener.
Проигрыватель может находиться в одном из четырех состояний:
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. } } });
Ошибки воспроизведения
Чтобы получать сообщения об ошибках, из-за которых не удается воспроизвести контент, реализуйте метод onPlayerError(PlaybackException error) в зарегистрированном объекте Player.Listener. При возникновении ошибки этот метод будет вызван непосредственно перед переходом состояния воспроизведения в 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, чтобы запускать дальнейшие события, например перемотку после перехода к другому мультимедийному объекту. - Слушатель намерен использовать несколько значений состояния, которые передаются через отдельные обратные вызовы, вместе или в сочетании с методами получения
Player. Например, использоватьPlayer.getCurrentWindowIndex()сTimeline, предоставленным вonTimelineChanged, безопасно только в обратном вызовеonEvents. - Прослушивателю важно, произошли ли события вместе.
Например,
onPlaybackStateChangedнаSTATE_BUFFERINGиз-за перехода к другому мультимедийному объекту.
В некоторых случаях прослушивателям может потребоваться объединить отдельные обратные вызовы с общим обратным вызовом onEvents, например чтобы записать причины изменения мультимедийного объекта с помощью onMediaItemTransition, но действовать только после того, как все изменения состояния можно будет использовать вместе в onEvents.
Как отслеживать события воспроизведения с помощью сопрограмм
Вы также можете запустить сопрограмму Kotlin с помощью Player.listenTo и указать нужный Player.Event:
Обратите внимание, что Player.listen и Player.listenTo можно вызывать из любого потока, а лямбда-выражение обратного вызова всегда вызывается в потоке, связанном с Player.getApplicationLooper. Поэтому вы можете безопасно обращаться к методам и свойствам состояния 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 в зависимости от того, какие события должны вызывать лямбда-функцию. Но в остальном функции
эквивалентны:
прослушать
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. Лямбда-выражение будет вызываться при возникновении любого из этих событий. Вы можете проверить параметр 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 в проигрывателе можно зарегистрировать AnalyticsListener, вызвав addAnalyticsListener. Реализации AnalyticsListener могут прослушивать подробные события, которые могут быть полезны для аналитики и ведения журналов. Подробную информацию можно найти на странице аналитики.
Назначить EventLogger
EventLogger – это AnalyticsListener, предоставляемый непосредственно библиотекой для ведения журналов. Добавьте EventLogger в ExoPlayer, чтобы включить полезное дополнительное ведение журнала с помощью одной строки:
Kotlin
player.addAnalyticsListener(EventLogger())
Java
player.addAnalyticsListener(new EventLogger());
Подробную информацию вы найдете на странице, посвященной ведению журнала отладки.
Активация событий в определенных позициях воспроизведения
В некоторых случаях требуется активировать события в определенных позициях воспроизведения. Для этого используется PlayerMessage. Объект PlayerMessage можно создать с помощью ExoPlayer.createMessage. Позицию воспроизведения, на которой должно выполняться действие, можно задать с помощью 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();