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

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

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

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

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

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

Это отлично подходит для многих случаев использования. В частности, вам следует серьезно рассмотреть использование 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. Icon , определяющий внешний вид. Значок должен быть установлен на одну из предопределенных констант при создании CommandButton.Builder . Обратите внимание, что это не фактический ресурс Bitmap или изображения. Общая константа помогает контроллерам выбирать подходящий ресурс для согласованного внешнего вида и поведения в пределах их собственного пользовательского интерфейса. Если ни одна из предопределенных констант значка не подходит для вашего варианта использования, вы можете использовать setCustomIconResId .
  2. Команда , определяющая действие, которое будет запущено, когда пользователь взаимодействует с кнопкой. Вы можете использовать setPlayerCommand для Player.Command или setSessionCommand для предопределенного или пользовательского SessionCommand .
  3. Слот , определяющий, где должна быть размещена кнопка в пользовательском интерфейсе контроллера. Это поле является необязательным и автоматически устанавливается на основе значка и команды . Например, оно позволяет указать, что кнопка должна отображаться в области навигации «вперед» пользовательского интерфейса вместо области «переполнения» по умолчанию.

Котлин

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 . Это обеспечивает более легкий доступ к наиболее распространенным элементам управления воспроизведением во время игры на телефоне.
  • Через элементы управления мультимедиа . Эта карусель показывает элементы управления для каждого запущенного сеанса мультимедиа.
  • На ТВ . Позволяет выполнять действия с физическими кнопками воспроизведения, управлять воспроизведением на платформе и управлять питанием (например, если телевизор, звуковая панель или A/V-ресивер выключаются или переключается вход, воспроизведение должно остановиться в приложении).
  • И любые другие внешние процессы, которые должны влиять на воспроизведение.

Это отлично подходит для многих случаев использования. В частности, вам следует серьезно рассмотреть использование 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. Icon , определяющий внешний вид. Значок должен быть установлен на одну из предопределенных констант при создании CommandButton.Builder . Обратите внимание, что это не фактический ресурс Bitmap или изображения. Общая константа помогает контроллерам выбирать подходящий ресурс для согласованного внешнего вида и поведения в пределах их собственного пользовательского интерфейса. Если ни одна из предопределенных констант значка не подходит для вашего варианта использования, вы можете использовать setCustomIconResId .
  2. Команда , определяющая действие, которое будет запущено, когда пользователь взаимодействует с кнопкой. Вы можете использовать setPlayerCommand для Player.Command или setSessionCommand для предопределенного или пользовательского SessionCommand .
  3. Слот , определяющий, где должна быть размещена кнопка в пользовательском интерфейсе контроллера. Это поле является необязательным и автоматически устанавливается на основе значка и команды . Например, оно позволяет указать, что кнопка должна отображаться в области навигации «вперед» пользовательского интерфейса вместо области «переполнения» по умолчанию.

Котлин

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.
              }
            });