メディア コントロール

Android のメディア コントロールは、クイック設定の近くにあります。複数のアプリからのセッションは、スワイプ可能なカルーセルに配列されます。カルーセルにはセッションのリストが表示されます。 次の順序にします。

  • スマートフォンでローカルに再生されるストリーム
  • リモート ストリーム(外部デバイスやキャスト セッションで検出されたストリームなど)
  • 以前の再開可能なセッション(最後に再生された順)

Android 13(API レベル 33)以降では、ユーザーが メディアを再生するアプリ用のメディア コントロール、メディア コントロールの操作ボタンのセット Player 状態から派生します。

こうすることで、メディア コントロールの一貫性を保ち、より洗練された デバイス間でのメディア コントロール エクスペリエンス。

図 1 は、スマートフォンとタブレット デバイスでの表示例を示しています。 できます。

<ph type="x-smartling-placeholder">
</ph> スマートフォンとタブレットでのメディア コントロールの表示例(ボタンがどのように表示されるかをサンプル トラックで例示しています)
図 1: スマートフォンとタブレット デバイスのメディア コントロール

システムは、Player の状態に応じて最大 5 つの操作ボタンを表示します。 次の表の説明をご覧ください。コンパクト モードでは、最初の 3 つの操作のみが 表示されます。他のメディア コントロールのレンダリング方法に合わせます。 Android プラットフォーム(Auto、アシスタント、Wear OS など)

スロット 条件 アクション
1 playWhenReady false または現在の再生回数 stateSTATE_ENDED です。 再生
playWhenReady が true で、現在の再生状態STATE_BUFFERING です。 読み込み中アイコン
playWhenReady が true で、現在の再生状態STATE_READY です。 一時停止
2 プレーヤー コマンド COMMAND_SEEK_TO_PREVIOUS または COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM を使用できる。 前へ
プレーヤー コマンド COMMAND_SEEK_TO_PREVIOUSCOMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM も使用できない。また、まだ配置されていないカスタム レイアウトのカスタム コマンドをスロットを埋めるために使用することができる。 カスタム
(Media3 ではまだサポートされていません)PlaybackState エクストラには、キー EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREVtrue ブール値が含まれます。 なし
3 プレーヤー コマンド COMMAND_SEEK_TO_NEXT または COMMAND_SEEK_TO_NEXT_MEDIA_ITEM を使用できる。 次へ
プレーヤー コマンド COMMAND_SEEK_TO_NEXTCOMMAND_SEEK_TO_NEXT_MEDIA_ITEM も使用できない。また、まだ配置されていないカスタム レイアウトのカスタム コマンドをスロットを埋めるために使用することができる。 カスタム
(Media3 ではまだサポートされていません)PlaybackState エクストラには、キー EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXTtrue ブール値が含まれます。 なし
4 まだ配置されていないカスタム レイアウトのカスタム コマンドを、スロットを埋めることができます。 カスタム
5 まだ配置されていないカスタム レイアウトのカスタム コマンドを、スロットを埋めることができます。 カスタム

カスタム コマンドは、 作成できます。

コマンドボタンをカスタマイズする

Jetpack Media3 を使用してシステムのメディア コントロールをカスタマイズするには、次の手順を行います。 セッションのカスタム レイアウトと、使用可能なコマンドを MediaSessionService を実装する際は、以下のように指定します。

  1. onCreate() で、MediaSession を作成します。 カスタム レイアウトを定義する 使用できます。

  2. MediaSession.Callback.onConnect() 内、 使用可能なコマンドを定義して、コントローラを認可します。 カスタム コマンド、 (ConnectionResult)にあります。

  3. MediaSession.Callback.onCustomCommand() 内、 ユーザーが選択したカスタム コマンドに応答します。

Kotlin

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)
    }
  }
}

Java

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 は、これらのコマンドをプレーヤーにデリゲートします。コマンド Media3 の Player で定義 インターフェースが自動的に あります。

カスタム コマンドを追加するを参照 をご覧ください。

Android 13 より前の動作

下位互換性を確保するため、システム UI は引き続き代替レイアウトを提供します。 Android 13 をターゲットとするよう更新されないアプリに対して通知アクションを使用する PlaybackState の情報を含まないもの。操作ボタンは MediaStyle に関連付けられた Notification.Action リストから取得されます。 通知を受け取ります。最大 5 つのアクションが、受信した順に が追加されました。コンパクト モードでは、最大 3 つのボタンが表示されます。 setShowActionsInCompactView() に渡される値。

カスタム アクションは、PlaybackState に追加された順序で配置されます。

次のコード例は、MediaStyle にアクションを追加する方法を示しています。 通知 :

Kotlin

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()

Java

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 の使用

このセクションでは、システムのメディア コントロールと統合する方法について説明します。 従来の MediaCompat API を使用します。

システムが MediaSessionMediaMetadata から以下の情報を取得して表示します。

  • 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 の値を設定する metadata。

メディア プレーヤーには、現在再生中のメディアの経過時間と、MediaSession PlaybackState にマッピングされたシークバーが表示されます。

メディア プレーヤーには、現在再生中のメディアの進行状況と MediaSession PlaybackState にマッピングされるシークバー。シークバー ユーザーは、メディアの位置を変更したり、メディアの経過時間を表示したりできます。 表示されます。シークバーを有効にするには、 PlaybackState.Builder#setActionsACTION_SEEK_TO を含めます。

スロット アクション 条件
1 再生 PlaybackState の現在の状態は次のいずれかです。 <ph type="x-smartling-placeholder">
    </ph>
  • 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 エクストラには、SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV キーの true ブール値が含まれます。
3 次へ PlaybackState アクションACTION_SKIP_TO_NEXT が含まれます。
カスタム PlaybackState アクションには ACTION_SKIP_TO_NEXT は含まれません。PlaybackStateカスタム アクションには、まだ配置されていないカスタム アクションが含まれます。
なし PlaybackState エクストラには、SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT キーの true ブール値が含まれます。
4 カスタム PlaybackState カスタム アクションには、まだ配置されていないカスタム アクションが含まれます。
5 カスタム PlaybackState カスタム アクションには、まだ配置されていないカスタム アクションが含まれます。

標準アクションを追加する

次のコードサンプルは、PlaybackState 標準と できます。

再生、一時停止、前へ、次へのアクションを設定します メディア セッションの PlaybackState

Kotlin

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)

Java

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 を使用し、代わりにエクストラを 確認します。

Kotlin

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

Java

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 に追加します。これらのアクションは 並べ替えられます。

Kotlin

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)

Java

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 にコマンドを送り返すことができます。コールバックを登録する必要があります。 これらのイベントに適切に応答できます

Kotlin

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)

Java

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);
        }
    }
};

メディアの再開

クイック設定領域にプレーヤー アプリが表示されるようにするには、有効な MediaSession トークンを使用して MediaStyle 通知を作成する必要があります。

MediaStyle 通知のタイトルを表示するには、次のコマンドを使用します。 NotificationBuilder.setContentTitle()

メディア プレーヤーのブランド アイコンを表示するには、NotificationBuilder.setSmallIcon() を使用します。

再生の再開をサポートするには、アプリで MediaBrowserService を実装する必要があります および MediaSessionMediaSessiononPlay() コールバックを実装する必要があります。

MediaBrowserService の実装

デバイスが起動すると、システムは最近使用された 5 つのメディアアプリを特定し、各アプリから再生を再開するためのコントロールを提供します。

システムは SystemUI 経由の接続によって MediaBrowserService へのアクセスを試みます。アプリでは、そのような接続を許可しなければなりません。許可しない場合、再生の再開はサポートされません。

SystemUI 経由の接続は、パッケージ名 com.android.systemui と署名によって検出し、確認できます。SystemUI はプラットフォーム署名によって署名されます。プラットフォーム署名に従ってチェックを行う方法の例については、UAMP アプリを確認してください。

再生の再開をサポートするには、MediaBrowserService に次の動作を実装する必要があります。

  • onGetRoot() は null 以外のルートをすばやく返す必要があります。その他の複雑なロジックは onLoadChildren() が処理するようにします。

  • ルートのメディア ID で onLoadChildren() を呼び出した場合、その結果には、子である FLAG_PLAYABLE が含まれていなければなりません。

  • MediaBrowserService は、EXTRA_RECENT クエリを受け取ったときに、直近に再生されたメディア アイテムを返す必要があります。返される値は、汎用的な関数ではなく、実際のメディア アイテムでなければなりません。

  • MediaBrowserService は、タイトルサブタイトルが空でない、適切な MediaDescription を指定する必要があります。またアイコンの URI またはアイコンのビットマップも設定します。

以下のコード例は、onGetRoot() の実装方法を示しています。

Kotlin

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)

Java

@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);
}