Изменения состояния проигрывателя (например, начало воспроизведения, буферизация или ошибки) вызывают события, которые отправляются зарегистрированным экземплярам 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(), могут меняться по разным причинам: переходы между плейлистами, обновления метаданных In-Stream или обновление текущего 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 в зависимости от того, какие события должны вызывать функцию 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) или к 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();