Архитектура рабочего медиаплеера: MediaBrowser + MediaSession

Короткий ответ: MediaSession должен жить в сервисе (MediaBrowserServiceCompat), UI подключается через MediaBrowser/MediaController, а уведомление с MediaStyle связано с sessionToken — так вы получите управление из шторки, гарнитуры, авто и часов. Ниже — минимальный рабочий план и практические советы.

Где разместить MediaSession и что он делает

MediaSession — «источник правды»: держит PlaybackState и MediaMetadata и принимает команды (play/pause/seek/skip). Поместите его в Foreground-сервис (MediaBrowserServiceCompat) вместе с ExoPlayer/MediaPlayer.

Что обязательно реализовать:

  • sessionToken = mediaSession.sessionToken;
  • mediaSession.setCallback(...) — в колбэках вызывайте методы вашего плеера;
  • updatePlaybackState(...) и updateMetadata(...) — единые методы для синхронизации состояния;
  • isActive = true при готовности/воспроизведении, выключайте при окончательном stop.

::tip Выделите функции updatePlaybackState() и updateMetadata(track) и вызывайте их из всех точек, меняющих трек/состояние — это уменьшит рассинхрон UI и уведомлений. ::

Подключение UI: MediaBrowser → MediaController

В Activity/Fragment:

  1. Создайте MediaBrowserCompat и подключитесь.
  2. В onConnected создайте MediaControllerCompat по mediaBrowser.sessionToken.
  3. Регистрируйте controller.registerCallback(...) — обновляйте кнопки и прогресс только через колбэки.
  4. Управляйте через controller.transportControls (play/pause/seek/skip).

Совет по архитектуре: UI — пассивный подписчик; вся логика — в сервисе. Не дублируйте состояние в Activity.

Гарнитура, аудиофокус и уведомления

Кнопки гарнитуры и медиакнопки системы приходят через MediaSession, если она активна и правильно настроен AudioFocus.

Практические шаги:

  • Запрос аудиофокуса перед стартом воспроизведения (AudioFocusRequest на API>=26).
  • На LOSS — ставьте на паузу; на LOSS_TRANSIENT_CAN_DUCK — понижайте громкость.
  • Обновляйте PlaybackState с корректной позицией и доступными действиями (ACTION_PLAY, ACTION_PAUSE, ACTION_SEEK_TO и т.д.).

Уведомление:

  • Создайте NotificationChannel (Android 8+).
  • Соберите NotificationCompat.Builder с androidx.media.app.NotificationCompat.MediaStyle().
  • Привяжите .setMediaSession(mediaSession.sessionToken) и используйте MediaButtonReceiver.buildMediaButtonPendingIntent(...) для действий.
  • При воспроизведении — startForeground(id, notification). При стопе — stopForeground(true) + stopSelf().

::warning Если кнопки в уведомлении не работают — чаще всего mediaSession.isActive == false или MediaButtonReceiver не объявлен/не используется корректно. ::

Частые ошибки

  • MediaSession неактивен — гарнитура и системные контролы «не видят» приложение. Решение: isActive = true при готовности.
  • PlaybackState не обновляется — UI и уведомление показывают старые данные. Решение: вызывать updatePlaybackState в каждом колбэке/Listener ExoPlayer.
  • Дублирование логики в Activity и Service — приводит к рассинхронности. Решение: сервис — единственный источник состояния.
  • Неправильный жизненный цикл сервиса — либо сервис живёт вечно, либо часто убивается. Решение: корректно использовать startForeground/stopForeground и освобождать ресурсы.

Мини‑чеклист перед релизом:

  • Воспроизведение продолжает работать в фоне.
  • Управление: шторка, экран блокировки, гарнитура, авто — протестировано.
  • Поведение при звонке и при переключении источника — корректное.
  • Уведомление показывает метаданные и исчезает при stop().

FAQ

  • Как быстро проверить, получает ли система медиакнопки?
    • Временно логируйте в MediaSession.Callback.onMediaButtonEvent или проверяйте, активна ли сессия и обновляется ли PlaybackState при нажатиях.
  • Нужно ли вручную регистрировать BroadcastReceiver для MEDIA_BUTTON?
    • Обычно нет: MediaSession и MediaButtonReceiver покрывают это. Receiver нужен в манифесте для некоторых старых сценариев, но чаще хватит MediaStyle + sessionToken.
  • Где хранить обложку трека?
    • В MediaMetadataCompat (METADATA_KEY_ALBUM_ART / ART). Загружайте асинхронно и обновляйте метаданные при готовности.

Если потребуется, могу дать компактный каркас сервиса/уведомления или пример интеграции с ExoPlayer.