Управляйте воспроизведением и рекламируйте его с помощью MediaSession.

Медиа-сеансы предоставляют универсальный способ взаимодействия с аудио- или видеоплеером. В Media3 проигрывателем по умолчанию является класс ExoPlayer , реализующий интерфейс Player . Подключение мультимедийного сеанса к проигрывателю позволяет приложению объявлять воспроизведение мультимедиа извне и получать команды воспроизведения из внешних источников.

Команды могут исходить от физических кнопок, таких как кнопка воспроизведения на гарнитуре или пульте дистанционного управления телевизора. Они также могут поступать из клиентских приложений, имеющих медиа-контроллер, например, отдавая команду «пауза» Google Assistant. Медиа-сеанс делегирует эти команды проигрывателю мультимедийного приложения.

Когда выбирать медиасессию

Когда вы реализуете MediaSession , вы позволяете пользователям управлять воспроизведением:

  • Через свои наушники . Часто пользователи могут использовать кнопки или сенсорные действия на своих наушниках, чтобы воспроизвести или приостановить воспроизведение мультимедиа или перейти к следующему или предыдущему треку.
  • Разговаривая с Google Assistant . Распространенным шаблоном является произнесение «ОК, Google, пауза», чтобы приостановить воспроизведение любого мультимедиа, которое в данный момент воспроизводится на устройстве.
  • Через свои часы Wear OS . Это упрощает доступ к наиболее распространенным элементам управления воспроизведением во время игры на телефоне.
  • Через средства управления СМИ . В этой карусели показаны элементы управления для каждого текущего сеанса мультимедиа.
  • По телевизору . Позволяет выполнять действия с физическими кнопками воспроизведения, управлением воспроизведением на платформе и управлением питанием (например, если телевизор, звуковая панель или аудио/видео-ресивер выключаются или переключается вход, воспроизведение должно остановиться в приложении).
  • И любые другие внешние процессы, которые должны влиять на воспроизведение.

Это отлично подходит для многих случаев использования. В частности, вам следует рассмотреть возможность использования MediaSession в следующих случаях:

  • Вы транслируете длинный видеоконтент , например фильмы или прямые телепередачи.
  • Вы транслируете длинный аудиоконтент , например подкасты или музыкальные плейлисты.
  • Вы создаете телевизионное приложение .

Однако не все варианты использования хорошо подходят для MediaSession . Возможно, вам захочется использовать только Player в следующих случаях:

  • Вы показываете краткий контент , для которого не требуется никакого внешнего управления или фонового воспроизведения.
  • Нет ни одного активного видео, например, пользователь прокручивает список, и на экране одновременно отображается несколько видео .
  • Вы воспроизводите одноразовое вводное или поясняющее видео , которое, как вы ожидаете, ваш пользователь будет активно смотреть без необходимости использования внешних элементов управления воспроизведением.
  • Ваш контент чувствителен к конфиденциальности, и вы не хотите, чтобы внешние процессы получали доступ к метаданным мультимедиа (например, режим инкогнито в браузере).

Если ваш вариант использования не соответствует ни одному из перечисленных выше, подумайте, согласны ли вы с тем, что ваше приложение продолжает воспроизведение, когда пользователь не взаимодействует активно с контентом. Если ответ положительный, вы, вероятно, захотите выбрать MediaSession . Если ответ отрицательный, вы, вероятно, захотите вместо этого использовать Player .

Создать медиа-сессию

Медиа-сессия живет вместе с игроком, которым она управляет. Вы можете создать медиасеанс с помощью объекта Context и Player . Вам следует создать и инициализировать медиа-сеанс, когда это необходимо, например, метод жизненного цикла onStart() или onResume() Activity или Fragment или метод onCreate() Service , которому принадлежит медиа-сеанс и связанный с ним проигрыватель.

Чтобы создать медиа-сеанс, инициализируйте Player и передайте его MediaSession.Builder следующим образом:

Котлин

val player = ExoPlayer.Builder(context).build()
val mediaSession = MediaSession.Builder(context, player).build()

Ява

ExoPlayer player = new ExoPlayer.Builder(context).build();
MediaSession mediaSession = new MediaSession.Builder(context, player).build();

Автоматическая обработка состояний

Библиотека Media3 автоматически обновляет медиа-сессию, используя состояние проигрывателя. Таким образом, вам не нужно вручную обрабатывать сопоставление игрока с сеансом.

Это отличается от сеанса мультимедиа платформы, где вам нужно было создать и поддерживать PlaybackState независимо от самого проигрывателя, например, для указания любых ошибок .

Уникальный идентификатор сеанса

По умолчанию MediaSession.Builder создает сеанс с пустой строкой в ​​качестве идентификатора сеанса. Этого достаточно, если приложение намерено создать только один экземпляр сеанса, что является наиболее распространенным случаем.

Если приложение хочет управлять несколькими экземплярами сеанса одновременно, оно должно гарантировать, что идентификатор каждого сеанса уникален. Идентификатор сеанса можно установить при создании сеанса с помощью MediaSession.Builder.setId(String id) .

Если вы видите исключение IllegalStateException приводящее к сбою вашего приложения с сообщением об ошибке IllegalStateException: Session ID must be unique. ID= , то вполне вероятно, что сеанс был неожиданно создан до того, как был освобожден ранее созданный экземпляр с тем же идентификатором. Чтобы избежать утечки сеансов из-за программной ошибки, такие случаи обнаруживаются и уведомляются путем создания исключения.

Предоставить контроль другим клиентам

Медиа-сеанс является ключом к управлению воспроизведением. Он позволяет вам перенаправлять команды из внешних источников на проигрыватель, который выполняет работу по воспроизведению вашего мультимедиа. Этими источниками могут быть физические кнопки, такие как кнопка воспроизведения на гарнитуре или пульте дистанционного управления телевизора, или косвенные команды, такие как команда Google Assistant «пауза». Аналогично, вы можете предоставить доступ к системе Android, чтобы упростить управление уведомлениями и экраном блокировки, или к часам Wear OS, чтобы вы могли управлять воспроизведением с циферблата. Внешние клиенты могут использовать медиа-контроллер для подачи команд воспроизведения в ваше медиа-приложение. Они принимаются вашим медиа-сеансом, который в конечном итоге делегирует команды медиаплееру.

Диаграмма, демонстрирующая взаимодействие между MediaSession и MediaController.
Рис. 1. Медиа-контроллер облегчает передачу команд из внешних источников в медиа-сеанс.

Когда контроллер собирается подключиться к вашему медиа-сеансу, вызывается метод onConnect() . Вы можете использовать предоставленную ControllerInfo , чтобы решить, принять или отклонить запрос. См. пример принятия запроса на подключение в разделе «Объявление пользовательских команд» .

После подключения контроллер может отправлять в сеанс команды воспроизведения. Затем сеанс делегирует эти команды игроку. Команды воспроизведения и списка воспроизведения, определенные в интерфейсе Player , автоматически обрабатываются сеансом.

Другие методы обратного вызова позволяют обрабатывать, например, запросы пользовательских команд и изменение списка воспроизведения . Эти обратные вызовы также включают объект ControllerInfo , поэтому вы можете изменить способ ответа на каждый запрос для каждого контроллера.

Изменить список воспроизведения

Медиа-сеанс может напрямую изменять список воспроизведения своего проигрывателя, как описано в руководстве ExoPlayer для списков воспроизведения . Контроллеры также могут изменять список воспроизведения, если контроллеру доступен COMMAND_SET_MEDIA_ITEM или COMMAND_CHANGE_MEDIA_ITEMS .

При добавлении новых элементов в список воспроизведения проигрывателю обычно требуются экземпляры MediaItem с определенным URI , чтобы их можно было воспроизводить. По умолчанию вновь добавленные элементы автоматически пересылаются в методы проигрывателя, такие как player.addMediaItem , если для них определен URI.

Если вы хотите настроить экземпляры MediaItem , добавленные в проигрыватель, вы можете переопределить onAddMediaItems() . Этот шаг необходим, если вы хотите поддерживать контроллеры, которые запрашивают носитель без определенного URI. Вместо этого MediaItem обычно имеет одно или несколько из следующих полей для описания запрошенного мультимедиа:

  • MediaItem.id : общий идентификатор, идентифицирующий носитель.
  • MediaItem.RequestMetadata.mediaUri : URI запроса, который может использовать пользовательскую схему и не обязательно воспроизводиться напрямую проигрывателем.
  • MediaItem.RequestMetadata.searchQuery : текстовый поисковый запрос, например, из Google Assistant.
  • MediaItem.MediaMetadata : структурированные метаданные, такие как «название» или «исполнитель».

Чтобы получить дополнительные параметры настройки для совершенно новых списков воспроизведения, вы можете дополнительно переопределить onSetMediaItems() , который позволяет вам определить начальный элемент и позицию в списке воспроизведения. Например, вы можете расширить один запрошенный элемент до всего списка воспроизведения и указать проигрывателю начать с индекса первоначально запрошенного элемента. Пример реализации onSetMediaItems() с этой функцией можно найти в демонстрационном приложении сеанса.

Управление настройками медиа-кнопки

Каждый контроллер, например System UI, Android Auto или Wear OS, может принимать собственные решения о том, какие кнопки показывать пользователю. Чтобы указать, какие элементы управления воспроизведением вы хотите предоставить пользователю, вы можете указать настройки кнопок мультимедиа в MediaSession . Эти настройки состоят из упорядоченного списка экземпляров CommandButton , каждый из которых определяет предпочтение кнопки в пользовательском интерфейсе.

Определить командные кнопки

Экземпляры CommandButton используются для определения предпочтений мультимедийных кнопок. Каждая кнопка определяет три аспекта желаемого элемента пользовательского интерфейса:

  1. Значок , определяющий внешний вид. При создании CommandButton.Builder для значка должна быть установлена ​​одна из предопределенных констант. Обратите внимание, что это не настоящий ресурс Bitmap или изображения. Универсальная константа помогает контроллерам выбирать подходящий ресурс для единообразного внешнего вида в их собственном пользовательском интерфейсе. Если ни одна из предопределенных констант значков не подходит для вашего варианта использования, вместо этого вы можете использовать setCustomIconResId .
  2. Команда , определяющая действие, запускаемое при взаимодействии пользователя с кнопкой. Вы можете использовать setPlayerCommand для Player.Command или setSessionCommand для предопределенного или пользовательского SessionCommand .
  3. Slot , определяющий, где должна быть размещена кнопка в пользовательском интерфейсе контроллера. Это поле является необязательным и устанавливается автоматически в зависимости от значка и команды . Например, он позволяет указать, что кнопка должна отображаться в области навигации «вперед» пользовательского интерфейса вместо области «переполнения» по умолчанию.

Котлин

val button =
  CommandButton.Builder(CommandButton.ICON_SKIP_FORWARD_15)
    .setSessionCommand(SessionCommand(CUSTOM_ACTION_ID, Bundle.EMPTY))
    .setSlots(CommandButton.SLOT_FORWARD)
    .build()

Ява

CommandButton button =
    new CommandButton.Builder(CommandButton.ICON_SKIP_FORWARD_15)
        .setSessionCommand(new SessionCommand(CUSTOM_ACTION_ID, Bundle.EMPTY))
        .setSlots(CommandButton.SLOT_FORWARD)
        .build();

Когда настройки медиа-кнопок определены, применяется следующий алгоритм:

  1. Для каждой кнопки CommandButton в настройках медиа-кнопки поместите кнопку в первый доступный и разрешенный слот .
  2. Если какой-либо из центральных, передних и задних слотов не заполнен кнопкой, добавьте для этого слота кнопки по умолчанию.

Вы можете использовать CommandButton.DisplayConstraints , чтобы создать предварительный просмотр того, как настройки кнопок мультимедиа будут разрешены в зависимости от ограничений отображения пользовательского интерфейса.

Установите настройки медиа-кнопки

Самый простой способ установить настройки медиа-кнопки — определить список при создании MediaSession . Альтернативно вы можете переопределить MediaSession.Callback.onConnect , чтобы настроить параметры мультимедийных кнопок для каждого подключенного контроллера.

Котлин

val mediaSession =
  MediaSession.Builder(context, player)
    .setMediaButtonPreferences(ImmutableList.of(likeButton, favoriteButton))
    .build()

Ява

MediaSession mediaSession =
  new MediaSession.Builder(context, player)
      .setMediaButtonPreferences(ImmutableList.of(likeButton, favoriteButton))
      .build();

Обновить настройки медиа-кнопки после взаимодействия с пользователем

После обработки взаимодействия с вашим плеером вы можете обновить кнопки, отображаемые в пользовательском интерфейсе контроллера. Типичным примером является кнопка-переключатель, которая меняет свой значок и действие после запуска действия, связанного с этой кнопкой. Чтобы обновить настройки медиа-кнопки, вы можете использовать MediaSession.setMediaButtonPreferences , чтобы обновить настройки для всех контроллеров или для конкретного контроллера:

Котлин

// Handle "favoritesButton" action, replace by opposite button
mediaSession.setMediaButtonPreferences(
  ImmutableList.of(likeButton, removeFromFavoritesButton))

Ява

// Handle "favoritesButton" action, replace by opposite button
mediaSession.setMediaButtonPreferences(
    ImmutableList.of(likeButton, removeFromFavoritesButton));

Добавляйте собственные команды и настраивайте поведение по умолчанию.

Доступные команды проигрывателя можно расширить с помощью пользовательских команд, а также можно перехватывать входящие команды проигрывателя и мультимедийные кнопки, чтобы изменить поведение по умолчанию.

Объявляйте и обрабатывайте пользовательские команды

Мультимедийные приложения могут определять собственные команды, которые, например, можно использовать в настройках мультимедийных кнопок . Например, вы можете реализовать кнопки, позволяющие пользователю сохранять элемент мультимедиа в списке избранных элементов. MediaController отправляет пользовательские команды, а MediaSession.Callback получает их.

Чтобы определить пользовательские команды, вам необходимо переопределить MediaSession.Callback.onConnect() чтобы установить доступные пользовательские команды для каждого подключенного контроллера.

Котлин

private class CustomMediaSessionCallback: MediaSession.Callback {
  // Configure commands available to the controller in onConnect()
  override fun onConnect(
    session: MediaSession,
    controller: MediaSession.ControllerInfo
  ): MediaSession.ConnectionResult {
    val sessionCommands = ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
        .add(SessionCommand(SAVE_TO_FAVORITES, Bundle.EMPTY))
        .build()
    return AcceptedResultBuilder(session)
        .setAvailableSessionCommands(sessionCommands)
        .build()
  }
}

Ява

class CustomMediaSessionCallback implements MediaSession.Callback {
  // Configure commands available to the controller in onConnect()
  @Override
  public ConnectionResult onConnect(
    MediaSession session,
    ControllerInfo controller) {
    SessionCommands sessionCommands =
        ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
            .add(new SessionCommand(SAVE_TO_FAVORITES, new Bundle()))
            .build();
    return new AcceptedResultBuilder(session)
        .setAvailableSessionCommands(sessionCommands)
        .build();
  }
}

Чтобы получать пользовательские запросы команд от MediaController , переопределите метод onCustomCommand() в Callback .

Котлин

private class CustomMediaSessionCallback: MediaSession.Callback {
  ...
  override fun onCustomCommand(
    session: MediaSession,
    controller: MediaSession.ControllerInfo,
    customCommand: SessionCommand,
    args: Bundle
  ): ListenableFuture<SessionResult> {
    if (customCommand.customAction == SAVE_TO_FAVORITES) {
      // Do custom logic here
      saveToFavorites(session.player.currentMediaItem)
      return Futures.immediateFuture(
        SessionResult(SessionResult.RESULT_SUCCESS)
      )
    }
    ...
  }
}

Ява

class CustomMediaSessionCallback implements MediaSession.Callback {
  ...
  @Override
  public ListenableFuture<SessionResult> onCustomCommand(
    MediaSession session, 
    ControllerInfo controller,
    SessionCommand customCommand,
    Bundle args
  ) {
    if(customCommand.customAction.equals(SAVE_TO_FAVORITES)) {
      // Do custom logic here
      saveToFavorites(session.getPlayer().getCurrentMediaItem());
      return Futures.immediateFuture(
        new SessionResult(SessionResult.RESULT_SUCCESS)
      );
    }
    ...
  }
}

Вы можете отслеживать, какой контроллер мультимедиа отправляет запрос, используя свойство packageName объекта MediaSession.ControllerInfo , которое передается в методы Callback . Это позволяет вам настроить поведение вашего приложения в ответ на заданную команду, если она исходит из системы, вашего собственного приложения или других клиентских приложений.

Настройте команды проигрывателя по умолчанию

Все команды по умолчанию и обработка состояний делегируются Player , находящемуся в MediaSession . Чтобы настроить поведение команды, определенной в интерфейсе Player , например play() или seekToNext() , оберните свой Player в ForwardingSimpleBasePlayer перед передачей его в MediaSession :

Котлин

val player = (logic to build a Player instance)

val forwardingPlayer = object : ForwardingSimpleBasePlayer(player) {
  // Customizations
}

val mediaSession = MediaSession.Builder(context, forwardingPlayer).build()

Ява

ExoPlayer player = (logic to build a Player instance)

ForwardingSimpleBasePlayer forwardingPlayer =
    new ForwardingSimpleBasePlayer(player) {
      // Customizations
    };

MediaSession mediaSession =
  new MediaSession.Builder(context, forwardingPlayer).build();

Дополнительные сведения о ForwardingSimpleBasePlayer см. в руководстве ExoPlayer по настройке .

Определить запрашивающий контроллер команды игрока

Когда вызов метода Player инициируется MediaController , вы можете определить источник происхождения с помощью MediaSession.controllerForCurrentRequest и получить ControllerInfo для текущего запроса:

Котлин

class CallerAwarePlayer(player: Player) :
  ForwardingSimpleBasePlayer(player) {

  override fun handleSeek(
    mediaItemIndex: Int,
    positionMs: Long,
    seekCommand: Int,
  ): ListenableFuture<*> {
    Log.d(
      "caller",
      "seek operation from package ${session.controllerForCurrentRequest?.packageName}",
    )
    return super.handleSeek(mediaItemIndex, positionMs, seekCommand)
  }
}

Ява

public class CallerAwarePlayer extends ForwardingSimpleBasePlayer {
  public CallerAwarePlayer(Player player) {
    super(player);
  }

  @Override
  protected ListenableFuture<?> handleSeek(
        int mediaItemIndex, long positionMs, int seekCommand) {
    Log.d(
        "caller",
        "seek operation from package: "
            + session.getControllerForCurrentRequest().getPackageName());
    return super.handleSeek(mediaItemIndex, positionMs, seekCommand);
  }
}

Настройте обработку медиа-кнопок

Мультимедийные кнопки — это аппаратные кнопки, имеющиеся на устройствах Android и других периферийных устройствах, например кнопка воспроизведения/паузы на гарнитуре Bluetooth. Media3 обрабатывает события медиа-кнопок, когда они поступают в сеанс, и вызывает соответствующий метод Player в проигрывателе сеанса .

Рекомендуется обрабатывать все входящие события медиа-кнопки в соответствующем методе Player . Для более сложных случаев использования события медиа-кнопки можно перехватить в MediaSession.Callback.onMediaButtonEvent(Intent) .

Обработка ошибок и отчетность

Существует два типа ошибок, которые генерирует сеанс и сообщает контроллерам. Неустранимые ошибки сообщают о техническом сбое воспроизведения сеансового проигрывателя, который прерывает воспроизведение. О фатальных ошибках автоматически сообщается контроллеру при их возникновении. Нефатальные ошибки — это нетехнические ошибки или ошибки политики, которые не прерывают воспроизведение и отправляются приложением на контроллеры вручную.

Фатальные ошибки воспроизведения

Игрок сообщает сеансу о фатальной ошибке воспроизведения, а затем сообщает контроллерам для вызова через Player.Listener.onPlayerError(PlaybackException) и Player.Listener.onPlayerErrorChanged(@Nullable PlaybackException) .

В таком случае состояние воспроизведения переходит в STATE_IDLE , а MediaController.getPlaybackError() возвращает PlaybackException , вызвавшее переход. Контроллер может проверить PlayerException.errorCode , чтобы получить информацию о причине ошибки.

Для обеспечения совместимости фатальная ошибка реплицируется в сеанс платформы путем перевода ее состояния в STATE_ERROR и установки кода ошибки и сообщения в соответствии с PlaybackException .

Настройка фатальных ошибок

Чтобы предоставить пользователю локализованную и содержательную информацию, код ошибки, сообщение об ошибке и дополнительные сведения о фатальной ошибке воспроизведения можно настроить с помощью ForwardingPlayer при построении сеанса:

Котлин

val forwardingPlayer = ErrorForwardingPlayer(player)
val session = MediaSession.Builder(context, forwardingPlayer).build()

Ява

Player forwardingPlayer = new ErrorForwardingPlayer(player);
MediaSession session =
    new MediaSession.Builder(context, forwardingPlayer).build();

Проигрыватель пересылки может использовать ForwardingSimpleBasePlayer , чтобы перехватить ошибку и настроить код ошибки, сообщение или дополнительные сведения. Таким же образом вы также можете генерировать новые ошибки, которых нет в исходном плеере:

Котлин

class ErrorForwardingPlayer (private val context: Context, player: Player) :
    ForwardingSimpleBasePlayer(player) {

  override fun getState(): State {
    var state = super.getState()
    if (state.playerError != null) {
      state =
        state.buildUpon()
          .setPlayerError(customizePlaybackException(state.playerError!!))
          .build()
    }
    return state
  }

  fun customizePlaybackException(error: PlaybackException): PlaybackException {
    val buttonLabel: String
    val errorMessage: String
    when (error.errorCode) {
      PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW -> {
        buttonLabel = context.getString(R.string.err_button_label_restart_stream)
        errorMessage = context.getString(R.string.err_msg_behind_live_window)
      }
      else -> {
        buttonLabel = context.getString(R.string.err_button_label_ok)
        errorMessage = context.getString(R.string.err_message_default)
      }
    }
    val extras = Bundle()
    extras.putString("button_label", buttonLabel)
    return PlaybackException(errorMessage, error.cause, error.errorCode, extras)
  }
}

Ява

class ErrorForwardingPlayer extends ForwardingSimpleBasePlayer {

  private final Context context;

  public ErrorForwardingPlayer(Context context, Player player) {
    super(player);
    this.context = context;
  }

  @Override
  protected State getState() {
    State state = super.getState();
    if (state.playerError != null) {
      state =
          state.buildUpon()
              .setPlayerError(customizePlaybackException(state.playerError))
              .build();
    }
    return state;
  }

  private PlaybackException customizePlaybackException(PlaybackException error) {
    String buttonLabel;
    String errorMessage;
    switch (error.errorCode) {
      case PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW:
        buttonLabel = context.getString(R.string.err_button_label_restart_stream);
        errorMessage = context.getString(R.string.err_msg_behind_live_window);
        break;
      default:
        buttonLabel = context.getString(R.string.err_button_label_ok);
        errorMessage = context.getString(R.string.err_message_default);
        break;
    }
    Bundle extras = new Bundle();
    extras.putString("button_label", buttonLabel);
    return new PlaybackException(errorMessage, error.getCause(), error.errorCode, extras);
  }
}

Нефатальные ошибки

Нефатальные ошибки, которые не происходят из-за технического исключения, могут быть отправлены приложением всем или конкретному контроллеру:

Котлин

val sessionError = SessionError(
  SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED,
  context.getString(R.string.error_message_authentication_expired),
)

// Option 1: Sending a nonfatal error to all controllers.
mediaSession.sendError(sessionError)

// Option 2: Sending a nonfatal error to the media notification controller only
// to set the error code and error message in the playback state of the platform
// media session.
mediaSession.mediaNotificationControllerInfo?.let {
  mediaSession.sendError(it, sessionError)
}

Ява

SessionError sessionError = new SessionError(
    SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED,
    context.getString(R.string.error_message_authentication_expired));

// Option 1: Sending a nonfatal error to all controllers.
mediaSession.sendError(sessionError);

// Option 2: Sending a nonfatal error to the media notification controller only
// to set the error code and error message in the playback state of the platform
// media session.
ControllerInfo mediaNotificationControllerInfo =
    mediaSession.getMediaNotificationControllerInfo();
if (mediaNotificationControllerInfo != null) {
  mediaSession.sendError(mediaNotificationControllerInfo, sessionError);
}

Когда нефатальная ошибка отправляется на контроллер медиа-уведомлений, код ошибки и сообщение об ошибке реплицируются в медиа-сеанс платформы, а PlaybackState.state не изменяется на STATE_ERROR .

Получать нефатальные ошибки

MediaController получает нефатальную ошибку, реализуя MediaController.Listener.onError :

Котлин

val future = MediaController.Builder(context, sessionToken)
  .setListener(object : MediaController.Listener {
    override fun onError(controller: MediaController, sessionError: SessionError) {
      // Handle nonfatal error.
    }
  })
  .buildAsync()

Ява

MediaController.Builder future =
    new MediaController.Builder(context, sessionToken)
        .setListener(
            new MediaController.Listener() {
              @Override
              public void onError(MediaController controller, SessionError sessionError) {
                // Handle nonfatal error.
              }
            });

,

Медиа-сеансы предоставляют универсальный способ взаимодействия с аудио- или видеоплеером. В Media3 проигрывателем по умолчанию является класс ExoPlayer , реализующий интерфейс Player . Подключение мультимедийного сеанса к проигрывателю позволяет приложению объявлять воспроизведение мультимедиа извне и получать команды воспроизведения из внешних источников.

Команды могут исходить от физических кнопок, таких как кнопка воспроизведения на гарнитуре или пульте дистанционного управления телевизора. Они также могут поступать из клиентских приложений, имеющих медиа-контроллер, например, отдавая команду «пауза» Google Assistant. Медиа-сеанс делегирует эти команды проигрывателю мультимедийного приложения.

Когда выбирать медиасессию

Когда вы реализуете MediaSession , вы позволяете пользователям управлять воспроизведением:

  • Через свои наушники . Часто пользователи могут использовать кнопки или сенсорные действия на своих наушниках, чтобы воспроизвести или приостановить воспроизведение мультимедиа или перейти к следующему или предыдущему треку.
  • Разговаривая с Google Assistant . Распространенным шаблоном является произнесение «ОК, Google, пауза», чтобы приостановить воспроизведение любого мультимедиа, которое в данный момент воспроизводится на устройстве.
  • Через свои часы Wear OS . Это упрощает доступ к наиболее распространенным элементам управления воспроизведением во время игры на телефоне.
  • Через средства управления СМИ . В этой карусели показаны элементы управления для каждого текущего сеанса мультимедиа.
  • По телевизору . Позволяет выполнять действия с физическими кнопками воспроизведения, управлением воспроизведением на платформе и управлением питанием (например, если телевизор, звуковая панель или аудио/видео-ресивер выключаются или переключается вход, воспроизведение должно остановиться в приложении).
  • И любые другие внешние процессы, которые должны влиять на воспроизведение.

Это отлично подходит для многих случаев использования. В частности, вам следует рассмотреть возможность использования MediaSession в следующих случаях:

  • Вы транслируете длинный видеоконтент , например фильмы или прямые телепередачи.
  • Вы транслируете длинный аудиоконтент , например подкасты или музыкальные плейлисты.
  • Вы создаете телевизионное приложение .

Однако не все варианты использования хорошо подходят для MediaSession . Возможно, вам захочется использовать только Player в следующих случаях:

  • Вы показываете краткий контент , для которого не требуется никакого внешнего управления или фонового воспроизведения.
  • Нет ни одного активного видео, например, пользователь прокручивает список, и на экране одновременно отображается несколько видео .
  • Вы воспроизводите одноразовое вводное или поясняющее видео , которое, как вы ожидаете, ваш пользователь будет активно смотреть без необходимости использования внешних элементов управления воспроизведением.
  • Ваш контент чувствителен к конфиденциальности, и вы не хотите, чтобы внешние процессы получали доступ к метаданным мультимедиа (например, режим инкогнито в браузере).

Если ваш вариант использования не соответствует ни одному из перечисленных выше, подумайте, согласны ли вы с тем, что ваше приложение продолжает воспроизведение, когда пользователь не взаимодействует активно с контентом. Если ответ положительный, вы, вероятно, захотите выбрать MediaSession . Если ответ отрицательный, вы, вероятно, захотите вместо этого использовать Player .

Создать медиа-сессию

Медиа-сессия живет вместе с игроком, которым она управляет. Вы можете создать медиасеанс с помощью объекта Context и Player . Вам следует создать и инициализировать медиа-сеанс, когда это необходимо, например, метод жизненного цикла onStart() или onResume() Activity или Fragment или метод onCreate() Service , которому принадлежит медиа-сеанс и связанный с ним проигрыватель.

Чтобы создать медиа-сеанс, инициализируйте Player и передайте его MediaSession.Builder следующим образом:

Котлин

val player = ExoPlayer.Builder(context).build()
val mediaSession = MediaSession.Builder(context, player).build()

Ява

ExoPlayer player = new ExoPlayer.Builder(context).build();
MediaSession mediaSession = new MediaSession.Builder(context, player).build();

Автоматическая обработка состояний

Библиотека Media3 автоматически обновляет медиа-сессию, используя состояние проигрывателя. Таким образом, вам не нужно вручную обрабатывать сопоставление игрока с сеансом.

Это отличается от сеанса мультимедиа платформы, где вам нужно было создать и поддерживать PlaybackState независимо от самого проигрывателя, например, для указания любых ошибок .

Уникальный идентификатор сеанса

По умолчанию MediaSession.Builder создает сеанс с пустой строкой в ​​качестве идентификатора сеанса. Этого достаточно, если приложение намерено создать только один экземпляр сеанса, что является наиболее распространенным случаем.

Если приложение хочет управлять несколькими экземплярами сеанса одновременно, оно должно гарантировать, что идентификатор каждого сеанса уникален. Идентификатор сеанса можно установить при создании сеанса с помощью MediaSession.Builder.setId(String id) .

Если вы видите исключение IllegalStateException приводящее к сбою вашего приложения с сообщением об ошибке IllegalStateException: Session ID must be unique. ID= , то вполне вероятно, что сеанс был неожиданно создан до того, как был освобожден ранее созданный экземпляр с тем же идентификатором. Чтобы избежать утечки сеансов из-за программной ошибки, такие случаи обнаруживаются и уведомляются путем создания исключения.

Предоставить контроль другим клиентам

Медиа-сеанс является ключом к управлению воспроизведением. Он позволяет вам перенаправлять команды из внешних источников на проигрыватель, который выполняет работу по воспроизведению вашего мультимедиа. Этими источниками могут быть физические кнопки, такие как кнопка воспроизведения на гарнитуре или пульте дистанционного управления телевизора, или косвенные команды, такие как команда Google Assistant «пауза». Аналогично, вы можете предоставить доступ к системе Android, чтобы упростить управление уведомлениями и экраном блокировки, или к часам Wear OS, чтобы вы могли управлять воспроизведением с циферблата. Внешние клиенты могут использовать медиа-контроллер для подачи команд воспроизведения в ваше медиа-приложение. Они принимаются вашим медиа-сеансом, который в конечном итоге делегирует команды медиаплееру.

Диаграмма, демонстрирующая взаимодействие между MediaSession и MediaController.
Рис. 1. Медиа-контроллер облегчает передачу команд из внешних источников в медиа-сеанс.

Когда контроллер собирается подключиться к вашему медиа-сеансу, вызывается метод onConnect() . Вы можете использовать предоставленную ControllerInfo , чтобы решить, принять или отклонить запрос. См. пример принятия запроса на подключение в разделе «Объявление пользовательских команд» .

После подключения контроллер может отправлять в сеанс команды воспроизведения. Затем сеанс делегирует эти команды игроку. Команды воспроизведения и списка воспроизведения, определенные в интерфейсе Player , автоматически обрабатываются сеансом.

Другие методы обратного вызова позволяют обрабатывать, например, запросы пользовательских команд и изменение списка воспроизведения . Эти обратные вызовы также включают объект ControllerInfo , поэтому вы можете изменить способ ответа на каждый запрос для каждого контроллера.

Изменить список воспроизведения

Медиа-сеанс может напрямую изменять список воспроизведения своего проигрывателя, как описано в руководстве ExoPlayer для списков воспроизведения . Контроллеры также могут изменять список воспроизведения, если контроллеру доступен COMMAND_SET_MEDIA_ITEM или COMMAND_CHANGE_MEDIA_ITEMS .

При добавлении новых элементов в список воспроизведения проигрывателю обычно требуются экземпляры MediaItem с определенным URI , чтобы их можно было воспроизводить. По умолчанию вновь добавленные элементы автоматически пересылаются в методы проигрывателя, такие как player.addMediaItem , если для них определен URI.

Если вы хотите настроить экземпляры MediaItem , добавленные в проигрыватель, вы можете переопределить onAddMediaItems() . Этот шаг необходим, если вы хотите поддерживать контроллеры, которые запрашивают носитель без определенного URI. Вместо этого MediaItem обычно имеет одно или несколько из следующих полей для описания запрошенного мультимедиа:

  • MediaItem.id : общий идентификатор, идентифицирующий носитель.
  • MediaItem.RequestMetadata.mediaUri : URI запроса, который может использовать пользовательскую схему и не обязательно воспроизводиться напрямую проигрывателем.
  • MediaItem.RequestMetadata.searchQuery : текстовый поисковый запрос, например, из Google Assistant.
  • MediaItem.MediaMetadata : структурированные метаданные, такие как «название» или «исполнитель».

Чтобы получить дополнительные параметры настройки для совершенно новых списков воспроизведения, вы можете дополнительно переопределить onSetMediaItems() , который позволяет вам определить начальный элемент и позицию в списке воспроизведения. Например, вы можете расширить один запрошенный элемент до всего списка воспроизведения и указать проигрывателю начать с индекса первоначально запрошенного элемента. Пример реализации onSetMediaItems() с этой функцией можно найти в демонстрационном приложении сеанса.

Управление настройками медиа-кнопки

Каждый контроллер, например System UI, Android Auto или Wear OS, может принимать собственные решения о том, какие кнопки показывать пользователю. Чтобы указать, какие элементы управления воспроизведением вы хотите предоставить пользователю, вы можете указать настройки кнопок мультимедиа в MediaSession . Эти настройки состоят из упорядоченного списка экземпляров CommandButton , каждый из которых определяет предпочтение кнопки в пользовательском интерфейсе.

Определить командные кнопки

Экземпляры CommandButton используются для определения предпочтений мультимедийных кнопок. Каждая кнопка определяет три аспекта желаемого элемента пользовательского интерфейса:

  1. Значок , определяющий внешний вид. При создании CommandButton.Builder для значка должна быть установлена ​​одна из предопределенных констант. Обратите внимание, что это не настоящий ресурс Bitmap или изображения. Универсальная константа помогает контроллерам выбирать подходящий ресурс для единообразного внешнего вида в их собственном пользовательском интерфейсе. Если ни одна из предопределенных констант значков не подходит для вашего варианта использования, вместо этого вы можете использовать setCustomIconResId .
  2. Команда , определяющая действие, запускаемое при взаимодействии пользователя с кнопкой. Вы можете использовать setPlayerCommand для Player.Command или setSessionCommand для предопределенного или пользовательского SessionCommand .
  3. Slot , определяющий, где должна быть размещена кнопка в пользовательском интерфейсе контроллера. Это поле является необязательным и устанавливается автоматически в зависимости от значка и команды . Например, он позволяет указать, что кнопка должна отображаться в области навигации «вперед» пользовательского интерфейса вместо области «переполнения» по умолчанию.

Котлин

val button =
  CommandButton.Builder(CommandButton.ICON_SKIP_FORWARD_15)
    .setSessionCommand(SessionCommand(CUSTOM_ACTION_ID, Bundle.EMPTY))
    .setSlots(CommandButton.SLOT_FORWARD)
    .build()

Ява

CommandButton button =
    new CommandButton.Builder(CommandButton.ICON_SKIP_FORWARD_15)
        .setSessionCommand(new SessionCommand(CUSTOM_ACTION_ID, Bundle.EMPTY))
        .setSlots(CommandButton.SLOT_FORWARD)
        .build();

Когда настройки медиа-кнопок определены, применяется следующий алгоритм:

  1. Для каждой кнопки CommandButton в настройках медиа-кнопки поместите кнопку в первый доступный и разрешенный слот .
  2. Если какой-либо из центральных, передних и задних слотов не заполнен кнопкой, добавьте для этого слота кнопки по умолчанию.

Вы можете использовать CommandButton.DisplayConstraints , чтобы создать предварительный просмотр того, как настройки кнопок мультимедиа будут разрешены в зависимости от ограничений отображения пользовательского интерфейса.

Установите настройки медиа-кнопки

Самый простой способ установить настройки медиа-кнопки — определить список при создании MediaSession . Альтернативно вы можете переопределить MediaSession.Callback.onConnect , чтобы настроить параметры мультимедийных кнопок для каждого подключенного контроллера.

Котлин

val mediaSession =
  MediaSession.Builder(context, player)
    .setMediaButtonPreferences(ImmutableList.of(likeButton, favoriteButton))
    .build()

Ява

MediaSession mediaSession =
  new MediaSession.Builder(context, player)
      .setMediaButtonPreferences(ImmutableList.of(likeButton, favoriteButton))
      .build();

Обновить настройки медиа-кнопки после взаимодействия с пользователем

После обработки взаимодействия с вашим плеером вы можете обновить кнопки, отображаемые в пользовательском интерфейсе контроллера. Типичным примером является кнопка-переключатель, которая меняет свой значок и действие после запуска действия, связанного с этой кнопкой. Чтобы обновить настройки медиа-кнопки, вы можете использовать MediaSession.setMediaButtonPreferences , чтобы обновить настройки для всех контроллеров или для конкретного контроллера:

Котлин

// Handle "favoritesButton" action, replace by opposite button
mediaSession.setMediaButtonPreferences(
  ImmutableList.of(likeButton, removeFromFavoritesButton))

Ява

// Handle "favoritesButton" action, replace by opposite button
mediaSession.setMediaButtonPreferences(
    ImmutableList.of(likeButton, removeFromFavoritesButton));

Добавляйте собственные команды и настраивайте поведение по умолчанию.

Доступные команды проигрывателя можно расширить с помощью пользовательских команд, а также можно перехватывать входящие команды проигрывателя и мультимедийные кнопки, чтобы изменить поведение по умолчанию.

Объявляйте и обрабатывайте пользовательские команды

Мультимедийные приложения могут определять собственные команды, которые, например, можно использовать в настройках мультимедийных кнопок . Например, вы можете реализовать кнопки, позволяющие пользователю сохранять элемент мультимедиа в списке избранных элементов. MediaController отправляет пользовательские команды, а MediaSession.Callback получает их.

Чтобы определить пользовательские команды, вам необходимо переопределить MediaSession.Callback.onConnect() чтобы установить доступные пользовательские команды для каждого подключенного контроллера.

Котлин

private class CustomMediaSessionCallback: MediaSession.Callback {
  // Configure commands available to the controller in onConnect()
  override fun onConnect(
    session: MediaSession,
    controller: MediaSession.ControllerInfo
  ): MediaSession.ConnectionResult {
    val sessionCommands = ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
        .add(SessionCommand(SAVE_TO_FAVORITES, Bundle.EMPTY))
        .build()
    return AcceptedResultBuilder(session)
        .setAvailableSessionCommands(sessionCommands)
        .build()
  }
}

Ява

class CustomMediaSessionCallback implements MediaSession.Callback {
  // Configure commands available to the controller in onConnect()
  @Override
  public ConnectionResult onConnect(
    MediaSession session,
    ControllerInfo controller) {
    SessionCommands sessionCommands =
        ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
            .add(new SessionCommand(SAVE_TO_FAVORITES, new Bundle()))
            .build();
    return new AcceptedResultBuilder(session)
        .setAvailableSessionCommands(sessionCommands)
        .build();
  }
}

Чтобы получать пользовательские запросы команд от MediaController , переопределите метод onCustomCommand() в Callback .

Котлин

private class CustomMediaSessionCallback: MediaSession.Callback {
  ...
  override fun onCustomCommand(
    session: MediaSession,
    controller: MediaSession.ControllerInfo,
    customCommand: SessionCommand,
    args: Bundle
  ): ListenableFuture<SessionResult> {
    if (customCommand.customAction == SAVE_TO_FAVORITES) {
      // Do custom logic here
      saveToFavorites(session.player.currentMediaItem)
      return Futures.immediateFuture(
        SessionResult(SessionResult.RESULT_SUCCESS)
      )
    }
    ...
  }
}

Ява

class CustomMediaSessionCallback implements MediaSession.Callback {
  ...
  @Override
  public ListenableFuture<SessionResult> onCustomCommand(
    MediaSession session, 
    ControllerInfo controller,
    SessionCommand customCommand,
    Bundle args
  ) {
    if(customCommand.customAction.equals(SAVE_TO_FAVORITES)) {
      // Do custom logic here
      saveToFavorites(session.getPlayer().getCurrentMediaItem());
      return Futures.immediateFuture(
        new SessionResult(SessionResult.RESULT_SUCCESS)
      );
    }
    ...
  }
}

Вы можете отслеживать, какой контроллер мультимедиа отправляет запрос, используя свойство packageName объекта MediaSession.ControllerInfo , которое передается в методы Callback . Это позволяет вам настроить поведение вашего приложения в ответ на заданную команду, если она исходит из системы, вашего собственного приложения или других клиентских приложений.

Настройте команды проигрывателя по умолчанию

Все команды по умолчанию и обработка состояний делегируются Player , находящемуся в MediaSession . Чтобы настроить поведение команды, определенной в интерфейсе Player , например play() или seekToNext() , оберните свой Player в ForwardingSimpleBasePlayer перед передачей его в MediaSession :

Котлин

val player = (logic to build a Player instance)

val forwardingPlayer = object : ForwardingSimpleBasePlayer(player) {
  // Customizations
}

val mediaSession = MediaSession.Builder(context, forwardingPlayer).build()

Ява

ExoPlayer player = (logic to build a Player instance)

ForwardingSimpleBasePlayer forwardingPlayer =
    new ForwardingSimpleBasePlayer(player) {
      // Customizations
    };

MediaSession mediaSession =
  new MediaSession.Builder(context, forwardingPlayer).build();

Дополнительные сведения о ForwardingSimpleBasePlayer см. в руководстве ExoPlayer по настройке .

Определить запрашивающий контроллер команды игрока

Когда вызов метода Player инициируется MediaController , вы можете определить источник происхождения с помощью MediaSession.controllerForCurrentRequest и получить ControllerInfo для текущего запроса:

Котлин

class CallerAwarePlayer(player: Player) :
  ForwardingSimpleBasePlayer(player) {

  override fun handleSeek(
    mediaItemIndex: Int,
    positionMs: Long,
    seekCommand: Int,
  ): ListenableFuture<*> {
    Log.d(
      "caller",
      "seek operation from package ${session.controllerForCurrentRequest?.packageName}",
    )
    return super.handleSeek(mediaItemIndex, positionMs, seekCommand)
  }
}

Ява

public class CallerAwarePlayer extends ForwardingSimpleBasePlayer {
  public CallerAwarePlayer(Player player) {
    super(player);
  }

  @Override
  protected ListenableFuture<?> handleSeek(
        int mediaItemIndex, long positionMs, int seekCommand) {
    Log.d(
        "caller",
        "seek operation from package: "
            + session.getControllerForCurrentRequest().getPackageName());
    return super.handleSeek(mediaItemIndex, positionMs, seekCommand);
  }
}

Настройте обработку медиа-кнопок

Мультимедийные кнопки — это аппаратные кнопки, имеющиеся на устройствах Android и других периферийных устройствах, например кнопка воспроизведения/паузы на гарнитуре Bluetooth. Media3 обрабатывает события медиа-кнопок, когда они поступают в сеанс, и вызывает соответствующий метод Player в проигрывателе сеанса .

Рекомендуется обрабатывать все входящие события медиа-кнопки в соответствующем методе Player . Для более сложных случаев использования события медиа-кнопки можно перехватить в MediaSession.Callback.onMediaButtonEvent(Intent) .

Обработка ошибок и отчетность

Существует два типа ошибок, которые генерирует сеанс и сообщает контроллерам. Неустранимые ошибки сообщают о техническом сбое воспроизведения сеансового проигрывателя, который прерывает воспроизведение. О фатальных ошибках автоматически сообщается контроллеру при их возникновении. Нефатальные ошибки — это нетехнические ошибки или ошибки политики, которые не прерывают воспроизведение и отправляются приложением на контроллеры вручную.

Фатальные ошибки воспроизведения

Игрок сообщает сеансу о фатальной ошибке воспроизведения, а затем сообщает контроллерам для вызова через Player.Listener.onPlayerError(PlaybackException) и Player.Listener.onPlayerErrorChanged(@Nullable PlaybackException) .

В таком случае состояние воспроизведения переходит в STATE_IDLE , а MediaController.getPlaybackError() возвращает PlaybackException , вызвавшее переход. Контроллер может проверить PlayerException.errorCode , чтобы получить информацию о причине ошибки.

Для обеспечения совместимости фатальная ошибка реплицируется в сеанс платформы путем перевода ее состояния в STATE_ERROR и установки кода ошибки и сообщения в соответствии с PlaybackException .

Настройка фатальных ошибок

Чтобы предоставить пользователю локализованную и содержательную информацию, код ошибки, сообщение об ошибке и дополнительные сведения о фатальной ошибке воспроизведения можно настроить с помощью ForwardingPlayer при построении сеанса:

Котлин

val forwardingPlayer = ErrorForwardingPlayer(player)
val session = MediaSession.Builder(context, forwardingPlayer).build()

Ява

Player forwardingPlayer = new ErrorForwardingPlayer(player);
MediaSession session =
    new MediaSession.Builder(context, forwardingPlayer).build();

Проигрыватель пересылки может использовать ForwardingSimpleBasePlayer , чтобы перехватить ошибку и настроить код ошибки, сообщение или дополнительные сведения. Таким же образом вы также можете генерировать новые ошибки, которых нет в исходном плеере:

Котлин

class ErrorForwardingPlayer (private val context: Context, player: Player) :
    ForwardingSimpleBasePlayer(player) {

  override fun getState(): State {
    var state = super.getState()
    if (state.playerError != null) {
      state =
        state.buildUpon()
          .setPlayerError(customizePlaybackException(state.playerError!!))
          .build()
    }
    return state
  }

  fun customizePlaybackException(error: PlaybackException): PlaybackException {
    val buttonLabel: String
    val errorMessage: String
    when (error.errorCode) {
      PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW -> {
        buttonLabel = context.getString(R.string.err_button_label_restart_stream)
        errorMessage = context.getString(R.string.err_msg_behind_live_window)
      }
      else -> {
        buttonLabel = context.getString(R.string.err_button_label_ok)
        errorMessage = context.getString(R.string.err_message_default)
      }
    }
    val extras = Bundle()
    extras.putString("button_label", buttonLabel)
    return PlaybackException(errorMessage, error.cause, error.errorCode, extras)
  }
}

Ява

class ErrorForwardingPlayer extends ForwardingSimpleBasePlayer {

  private final Context context;

  public ErrorForwardingPlayer(Context context, Player player) {
    super(player);
    this.context = context;
  }

  @Override
  protected State getState() {
    State state = super.getState();
    if (state.playerError != null) {
      state =
          state.buildUpon()
              .setPlayerError(customizePlaybackException(state.playerError))
              .build();
    }
    return state;
  }

  private PlaybackException customizePlaybackException(PlaybackException error) {
    String buttonLabel;
    String errorMessage;
    switch (error.errorCode) {
      case PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW:
        buttonLabel = context.getString(R.string.err_button_label_restart_stream);
        errorMessage = context.getString(R.string.err_msg_behind_live_window);
        break;
      default:
        buttonLabel = context.getString(R.string.err_button_label_ok);
        errorMessage = context.getString(R.string.err_message_default);
        break;
    }
    Bundle extras = new Bundle();
    extras.putString("button_label", buttonLabel);
    return new PlaybackException(errorMessage, error.getCause(), error.errorCode, extras);
  }
}

Нефатальные ошибки

Нефатальные ошибки, которые не происходят из-за технического исключения, могут быть отправлены приложением всем или конкретному контроллеру:

Котлин

val sessionError = SessionError(
  SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED,
  context.getString(R.string.error_message_authentication_expired),
)

// Option 1: Sending a nonfatal error to all controllers.
mediaSession.sendError(sessionError)

// Option 2: Sending a nonfatal error to the media notification controller only
// to set the error code and error message in the playback state of the platform
// media session.
mediaSession.mediaNotificationControllerInfo?.let {
  mediaSession.sendError(it, sessionError)
}

Ява

SessionError sessionError = new SessionError(
    SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED,
    context.getString(R.string.error_message_authentication_expired));

// Option 1: Sending a nonfatal error to all controllers.
mediaSession.sendError(sessionError);

// Option 2: Sending a nonfatal error to the media notification controller only
// to set the error code and error message in the playback state of the platform
// media session.
ControllerInfo mediaNotificationControllerInfo =
    mediaSession.getMediaNotificationControllerInfo();
if (mediaNotificationControllerInfo != null) {
  mediaSession.sendError(mediaNotificationControllerInfo, sessionError);
}

Когда нефатальная ошибка отправляется на контроллер медиа-уведомлений, код ошибки и сообщение об ошибке реплицируются в медиа-сеанс платформы, а PlaybackState.state не изменяется на STATE_ERROR .

Получать нефатальные ошибки

MediaController получает нефатальную ошибку, реализуя MediaController.Listener.onError :

Котлин

val future = MediaController.Builder(context, sessionToken)
  .setListener(object : MediaController.Listener {
    override fun onError(controller: MediaController, sessionError: SessionError) {
      // Handle nonfatal error.
    }
  })
  .buildAsync()

Ява

MediaController.Builder future =
    new MediaController.Builder(context, sessionToken)
        .setListener(
            new MediaController.Listener() {
              @Override
              public void onError(MediaController controller, SessionError sessionError) {
                // Handle nonfatal error.
              }
            });

,

Медиа-сеансы предоставляют универсальный способ взаимодействия с аудио- или видеоплеером. В Media3 проигрывателем по умолчанию является класс ExoPlayer , реализующий интерфейс Player . Подключение мультимедийного сеанса к проигрывателю позволяет приложению объявлять воспроизведение мультимедиа извне и получать команды воспроизведения из внешних источников.

Команды могут исходить от физических кнопок, таких как кнопка воспроизведения на гарнитуре или пульте дистанционного управления телевизора. Они также могут поступать из клиентских приложений, имеющих медиа-контроллер, например, отдавая команду «пауза» Google Assistant. Медиа-сеанс делегирует эти команды проигрывателю мультимедийного приложения.

Когда выбирать медиасессию

Когда вы реализуете MediaSession , вы позволяете пользователям управлять воспроизведением:

  • Через свои наушники . Часто пользователи могут использовать кнопки или сенсорные действия на своих наушниках, чтобы воспроизвести или приостановить воспроизведение мультимедиа или перейти к следующему или предыдущему треку.
  • Разговаривая с Google Assistant . Распространенным шаблоном является произнесение «ОК, Google, пауза», чтобы приостановить воспроизведение любого мультимедиа, которое в данный момент воспроизводится на устройстве.
  • Через свои часы Wear OS . Это упрощает доступ к наиболее распространенным элементам управления воспроизведением во время игры на телефоне.
  • Через средства управления СМИ . В этой карусели показаны элементы управления для каждого текущего сеанса мультимедиа.
  • По телевизору . Позволяет выполнять действия с физическими кнопками воспроизведения, управлением воспроизведением на платформе и управлением питанием (например, если телевизор, звуковая панель или аудио/видео-ресивер выключаются или переключается вход, воспроизведение должно остановиться в приложении).
  • И любые другие внешние процессы, которые должны влиять на воспроизведение.

Это отлично подходит для многих случаев использования. В частности, вам следует рассмотреть возможность использования MediaSession в следующих случаях:

  • Вы транслируете длинный видеоконтент , например фильмы или прямые телепередачи.
  • Вы транслируете длинный аудиоконтент , например подкасты или музыкальные плейлисты.
  • Вы создаете телевизионное приложение .

Однако не все варианты использования хорошо подходят для MediaSession . Возможно, вам захочется использовать только Player в следующих случаях:

  • Вы показываете краткий контент , для которого не требуется никакого внешнего управления или фонового воспроизведения.
  • Нет ни одного активного видео, например, пользователь прокручивает список, и на экране одновременно отображается несколько видео .
  • Вы воспроизводите одноразовое вводное или поясняющее видео , которое, как вы ожидаете, ваш пользователь будет активно смотреть без необходимости использования внешних элементов управления воспроизведением.
  • Ваш контент чувствителен к конфиденциальности, и вы не хотите, чтобы внешние процессы получали доступ к метаданным мультимедиа (например, режим инкогнито в браузере).

Если ваш вариант использования не соответствует ни одному из перечисленных выше, подумайте, согласны ли вы с тем, что ваше приложение продолжает воспроизведение, когда пользователь не взаимодействует активно с контентом. Если ответ положительный, вы, вероятно, захотите выбрать MediaSession . Если ответ отрицательный, вы, вероятно, захотите вместо этого использовать Player .

Создать медиа-сессию

Медиа-сессия живет вместе с игроком, которым она управляет. Вы можете создать медиасеанс с помощью объекта Context и Player . Вам следует создать и инициализировать медиа-сеанс, когда это необходимо, например, метод жизненного цикла onStart() или onResume() Activity или Fragment или метод onCreate() Service , которому принадлежит медиа-сеанс и связанный с ним проигрыватель.

Чтобы создать медиа-сеанс, инициализируйте Player и передайте его MediaSession.Builder следующим образом:

Котлин

val player = ExoPlayer.Builder(context).build()
val mediaSession = MediaSession.Builder(context, player).build()

Ява

ExoPlayer player = new ExoPlayer.Builder(context).build();
MediaSession mediaSession = new MediaSession.Builder(context, player).build();

Автоматическая обработка состояний

Библиотека Media3 автоматически обновляет медиа-сессию, используя состояние проигрывателя. Таким образом, вам не нужно вручную обрабатывать сопоставление игрока с сеансом.

Это отличается от сеанса мультимедиа платформы, где вам нужно было создать и поддерживать PlaybackState независимо от самого проигрывателя, например, для указания любых ошибок .

Уникальный идентификатор сеанса

По умолчанию MediaSession.Builder создает сеанс с пустой строкой в ​​качестве идентификатора сеанса. Этого достаточно, если приложение намерено создать только один экземпляр сеанса, что является наиболее распространенным случаем.

Если приложение хочет управлять несколькими экземплярами сеанса одновременно, оно должно гарантировать, что идентификатор каждого сеанса уникален. Идентификатор сеанса можно установить при создании сеанса с помощью MediaSession.Builder.setId(String id) .

Если вы видите исключение IllegalStateException приводящее к сбою вашего приложения с сообщением об ошибке IllegalStateException: Session ID must be unique. ID= , то вполне вероятно, что сеанс был неожиданно создан до того, как был освобожден ранее созданный экземпляр с тем же идентификатором. Чтобы избежать утечки сеансов из-за программной ошибки, такие случаи обнаруживаются и уведомляются путем создания исключения.

Предоставить контроль другим клиентам

Медиа-сеанс является ключом к управлению воспроизведением. Он позволяет вам перенаправлять команды из внешних источников на проигрыватель, который выполняет работу по воспроизведению вашего мультимедиа. Этими источниками могут быть физические кнопки, такие как кнопка воспроизведения на гарнитуре или пульте дистанционного управления телевизора, или косвенные команды, такие как команда Google Assistant «пауза». Аналогично, вы можете предоставить доступ к системе Android, чтобы упростить управление уведомлениями и экраном блокировки, или к часам Wear OS, чтобы вы могли управлять воспроизведением с циферблата. Внешние клиенты могут использовать медиа-контроллер для подачи команд воспроизведения в ваше медиа-приложение. Они принимаются вашим медиа-сеансом, который в конечном итоге делегирует команды медиаплееру.

Диаграмма, демонстрирующая взаимодействие между MediaSession и MediaController.
Рис. 1. Медиа-контроллер облегчает передачу команд из внешних источников в медиа-сеанс.

Когда контроллер собирается подключиться к вашему медиа-сеансу, вызывается метод onConnect() . Вы можете использовать предоставленную ControllerInfo , чтобы решить, принять или отклонить запрос. См. пример принятия запроса на подключение в разделе «Объявление пользовательских команд» .

После подключения контроллер может отправлять в сеанс команды воспроизведения. Затем сеанс делегирует эти команды игроку. Команды воспроизведения и списка воспроизведения, определенные в интерфейсе Player , автоматически обрабатываются сеансом.

Другие методы обратного вызова позволяют обрабатывать, например, запросы пользовательских команд и изменение списка воспроизведения . Эти обратные вызовы также включают объект ControllerInfo , поэтому вы можете изменить способ ответа на каждый запрос для каждого контроллера.

Изменить список воспроизведения

Медиа-сеанс может напрямую изменять список воспроизведения своего проигрывателя, как описано в руководстве ExoPlayer для списков воспроизведения . Контроллеры также могут изменять список воспроизведения, если контроллеру доступен COMMAND_SET_MEDIA_ITEM или COMMAND_CHANGE_MEDIA_ITEMS .

При добавлении новых элементов в список воспроизведения проигрывателю обычно требуются экземпляры MediaItem с определенным URI , чтобы их можно было воспроизводить. По умолчанию вновь добавленные элементы автоматически пересылаются в методы проигрывателя, такие как player.addMediaItem , если для них определен URI.

Если вы хотите настроить экземпляры MediaItem , добавленные в проигрыватель, вы можете переопределить onAddMediaItems() . Этот шаг необходим, если вы хотите поддерживать контроллеры, которые запрашивают носитель без определенного URI. Вместо этого MediaItem обычно имеет одно или несколько из следующих полей для описания запрошенного мультимедиа:

  • MediaItem.id : общий идентификатор, идентифицирующий носитель.
  • MediaItem.RequestMetadata.mediaUri : URI запроса, который может использовать пользовательскую схему и не обязательно воспроизводиться напрямую проигрывателем.
  • MediaItem.RequestMetadata.searchQuery : текстовый поисковый запрос, например, из Google Assistant.
  • MediaItem.MediaMetadata : структурированные метаданные, такие как «название» или «исполнитель».

Чтобы получить дополнительные параметры настройки для совершенно новых списков воспроизведения, вы можете дополнительно переопределить onSetMediaItems() , который позволяет вам определить начальный элемент и позицию в списке воспроизведения. Например, вы можете расширить один запрошенный элемент до всего списка воспроизведения и указать проигрывателю начать с индекса первоначально запрошенного элемента. Пример реализации onSetMediaItems() с этой функцией можно найти в демонстрационном приложении сеанса.

Управление настройками медиа-кнопки

Каждый контроллер, например System UI, Android Auto или Wear OS, может принимать собственные решения о том, какие кнопки показывать пользователю. Чтобы указать, какие элементы управления воспроизведением вы хотите предоставить пользователю, вы можете указать настройки кнопок мультимедиа в MediaSession . Эти настройки состоят из упорядоченного списка экземпляров CommandButton , каждый из которых определяет предпочтение кнопки в пользовательском интерфейсе.

Определить командные кнопки

Экземпляры CommandButton используются для определения предпочтений мультимедийных кнопок. Каждая кнопка определяет три аспекта желаемого элемента пользовательского интерфейса:

  1. Значок , определяющий внешний вид. При создании CommandButton.Builder для значка должна быть установлена ​​одна из предопределенных констант. Обратите внимание, что это не настоящий ресурс Bitmap или изображения. Универсальная константа помогает контроллерам выбирать подходящий ресурс для единообразного внешнего вида в их собственном пользовательском интерфейсе. Если ни одна из предопределенных констант значков не подходит для вашего варианта использования, вместо этого вы можете использовать setCustomIconResId .
  2. Команда , определяющая действие, запускаемое при взаимодействии пользователя с кнопкой. Вы можете использовать setPlayerCommand для Player.Command или setSessionCommand для предопределенного или пользовательского SessionCommand .
  3. Slot , определяющий, где должна быть размещена кнопка в пользовательском интерфейсе контроллера. Это поле является необязательным и устанавливается автоматически в зависимости от значка и команды . Например, он позволяет указать, что кнопка должна отображаться в области навигации «вперед» пользовательского интерфейса вместо области «переполнения» по умолчанию.

Котлин

val button =
  CommandButton.Builder(CommandButton.ICON_SKIP_FORWARD_15)
    .setSessionCommand(SessionCommand(CUSTOM_ACTION_ID, Bundle.EMPTY))
    .setSlots(CommandButton.SLOT_FORWARD)
    .build()

Ява

CommandButton button =
    new CommandButton.Builder(CommandButton.ICON_SKIP_FORWARD_15)
        .setSessionCommand(new SessionCommand(CUSTOM_ACTION_ID, Bundle.EMPTY))
        .setSlots(CommandButton.SLOT_FORWARD)
        .build();

Когда настройки медиа-кнопок определены, применяется следующий алгоритм:

  1. Для каждой кнопки CommandButton в настройках медиа-кнопки поместите кнопку в первый доступный и разрешенный слот .
  2. Если какой-либо из центральных, передних и задних слотов не заполнен кнопкой, добавьте для этого слота кнопки по умолчанию.

Вы можете использовать CommandButton.DisplayConstraints , чтобы создать предварительный просмотр того, как настройки кнопок мультимедиа будут разрешены в зависимости от ограничений отображения пользовательского интерфейса.

Установите настройки медиа-кнопки

Самый простой способ установить настройки медиа-кнопки — определить список при создании MediaSession . Альтернативно вы можете переопределить MediaSession.Callback.onConnect , чтобы настроить параметры мультимедийных кнопок для каждого подключенного контроллера.

Котлин

val mediaSession =
  MediaSession.Builder(context, player)
    .setMediaButtonPreferences(ImmutableList.of(likeButton, favoriteButton))
    .build()

Ява

MediaSession mediaSession =
  new MediaSession.Builder(context, player)
      .setMediaButtonPreferences(ImmutableList.of(likeButton, favoriteButton))
      .build();

Обновить настройки медиа-кнопки после взаимодействия с пользователем

После обработки взаимодействия с вашим плеером вы можете обновить кнопки, отображаемые в пользовательском интерфейсе контроллера. Типичным примером является кнопка-переключатель, которая меняет свой значок и действие после запуска действия, связанного с этой кнопкой. Чтобы обновить настройки медиа-кнопки, вы можете использовать MediaSession.setMediaButtonPreferences , чтобы обновить настройки для всех контроллеров или для конкретного контроллера:

Котлин

// Handle "favoritesButton" action, replace by opposite button
mediaSession.setMediaButtonPreferences(
  ImmutableList.of(likeButton, removeFromFavoritesButton))

Ява

// Handle "favoritesButton" action, replace by opposite button
mediaSession.setMediaButtonPreferences(
    ImmutableList.of(likeButton, removeFromFavoritesButton));

Добавляйте собственные команды и настраивайте поведение по умолчанию.

Доступные команды проигрывателя можно расширить с помощью пользовательских команд, а также можно перехватывать входящие команды проигрывателя и мультимедийные кнопки, чтобы изменить поведение по умолчанию.

Объявляйте и обрабатывайте пользовательские команды

Мультимедийные приложения могут определять собственные команды, которые, например, можно использовать в настройках мультимедийных кнопок . Например, вы можете реализовать кнопки, позволяющие пользователю сохранять элемент мультимедиа в списке избранных элементов. MediaController отправляет пользовательские команды, а MediaSession.Callback получает их.

Чтобы определить пользовательские команды, вам необходимо переопределить MediaSession.Callback.onConnect() чтобы установить доступные пользовательские команды для каждого подключенного контроллера.

Котлин

private class CustomMediaSessionCallback: MediaSession.Callback {
  // Configure commands available to the controller in onConnect()
  override fun onConnect(
    session: MediaSession,
    controller: MediaSession.ControllerInfo
  ): MediaSession.ConnectionResult {
    val sessionCommands = ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
        .add(SessionCommand(SAVE_TO_FAVORITES, Bundle.EMPTY))
        .build()
    return AcceptedResultBuilder(session)
        .setAvailableSessionCommands(sessionCommands)
        .build()
  }
}

Ява

class CustomMediaSessionCallback implements MediaSession.Callback {
  // Configure commands available to the controller in onConnect()
  @Override
  public ConnectionResult onConnect(
    MediaSession session,
    ControllerInfo controller) {
    SessionCommands sessionCommands =
        ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
            .add(new SessionCommand(SAVE_TO_FAVORITES, new Bundle()))
            .build();
    return new AcceptedResultBuilder(session)
        .setAvailableSessionCommands(sessionCommands)
        .build();
  }
}

Чтобы получать пользовательские запросы команд от MediaController , переопределите метод onCustomCommand() в Callback .

Котлин

private class CustomMediaSessionCallback: MediaSession.Callback {
  ...
  override fun onCustomCommand(
    session: MediaSession,
    controller: MediaSession.ControllerInfo,
    customCommand: SessionCommand,
    args: Bundle
  ): ListenableFuture<SessionResult> {
    if (customCommand.customAction == SAVE_TO_FAVORITES) {
      // Do custom logic here
      saveToFavorites(session.player.currentMediaItem)
      return Futures.immediateFuture(
        SessionResult(SessionResult.RESULT_SUCCESS)
      )
    }
    ...
  }
}

Ява

class CustomMediaSessionCallback implements MediaSession.Callback {
  ...
  @Override
  public ListenableFuture<SessionResult> onCustomCommand(
    MediaSession session, 
    ControllerInfo controller,
    SessionCommand customCommand,
    Bundle args
  ) {
    if(customCommand.customAction.equals(SAVE_TO_FAVORITES)) {
      // Do custom logic here
      saveToFavorites(session.getPlayer().getCurrentMediaItem());
      return Futures.immediateFuture(
        new SessionResult(SessionResult.RESULT_SUCCESS)
      );
    }
    ...
  }
}

Вы можете отслеживать, какой контроллер мультимедиа отправляет запрос, используя свойство packageName объекта MediaSession.ControllerInfo , которое передается в методы Callback . Это позволяет вам настроить поведение вашего приложения в ответ на заданную команду, если она исходит из системы, вашего собственного приложения или других клиентских приложений.

Настройте команды проигрывателя по умолчанию

Все команды по умолчанию и обработка состояний делегируются Player , находящемуся в MediaSession . Чтобы настроить поведение команды, определенной в интерфейсе Player , например play() или seekToNext() , оберните свой Player в ForwardingSimpleBasePlayer перед передачей его в MediaSession :

Котлин

val player = (logic to build a Player instance)

val forwardingPlayer = object : ForwardingSimpleBasePlayer(player) {
  // Customizations
}

val mediaSession = MediaSession.Builder(context, forwardingPlayer).build()

Ява

ExoPlayer player = (logic to build a Player instance)

ForwardingSimpleBasePlayer forwardingPlayer =
    new ForwardingSimpleBasePlayer(player) {
      // Customizations
    };

MediaSession mediaSession =
  new MediaSession.Builder(context, forwardingPlayer).build();

Дополнительные сведения о ForwardingSimpleBasePlayer см. в руководстве ExoPlayer по настройке .

Определить запрашивающий контроллер команды игрока

Когда вызов метода Player инициируется MediaController , вы можете определить источник происхождения с помощью MediaSession.controllerForCurrentRequest и получить ControllerInfo для текущего запроса:

Котлин

class CallerAwarePlayer(player: Player) :
  ForwardingSimpleBasePlayer(player) {

  override fun handleSeek(
    mediaItemIndex: Int,
    positionMs: Long,
    seekCommand: Int,
  ): ListenableFuture<*> {
    Log.d(
      "caller",
      "seek operation from package ${session.controllerForCurrentRequest?.packageName}",
    )
    return super.handleSeek(mediaItemIndex, positionMs, seekCommand)
  }
}

Ява

public class CallerAwarePlayer extends ForwardingSimpleBasePlayer {
  public CallerAwarePlayer(Player player) {
    super(player);
  }

  @Override
  protected ListenableFuture<?> handleSeek(
        int mediaItemIndex, long positionMs, int seekCommand) {
    Log.d(
        "caller",
        "seek operation from package: "
            + session.getControllerForCurrentRequest().getPackageName());
    return super.handleSeek(mediaItemIndex, positionMs, seekCommand);
  }
}

Настройте обработку медиа-кнопок

Мультимедийные кнопки — это аппаратные кнопки, имеющиеся на устройствах Android и других периферийных устройствах, например кнопка воспроизведения/паузы на гарнитуре Bluetooth. Media3 обрабатывает события медиа-кнопок, когда они поступают в сеанс, и вызывает соответствующий метод Player в проигрывателе сеанса .

Рекомендуется обрабатывать все входящие события медиа-кнопки в соответствующем методе Player . Для более сложных случаев использования события медиа-кнопки можно перехватить в MediaSession.Callback.onMediaButtonEvent(Intent) .

Обработка ошибок и отчетность

Существует два типа ошибок, которые генерирует сеанс и сообщает контроллерам. Неустранимые ошибки сообщают о техническом сбое воспроизведения сеансового проигрывателя, который прерывает воспроизведение. О фатальных ошибках автоматически сообщается контроллеру при их возникновении. Нефатальные ошибки — это нетехнические ошибки или ошибки политики, которые не прерывают воспроизведение и отправляются приложением на контроллеры вручную.

Фатальные ошибки воспроизведения

Игрок сообщает сеансу о фатальной ошибке воспроизведения, а затем сообщает контроллерам для вызова через Player.Listener.onPlayerError(PlaybackException) и Player.Listener.onPlayerErrorChanged(@Nullable PlaybackException) .

В таком случае состояние воспроизведения переходит в STATE_IDLE , а MediaController.getPlaybackError() возвращает PlaybackException , вызвавшее переход. Контроллер может проверить PlayerException.errorCode , чтобы получить информацию о причине ошибки.

Для обеспечения совместимости фатальная ошибка реплицируется в сеанс платформы путем перевода ее состояния в STATE_ERROR и установки кода ошибки и сообщения в соответствии с PlaybackException .

Настройка фатальных ошибок

Чтобы предоставить пользователю локализованную и содержательную информацию, код ошибки, сообщение об ошибке и дополнительные сведения о фатальной ошибке воспроизведения можно настроить с помощью ForwardingPlayer при построении сеанса:

Котлин

val forwardingPlayer = ErrorForwardingPlayer(player)
val session = MediaSession.Builder(context, forwardingPlayer).build()

Ява

Player forwardingPlayer = new ErrorForwardingPlayer(player);
MediaSession session =
    new MediaSession.Builder(context, forwardingPlayer).build();

Проигрыватель пересылки может использовать ForwardingSimpleBasePlayer , чтобы перехватить ошибку и настроить код ошибки, сообщение или дополнительные сведения. Таким же образом вы также можете генерировать новые ошибки, которых нет в исходном плеере:

Котлин

class ErrorForwardingPlayer (private val context: Context, player: Player) :
    ForwardingSimpleBasePlayer(player) {

  override fun getState(): State {
    var state = super.getState()
    if (state.playerError != null) {
      state =
        state.buildUpon()
          .setPlayerError(customizePlaybackException(state.playerError!!))
          .build()
    }
    return state
  }

  fun customizePlaybackException(error: PlaybackException): PlaybackException {
    val buttonLabel: String
    val errorMessage: String
    when (error.errorCode) {
      PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW -> {
        buttonLabel = context.getString(R.string.err_button_label_restart_stream)
        errorMessage = context.getString(R.string.err_msg_behind_live_window)
      }
      else -> {
        buttonLabel = context.getString(R.string.err_button_label_ok)
        errorMessage = context.getString(R.string.err_message_default)
      }
    }
    val extras = Bundle()
    extras.putString("button_label", buttonLabel)
    return PlaybackException(errorMessage, error.cause, error.errorCode, extras)
  }
}

Ява

class ErrorForwardingPlayer extends ForwardingSimpleBasePlayer {

  private final Context context;

  public ErrorForwardingPlayer(Context context, Player player) {
    super(player);
    this.context = context;
  }

  @Override
  protected State getState() {
    State state = super.getState();
    if (state.playerError != null) {
      state =
          state.buildUpon()
              .setPlayerError(customizePlaybackException(state.playerError))
              .build();
    }
    return state;
  }

  private PlaybackException customizePlaybackException(PlaybackException error) {
    String buttonLabel;
    String errorMessage;
    switch (error.errorCode) {
      case PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW:
        buttonLabel = context.getString(R.string.err_button_label_restart_stream);
        errorMessage = context.getString(R.string.err_msg_behind_live_window);
        break;
      default:
        buttonLabel = context.getString(R.string.err_button_label_ok);
        errorMessage = context.getString(R.string.err_message_default);
        break;
    }
    Bundle extras = new Bundle();
    extras.putString("button_label", buttonLabel);
    return new PlaybackException(errorMessage, error.getCause(), error.errorCode, extras);
  }
}

Нефатальные ошибки

Нефатальные ошибки, которые не происходят из-за технического исключения, могут быть отправлены приложением всем или конкретному контроллеру:

Котлин

val sessionError = SessionError(
  SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED,
  context.getString(R.string.error_message_authentication_expired),
)

// Option 1: Sending a nonfatal error to all controllers.
mediaSession.sendError(sessionError)

// Option 2: Sending a nonfatal error to the media notification controller only
// to set the error code and error message in the playback state of the platform
// media session.
mediaSession.mediaNotificationControllerInfo?.let {
  mediaSession.sendError(it, sessionError)
}

Ява

SessionError sessionError = new SessionError(
    SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED,
    context.getString(R.string.error_message_authentication_expired));

// Option 1: Sending a nonfatal error to all controllers.
mediaSession.sendError(sessionError);

// Option 2: Sending a nonfatal error to the media notification controller only
// to set the error code and error message in the playback state of the platform
// media session.
ControllerInfo mediaNotificationControllerInfo =
    mediaSession.getMediaNotificationControllerInfo();
if (mediaNotificationControllerInfo != null) {
  mediaSession.sendError(mediaNotificationControllerInfo, sessionError);
}

Когда нефатальная ошибка отправляется на контроллер медиа-уведомлений, код ошибки и сообщение об ошибке реплицируются в медиа-сеанс платформы, а PlaybackState.state не изменяется на STATE_ERROR .

Получать нефатальные ошибки

MediaController получает нефатальную ошибку, реализуя MediaController.Listener.onError :

Котлин

val future = MediaController.Builder(context, sessionToken)
  .setListener(object : MediaController.Listener {
    override fun onError(controller: MediaController, sessionError: SessionError) {
      // Handle nonfatal error.
    }
  })
  .buildAsync()

Ява

MediaController.Builder future =
    new MediaController.Builder(context, sessionToken)
        .setListener(
            new MediaController.Listener() {
              @Override
              public void onError(MediaController controller, SessionError sessionError) {
                // Handle nonfatal error.
              }
            });