События игрока

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

  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. Например, использовать 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();