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

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

  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 в зависимости от того, какие события должны вызывать функцию 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();