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

Элементы управления мультимедиа в 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 недоступна, а пользовательская команда из пользовательского макета, которая еще не была размещена, доступна для заполнения слота. Обычай
Дополнительные возможности сеанса включают 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 недоступны, а пользовательская команда из пользовательского макета, которая еще не была размещена, доступна для заполнения слота. Обычай
Дополнительные возможности сеанса включают 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);
}