Средства управления мультимедиа

Элементы управления мультимедиа в Android расположены рядом с быстрыми настройками. Сеансы из нескольких приложений организованы в пролистываемую карусель. В карусели сеансы перечислены в следующем порядке:

  • Потоки, воспроизводимые локально на телефоне
  • Удаленные потоки, например обнаруженные на внешних устройствах или в сеансах трансляции.
  • Предыдущие возобновляемые сеансы в том порядке, в котором они проводились в последний раз.

Начиная с Android 13 (уровень API 33), чтобы гарантировать пользователям доступ к богатому набору элементов управления мультимедиа для приложений, воспроизводящих мультимедиа, кнопки действий на элементах управления мультимедиа извлекаются из состояния Player .

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

На рис. 1 показан пример того, как это выглядит на телефоне и планшете соответственно.

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

Система отображает до пяти кнопок действий в зависимости от состояния Player , как описано в следующей таблице. В компактном режиме отображаются только первые три слота действий. Это соответствует тому, как элементы управления мультимедиа отображаются на других платформах Android, таких как Auto, Assistant и Wear OS.

Слот Критерии Действие
1 playWhenReady имеет значение false или текущее состояние воспроизведенияSTATE_ENDED . Играть
playWhenReady имеет значение true, а текущее состояние воспроизведенияSTATE_BUFFERING . Загрузка счетчика
playWhenReady имеет значение true, а текущее состояние воспроизведенияSTATE_READY . Пауза
2 Доступна команда проигрывателя COMMAND_SEEK_TO_PREVIOUS или COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM . Предыдущий
Ни команда игрока COMMAND_SEEK_TO_PREVIOUS , ни COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM недоступна, а пользовательская команда из пользовательского макета, которая еще не была размещена, доступна для заполнения слота. Обычай
(пока не поддерживается в Media3) Дополнительные возможности PlaybackState включают true логическое значение для ключа EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV . Пустой
3 Доступна команда проигрывателя COMMAND_SEEK_TO_NEXT или COMMAND_SEEK_TO_NEXT_MEDIA_ITEM . Следующий
Ни команда игрока COMMAND_SEEK_TO_NEXT , ни COMMAND_SEEK_TO_NEXT_MEDIA_ITEM недоступны, а пользовательская команда из пользовательского макета, которая еще не была размещена, доступна для заполнения слота. Обычай
(пока не поддерживается в Media3). Дополнительные возможности PlaybackState включают true логическое значение для ключа EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT . Пустой
4 Пользовательская команда из пользовательского макета, которая еще не была размещена, доступна для заполнения слота. Обычай
5 Пользовательская команда из пользовательского макета, которая еще не была размещена, доступна для заполнения слота. Обычай

Пользовательские команды размещаются в том порядке, в котором они были добавлены в пользовательский макет.

Настройте командные кнопки

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

  1. В onCreate() создайте MediaSession и определите собственный макет командных кнопок.

  2. В MediaSession.Callback.onConnect() авторизуйте контроллеры, определив их доступные команды, включая пользовательские команды , в ConnectionResult .

  3. В MediaSession.Callback.onCustomCommand() ответьте на пользовательскую команду, выбранную пользователем.

Котлин

class PlaybackService : MediaSessionService() {
  private val customCommandFavorites = SessionCommand(ACTION_FAVORITES, Bundle.EMPTY)
  private var mediaSession: MediaSession? = null

  override fun onCreate() {
    super.onCreate()
    val favoriteButton =
      CommandButton.Builder()
        .setDisplayName("Save to favorites")
        .setIconResId(R.drawable.favorite_icon)
        .setSessionCommand(customCommandFavorites)
        .build()
    val player = ExoPlayer.Builder(this).build()
    // Build the session with a custom layout.
    mediaSession =
      MediaSession.Builder(this, player)
        .setCallback(MyCallback())
        .setCustomLayout(ImmutableList.of(favoriteButton))
        .build()
  }

  private inner class MyCallback : MediaSession.Callback {
    override fun onConnect(
      session: MediaSession,
      controller: MediaSession.ControllerInfo
    ): ConnectionResult {
    // Set available player and session commands.
    return AcceptedResultBuilder(session)
      .setAvailablePlayerCommands(
        ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
          .remove(COMMAND_SEEK_TO_NEXT)
          .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
          .remove(COMMAND_SEEK_TO_PREVIOUS)
          .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
          .build()
      )
      .setAvailableSessionCommands(
        ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
          .add(customCommandFavorites)
          .build()
      )
      .build()
    }

    override fun onCustomCommand(
      session: MediaSession,
      controller: MediaSession.ControllerInfo,
      customCommand: SessionCommand,
      args: Bundle
    ): ListenableFuture {
      if (customCommand.customAction == ACTION_FAVORITES) {
        // Do custom logic here
        saveToFavorites(session.player.currentMediaItem)
        return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
      }
      return super.onCustomCommand(session, controller, customCommand, args)
    }
  }
}

Ява

public class PlaybackService extends MediaSessionService {
  private static final SessionCommand CUSTOM_COMMAND_FAVORITES =
      new SessionCommand("ACTION_FAVORITES", Bundle.EMPTY);
  @Nullable private MediaSession mediaSession;

  public void onCreate() {
    super.onCreate();
    CommandButton favoriteButton =
        new CommandButton.Builder()
            .setDisplayName("Save to favorites")
            .setIconResId(R.drawable.favorite_icon)
            .setSessionCommand(CUSTOM_COMMAND_FAVORITES)
            .build();
    Player player = new ExoPlayer.Builder(this).build();
    // Build the session with a custom layout.
    mediaSession =
        new MediaSession.Builder(this, player)
            .setCallback(new MyCallback())
            .setCustomLayout(ImmutableList.of(favoriteButton))
            .build();
  }

  private static class MyCallback implements MediaSession.Callback {
    @Override
    public ConnectionResult onConnect(
        MediaSession session, MediaSession.ControllerInfo controller) {
      // Set available player and session commands.
      return new AcceptedResultBuilder(session)
          .setAvailablePlayerCommands(
              ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
                .remove(COMMAND_SEEK_TO_NEXT)
                .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
                .remove(COMMAND_SEEK_TO_PREVIOUS)
                .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
                .build())
          .setAvailableSessionCommands(
              ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
                .add(CUSTOM_COMMAND_FAVORITES)
                .build())
          .build();
    }

    public ListenableFuture onCustomCommand(
        MediaSession session,
        MediaSession.ControllerInfo controller,
        SessionCommand customCommand,
        Bundle args) {
      if (customCommand.customAction.equals(CUSTOM_COMMAND_FAVORITES.customAction)) {
        // Do custom logic here
        saveToFavorites(session.getPlayer().getCurrentMediaItem());
        return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_SUCCESS));
      }
      return MediaSession.Callback.super.onCustomCommand(
          session, controller, customCommand, args);
    }
  }
}

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

При использовании Jetpack Media3 при реализации MediaSession ваш PlaybackState автоматически обновляется с помощью медиаплеера. Аналогичным образом, когда вы реализуете MediaSessionService , библиотека автоматически публикует для вас уведомление MediaStyle и поддерживает его актуальность.

Реагировать на кнопки действий

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

Инструкции о том, как реагировать на пользовательскую команду, см. в разделе Добавление пользовательских команд .

Поведение до версии Android 13

В целях обратной совместимости системный пользовательский интерфейс по-прежнему предоставляет альтернативный макет, в котором используются действия уведомлений для приложений, которые не обновляются до Android 13 или не включают информацию PlaybackState . Кнопки действий извлекаются из списка Notification.Action , прикрепленного к уведомлению MediaStyle . Система отображает до пяти действий в том порядке, в котором они были добавлены. В компактном режиме отображается до трех кнопок, что определяется значениями, переданными в setShowActionsInCompactView() .

Пользовательские действия размещаются в том порядке, в котором они были добавлены в PlaybackState .

В следующем примере кода показано, как добавить действия в уведомление MediaStyle:

Котлин

import androidx.core.app.NotificationCompat
import androidx.media3.session.MediaStyleNotificationHelper

var notification = NotificationCompat.Builder(context, CHANNEL_ID)
        // Show controls on lock screen even when user hides sensitive content.
        .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
        .setSmallIcon(R.drawable.ic_stat_player)
        // Add media control buttons that invoke intents in your media service
        .addAction(R.drawable.ic_prev, "Previous", prevPendingIntent) // #0
        .addAction(R.drawable.ic_pause, "Pause", pausePendingIntent) // #1
        .addAction(R.drawable.ic_next, "Next", nextPendingIntent) // #2
        // Apply the media style template
        .setStyle(MediaStyleNotificationHelper.MediaStyle(mediaSession)
                .setShowActionsInCompactView(1 /* #1: pause button */))
        .setContentTitle("Wonderful music")
        .setContentText("My Awesome Band")
        .setLargeIcon(albumArtBitmap)
        .build()

Ява

import androidx.core.app.NotificationCompat;
import androidx.media3.session.MediaStyleNotificationHelper;

NotificationCompat.Builder notification = new NotificationCompat.Builder(context, CHANNEL_ID)
        .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
        .setSmallIcon(R.drawable.ic_stat_player)
        .addAction(R.drawable.ic_prev, "Previous", prevPendingIntent)
        .addAction(R.drawable.ic_pause, "Pause", pausePendingIntent)
        .addAction(R.drawable.ic_next, "Next", nextPendingIntent)
        .setStyle(new MediaStyleNotificationHelper.MediaStyle(mediaSession)
                .setShowActionsInCompactView(1 /* #1: pause button */))
        .setContentTitle("Wonderful music")
        .setContentText("My Awesome Band")
        .setLargeIcon(albumArtBitmap)
        .build();

Поддержка возобновления СМИ

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

Функцию возобновления воспроизведения можно включить и выключить с помощью приложения «Настройки» в разделе «Звук» > «Медиа» . Пользователь также может получить доступ к настройкам, коснувшись значка шестеренки, который появляется после пролистывания расширенной карусели.

Media3 предлагает API, упрощающие поддержку возобновления мультимедиа. Инструкции по реализации этой функции см. в документации по возобновлению воспроизведения с помощью Media3 .

Использование устаревших API-интерфейсов мультимедиа

В этом разделе объясняется, как интегрироваться с системными элементами управления мультимедиа с помощью устаревших API MediaCompat.

Система получает следующую информацию из MediaMetadata MediaSession и отображает ее, когда она доступна:

  • METADATA_KEY_ALBUM_ART_URI
  • METADATA_KEY_TITLE
  • METADATA_KEY_DISPLAY_TITLE
  • METADATA_KEY_ARTIST
  • METADATA_KEY_DURATION (если продолжительность не установлена, панель поиска не показывает прогресс)

Чтобы убедиться, что у вас есть действительное и точное уведомление об управлении мультимедиа, установите в качестве значения метаданных METADATA_KEY_TITLE или METADATA_KEY_DISPLAY_TITLE название медиафайла, воспроизводимого в данный момент.

Медиаплеер показывает прошедшее время для воспроизводимого в данный момент мультимедиа, а также панель поиска, которая сопоставлена ​​с MediaSession PlaybackState .

Медиаплеер показывает ход воспроизведения текущего мультимедиа, а также панель поиска, которая сопоставлена ​​с MediaSession PlaybackState . Панель поиска позволяет пользователям изменять положение и отображать прошедшее время для элемента мультимедиа. Чтобы панель поиска была включена, необходимо реализовать PlaybackState.Builder#setActions и включить ACTION_SEEK_TO .

Слот Действие Критерии
1 Играть Текущее состояние PlaybackState — одно из следующих:
  • STATE_NONE
  • STATE_STOPPED
  • STATE_PAUSED
  • STATE_ERROR
Загрузка счетчика Текущее состояние PlaybackState является одним из следующих:
  • STATE_CONNECTING
  • STATE_BUFFERING
Пауза Текущее состояние PlaybackState не отличается от вышеперечисленного.
2 Предыдущий Действия PlaybackState включают ACTION_SKIP_TO_PREVIOUS .
Обычай Действия PlaybackState не включают ACTION_SKIP_TO_PREVIOUS , а настраиваемые действия PlaybackState включают настраиваемое действие, которое еще не было размещено.
Пустой Дополнительные возможности PlaybackState включают true логическое значение для ключа SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV .
3 Следующий Действия PlaybackState включают ACTION_SKIP_TO_NEXT .
Обычай Действия PlaybackState не включают ACTION_SKIP_TO_NEXT , а настраиваемые действия PlaybackState включают настраиваемое действие, которое еще не было размещено.
Пустой Дополнительные возможности PlaybackState включают true логическое значение для ключа SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT .
4 Обычай Настраиваемые действия PlaybackState включают настраиваемое действие, которое еще не было размещено.
5 Обычай Настраиваемые действия PlaybackState включают настраиваемое действие, которое еще не было размещено.

Добавьте стандартные действия

В следующих примерах кода показано, как добавить стандартные и настраиваемые действия PlaybackState .

Для воспроизведения, паузы, предыдущего и следующего задайте эти действия в PlaybackState для сеанса мультимедиа.

Котлин

val session = MediaSessionCompat(context, TAG)
val playbackStateBuilder = PlaybackStateCompat.Builder()
val style = NotificationCompat.MediaStyle()

// For this example, the media is currently paused:
val state = PlaybackStateCompat.STATE_PAUSED
val position = 0L
val playbackSpeed = 1f
playbackStateBuilder.setState(state, position, playbackSpeed)

// And the user can play, skip to next or previous, and seek
val stateActions = PlaybackStateCompat.ACTION_PLAY
    or PlaybackStateCompat.ACTION_PLAY_PAUSE
    or PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
    or PlaybackStateCompat.ACTION_SKIP_TO_NEXT
    or PlaybackStateCompat.ACTION_SEEK_TO // adding the seek action enables seeking with the seekbar
playbackStateBuilder.setActions(stateActions)

// ... do more setup here ...

session.setPlaybackState(playbackStateBuilder.build())
style.setMediaSession(session.sessionToken)
notificationBuilder.setStyle(style)

Ява

MediaSessionCompat session = new MediaSessionCompat(context, TAG);
PlaybackStateCompat.Builder playbackStateBuilder = new PlaybackStateCompat.Builder();
NotificationCompat.MediaStyle style = new NotificationCompat.MediaStyle();

// For this example, the media is currently paused:
int state = PlaybackStateCompat.STATE_PAUSED;
long position = 0L;
float playbackSpeed = 1f;
playbackStateBuilder.setState(state, position, playbackSpeed);

// And the user can play, skip to next or previous, and seek
long stateActions = PlaybackStateCompat.ACTION_PLAY
    | PlaybackStateCompat.ACTION_PLAY_PAUSE
    | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
    | PlaybackStateCompat.ACTION_SKIP_TO_NEXT
    | PlaybackStateCompat.ACTION_SEEK_TO; // adding this enables the seekbar thumb
playbackStateBuilder.setActions(stateActions);

// ... do more setup here ...

session.setPlaybackState(playbackStateBuilder.build());
style.setMediaSession(session.getSessionToken());
notificationBuilder.setStyle(style);

Если вам не нужны кнопки в предыдущем или следующем слотах, не добавляйте ACTION_SKIP_TO_PREVIOUS или ACTION_SKIP_TO_NEXT , а вместо этого добавьте в сеанс дополнительные функции:

Котлин

session.setExtras(Bundle().apply {
    putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true)
    putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true)
})

Ява

Bundle extras = new Bundle();
extras.putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true);
extras.putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true);
session.setExtras(extras);

Добавить специальные действия

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

Котлин

val customAction = PlaybackStateCompat.CustomAction.Builder(
    "com.example.MY_CUSTOM_ACTION", // action ID
    "Custom Action", // title - used as content description for the button
    R.drawable.ic_custom_action
).build()

playbackStateBuilder.addCustomAction(customAction)

Ява

PlaybackStateCompat.CustomAction customAction = new PlaybackStateCompat.CustomAction.Builder(
        "com.example.MY_CUSTOM_ACTION", // action ID
        "Custom Action", // title - used as content description for the button
        R.drawable.ic_custom_action
).build();

playbackStateBuilder.addCustomAction(customAction);

Реагирование на действия PlaybackState

Когда пользователь нажимает кнопку, SystemUI использует MediaController.TransportControls для отправки команды обратно в MediaSession . Вам необходимо зарегистрировать обратный вызов, который сможет правильно реагировать на эти события.

Котлин

val callback = object: MediaSession.Callback() {
    override fun onPlay() {
        // start playback
    }

    override fun onPause() {
        // pause playback
    }

    override fun onSkipToPrevious() {
        // skip to previous
    }

    override fun onSkipToNext() {
        // skip to next
    }

    override fun onSeekTo(pos: Long) {
        // jump to position in track
    }

    override fun onCustomAction(action: String, extras: Bundle?) {
        when (action) {
            CUSTOM_ACTION_1 -> doCustomAction1(extras)
            CUSTOM_ACTION_2 -> doCustomAction2(extras)
            else -> {
                Log.w(TAG, "Unknown custom action $action")
            }
        }
    }

}

session.setCallback(callback)

Ява

MediaSession.Callback callback = new MediaSession.Callback() {
    @Override
    public void onPlay() {
        // start playback
    }

    @Override
    public void onPause() {
        // pause playback
    }

    @Override
    public void onSkipToPrevious() {
        // skip to previous
    }

    @Override
    public void onSkipToNext() {
        // skip to next
    }

    @Override
    public void onSeekTo(long pos) {
        // jump to position in track
    }

    @Override
    public void onCustomAction(String action, Bundle extras) {
        if (action.equals(CUSTOM_ACTION_1)) {
            doCustomAction1(extras);
        } else if (action.equals(CUSTOM_ACTION_2)) {
            doCustomAction2(extras);
        } else {
            Log.w(TAG, "Unknown custom action " + action);
        }
    }
};

Возобновление работы СМИ

Чтобы приложение проигрывателя появилось в области настроек быстрых настроек, необходимо создать уведомление MediaStyle с действительным токеном MediaSession .

Чтобы отобразить заголовок уведомления MediaStyle, используйте NotificationBuilder.setContentTitle() .

Чтобы отобразить значок бренда для медиаплеера, используйте NotificationBuilder.setSmallIcon() .

Чтобы поддерживать возобновление воспроизведения, приложения должны реализовать MediaBrowserService и MediaSession . Ваш MediaSession должен реализовать обратный вызов onPlay() .

Реализация MediaBrowserService

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

Система пытается связаться с вашим MediaBrowserService с помощью соединения из SystemUI. Ваше приложение должно разрешать такие соединения, иначе оно не сможет поддерживать возобновление воспроизведения.

Подключения из SystemUI можно идентифицировать и проверить с помощью имени пакета com.android.systemui и подписи. SystemUI подписан подписью платформы. Пример проверки подписи платформы можно найти в приложении UAMP .

Чтобы поддерживать возобновление воспроизведения, ваш MediaBrowserService должен реализовать следующее поведение:

  • onGetRoot() должен быстро возвращать ненулевой корень. Другая сложная логика должна обрабатываться в onLoadChildren()

  • Когда onLoadChildren() вызывается для идентификатора корневого носителя, результат должен содержать дочерний элемент FLAG_PLAYABLE .

  • MediaBrowserService должен возвращать последний воспроизведенный мультимедийный элемент при получении запроса EXTRA_RECENT . Возвращаемое значение должно быть реальным элементом мультимедиа, а не универсальной функцией.

  • MediaBrowserService должен предоставить соответствующее MediaDescription с непустым заголовком и подзаголовком . Он также должен установить URI значка или растровое изображение значка .

Следующие примеры кода иллюстрируют, как реализовать onGetRoot() .

Котлин

override fun onGetRoot(
    clientPackageName: String,
    clientUid: Int,
    rootHints: Bundle?
): BrowserRoot? {
    ...
    // Verify that the specified package is SystemUI. You'll need to write your 
    // own logic to do this.
    if (isSystem(clientPackageName, clientUid)) {
        rootHints?.let {
            if (it.getBoolean(BrowserRoot.EXTRA_RECENT)) {
                // Return a tree with a single playable media item for resumption.
                val extras = Bundle().apply {
                    putBoolean(BrowserRoot.EXTRA_RECENT, true)
                }
                return BrowserRoot(MY_RECENTS_ROOT_ID, extras)
            }
        }
        // You can return your normal tree if the EXTRA_RECENT flag is not present.
        return BrowserRoot(MY_MEDIA_ROOT_ID, null)
    }
    // Return an empty tree to disallow browsing.
    return BrowserRoot(MY_EMPTY_ROOT_ID, null)

Ява

@Override
public BrowserRoot onGetRoot(String clientPackageName, int clientUid,
    Bundle rootHints) {
    ...
    // Verify that the specified package is SystemUI. You'll need to write your
    // own logic to do this.
    if (isSystem(clientPackageName, clientUid)) {
        if (rootHints != null) {
            if (rootHints.getBoolean(BrowserRoot.EXTRA_RECENT)) {
                // Return a tree with a single playable media item for resumption.
                Bundle extras = new Bundle();
                extras.putBoolean(BrowserRoot.EXTRA_RECENT, true);
                return new BrowserRoot(MY_RECENTS_ROOT_ID, extras);
            }
        }
        // You can return your normal tree if the EXTRA_RECENT flag is not present.
        return new BrowserRoot(MY_MEDIA_ROOT_ID, null);
    }
    // Return an empty tree to disallow browsing.
    return new BrowserRoot(MY_EMPTY_ROOT_ID, null);
}
,

Элементы управления мультимедиа в Android расположены рядом с быстрыми настройками. Сеансы из нескольких приложений организованы в пролистываемую карусель. В карусели сеансы перечислены в следующем порядке:

  • Потоки, воспроизводимые локально на телефоне
  • Удаленные потоки, например обнаруженные на внешних устройствах или в сеансах трансляции.
  • Предыдущие возобновляемые сеансы в том порядке, в котором они проводились в последний раз.

Начиная с Android 13 (уровень API 33), чтобы гарантировать пользователям доступ к богатому набору элементов управления мультимедиа для приложений, воспроизводящих мультимедиа, кнопки действий на элементах управления мультимедиа извлекаются из состояния Player .

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

На рис. 1 показан пример того, как это выглядит на телефоне и планшете соответственно.

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

Система отображает до пяти кнопок действий в зависимости от состояния Player , как описано в следующей таблице. В компактном режиме отображаются только первые три слота действий. Это соответствует тому, как элементы управления мультимедиа отображаются на других платформах Android, таких как Auto, Assistant и Wear OS.

Слот Критерии Действие
1 playWhenReady имеет значение false или текущее состояние воспроизведенияSTATE_ENDED . Играть
playWhenReady имеет значение true, а текущее состояние воспроизведенияSTATE_BUFFERING . Загрузка счетчика
playWhenReady имеет значение true, а текущее состояние воспроизведенияSTATE_READY . Пауза
2 Доступна команда проигрывателя COMMAND_SEEK_TO_PREVIOUS или COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM . Предыдущий
Ни команда игрока COMMAND_SEEK_TO_PREVIOUS , ни COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM недоступна, а пользовательская команда из пользовательского макета, которая еще не была размещена, доступна для заполнения слота. Обычай
(пока не поддерживается в Media3) Дополнительные возможности PlaybackState включают true логическое значение для ключа EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV . Пустой
3 Доступна команда проигрывателя COMMAND_SEEK_TO_NEXT или COMMAND_SEEK_TO_NEXT_MEDIA_ITEM . Следующий
Ни команда игрока COMMAND_SEEK_TO_NEXT , ни COMMAND_SEEK_TO_NEXT_MEDIA_ITEM недоступны, а пользовательская команда из пользовательского макета, которая еще не была размещена, доступна для заполнения слота. Обычай
(пока не поддерживается в Media3) Дополнительные возможности PlaybackState включают true логическое значение для ключа EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT . Пустой
4 Пользовательская команда из пользовательского макета, которая еще не была размещена, доступна для заполнения слота. Обычай
5 Пользовательская команда из пользовательского макета, которая еще не была размещена, доступна для заполнения слота. Обычай

Пользовательские команды размещаются в том порядке, в котором они были добавлены в пользовательский макет.

Настройте командные кнопки

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

  1. В onCreate() создайте MediaSession и определите собственный макет командных кнопок.

  2. В MediaSession.Callback.onConnect() авторизуйте контроллеры, определив их доступные команды, включая пользовательские команды , в ConnectionResult .

  3. В MediaSession.Callback.onCustomCommand() ответьте на пользовательскую команду, выбранную пользователем.

Котлин

class PlaybackService : MediaSessionService() {
  private val customCommandFavorites = SessionCommand(ACTION_FAVORITES, Bundle.EMPTY)
  private var mediaSession: MediaSession? = null

  override fun onCreate() {
    super.onCreate()
    val favoriteButton =
      CommandButton.Builder()
        .setDisplayName("Save to favorites")
        .setIconResId(R.drawable.favorite_icon)
        .setSessionCommand(customCommandFavorites)
        .build()
    val player = ExoPlayer.Builder(this).build()
    // Build the session with a custom layout.
    mediaSession =
      MediaSession.Builder(this, player)
        .setCallback(MyCallback())
        .setCustomLayout(ImmutableList.of(favoriteButton))
        .build()
  }

  private inner class MyCallback : MediaSession.Callback {
    override fun onConnect(
      session: MediaSession,
      controller: MediaSession.ControllerInfo
    ): ConnectionResult {
    // Set available player and session commands.
    return AcceptedResultBuilder(session)
      .setAvailablePlayerCommands(
        ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
          .remove(COMMAND_SEEK_TO_NEXT)
          .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
          .remove(COMMAND_SEEK_TO_PREVIOUS)
          .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
          .build()
      )
      .setAvailableSessionCommands(
        ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
          .add(customCommandFavorites)
          .build()
      )
      .build()
    }

    override fun onCustomCommand(
      session: MediaSession,
      controller: MediaSession.ControllerInfo,
      customCommand: SessionCommand,
      args: Bundle
    ): ListenableFuture {
      if (customCommand.customAction == ACTION_FAVORITES) {
        // Do custom logic here
        saveToFavorites(session.player.currentMediaItem)
        return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
      }
      return super.onCustomCommand(session, controller, customCommand, args)
    }
  }
}

Ява

public class PlaybackService extends MediaSessionService {
  private static final SessionCommand CUSTOM_COMMAND_FAVORITES =
      new SessionCommand("ACTION_FAVORITES", Bundle.EMPTY);
  @Nullable private MediaSession mediaSession;

  public void onCreate() {
    super.onCreate();
    CommandButton favoriteButton =
        new CommandButton.Builder()
            .setDisplayName("Save to favorites")
            .setIconResId(R.drawable.favorite_icon)
            .setSessionCommand(CUSTOM_COMMAND_FAVORITES)
            .build();
    Player player = new ExoPlayer.Builder(this).build();
    // Build the session with a custom layout.
    mediaSession =
        new MediaSession.Builder(this, player)
            .setCallback(new MyCallback())
            .setCustomLayout(ImmutableList.of(favoriteButton))
            .build();
  }

  private static class MyCallback implements MediaSession.Callback {
    @Override
    public ConnectionResult onConnect(
        MediaSession session, MediaSession.ControllerInfo controller) {
      // Set available player and session commands.
      return new AcceptedResultBuilder(session)
          .setAvailablePlayerCommands(
              ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
                .remove(COMMAND_SEEK_TO_NEXT)
                .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
                .remove(COMMAND_SEEK_TO_PREVIOUS)
                .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
                .build())
          .setAvailableSessionCommands(
              ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
                .add(CUSTOM_COMMAND_FAVORITES)
                .build())
          .build();
    }

    public ListenableFuture onCustomCommand(
        MediaSession session,
        MediaSession.ControllerInfo controller,
        SessionCommand customCommand,
        Bundle args) {
      if (customCommand.customAction.equals(CUSTOM_COMMAND_FAVORITES.customAction)) {
        // Do custom logic here
        saveToFavorites(session.getPlayer().getCurrentMediaItem());
        return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_SUCCESS));
      }
      return MediaSession.Callback.super.onCustomCommand(
          session, controller, customCommand, args);
    }
  }
}

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

При использовании Jetpack Media3 при реализации MediaSession ваш PlaybackState автоматически обновляется с помощью медиаплеера. Аналогичным образом, когда вы реализуете MediaSessionService , библиотека автоматически публикует для вас уведомление MediaStyle и поддерживает его актуальность.

Реагировать на кнопки действий

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

Инструкции о том, как реагировать на пользовательскую команду, см. в разделе Добавление пользовательских команд .

Поведение до версии Android 13

В целях обратной совместимости системный пользовательский интерфейс по-прежнему предоставляет альтернативный макет, в котором используются действия уведомлений для приложений, которые не обновляются до Android 13 или не включают информацию PlaybackState . Кнопки действий извлекаются из списка Notification.Action , прикрепленного к уведомлению MediaStyle . Система отображает до пяти действий в том порядке, в котором они были добавлены. В компактном режиме отображается до трех кнопок, определяемых значениями, переданными в setShowActionsInCompactView() .

Пользовательские действия размещаются в том порядке, в котором они были добавлены в PlaybackState .

В следующем примере кода показано, как добавить действия в уведомление MediaStyle:

Котлин

import androidx.core.app.NotificationCompat
import androidx.media3.session.MediaStyleNotificationHelper

var notification = NotificationCompat.Builder(context, CHANNEL_ID)
        // Show controls on lock screen even when user hides sensitive content.
        .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
        .setSmallIcon(R.drawable.ic_stat_player)
        // Add media control buttons that invoke intents in your media service
        .addAction(R.drawable.ic_prev, "Previous", prevPendingIntent) // #0
        .addAction(R.drawable.ic_pause, "Pause", pausePendingIntent) // #1
        .addAction(R.drawable.ic_next, "Next", nextPendingIntent) // #2
        // Apply the media style template
        .setStyle(MediaStyleNotificationHelper.MediaStyle(mediaSession)
                .setShowActionsInCompactView(1 /* #1: pause button */))
        .setContentTitle("Wonderful music")
        .setContentText("My Awesome Band")
        .setLargeIcon(albumArtBitmap)
        .build()

Ява

import androidx.core.app.NotificationCompat;
import androidx.media3.session.MediaStyleNotificationHelper;

NotificationCompat.Builder notification = new NotificationCompat.Builder(context, CHANNEL_ID)
        .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
        .setSmallIcon(R.drawable.ic_stat_player)
        .addAction(R.drawable.ic_prev, "Previous", prevPendingIntent)
        .addAction(R.drawable.ic_pause, "Pause", pausePendingIntent)
        .addAction(R.drawable.ic_next, "Next", nextPendingIntent)
        .setStyle(new MediaStyleNotificationHelper.MediaStyle(mediaSession)
                .setShowActionsInCompactView(1 /* #1: pause button */))
        .setContentTitle("Wonderful music")
        .setContentText("My Awesome Band")
        .setLargeIcon(albumArtBitmap)
        .build();

Поддержка возобновления СМИ

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

Функцию возобновления воспроизведения можно включить и выключить с помощью приложения «Настройки» в разделе «Звук» > «Медиа» . Пользователь также может получить доступ к настройкам, коснувшись значка шестеренки, который появляется после пролистывания расширенной карусели.

Media3 предлагает API, упрощающие поддержку возобновления мультимедиа. Инструкции по реализации этой функции см. в документации по возобновлению воспроизведения с помощью Media3 .

Использование устаревших API-интерфейсов мультимедиа

В этом разделе объясняется, как интегрироваться с системными элементами управления мультимедиа с помощью устаревших API MediaCompat.

Система получает следующую информацию из MediaMetadata MediaSession и отображает ее, когда она доступна:

  • METADATA_KEY_ALBUM_ART_URI
  • METADATA_KEY_TITLE
  • METADATA_KEY_DISPLAY_TITLE
  • METADATA_KEY_ARTIST
  • METADATA_KEY_DURATION (если продолжительность не установлена, панель поиска не показывает прогресс)

Чтобы убедиться, что у вас есть действительное и точное уведомление об управлении мультимедиа, установите в качестве значения метаданных METADATA_KEY_TITLE или METADATA_KEY_DISPLAY_TITLE название медиафайла, воспроизводимого в данный момент.

Медиаплеер показывает прошедшее время для воспроизводимого в данный момент мультимедиа, а также панель поиска, которая сопоставлена ​​с MediaSession PlaybackState .

Медиаплеер показывает ход воспроизведения текущего мультимедиа, а также панель поиска, которая сопоставлена ​​с MediaSession PlaybackState . Панель поиска позволяет пользователям изменять положение и отображать прошедшее время для элемента мультимедиа. Чтобы панель поиска была включена, необходимо реализовать PlaybackState.Builder#setActions и включить ACTION_SEEK_TO .

Слот Действие Критерии
1 Играть Текущее состояние PlaybackState — одно из следующих:
  • STATE_NONE
  • STATE_STOPPED
  • STATE_PAUSED
  • STATE_ERROR
Загрузка счетчика Текущее состояние PlaybackState — одно из следующих:
  • STATE_CONNECTING
  • STATE_BUFFERING
Пауза Текущее состояние PlaybackState не отличается от вышеперечисленного.
2 Предыдущий Действия PlaybackState включают ACTION_SKIP_TO_PREVIOUS .
Обычай Действия PlaybackState не включают ACTION_SKIP_TO_PREVIOUS , а настраиваемые действия PlaybackState включают настраиваемое действие, которое еще не было размещено.
Пустой Дополнительные возможности PlaybackState включают true логическое значение для ключа SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV .
3 Следующий Действия PlaybackState включают ACTION_SKIP_TO_NEXT .
Обычай Действия PlaybackState не включают ACTION_SKIP_TO_NEXT , а настраиваемые действия PlaybackState включают настраиваемое действие, которое еще не было размещено.
Пустой Дополнительные возможности PlaybackState включают true логическое значение для ключа SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT .
4 Обычай Настраиваемые действия PlaybackState включают настраиваемое действие, которое еще не было размещено.
5 Обычай Настраиваемые действия PlaybackState включают настраиваемое действие, которое еще не было размещено.

Добавьте стандартные действия

В следующих примерах кода показано, как добавить стандартные и настраиваемые действия PlaybackState .

Для воспроизведения, паузы, предыдущего и следующего задайте эти действия в PlaybackState для сеанса мультимедиа.

Котлин

val session = MediaSessionCompat(context, TAG)
val playbackStateBuilder = PlaybackStateCompat.Builder()
val style = NotificationCompat.MediaStyle()

// For this example, the media is currently paused:
val state = PlaybackStateCompat.STATE_PAUSED
val position = 0L
val playbackSpeed = 1f
playbackStateBuilder.setState(state, position, playbackSpeed)

// And the user can play, skip to next or previous, and seek
val stateActions = PlaybackStateCompat.ACTION_PLAY
    or PlaybackStateCompat.ACTION_PLAY_PAUSE
    or PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
    or PlaybackStateCompat.ACTION_SKIP_TO_NEXT
    or PlaybackStateCompat.ACTION_SEEK_TO // adding the seek action enables seeking with the seekbar
playbackStateBuilder.setActions(stateActions)

// ... do more setup here ...

session.setPlaybackState(playbackStateBuilder.build())
style.setMediaSession(session.sessionToken)
notificationBuilder.setStyle(style)

Ява

MediaSessionCompat session = new MediaSessionCompat(context, TAG);
PlaybackStateCompat.Builder playbackStateBuilder = new PlaybackStateCompat.Builder();
NotificationCompat.MediaStyle style = new NotificationCompat.MediaStyle();

// For this example, the media is currently paused:
int state = PlaybackStateCompat.STATE_PAUSED;
long position = 0L;
float playbackSpeed = 1f;
playbackStateBuilder.setState(state, position, playbackSpeed);

// And the user can play, skip to next or previous, and seek
long stateActions = PlaybackStateCompat.ACTION_PLAY
    | PlaybackStateCompat.ACTION_PLAY_PAUSE
    | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
    | PlaybackStateCompat.ACTION_SKIP_TO_NEXT
    | PlaybackStateCompat.ACTION_SEEK_TO; // adding this enables the seekbar thumb
playbackStateBuilder.setActions(stateActions);

// ... do more setup here ...

session.setPlaybackState(playbackStateBuilder.build());
style.setMediaSession(session.getSessionToken());
notificationBuilder.setStyle(style);

Если вам не нужны кнопки в предыдущем или следующем слотах, не добавляйте ACTION_SKIP_TO_PREVIOUS или ACTION_SKIP_TO_NEXT , а вместо этого добавьте в сеанс дополнительные функции:

Котлин

session.setExtras(Bundle().apply {
    putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true)
    putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true)
})

Ява

Bundle extras = new Bundle();
extras.putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true);
extras.putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true);
session.setExtras(extras);

Добавить специальные действия

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

Котлин

val customAction = PlaybackStateCompat.CustomAction.Builder(
    "com.example.MY_CUSTOM_ACTION", // action ID
    "Custom Action", // title - used as content description for the button
    R.drawable.ic_custom_action
).build()

playbackStateBuilder.addCustomAction(customAction)

Ява

PlaybackStateCompat.CustomAction customAction = new PlaybackStateCompat.CustomAction.Builder(
        "com.example.MY_CUSTOM_ACTION", // action ID
        "Custom Action", // title - used as content description for the button
        R.drawable.ic_custom_action
).build();

playbackStateBuilder.addCustomAction(customAction);

Реагирование на действия PlaybackState

Когда пользователь нажимает кнопку, SystemUI использует MediaController.TransportControls для отправки команды обратно в MediaSession . Вам необходимо зарегистрировать обратный вызов, который сможет правильно реагировать на эти события.

Котлин

val callback = object: MediaSession.Callback() {
    override fun onPlay() {
        // start playback
    }

    override fun onPause() {
        // pause playback
    }

    override fun onSkipToPrevious() {
        // skip to previous
    }

    override fun onSkipToNext() {
        // skip to next
    }

    override fun onSeekTo(pos: Long) {
        // jump to position in track
    }

    override fun onCustomAction(action: String, extras: Bundle?) {
        when (action) {
            CUSTOM_ACTION_1 -> doCustomAction1(extras)
            CUSTOM_ACTION_2 -> doCustomAction2(extras)
            else -> {
                Log.w(TAG, "Unknown custom action $action")
            }
        }
    }

}

session.setCallback(callback)

Ява

MediaSession.Callback callback = new MediaSession.Callback() {
    @Override
    public void onPlay() {
        // start playback
    }

    @Override
    public void onPause() {
        // pause playback
    }

    @Override
    public void onSkipToPrevious() {
        // skip to previous
    }

    @Override
    public void onSkipToNext() {
        // skip to next
    }

    @Override
    public void onSeekTo(long pos) {
        // jump to position in track
    }

    @Override
    public void onCustomAction(String action, Bundle extras) {
        if (action.equals(CUSTOM_ACTION_1)) {
            doCustomAction1(extras);
        } else if (action.equals(CUSTOM_ACTION_2)) {
            doCustomAction2(extras);
        } else {
            Log.w(TAG, "Unknown custom action " + action);
        }
    }
};

Возобновление работы СМИ

Чтобы приложение проигрывателя появилось в области настроек быстрых настроек, необходимо создать уведомление MediaStyle с действительным токеном MediaSession .

Чтобы отобразить заголовок уведомления MediaStyle, используйте NotificationBuilder.setContentTitle() .

Чтобы отобразить значок бренда для медиаплеера, используйте NotificationBuilder.setSmallIcon() .

Чтобы поддерживать возобновление воспроизведения, приложения должны реализовать MediaBrowserService и MediaSession . Ваш MediaSession должен реализовать обратный вызов onPlay() .

Реализация MediaBrowserService

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

Система пытается связаться с вашим MediaBrowserService с помощью соединения из SystemUI. Ваше приложение должно разрешать такие соединения, иначе оно не сможет поддерживать возобновление воспроизведения.

Подключения из SystemUI можно идентифицировать и проверить с помощью имени пакета com.android.systemui и подписи. SystemUI подписан подписью платформы. Пример проверки подписи платформы можно найти в приложении UAMP .

Чтобы поддерживать возобновление воспроизведения, ваш MediaBrowserService должен реализовать следующее поведение:

  • onGetRoot() должен быстро возвращать ненулевой корень. Другая сложная логика должна обрабатываться в onLoadChildren()

  • Когда onLoadChildren() вызывается для идентификатора корневого носителя, результат должен содержать дочерний элемент FLAG_PLAYABLE .

  • MediaBrowserService должен возвращать последний воспроизведенный мультимедийный элемент при получении запроса EXTRA_RECENT . Возвращаемое значение должно быть реальным элементом мультимедиа, а не универсальной функцией.

  • MediaBrowserService должен предоставить соответствующее MediaDescription с непустым заголовком и подзаголовком . Он также должен установить URI значка или растровое изображение значка .

Следующие примеры кода иллюстрируют, как реализовать onGetRoot() .

Котлин

override fun onGetRoot(
    clientPackageName: String,
    clientUid: Int,
    rootHints: Bundle?
): BrowserRoot? {
    ...
    // Verify that the specified package is SystemUI. You'll need to write your 
    // own logic to do this.
    if (isSystem(clientPackageName, clientUid)) {
        rootHints?.let {
            if (it.getBoolean(BrowserRoot.EXTRA_RECENT)) {
                // Return a tree with a single playable media item for resumption.
                val extras = Bundle().apply {
                    putBoolean(BrowserRoot.EXTRA_RECENT, true)
                }
                return BrowserRoot(MY_RECENTS_ROOT_ID, extras)
            }
        }
        // You can return your normal tree if the EXTRA_RECENT flag is not present.
        return BrowserRoot(MY_MEDIA_ROOT_ID, null)
    }
    // Return an empty tree to disallow browsing.
    return BrowserRoot(MY_EMPTY_ROOT_ID, null)

Ява

@Override
public BrowserRoot onGetRoot(String clientPackageName, int clientUid,
    Bundle rootHints) {
    ...
    // Verify that the specified package is SystemUI. You'll need to write your
    // own logic to do this.
    if (isSystem(clientPackageName, clientUid)) {
        if (rootHints != null) {
            if (rootHints.getBoolean(BrowserRoot.EXTRA_RECENT)) {
                // Return a tree with a single playable media item for resumption.
                Bundle extras = new Bundle();
                extras.putBoolean(BrowserRoot.EXTRA_RECENT, true);
                return new BrowserRoot(MY_RECENTS_ROOT_ID, extras);
            }
        }
        // You can return your normal tree if the EXTRA_RECENT flag is not present.
        return new BrowserRoot(MY_MEDIA_ROOT_ID, null);
    }
    // Return an empty tree to disallow browsing.
    return new BrowserRoot(MY_EMPTY_ROOT_ID, null);
}
,

Элементы управления мультимедиа в Android расположены рядом с быстрыми настройками. Сеансы из нескольких приложений организованы в пролистываемую карусель. В карусели сеансы перечислены в следующем порядке:

  • Потоки, воспроизводимые локально на телефоне
  • Удаленные потоки, например обнаруженные на внешних устройствах или в сеансах трансляции.
  • Предыдущие возобновляемые сеансы в том порядке, в котором они проводились в последний раз.

Начиная с Android 13 (уровень API 33), чтобы гарантировать пользователям доступ к богатому набору элементов управления мультимедиа для приложений, воспроизводящих мультимедиа, кнопки действий на элементах управления мультимедиа извлекаются из состояния Player .

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

На рис. 1 показан пример того, как это выглядит на телефоне и планшете соответственно.

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

Система отображает до пяти кнопок действий в зависимости от состояния Player , как описано в следующей таблице. В компактном режиме отображаются только первые три слота действий. Это соответствует тому, как элементы управления мультимедиа отображаются на других платформах Android, таких как Auto, Assistant и Wear OS.

Слот Критерии Действие
1 playWhenReady имеет значение false или текущее состояние воспроизведенияSTATE_ENDED . Играть
playWhenReady имеет значение true, а текущее состояние воспроизведенияSTATE_BUFFERING . Загрузка счетчика
playWhenReady имеет значение true, а текущее состояние воспроизведенияSTATE_READY . Пауза
2 Доступна команда проигрывателя COMMAND_SEEK_TO_PREVIOUS или COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM . Предыдущий
Ни команда игрока COMMAND_SEEK_TO_PREVIOUS , ни COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM недоступна, а пользовательская команда из пользовательского макета, которая еще не была размещена, доступна для заполнения слота. Обычай
(пока не поддерживается в Media3) Дополнительные возможности PlaybackState включают true логическое значение для ключа EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV . Пустой
3 Доступна команда проигрывателя COMMAND_SEEK_TO_NEXT или COMMAND_SEEK_TO_NEXT_MEDIA_ITEM . Следующий
Ни команда игрока COMMAND_SEEK_TO_NEXT , ни COMMAND_SEEK_TO_NEXT_MEDIA_ITEM недоступны, а пользовательская команда из пользовательского макета, которая еще не была размещена, доступна для заполнения слота. Обычай
(пока не поддерживается в Media3) Дополнительные возможности PlaybackState включают true логическое значение для ключа EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT . Пустой
4 Пользовательская команда из пользовательского макета, которая еще не была размещена, доступна для заполнения слота. Обычай
5 Пользовательская команда из пользовательского макета, которая еще не была размещена, доступна для заполнения слота. Обычай

Пользовательские команды размещаются в том порядке, в котором они были добавлены в пользовательский макет.

Настройте командные кнопки

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

  1. В onCreate() создайте MediaSession и определите собственный макет командных кнопок.

  2. В MediaSession.Callback.onConnect() авторизуйте контроллеры, определив их доступные команды, включая пользовательские команды , в ConnectionResult .

  3. В MediaSession.Callback.onCustomCommand() ответьте на пользовательскую команду, выбранную пользователем.

Котлин

class PlaybackService : MediaSessionService() {
  private val customCommandFavorites = SessionCommand(ACTION_FAVORITES, Bundle.EMPTY)
  private var mediaSession: MediaSession? = null

  override fun onCreate() {
    super.onCreate()
    val favoriteButton =
      CommandButton.Builder()
        .setDisplayName("Save to favorites")
        .setIconResId(R.drawable.favorite_icon)
        .setSessionCommand(customCommandFavorites)
        .build()
    val player = ExoPlayer.Builder(this).build()
    // Build the session with a custom layout.
    mediaSession =
      MediaSession.Builder(this, player)
        .setCallback(MyCallback())
        .setCustomLayout(ImmutableList.of(favoriteButton))
        .build()
  }

  private inner class MyCallback : MediaSession.Callback {
    override fun onConnect(
      session: MediaSession,
      controller: MediaSession.ControllerInfo
    ): ConnectionResult {
    // Set available player and session commands.
    return AcceptedResultBuilder(session)
      .setAvailablePlayerCommands(
        ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
          .remove(COMMAND_SEEK_TO_NEXT)
          .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
          .remove(COMMAND_SEEK_TO_PREVIOUS)
          .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
          .build()
      )
      .setAvailableSessionCommands(
        ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
          .add(customCommandFavorites)
          .build()
      )
      .build()
    }

    override fun onCustomCommand(
      session: MediaSession,
      controller: MediaSession.ControllerInfo,
      customCommand: SessionCommand,
      args: Bundle
    ): ListenableFuture {
      if (customCommand.customAction == ACTION_FAVORITES) {
        // Do custom logic here
        saveToFavorites(session.player.currentMediaItem)
        return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
      }
      return super.onCustomCommand(session, controller, customCommand, args)
    }
  }
}

Ява

public class PlaybackService extends MediaSessionService {
  private static final SessionCommand CUSTOM_COMMAND_FAVORITES =
      new SessionCommand("ACTION_FAVORITES", Bundle.EMPTY);
  @Nullable private MediaSession mediaSession;

  public void onCreate() {
    super.onCreate();
    CommandButton favoriteButton =
        new CommandButton.Builder()
            .setDisplayName("Save to favorites")
            .setIconResId(R.drawable.favorite_icon)
            .setSessionCommand(CUSTOM_COMMAND_FAVORITES)
            .build();
    Player player = new ExoPlayer.Builder(this).build();
    // Build the session with a custom layout.
    mediaSession =
        new MediaSession.Builder(this, player)
            .setCallback(new MyCallback())
            .setCustomLayout(ImmutableList.of(favoriteButton))
            .build();
  }

  private static class MyCallback implements MediaSession.Callback {
    @Override
    public ConnectionResult onConnect(
        MediaSession session, MediaSession.ControllerInfo controller) {
      // Set available player and session commands.
      return new AcceptedResultBuilder(session)
          .setAvailablePlayerCommands(
              ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
                .remove(COMMAND_SEEK_TO_NEXT)
                .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
                .remove(COMMAND_SEEK_TO_PREVIOUS)
                .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
                .build())
          .setAvailableSessionCommands(
              ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
                .add(CUSTOM_COMMAND_FAVORITES)
                .build())
          .build();
    }

    public ListenableFuture onCustomCommand(
        MediaSession session,
        MediaSession.ControllerInfo controller,
        SessionCommand customCommand,
        Bundle args) {
      if (customCommand.customAction.equals(CUSTOM_COMMAND_FAVORITES.customAction)) {
        // Do custom logic here
        saveToFavorites(session.getPlayer().getCurrentMediaItem());
        return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_SUCCESS));
      }
      return MediaSession.Callback.super.onCustomCommand(
          session, controller, customCommand, args);
    }
  }
}

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

При использовании Jetpack Media3 при реализации MediaSession ваш PlaybackState автоматически обновляется с помощью медиаплеера. Аналогичным образом, когда вы реализуете MediaSessionService , библиотека автоматически публикует для вас уведомление MediaStyle и поддерживает его актуальность.

Реагировать на кнопки действий

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

Инструкции о том, как реагировать на пользовательскую команду, см. в разделе Добавление пользовательских команд .

Поведение до версии Android 13

В целях обратной совместимости системный пользовательский интерфейс по-прежнему предоставляет альтернативный макет, в котором используются действия уведомлений для приложений, которые не обновляются до Android 13 или не включают информацию PlaybackState . Кнопки действий извлекаются из списка Notification.Action , прикрепленного к уведомлению MediaStyle . Система отображает до пяти действий в том порядке, в котором они были добавлены. В компактном режиме отображается до трех кнопок, что определяется значениями, переданными в setShowActionsInCompactView() .

Пользовательские действия размещаются в том порядке, в котором они были добавлены в PlaybackState .

В следующем примере кода показано, как добавить действия в уведомление MediaStyle:

Котлин

import androidx.core.app.NotificationCompat
import androidx.media3.session.MediaStyleNotificationHelper

var notification = NotificationCompat.Builder(context, CHANNEL_ID)
        // Show controls on lock screen even when user hides sensitive content.
        .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
        .setSmallIcon(R.drawable.ic_stat_player)
        // Add media control buttons that invoke intents in your media service
        .addAction(R.drawable.ic_prev, "Previous", prevPendingIntent) // #0
        .addAction(R.drawable.ic_pause, "Pause", pausePendingIntent) // #1
        .addAction(R.drawable.ic_next, "Next", nextPendingIntent) // #2
        // Apply the media style template
        .setStyle(MediaStyleNotificationHelper.MediaStyle(mediaSession)
                .setShowActionsInCompactView(1 /* #1: pause button */))
        .setContentTitle("Wonderful music")
        .setContentText("My Awesome Band")
        .setLargeIcon(albumArtBitmap)
        .build()

Ява

import androidx.core.app.NotificationCompat;
import androidx.media3.session.MediaStyleNotificationHelper;

NotificationCompat.Builder notification = new NotificationCompat.Builder(context, CHANNEL_ID)
        .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
        .setSmallIcon(R.drawable.ic_stat_player)
        .addAction(R.drawable.ic_prev, "Previous", prevPendingIntent)
        .addAction(R.drawable.ic_pause, "Pause", pausePendingIntent)
        .addAction(R.drawable.ic_next, "Next", nextPendingIntent)
        .setStyle(new MediaStyleNotificationHelper.MediaStyle(mediaSession)
                .setShowActionsInCompactView(1 /* #1: pause button */))
        .setContentTitle("Wonderful music")
        .setContentText("My Awesome Band")
        .setLargeIcon(albumArtBitmap)
        .build();

Поддержка возобновления СМИ

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

Функцию возобновления воспроизведения можно включить и выключить с помощью приложения «Настройки» в разделе «Звук» > «Медиа» . Пользователь также может получить доступ к настройкам, коснувшись значка шестеренки, который появляется после пролистывания расширенной карусели.

Media3 предлагает API, упрощающие поддержку возобновления мультимедиа. Инструкции по реализации этой функции см. в документации по возобновлению воспроизведения с помощью Media3 .

Использование устаревших API-интерфейсов мультимедиа

В этом разделе объясняется, как интегрироваться с системными элементами управления мультимедиа с помощью устаревших API MediaCompat.

Система получает следующую информацию из MediaMetadata MediaSession и отображает ее, когда она доступна:

  • METADATA_KEY_ALBUM_ART_URI
  • METADATA_KEY_TITLE
  • METADATA_KEY_DISPLAY_TITLE
  • METADATA_KEY_ARTIST
  • METADATA_KEY_DURATION (если продолжительность не установлена, панель поиска не показывает прогресс)

Чтобы убедиться, что у вас есть действительное и точное уведомление об управлении мультимедиа, установите в качестве значения метаданных METADATA_KEY_TITLE или METADATA_KEY_DISPLAY_TITLE название медиафайла, воспроизводимого в данный момент.

Медиаплеер показывает прошедшее время для воспроизводимого в данный момент мультимедиа, а также панель поиска, которая сопоставлена ​​с MediaSession PlaybackState .

Медиаплеер показывает ход воспроизведения текущего мультимедиа, а также панель поиска, которая сопоставлена ​​с MediaSession PlaybackState . Панель поиска позволяет пользователям изменять положение и отображать прошедшее время для элемента мультимедиа. Чтобы панель поиска была включена, необходимо реализовать PlaybackState.Builder#setActions и включить ACTION_SEEK_TO .

Слот Действие Критерии
1 Играть Текущее состояние PlaybackState — одно из следующих:
  • STATE_NONE
  • STATE_STOPPED
  • STATE_PAUSED
  • STATE_ERROR
Загрузка счетчика Текущее состояние PlaybackState является одним из следующих:
  • STATE_CONNECTING
  • STATE_BUFFERING
Пауза Текущее состояние PlaybackState не отличается от вышеперечисленного.
2 Предыдущий Действия PlaybackState включают ACTION_SKIP_TO_PREVIOUS .
Обычай Действия PlaybackState не включают ACTION_SKIP_TO_PREVIOUS , а настраиваемые действия PlaybackState включают настраиваемое действие, которое еще не было размещено.
Пустой Дополнительные возможности PlaybackState включают true логическое значение для ключа SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV .
3 Следующий Действия PlaybackState включают ACTION_SKIP_TO_NEXT .
Обычай Действия PlaybackState не включают ACTION_SKIP_TO_NEXT , а настраиваемые действия PlaybackState включают настраиваемое действие, которое еще не было размещено.
Пустой Дополнительные возможности PlaybackState включают true логическое значение для ключа SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT .
4 Обычай Настраиваемые действия PlaybackState включают настраиваемое действие, которое еще не было размещено.
5 Обычай Настраиваемые действия PlaybackState включают настраиваемое действие, которое еще не было размещено.

Добавьте стандартные действия

В следующих примерах кода показано, как добавить стандартные и настраиваемые действия PlaybackState .

Для воспроизведения, паузы, предыдущего и следующего задайте эти действия в PlaybackState для сеанса мультимедиа.

Котлин

val session = MediaSessionCompat(context, TAG)
val playbackStateBuilder = PlaybackStateCompat.Builder()
val style = NotificationCompat.MediaStyle()

// For this example, the media is currently paused:
val state = PlaybackStateCompat.STATE_PAUSED
val position = 0L
val playbackSpeed = 1f
playbackStateBuilder.setState(state, position, playbackSpeed)

// And the user can play, skip to next or previous, and seek
val stateActions = PlaybackStateCompat.ACTION_PLAY
    or PlaybackStateCompat.ACTION_PLAY_PAUSE
    or PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
    or PlaybackStateCompat.ACTION_SKIP_TO_NEXT
    or PlaybackStateCompat.ACTION_SEEK_TO // adding the seek action enables seeking with the seekbar
playbackStateBuilder.setActions(stateActions)

// ... do more setup here ...

session.setPlaybackState(playbackStateBuilder.build())
style.setMediaSession(session.sessionToken)
notificationBuilder.setStyle(style)

Ява

MediaSessionCompat session = new MediaSessionCompat(context, TAG);
PlaybackStateCompat.Builder playbackStateBuilder = new PlaybackStateCompat.Builder();
NotificationCompat.MediaStyle style = new NotificationCompat.MediaStyle();

// For this example, the media is currently paused:
int state = PlaybackStateCompat.STATE_PAUSED;
long position = 0L;
float playbackSpeed = 1f;
playbackStateBuilder.setState(state, position, playbackSpeed);

// And the user can play, skip to next or previous, and seek
long stateActions = PlaybackStateCompat.ACTION_PLAY
    | PlaybackStateCompat.ACTION_PLAY_PAUSE
    | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
    | PlaybackStateCompat.ACTION_SKIP_TO_NEXT
    | PlaybackStateCompat.ACTION_SEEK_TO; // adding this enables the seekbar thumb
playbackStateBuilder.setActions(stateActions);

// ... do more setup here ...

session.setPlaybackState(playbackStateBuilder.build());
style.setMediaSession(session.getSessionToken());
notificationBuilder.setStyle(style);

Если вам не нужны кнопки в предыдущем или следующем слотах, не добавляйте ACTION_SKIP_TO_PREVIOUS или ACTION_SKIP_TO_NEXT , а вместо этого добавьте в сеанс дополнительные функции:

Котлин

session.setExtras(Bundle().apply {
    putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true)
    putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true)
})

Ява

Bundle extras = new Bundle();
extras.putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true);
extras.putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true);
session.setExtras(extras);

Добавить специальные действия

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

Котлин

val customAction = PlaybackStateCompat.CustomAction.Builder(
    "com.example.MY_CUSTOM_ACTION", // action ID
    "Custom Action", // title - used as content description for the button
    R.drawable.ic_custom_action
).build()

playbackStateBuilder.addCustomAction(customAction)

Ява

PlaybackStateCompat.CustomAction customAction = new PlaybackStateCompat.CustomAction.Builder(
        "com.example.MY_CUSTOM_ACTION", // action ID
        "Custom Action", // title - used as content description for the button
        R.drawable.ic_custom_action
).build();

playbackStateBuilder.addCustomAction(customAction);

Реагирование на действия PlaybackState

Когда пользователь нажимает кнопку, SystemUI использует MediaController.TransportControls для отправки команды обратно в MediaSession . Вам необходимо зарегистрировать обратный вызов, который сможет правильно реагировать на эти события.

Котлин

val callback = object: MediaSession.Callback() {
    override fun onPlay() {
        // start playback
    }

    override fun onPause() {
        // pause playback
    }

    override fun onSkipToPrevious() {
        // skip to previous
    }

    override fun onSkipToNext() {
        // skip to next
    }

    override fun onSeekTo(pos: Long) {
        // jump to position in track
    }

    override fun onCustomAction(action: String, extras: Bundle?) {
        when (action) {
            CUSTOM_ACTION_1 -> doCustomAction1(extras)
            CUSTOM_ACTION_2 -> doCustomAction2(extras)
            else -> {
                Log.w(TAG, "Unknown custom action $action")
            }
        }
    }

}

session.setCallback(callback)

Ява

MediaSession.Callback callback = new MediaSession.Callback() {
    @Override
    public void onPlay() {
        // start playback
    }

    @Override
    public void onPause() {
        // pause playback
    }

    @Override
    public void onSkipToPrevious() {
        // skip to previous
    }

    @Override
    public void onSkipToNext() {
        // skip to next
    }

    @Override
    public void onSeekTo(long pos) {
        // jump to position in track
    }

    @Override
    public void onCustomAction(String action, Bundle extras) {
        if (action.equals(CUSTOM_ACTION_1)) {
            doCustomAction1(extras);
        } else if (action.equals(CUSTOM_ACTION_2)) {
            doCustomAction2(extras);
        } else {
            Log.w(TAG, "Unknown custom action " + action);
        }
    }
};

Возобновление работы СМИ

Чтобы приложение проигрывателя появилось в области настроек быстрых настроек, необходимо создать уведомление MediaStyle с действительным токеном MediaSession .

Чтобы отобразить заголовок уведомления MediaStyle, используйте NotificationBuilder.setContentTitle() .

Чтобы отобразить значок бренда для медиаплеера, используйте NotificationBuilder.setSmallIcon() .

Чтобы поддерживать возобновление воспроизведения, приложения должны реализовать MediaBrowserService и MediaSession . Ваш MediaSession должен реализовать обратный вызов onPlay() .

Реализация MediaBrowserService

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

Система пытается связаться с вашим MediaBrowserService с помощью соединения из SystemUI. Ваше приложение должно разрешать такие соединения, иначе оно не сможет поддерживать возобновление воспроизведения.

Подключения из SystemUI можно идентифицировать и проверить с помощью имени пакета com.android.systemui и подписи. SystemUI подписан подписью платформы. Пример проверки подписи платформы можно найти в приложении UAMP .

Чтобы поддерживать возобновление воспроизведения, ваш MediaBrowserService должен реализовать следующее поведение:

  • onGetRoot() должен быстро возвращать ненулевой корень. Другая сложная логика должна обрабатываться в onLoadChildren()

  • Когда onLoadChildren() вызывается для идентификатора корневого носителя, результат должен содержать дочерний элемент FLAG_PLAYABLE .

  • MediaBrowserService должен возвращать последний воспроизведенный мультимедийный элемент при получении запроса EXTRA_RECENT . Возвращаемое значение должно быть реальным элементом мультимедиа, а не универсальной функцией.

  • MediaBrowserService должен предоставить соответствующее MediaDescription с непустым заголовком и подзаголовком . Он также должен установить URI значка или растровое изображение значка .

Следующие примеры кода иллюстрируют, как реализовать onGetRoot() .

Котлин

override fun onGetRoot(
    clientPackageName: String,
    clientUid: Int,
    rootHints: Bundle?
): BrowserRoot? {
    ...
    // Verify that the specified package is SystemUI. You'll need to write your 
    // own logic to do this.
    if (isSystem(clientPackageName, clientUid)) {
        rootHints?.let {
            if (it.getBoolean(BrowserRoot.EXTRA_RECENT)) {
                // Return a tree with a single playable media item for resumption.
                val extras = Bundle().apply {
                    putBoolean(BrowserRoot.EXTRA_RECENT, true)
                }
                return BrowserRoot(MY_RECENTS_ROOT_ID, extras)
            }
        }
        // You can return your normal tree if the EXTRA_RECENT flag is not present.
        return BrowserRoot(MY_MEDIA_ROOT_ID, null)
    }
    // Return an empty tree to disallow browsing.
    return BrowserRoot(MY_EMPTY_ROOT_ID, null)

Ява

@Override
public BrowserRoot onGetRoot(String clientPackageName, int clientUid,
    Bundle rootHints) {
    ...
    // Verify that the specified package is SystemUI. You'll need to write your
    // own logic to do this.
    if (isSystem(clientPackageName, clientUid)) {
        if (rootHints != null) {
            if (rootHints.getBoolean(BrowserRoot.EXTRA_RECENT)) {
                // Return a tree with a single playable media item for resumption.
                Bundle extras = new Bundle();
                extras.putBoolean(BrowserRoot.EXTRA_RECENT, true);
                return new BrowserRoot(MY_RECENTS_ROOT_ID, extras);
            }
        }
        // You can return your normal tree if the EXTRA_RECENT flag is not present.
        return new BrowserRoot(MY_MEDIA_ROOT_ID, null);
    }
    // Return an empty tree to disallow browsing.
    return new BrowserRoot(MY_EMPTY_ROOT_ID, null);
}
,

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

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

Начиная с Android 13 (уровень API 33), чтобы убедиться, что пользователи могут получить доступ к богатому набору элементов управления медиа для приложений, воспроизводимых мультимедиа, кнопки действий на средствах управления носителями получаются из состояния Player .

Таким образом, вы можете представить последовательный набор элементов управления носителями и более отполированного опыта управления носителями на разных устройствах.

На рисунке 1 показан пример того, как это выглядит на телефоне и планшетном устройстве соответственно.

Управление среды с точки зрения того, как они появляются на устройствах по телефону и планшетам, используя пример образец трека, показывающий, как могут появляться кнопки
Рисунок 1: Управление носителями на устройствах по телефону и планшетам

Система отображает до пяти кнопок действия на основе состояния Player , как описано в следующей таблице. В компактном режиме отображаются только первые три слота действий. Это согласуется с тем, как управления средствами для носителей отображаются на других платформах Android, таких как Auto, Assistant и Wear OS.

Слот Критерии Действие
1 playWhenReady является ложным, или текущее состояние воспроизведения является STATE_ENDED . Играть
playWhenReady - это правда, и текущее состояние воспроизведения - STATE_BUFFERING . Загрузка прядильщика
playWhenReady - это правда, и текущее состояние воспроизведения - STATE_READY . Пауза
2 Команда команды COMMAND_SEEK_TO_PREVIOUS или COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM доступен. Предыдущий
Ни команда Player COMMAND_SEEK_TO_PREVIOUS , ни COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM доступна, и пользовательская команда из пользовательской макета, которая еще не была размещена, доступна для заполнения слота. Обычай
(Еще не поддерживается с Media3) Дополнительные данные PlaybackState включают в себя true логическое значение для ключа EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV . Пустой
3 Команда команды COMMAND_SEEK_TO_NEXT или COMMAND_SEEK_TO_NEXT_MEDIA_ITEM доступен. Следующий
Ни команда Player COMMAND_SEEK_TO_NEXT , ни COMMAND_SEEK_TO_NEXT_MEDIA_ITEM не доступна, и пользовательская команда из пользовательской макета, которая еще не была размещена, доступна для заполнения слота. Обычай
(Еще не поддерживается с Media3) Дополнительные данные PlaybackState включают в себя true логическое значение для ключа EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT . Пустой
4 Пользовательская команда из пользовательского макета, которая еще не была размещена, доступна для заполнения слота. Обычай
5 Пользовательская команда из пользовательского макета, которая еще не была размещена, доступна для заполнения слота. Обычай

Пользовательские команды размещаются в порядке, в котором они были добавлены в пользовательский макет.

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

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

  1. В onCreate() , создайте MediaSession и определите пользовательский макет командных кнопок.

  2. В MediaSession.Callback.onConnect() разрешите контроллеры, определяя их доступные команды, включая пользовательские команды , в ConnectionResult .

  3. В MediaSession.Callback.onCustomCommand() ответьте на пользовательскую команду, выбранная пользователем.

Котлин

class PlaybackService : MediaSessionService() {
  private val customCommandFavorites = SessionCommand(ACTION_FAVORITES, Bundle.EMPTY)
  private var mediaSession: MediaSession? = null

  override fun onCreate() {
    super.onCreate()
    val favoriteButton =
      CommandButton.Builder()
        .setDisplayName("Save to favorites")
        .setIconResId(R.drawable.favorite_icon)
        .setSessionCommand(customCommandFavorites)
        .build()
    val player = ExoPlayer.Builder(this).build()
    // Build the session with a custom layout.
    mediaSession =
      MediaSession.Builder(this, player)
        .setCallback(MyCallback())
        .setCustomLayout(ImmutableList.of(favoriteButton))
        .build()
  }

  private inner class MyCallback : MediaSession.Callback {
    override fun onConnect(
      session: MediaSession,
      controller: MediaSession.ControllerInfo
    ): ConnectionResult {
    // Set available player and session commands.
    return AcceptedResultBuilder(session)
      .setAvailablePlayerCommands(
        ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
          .remove(COMMAND_SEEK_TO_NEXT)
          .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
          .remove(COMMAND_SEEK_TO_PREVIOUS)
          .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
          .build()
      )
      .setAvailableSessionCommands(
        ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
          .add(customCommandFavorites)
          .build()
      )
      .build()
    }

    override fun onCustomCommand(
      session: MediaSession,
      controller: MediaSession.ControllerInfo,
      customCommand: SessionCommand,
      args: Bundle
    ): ListenableFuture {
      if (customCommand.customAction == ACTION_FAVORITES) {
        // Do custom logic here
        saveToFavorites(session.player.currentMediaItem)
        return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
      }
      return super.onCustomCommand(session, controller, customCommand, args)
    }
  }
}

Ява

public class PlaybackService extends MediaSessionService {
  private static final SessionCommand CUSTOM_COMMAND_FAVORITES =
      new SessionCommand("ACTION_FAVORITES", Bundle.EMPTY);
  @Nullable private MediaSession mediaSession;

  public void onCreate() {
    super.onCreate();
    CommandButton favoriteButton =
        new CommandButton.Builder()
            .setDisplayName("Save to favorites")
            .setIconResId(R.drawable.favorite_icon)
            .setSessionCommand(CUSTOM_COMMAND_FAVORITES)
            .build();
    Player player = new ExoPlayer.Builder(this).build();
    // Build the session with a custom layout.
    mediaSession =
        new MediaSession.Builder(this, player)
            .setCallback(new MyCallback())
            .setCustomLayout(ImmutableList.of(favoriteButton))
            .build();
  }

  private static class MyCallback implements MediaSession.Callback {
    @Override
    public ConnectionResult onConnect(
        MediaSession session, MediaSession.ControllerInfo controller) {
      // Set available player and session commands.
      return new AcceptedResultBuilder(session)
          .setAvailablePlayerCommands(
              ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
                .remove(COMMAND_SEEK_TO_NEXT)
                .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
                .remove(COMMAND_SEEK_TO_PREVIOUS)
                .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
                .build())
          .setAvailableSessionCommands(
              ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
                .add(CUSTOM_COMMAND_FAVORITES)
                .build())
          .build();
    }

    public ListenableFuture onCustomCommand(
        MediaSession session,
        MediaSession.ControllerInfo controller,
        SessionCommand customCommand,
        Bundle args) {
      if (customCommand.customAction.equals(CUSTOM_COMMAND_FAVORITES.customAction)) {
        // Do custom logic here
        saveToFavorites(session.getPlayer().getCurrentMediaItem());
        return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_SUCCESS));
      }
      return MediaSession.Callback.super.onCustomCommand(
          session, controller, customCommand, args);
    }
  }
}

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

С помощью JetPack Media3, когда вы внедряете MediaSession , ваш PlaybackState автоматически поддерживает актуальность со СМИ. Точно так же, когда вы внедряете MediaSessionService , библиотека автоматически публикует для вас уведомление MediaStyle и поддерживает его.

Ответьте на кнопки действий

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

См. Добавить пользовательские команды для руководства о том, как ответить на пользовательскую команду.

Предварительно-ан-андродея 13 поведение

Для обратной совместимости System UI продолжает предоставлять альтернативный макет, который использует действия уведомления для приложений, которые не обновляются для Target Android 13, или которые не включают информацию PlaybackState . Кнопки действий получены из списка Notification.Action Прикрепленный к уведомлению MediaStyle . Система отображает до пяти действий в том порядке, в котором они были добавлены. В компактном режиме показаны до трех кнопок, определяемых значениями, передаваемыми в setShowActionsInCompactView() .

Пользовательские действия размещаются в том порядке, в котором они были добавлены в PlaybackState .

Следующий пример кода показывает, как добавить действия в уведомление MediaStyle:

Котлин

import androidx.core.app.NotificationCompat
import androidx.media3.session.MediaStyleNotificationHelper

var notification = NotificationCompat.Builder(context, CHANNEL_ID)
        // Show controls on lock screen even when user hides sensitive content.
        .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
        .setSmallIcon(R.drawable.ic_stat_player)
        // Add media control buttons that invoke intents in your media service
        .addAction(R.drawable.ic_prev, "Previous", prevPendingIntent) // #0
        .addAction(R.drawable.ic_pause, "Pause", pausePendingIntent) // #1
        .addAction(R.drawable.ic_next, "Next", nextPendingIntent) // #2
        // Apply the media style template
        .setStyle(MediaStyleNotificationHelper.MediaStyle(mediaSession)
                .setShowActionsInCompactView(1 /* #1: pause button */))
        .setContentTitle("Wonderful music")
        .setContentText("My Awesome Band")
        .setLargeIcon(albumArtBitmap)
        .build()

Ява

import androidx.core.app.NotificationCompat;
import androidx.media3.session.MediaStyleNotificationHelper;

NotificationCompat.Builder notification = new NotificationCompat.Builder(context, CHANNEL_ID)
        .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
        .setSmallIcon(R.drawable.ic_stat_player)
        .addAction(R.drawable.ic_prev, "Previous", prevPendingIntent)
        .addAction(R.drawable.ic_pause, "Pause", pausePendingIntent)
        .addAction(R.drawable.ic_next, "Next", nextPendingIntent)
        .setStyle(new MediaStyleNotificationHelper.MediaStyle(mediaSession)
                .setShowActionsInCompactView(1 /* #1: pause button */))
        .setContentTitle("Wonderful music")
        .setContentText("My Awesome Band")
        .setLargeIcon(albumArtBitmap)
        .build();

Поддержка возобновления СМИ

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

Функция воспроизведения воспроизведения может быть включена и выключена, используя приложение «Настройки», под параметрами Sound> Media . Пользователь также может получить доступ к настройкам, нажав к значке передачи, который появляется после прохождения на расширенной карусели.

Media3 предлагает API, чтобы облегчить возобновление возобновления медиа. Смотрите возобновление воспроизведения с документацией Media3 для руководства по реализации этой функции.

Использование APIS Legacy Media API

В этом разделе объясняется, как интегрироваться с системными средствами управления носителями, используя API legacy mediacompat.

Система извлекает следующую информацию из MediaMetadata в MediaSession и отображает ее, когда она доступна:

  • METADATA_KEY_ALBUM_ART_URI
  • METADATA_KEY_TITLE
  • METADATA_KEY_DISPLAY_TITLE
  • METADATA_KEY_ARTIST
  • METADATA_KEY_DURATION (если продолжительность не установлена, батончик не показывает прогресса)

Чтобы убедиться, что у вас есть достоверное и точное уведомление о управлении носителем, установите значение METADATA_KEY_TITLE или METADATA_KEY_DISPLAY_TITLE клей_дисплей_title в название в настоящее время воспроизводится медиа.

Медиаплеер показывает прошедшее время для в настоящее время играющих средств массовой информации, а также бар Seek Bar, которая сопоставлена ​​с PlaybackState MediaSession .

Медиаплеер демонстрирует прогресс для в настоящее время игровых СМИ, а также бар Seek Bar, который сопоставлен с MediaSession PlaybackState . Бар Seek позволяет пользователям изменить позицию и отображает истеченное время для элемента медиа. Для того, чтобы включил в систему поиска, вы должны реализовать PlaybackState.Builder#setActions и включить ACTION_SEEK_TO .

Слот Действие Критерии
1 Играть Текущее состояние PlaybackState - это одно из следующих:
  • STATE_NONE
  • STATE_STOPPED
  • STATE_PAUSED
  • STATE_ERROR
Загрузка прядильщика Текущее состояние PlaybackState является одним из следующих:
  • STATE_CONNECTING
  • STATE_BUFFERING
Пауза Текущее состояние PlaybackState не является ни одним из вышеперечисленных.
2 Предыдущий Действия PlaybackState включают ACTION_SKIP_TO_PREVIOUS .
Обычай Действия PlaybackState не включают в себя ACTION_SKIP_TO_PREVIOUS , а PlaybackState Пользовательские действия включают в себя пользовательское действие, которое еще не было размещено.
Пустой Дополнительные дополнения PlaybackState включают в себя true логическое значение для ключа SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV .
3 Следующий Действия PlaybackState включают ACTION_SKIP_TO_NEXT .
Обычай Действия PlaybackState не включают в себя ACTION_SKIP_TO_NEXT а пользовательские действия PlaybackState включают в себя пользовательское действие, которое еще не было размещено.
Пустой Дополнительные дополнения PlaybackState включают в себя true логическое значение для ключа SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT .
4 Обычай Пользовательские действия PlaybackState включают в себя пользовательское действие, которое еще не было размещено.
5 Обычай Пользовательские действия PlaybackState включают в себя пользовательское действие, которое еще не было размещено.

Добавьте стандартные действия

Следующие примеры кода иллюстрируют, как PlaybackState стандартные и пользовательские действия.

Для игры, пауза, предыдущий и следующий, установите эти действия в PlaybackState для медиа -сессии.

Котлин

val session = MediaSessionCompat(context, TAG)
val playbackStateBuilder = PlaybackStateCompat.Builder()
val style = NotificationCompat.MediaStyle()

// For this example, the media is currently paused:
val state = PlaybackStateCompat.STATE_PAUSED
val position = 0L
val playbackSpeed = 1f
playbackStateBuilder.setState(state, position, playbackSpeed)

// And the user can play, skip to next or previous, and seek
val stateActions = PlaybackStateCompat.ACTION_PLAY
    or PlaybackStateCompat.ACTION_PLAY_PAUSE
    or PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
    or PlaybackStateCompat.ACTION_SKIP_TO_NEXT
    or PlaybackStateCompat.ACTION_SEEK_TO // adding the seek action enables seeking with the seekbar
playbackStateBuilder.setActions(stateActions)

// ... do more setup here ...

session.setPlaybackState(playbackStateBuilder.build())
style.setMediaSession(session.sessionToken)
notificationBuilder.setStyle(style)

Ява

MediaSessionCompat session = new MediaSessionCompat(context, TAG);
PlaybackStateCompat.Builder playbackStateBuilder = new PlaybackStateCompat.Builder();
NotificationCompat.MediaStyle style = new NotificationCompat.MediaStyle();

// For this example, the media is currently paused:
int state = PlaybackStateCompat.STATE_PAUSED;
long position = 0L;
float playbackSpeed = 1f;
playbackStateBuilder.setState(state, position, playbackSpeed);

// And the user can play, skip to next or previous, and seek
long stateActions = PlaybackStateCompat.ACTION_PLAY
    | PlaybackStateCompat.ACTION_PLAY_PAUSE
    | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
    | PlaybackStateCompat.ACTION_SKIP_TO_NEXT
    | PlaybackStateCompat.ACTION_SEEK_TO; // adding this enables the seekbar thumb
playbackStateBuilder.setActions(stateActions);

// ... do more setup here ...

session.setPlaybackState(playbackStateBuilder.build());
style.setMediaSession(session.getSessionToken());
notificationBuilder.setStyle(style);

Если вам не нужны какие -либо кнопки в предыдущем или следующем слотах, не добавляйте ACTION_SKIP_TO_PREVIOUS или ACTION_SKIP_TO_NEXT , а вместо этого добавляйте дополнения к сеансу:

Котлин

session.setExtras(Bundle().apply {
    putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true)
    putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true)
})

Ява

Bundle extras = new Bundle();
extras.putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true);
extras.putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true);
session.setExtras(extras);

Добавьте пользовательские действия

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

Котлин

val customAction = PlaybackStateCompat.CustomAction.Builder(
    "com.example.MY_CUSTOM_ACTION", // action ID
    "Custom Action", // title - used as content description for the button
    R.drawable.ic_custom_action
).build()

playbackStateBuilder.addCustomAction(customAction)

Ява

PlaybackStateCompat.CustomAction customAction = new PlaybackStateCompat.CustomAction.Builder(
        "com.example.MY_CUSTOM_ACTION", // action ID
        "Custom Action", // title - used as content description for the button
        R.drawable.ic_custom_action
).build();

playbackStateBuilder.addCustomAction(customAction);

Отвечая на действия PlaybackState

Когда пользователь нажимает на кнопку, Systemui использует MediaController.TransportControls для отправки команды обратно в MediaSession . Вам нужно зарегистрировать обратный вызов, который может правильно реагировать на эти события.

Котлин

val callback = object: MediaSession.Callback() {
    override fun onPlay() {
        // start playback
    }

    override fun onPause() {
        // pause playback
    }

    override fun onSkipToPrevious() {
        // skip to previous
    }

    override fun onSkipToNext() {
        // skip to next
    }

    override fun onSeekTo(pos: Long) {
        // jump to position in track
    }

    override fun onCustomAction(action: String, extras: Bundle?) {
        when (action) {
            CUSTOM_ACTION_1 -> doCustomAction1(extras)
            CUSTOM_ACTION_2 -> doCustomAction2(extras)
            else -> {
                Log.w(TAG, "Unknown custom action $action")
            }
        }
    }

}

session.setCallback(callback)

Ява

MediaSession.Callback callback = new MediaSession.Callback() {
    @Override
    public void onPlay() {
        // start playback
    }

    @Override
    public void onPause() {
        // pause playback
    }

    @Override
    public void onSkipToPrevious() {
        // skip to previous
    }

    @Override
    public void onSkipToNext() {
        // skip to next
    }

    @Override
    public void onSeekTo(long pos) {
        // jump to position in track
    }

    @Override
    public void onCustomAction(String action, Bundle extras) {
        if (action.equals(CUSTOM_ACTION_1)) {
            doCustomAction1(extras);
        } else if (action.equals(CUSTOM_ACTION_2)) {
            doCustomAction2(extras);
        } else {
            Log.w(TAG, "Unknown custom action " + action);
        }
    }
};

Возобновление медиа

Чтобы приложение вашего игрока появилось в области быстрого настройки, вы должны создать уведомление MediaStyle с действительным токеном MediaSession .

Чтобы отобразить заголовок для уведомления MediaStyle, используйте NotificationBuilder.setContentTitle() .

Чтобы отобразить значок бренда для медиаплеера, используйте NotificationBuilder.setSmallIcon() .

Чтобы поддержать возобновление воспроизведения, приложения должны внедрить MediaBrowserService и MediaSession . Ваша MediaSession должна реализовать обратный вызов onPlay() .

Реализация MediaBrowserService

После того, как устройство загружается, система ищет пять самых недавно использованных приложений для медиа и предоставляет элементы управления, которые можно использовать для перезапуска игры из каждого приложения.

Система пытается связаться с вашим MediaBrowserService с помощью соединения от SystemUI. Ваше приложение должно разрешать такие соединения, в противном случае оно не может поддерживать возобновление воспроизведения.

Подключения из SystemUI можно определить и проверить с помощью имени пакета com.android.systemui и подписи. SystemUI подписана с подписью платформы. Пример того, как проверить подпись платформы, можно найти в приложении UAMP .

Чтобы поддержать возобновление воспроизведения, ваш MediaBrowserService должен реализовать такое поведение:

  • onGetRoot() должен быстро вернуть не нулевой корень. Другая сложная логика должна быть обработана в onLoadChildren()

  • Когда onLoadChildren() вызывается на идентификаторе корневого носителя, результат должен содержать ребенка FLAG_PLEABE .

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

  • MediaBrowserService должен предоставить соответствующую медиайдпикцию с непустым названием и субтитрами . Он также должен установить значок URI или растровый карту значка .

Следующие примеры кода показывают, как реализовать onGetRoot() .

Котлин

override fun onGetRoot(
    clientPackageName: String,
    clientUid: Int,
    rootHints: Bundle?
): BrowserRoot? {
    ...
    // Verify that the specified package is SystemUI. You'll need to write your 
    // own logic to do this.
    if (isSystem(clientPackageName, clientUid)) {
        rootHints?.let {
            if (it.getBoolean(BrowserRoot.EXTRA_RECENT)) {
                // Return a tree with a single playable media item for resumption.
                val extras = Bundle().apply {
                    putBoolean(BrowserRoot.EXTRA_RECENT, true)
                }
                return BrowserRoot(MY_RECENTS_ROOT_ID, extras)
            }
        }
        // You can return your normal tree if the EXTRA_RECENT flag is not present.
        return BrowserRoot(MY_MEDIA_ROOT_ID, null)
    }
    // Return an empty tree to disallow browsing.
    return BrowserRoot(MY_EMPTY_ROOT_ID, null)

Ява

@Override
public BrowserRoot onGetRoot(String clientPackageName, int clientUid,
    Bundle rootHints) {
    ...
    // Verify that the specified package is SystemUI. You'll need to write your
    // own logic to do this.
    if (isSystem(clientPackageName, clientUid)) {
        if (rootHints != null) {
            if (rootHints.getBoolean(BrowserRoot.EXTRA_RECENT)) {
                // Return a tree with a single playable media item for resumption.
                Bundle extras = new Bundle();
                extras.putBoolean(BrowserRoot.EXTRA_RECENT, true);
                return new BrowserRoot(MY_RECENTS_ROOT_ID, extras);
            }
        }
        // You can return your normal tree if the EXTRA_RECENT flag is not present.
        return new BrowserRoot(MY_MEDIA_ROOT_ID, null);
    }
    // Return an empty tree to disallow browsing.
    return new BrowserRoot(MY_EMPTY_ROOT_ID, null);
}