Медиа-сессии предоставляют универсальный способ взаимодействия с аудио- или видеоплеером. В Media3 плеером по умолчанию является класс ExoPlayer
, реализующий интерфейс Player
. Подключение медиа-сессии к плееру позволяет приложению рекламировать воспроизведение медиаконтента извне и получать команды воспроизведения из внешних источников.
Команды могут поступать от физических кнопок, таких как кнопка воспроизведения на гарнитуре или пульте дистанционного управления телевизора. Они также могут поступать от клиентских приложений с контроллером мультимедиа, например, команда «пауза» для Google Ассистента. Медиасеанс делегирует эти команды проигрывателю медиаприложения.
Когда выбирать медиасессию
При реализации MediaSession
вы позволяете пользователям управлять воспроизведением:
- Через наушники . На наушниках часто предусмотрены кнопки или сенсорные взаимодействия, которые пользователь может выполнить на своих наушниках, чтобы воспроизвести или приостановить воспроизведение медиафайлов, а также перейти к следующему или предыдущему треку.
- С помощью Google Assistant . Распространенный способ приостановить любой медиафайл, воспроизводимый на устройстве, — сказать «Окей, Google, пауза».
- Через часы Wear OS . Это обеспечивает более удобный доступ к наиболее распространённым элементам управления воспроизведением во время игры на телефоне.
- Через элементы управления медиа . Эта карусель показывает элементы управления для каждого запущенного сеанса мультимедиа.
- На телевизоре . Позволяет выполнять действия с физическими кнопками воспроизведения, управлять воспроизведением на платформе и управлять питанием (например, если телевизор, звуковая панель или A/V-ресивер выключаются или переключается вход, воспроизведение должно останавливаться в приложении).
- Через элементы управления мультимедиа Android Auto . Это позволяет безопасно управлять воспроизведением во время вождения.
- И любые другие внешние процессы, которые должны влиять на воспроизведение.
Это отлично подходит для многих случаев. В частности, вам стоит рассмотреть возможность использования 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 Ассистента. Аналогичным образом, вы можете предоставить доступ к системе 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()
с этой функцией можно найти в демонстрационном приложении сеанса.
Управление настройками кнопок мультимедиа
Каждый контроллер, например, System UI, Android Auto или Wear OS, может самостоятельно определять, какие кнопки отображать пользователю. Чтобы указать, какие элементы управления воспроизведением вы хотите предоставить пользователю, можно задать настройки кнопок мультимедиа в MediaSession
. Эти настройки представляют собой упорядоченный список экземпляров CommandButton
, каждый из которых определяет настройку кнопки в пользовательском интерфейсе.
Определить кнопки команд
Экземпляры CommandButton
используются для определения настроек кнопок мультимедиа. Каждая кнопка определяет три аспекта желаемого элемента пользовательского интерфейса:
- Значок , определяющий внешний вид. При создании
CommandButton.Builder
значку необходимо задать одну из предопределённых констант. Обратите внимание, что это не ресурс растрового изображения или изображения. Универсальная константа помогает контроллерам выбирать подходящий ресурс для обеспечения единообразного внешнего вида в их пользовательском интерфейсе. Если ни одна из предопределённых констант значка не подходит для вашего варианта использования, вы можете использоватьsetCustomIconResId
. - Команда Command определяет действие, вызываемое при взаимодействии пользователя с кнопкой. Вы можете использовать
setPlayerCommand
дляPlayer.Command
илиsetSessionCommand
для предопределенного или пользовательскогоSessionCommand
. - Поле « Слот» определяет, где должна располагаться кнопка в пользовательском интерфейсе контроллера. Это поле необязательно и устанавливается автоматически в зависимости от значений « Значок» и «Команда» . Например, оно позволяет указать, что кнопка должна отображаться в области навигации «вперед» пользовательского интерфейса вместо области «переполнения» по умолчанию.
Котлин
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();
После определения настроек кнопок мультимедиа применяется следующий алгоритм:
- Для каждой
CommandButton
в настройках кнопок мультимедиа поместите кнопку в первый доступный и разрешенный слот . - Если какой-либо из центральных, передних и задних слотов не занят кнопкой, добавьте для этого слота кнопки по умолчанию.
Вы можете использовать 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. } });