Медиа-сеансы предоставляют универсальный способ взаимодействия с аудио- или видеоплеером. В Media3 проигрывателем по умолчанию является класс ExoPlayer
, реализующий интерфейс Player
. Подключение мультимедийного сеанса к проигрывателю позволяет приложению объявлять воспроизведение мультимедиа извне и получать команды воспроизведения из внешних источников.
Команды могут исходить от физических кнопок, таких как кнопка воспроизведения на гарнитуре или пульте дистанционного управления телевизора. Они также могут поступать из клиентских приложений, имеющих медиа-контроллер, например, отдавая команду «пауза» Google Assistant. Медиа-сеанс делегирует эти команды проигрывателю мультимедийного приложения.
Когда выбирать медиасессию
Когда вы реализуете MediaSession
, вы позволяете пользователям управлять воспроизведением:
- Через свои наушники . Часто существуют кнопки или сенсорные действия, которые пользователь может выполнять на своих наушниках, чтобы воспроизвести или приостановить воспроизведение мультимедиа или перейти к следующему или предыдущему треку.
- Разговаривая с Google Ассистентом . Распространенным шаблоном является произнесение «ОК, 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 автоматически обновляет медиа-сессию, используя состояние проигрывателя. Таким образом, вам не нужно вручную обрабатывать сопоставление игрока с сеансом.
Это отход от устаревшего подхода, при котором вам нужно было создавать и поддерживать PlaybackStateCompat
независимо от самого проигрывателя, например, для указания любых ошибок.
Уникальный идентификатор сеанса
По умолчанию MediaSession.Builder
создает сеанс с пустой строкой в качестве идентификатора сеанса. Этого достаточно, если приложение намерено создать только один экземпляр сеанса, что является наиболее распространенным случаем.
Если приложение хочет управлять несколькими экземплярами сеанса одновременно, оно должно гарантировать, что идентификатор каждого сеанса уникален. Идентификатор сеанса можно установить при создании сеанса с помощью MediaSession.Builder.setId(String id)
.
Если вы видите IllegalStateException
, приводящее к сбою вашего приложения с сообщением об ошибке IllegalStateException: Session ID must be unique. ID=
то вполне вероятно, что сеанс был неожиданно создан до того, как был освобожден ранее созданный экземпляр с тем же идентификатором. Чтобы избежать утечки сеансов из-за программной ошибки, такие случаи обнаруживаются и уведомляются путем создания исключения.
Предоставить контроль другим клиентам
Медиа-сеанс является ключом к управлению воспроизведением. Он позволяет вам перенаправлять команды из внешних источников на проигрыватель, который выполняет работу по воспроизведению вашего мультимедиа. Этими источниками могут быть физические кнопки, такие как кнопка воспроизведения на гарнитуре или пульте дистанционного управления телевизора, или косвенные команды, такие как команда Google Assistant «пауза». Аналогично, вы можете предоставить доступ к системе Android, чтобы упростить управление уведомлениями и экраном блокировки, или к часам Wear OS, чтобы вы могли управлять воспроизведением с циферблата. Внешние клиенты могут использовать медиа-контроллер для подачи команд воспроизведения в ваше медиа-приложение. Они принимаются вашим медиа-сеансом, который в конечном итоге делегирует команды медиаплееру.
Когда контроллер собирается подключиться к вашему медиа-сеансу, вызывается метод 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()
с этой функцией можно найти в демонстрационном приложении сеанса.
Управление пользовательским макетом и пользовательскими командами
В следующих разделах описывается, как рекламировать настраиваемый макет настраиваемых командных кнопок клиентским приложениям и разрешать контроллерам отправлять настраиваемые команды.
Определите собственный макет сеанса
Чтобы указать клиентским приложениям, какие элементы управления воспроизведением вы хотите предоставить пользователю, задайте собственный макет сеанса при создании MediaSession
в методе onCreate()
вашего сервиса.
Котлин
override fun onCreate() { super.onCreate() val likeButton = CommandButton.Builder() .setDisplayName("Like") .setIconResId(R.drawable.like_icon) .setSessionCommand(SessionCommand(SessionCommand.COMMAND_CODE_SESSION_SET_RATING)) .build() val favoriteButton = CommandButton.Builder() .setDisplayName("Save to favorites") .setIconResId(R.drawable.favorite_icon) .setSessionCommand(SessionCommand(SAVE_TO_FAVORITES, Bundle())) .build() session = MediaSession.Builder(this, player) .setCallback(CustomMediaSessionCallback()) .setCustomLayout(ImmutableList.of(likeButton, favoriteButton)) .build() }
Ява
@Override public void onCreate() { super.onCreate(); CommandButton likeButton = new CommandButton.Builder() .setDisplayName("Like") .setIconResId(R.drawable.like_icon) .setSessionCommand(new SessionCommand(SessionCommand.COMMAND_CODE_SESSION_SET_RATING)) .build(); CommandButton favoriteButton = new CommandButton.Builder() .setDisplayName("Save to favorites") .setIconResId(R.drawable.favorite_icon) .setSessionCommand(new SessionCommand(SAVE_TO_FAVORITES, new Bundle())) .build(); Player player = new ExoPlayer.Builder(this).build(); mediaSession = new MediaSession.Builder(this, player) .setCallback(new CustomMediaSessionCallback()) .setCustomLayout(ImmutableList.of(likeButton, favoriteButton)) .build(); }
Объявить доступного игрока и пользовательские команды
Медиа-приложения могут определять собственные команды, которые, например, можно использовать в произвольном макете. Например, вы можете реализовать кнопки, позволяющие пользователю сохранять элемент мультимедиа в списке избранных элементов. MediaController
отправляет пользовательские команды, а MediaSession.Callback
получает их.
Вы можете определить, какие пользовательские команды сеанса доступны для MediaController
, когда он подключается к вашему медиа-сеансу. Это достигается путем переопределения MediaSession.Callback.onConnect()
. Настройте и верните набор доступных команд при принятии запроса на соединение от MediaController
в методе обратного вызова onConnect
:
Котлин
private inner 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 inner 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
. Это позволяет вам настроить поведение вашего приложения в ответ на заданную команду, если она исходит из системы, вашего собственного приложения или других клиентских приложений.
Обновление пользовательского макета после взаимодействия с пользователем
После обработки пользовательской команды или любого другого взаимодействия с вашим плеером вы можете обновить макет, отображаемый в пользовательском интерфейсе контроллера. Типичным примером является кнопка-переключатель, которая меняет свой значок после запуска действия, связанного с этой кнопкой. Чтобы обновить макет, вы можете использовать MediaSession.setCustomLayout
:
Котлин
val removeFromFavoritesButton = CommandButton.Builder() .setDisplayName("Remove from favorites") .setIconResId(R.drawable.favorite_remove_icon) .setSessionCommand(SessionCommand(REMOVE_FROM_FAVORITES, Bundle())) .build() mediaSession.setCustomLayout(ImmutableList.of(likeButton, removeFromFavoritesButton))
Ява
CommandButton removeFromFavoritesButton = new CommandButton.Builder() .setDisplayName("Remove from favorites") .setIconResId(R.drawable.favorite_remove_icon) .setSessionCommand(new SessionCommand(REMOVE_FROM_FAVORITES, new Bundle())) .build(); mediaSession.setCustomLayout(ImmutableList.of(likeButton, removeFromFavoritesButton));
Настройте поведение команды воспроизведения
Чтобы настроить поведение команды, определенной в интерфейсе Player
, например play()
или seekToNext()
, оберните свой Player
в ForwardingPlayer
.
Котлин
val player = ExoPlayer.Builder(context).build() val forwardingPlayer = object : ForwardingPlayer(player) { override fun play() { // Add custom logic super.play() } override fun setPlayWhenReady(playWhenReady: Boolean) { // Add custom logic super.setPlayWhenReady(playWhenReady) } } val mediaSession = MediaSession.Builder(context, forwardingPlayer).build()
Ява
ExoPlayer player = new ExoPlayer.Builder(context).build(); ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player) { @Override public void play() { // Add custom logic super.play(); } @Override public void setPlayWhenReady(boolean playWhenReady) { // Add custom logic super.setPlayWhenReady(playWhenReady); } }; MediaSession mediaSession = new MediaSession.Builder(context, forwardingPlayer).build();
Дополнительные сведения о ForwardingPlayer
см. в руководстве ExoPlayer по настройке .
Определить запрашивающий контроллер команды игрока
Когда вызов метода Player
инициируется MediaController
, вы можете определить источник происхождения с помощью MediaSession.controllerForCurrentRequest
и получить ControllerInfo
для текущего запроса:
Котлин
class CallerAwareForwardingPlayer(player: Player) : ForwardingPlayer(player) { override fun seekToNext() { Log.d( "caller", "seekToNext called from package ${session.controllerForCurrentRequest?.packageName}" ) super.seekToNext() } }
Ява
public class CallerAwareForwardingPlayer extends ForwardingPlayer { public CallerAwareForwardingPlayer(Player player) { super(player); } @Override public void seekToNext() { Log.d( "caller", "seekToNext called from package: " + session.getControllerForCurrentRequest().getPackageName()); super.seekToNext(); } }
Реагировать на медиа-кнопки
Мультимедийные кнопки — это аппаратные кнопки, имеющиеся на устройствах Android и других периферийных устройствах, например кнопка воспроизведения/паузы на гарнитуре Bluetooth. Media3 обрабатывает события медиа-кнопок, когда они поступают в сеанс, и вызывает соответствующий метод Player
в проигрывателе сеанса .
Приложение может переопределить поведение по умолчанию, переопределив MediaSession.Callback.onMediaButtonEvent(Intent)
. В таком случае приложение может/должно обрабатывать все особенности API самостоятельно.
Обработка ошибок и отчетность
Существует два типа ошибок, которые генерирует сеанс и сообщает контроллерам. Неустранимые ошибки сообщают о техническом сбое воспроизведения сеансового проигрывателя, который прерывает воспроизведение. О фатальных ошибках автоматически сообщается контроллеру при их возникновении. Нефатальные ошибки — это нетехнические ошибки или ошибки политики, которые не прерывают воспроизведение и отправляются приложением на контроллеры вручную.
Фатальные ошибки воспроизведения
Игрок сообщает сеансу о фатальной ошибке воспроизведения, а затем сообщает контроллерам для вызова через Player.Listener.onPlayerError(PlaybackException)
и Player.Listener.onPlayerErrorChanged(@Nullable PlaybackException)
.
В таком случае состояние воспроизведения переходит в STATE_IDLE
, а MediaController.getPlaybackError()
возвращает PlaybackException
, вызвавшее переход. Контроллер может проверить PlayerException.errorCode
, чтобы получить информацию о причине ошибки.
Для обеспечения совместимости фатальная ошибка реплицируется в PlaybackStateCompat
сеанса платформы путем перевода ее состояния в 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();
Пересылающий проигрыватель регистрирует Player.Listener
для фактического проигрывателя и перехватывает обратные вызовы, сообщающие об ошибке. Затем настраиваемое PlaybackException
делегируется прослушивателям, зарегистрированным на пересылающем проигрывателе. Чтобы это работало, пересылающий проигрыватель переопределяет Player.addListener
и Player.removeListener
, чтобы иметь доступ к прослушивателям, с помощью которых можно отправлять индивидуальный код ошибки, сообщение или дополнительные сведения:
Котлин
class ErrorForwardingPlayer(private val context: Context, player: Player) : ForwardingPlayer(player) { private val listeners: MutableList<Player.Listener> = mutableListOf() private var customizedPlaybackException: PlaybackException? = null init { player.addListener(ErrorCustomizationListener()) } override fun addListener(listener: Player.Listener) { listeners.add(listener) } override fun removeListener(listener: Player.Listener) { listeners.remove(listener) } override fun getPlayerError(): PlaybackException? { return customizedPlaybackException } private inner class ErrorCustomizationListener : Player.Listener { override fun onPlayerErrorChanged(error: PlaybackException?) { customizedPlaybackException = error?.let { customizePlaybackException(it) } listeners.forEach { it.onPlayerErrorChanged(customizedPlaybackException) } } override fun onPlayerError(error: PlaybackException) { listeners.forEach { it.onPlayerError(customizedPlaybackException!!) } } private 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) } // Apps can customize further error messages by adding more branches. 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) } override fun onEvents(player: Player, events: Player.Events) { listeners.forEach { it.onEvents(player, events) } } // Delegate all other callbacks to all listeners without changing arguments like onEvents. } }
Ява
private static class ErrorForwardingPlayer extends ForwardingPlayer { private final Context context; private List<Player.Listener> listeners; @Nullable private PlaybackException customizedPlaybackException; public ErrorForwardingPlayer(Context context, Player player) { super(player); this.context = context; listeners = new ArrayList<>(); player.addListener(new ErrorCustomizationListener()); } @Override public void addListener(Player.Listener listener) { listeners.add(listener); } @Override public void removeListener(Player.Listener listener) { listeners.remove(listener); } @Nullable @Override public PlaybackException getPlayerError() { return customizedPlaybackException; } private class ErrorCustomizationListener implements Listener { @Override public void onPlayerErrorChanged(@Nullable PlaybackException error) { customizedPlaybackException = error != null ? customizePlaybackException(error, context) : null; for (int i = 0; i < listeners.size(); i++) { listeners.get(i).onPlayerErrorChanged(customizedPlaybackException); } } @Override public void onPlayerError(PlaybackException error) { for (int i = 0; i < listeners.size(); i++) { listeners.get(i).onPlayerError(checkNotNull(customizedPlaybackException)); } } private PlaybackException customizePlaybackException( PlaybackException error, Context context) { 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; // Apps can customize further error messages by adding more case statements. 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); } @Override public void onEvents(Player player, Events events) { for (int i = 0; i < listeners.size(); i++) { listeners.get(i).onEvents(player, events); } } // Delegate all other callbacks to all listeners without changing arguments like onEvents. } }
Нефатальные ошибки
Нефатальные ошибки, не возникшие в результате технического исключения, могут быть отправлены приложением всем или конкретному контроллеру:
Котлин
val sessionError = SessionError( SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED, context.getString(R.string.error_message_authentication_expired), ) // Sending a nonfatal error to all controllers. mediaSession.sendError(sessionError) // Interoperability: Sending a nonfatal error to the media notification controller 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)); // Sending a nonfatal error to all controllers. mediaSession.sendError(sessionError); // Interoperability: Sending a nonfatal error to the media notification controller 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); }
Нефатальная ошибка, отправленная на контроллер мультимедийных уведомлений, реплицируется в PlaybackStateCompat
сеанса платформы. Таким образом, только код ошибки и сообщение об ошибке соответственно устанавливаются в PlaybackStateCompat
, а PlaybackStateCompat.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. } });