Kontrol media di Android terletak di dekat Setelan Cepat. Sesi dari beberapa aplikasi disusun dalam carousel yang dapat digeser. Carousel ini mencantumkan sesi dalam urutan berikut:
- Streaming yang diputar secara lokal di ponsel
- Streaming jarak jauh, seperti yang terdeteksi pada perangkat eksternal atau sesi transmisi
- Sesi sebelumnya yang dapat dilanjutkan, sesuai urutan pemutaran terakhirnya
Mulai Android 13 (API level 33), untuk memastikan pengguna dapat mengakses kumpulan kontrol media yang lengkap untuk aplikasi yang memutar media, tombol tindakan pada kontrol media berasal dari status Player.
Dengan cara ini, Anda dapat menyajikan kumpulan kontrol media yang konsisten dan pengalaman kontrol media yang lebih baik di seluruh perangkat.
Gambar 1 menunjukkan contoh tampilan ini di perangkat ponsel dan tablet.
Sistem menampilkan hingga lima tombol tindakan berdasarkan status Player seperti yang dijelaskan dalam tabel berikut. Dalam mode ringkas, hanya tiga slot tindakan pertama yang ditampilkan. Hal ini selaras dengan cara kontrol media dirender di platform Android lain seperti Auto, Asisten, dan Wear OS.
| Slot | Kriteria | Tindakan |
|---|---|---|
| 1 |
playWhenReady adalah salah atau
status pemutaran saat ini adalah STATE_ENDED.
|
Putar |
playWhenReady adalah benar dan status pemutaran saat ini adalah STATE_BUFFERING.
|
Memuat indikator lingkaran berputar | |
playWhenReady adalah benar dan status pemutaran saat ini adalah STATE_READY.
|
Jeda | |
| 2 |
Preferensi tombol media
tombol berisi tombol kustom untuk
CommandButton.SLOT_BACK
|
Kustom |
Perintah pemutar
COMMAND_SEEK_TO_PREVIOUS atau
COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM tersedia.
|
Sebelumnya | |
| Tombol kustom maupun salah satu perintah yang tercantum tidak tersedia. | Kosong | |
| 3 |
Preferensi tombol media
berisi tombol kustom untuk
CommandButton.SLOT_FORWARD
|
Kustom |
Perintah pemutar
COMMAND_SEEK_TO_NEXT atau
COMMAND_SEEK_TO_NEXT_MEDIA_ITEM tersedia.
|
Berikutnya | |
| Tombol kustom maupun salah satu perintah yang tercantum tidak tersedia. | Kosong | |
| 4 |
Preferensi tombol media berisi tombol kustom untuk
CommandButton.SLOT_OVERFLOW yang belum ditempatkan.
|
Kustom |
| 5 |
Preferensi tombol media berisi tombol kustom untuk
CommandButton.SLOT_OVERFLOW yang belum ditempatkan.
|
Kustom |
Tombol tambahan kustom ditempatkan sesuai urutan penambahannya ke preferensi tombol media.
Menyesuaikan tombol perintah
Untuk menyesuaikan kontrol media sistem dengan Jetpack Media3, Anda dapat menetapkan preferensi tombol media sesi dan perintah pengontrol yang tersedia sesuai dengan:
Buat
MediaSessiondan tentukan preferensi tombol media untuk tombol perintah kustom.Di
MediaSession.Callback.onConnect(), berikan otorisasi kepada pengontrol dengan menentukan perintah yang tersedia, termasuk perintah kustom, diConnectionResult.Di
MediaSession.Callback.onCustomCommand(), respons perintah kustom yang dipilih oleh pengguna.
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(CommandButton.ICON_HEART_UNFILLED) .setDisplayName("Save to favorites") .setSessionCommand(customCommandFavorites) .build() val player = ExoPlayer.Builder(this).build() // Build the session with a custom layout. mediaSession = MediaSession.Builder(this, player) .setCallback(MyCallback()) .setMediaButtonPreferences(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) .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(CommandButton.ICON_HEART_UNFILLED) .setDisplayName("Save to favorites") .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()) .setMediaButtonPreferences(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) .setAvailableSessionCommands( ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon() .add(CUSTOM_COMMAND_FAVORITES) .build()) .build(); } public ListenableFutureonCustomCommand( 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); } } }
Untuk mempelajari lebih lanjut cara mengonfigurasi MediaSession agar klien seperti
sistem dapat terhubung ke aplikasi media Anda, lihat
Memberikan kontrol kepada klien lain.
Dengan Jetpack Media3, saat Anda mengimplementasikan MediaSession, PlaybackState akan otomatis diperbarui dengan pemutar media. Demikian pula, saat Anda
mengimplementasikan MediaSessionService, library akan otomatis memublikasikan
MediaStyle notifikasi
untuk Anda dan terus memperbaruinya.
Merespons tombol tindakan
Saat pengguna mengetuk tombol tindakan di kontrol media sistem, MediaController sistem akan mengirimkan perintah pemutaran ke MediaSession Anda. MediaSession kemudian akan mendelegasikan perintah tersebut ke pemutar. Perintah
yang ditentukan dalam antarmuka Player
`Player` Media3 akan otomatis ditangani oleh sesi
media.
Lihat Menambahkan perintah kustom untuk panduan tentang cara merespons perintah kustom.
Mendukung pelanjutan media
Pelanjutan media memungkinkan pengguna memulai ulang sesi sebelumnya dari carousel tanpa harus memulai aplikasi. Saat pemutaran dimulai, pengguna berinteraksi dengan kontrol media seperti biasa.
Fitur pelanjutan pemutaran dapat diaktifkan dan dinonaktifkan menggunakan aplikasi Setelan, di opsi Suara > Media. Pengguna juga dapat mengakses Setelan dengan mengetuk ikon roda gigi yang muncul setelah menggeser carousel yang diperluas.
Media3 menawarkan API untuk memudahkan dukungan pelanjutan media. Lihat dokumentasi Pelanjutan pemutaran dengan Media3 untuk panduan tentang cara mengimplementasikan fitur ini.
Menggunakan API media lama
Bagian ini menjelaskan cara berintegrasi dengan kontrol media sistem menggunakan MediaCompat API lama.
Sistem mengambil informasi berikut dari MediaSession's MediaMetadata, lalu menampilkannya saat tersedia:
METADATA_KEY_ALBUM_ART_URIMETADATA_KEY_TITLEMETADATA_KEY_DISPLAY_TITLEMETADATA_KEY_ARTISTMETADATA_KEY_DURATION(Jika durasi tidak ditetapkan, kolom pencari tidak akan menampilkan progres)
Untuk memastikan Anda memiliki notifikasi kontrol media yang valid dan akurat, tetapkan nilai metadata METADATA_KEY_TITLE atau METADATA_KEY_DISPLAY_TITLE ke judul media yang sedang diputar.
Pemutar media menampilkan waktu berlalu untuk media yang sedang diputar, beserta kolom pencari yang dipetakan ke MediaSession
PlaybackState.
Pemutar media menampilkan progres untuk media yang sedang diputar, beserta kolom pencari yang dipetakan ke MediaSession PlaybackState. Kolom pencari memungkinkan pengguna mengubah posisi dan menampilkan waktu berlalu untuk item media. Agar kolom pencari diaktifkan, Anda harus mengimplementasikan PlaybackState.Builder#setActions dan menyertakan ACTION_SEEK_TO.
| Slot | Tindakan | Kriteria |
|---|---|---|
| 1 | Putar |
PlaybackState
|
| Memuat indikator lingkaran berputar |
Status PlaybackState saat ini adalah salah satu dari berikut:
|
|
| Jeda | Status PlaybackState saat ini bukan satu pun dari yang disebutkan di atas. |
|
| 2 | Sebelumnya | Tindakan PlaybackState menyertakan ACTION_SKIP_TO_PREVIOUS. |
| Kustom | PlaybackState tindakan tidak menyertakan ACTION_SKIP_TO_PREVIOUS, dan PlaybackState tindakan kustom menyertakan tindakan kustom yang belum dilakukan. |
|
| Kosong | Tambahan PlaybackState menyertakan nilai boolean true untuk kunci SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV. |
|
| 3 | Berikutnya | Tindakan PlaybackState menyertakan ACTION_SKIP_TO_NEXT. |
| Kustom | PlaybackState tindakan tidak menyertakan ACTION_SKIP_TO_NEXT, dan PlaybackState tindakan kustom menyertakan tindakan kustom yang belum dilakukan. |
|
| Kosong | Tambahan PlaybackState menyertakan nilai boolean true untuk kunci SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT. |
|
| 4 | Kustom | Tindakan kustom PlaybackState menyertakan tindakan kustom yang belum dilakukan. |
| 5 | Kustom | PlaybackState tindakan kustom menyertakan tindakan kustom yang belum dilakukan. |
Menambahkan tindakan standar
Contoh kode berikut ini menggambarkan cara menambahkan tindakan standar dan kustom PlaybackState.
Untuk putar, jeda, sebelumnya, dan berikutnya, tetapkan tindakan ini di PlaybackState untuk sesi media.
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);
Jika Anda tidak ingin tombol apa pun di slot sebelumnya atau berikutnya, jangan tambahkan ACTION_SKIP_TO_PREVIOUS atau ACTION_SKIP_TO_NEXT, dan tambahkan tambahan ke sesi:
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);
Menambahkan tindakan kustom
Untuk tindakan lain yang ingin Anda tampilkan di kontrol media, Anda dapat membuat
PlaybackStateCompat.CustomAction
dan menambahkannya ke PlaybackState sebagai gantinya. Tindakan ini ditampilkan sesuai urutan penambahannya.
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);
Merespons tindakan PlaybackState
Saat pengguna mengetuk tombol, SystemUI menggunakan
MediaController.TransportControls
untuk mengirim perintah kembali ke MediaSession. Anda harus mendaftarkan callback yang dapat merespons peristiwa ini dengan benar.
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); } } };
Pelanjutan Media
Agar aplikasi pemutar media muncul di area setelan cepat, Anda harus membuat notifikasi MediaStyle dengan token MediaSession yang valid.
Untuk menampilkan judul notifikasi MediaStyle, gunakan NotificationBuilder.setContentTitle().
Untuk menampilkan ikon merek bagi pemutar media, gunakan NotificationBuilder.setSmallIcon().
Untuk mendukung pelanjutan pemutaran, aplikasi harus mengimplementasikan MediaBrowserService dan MediaSession. MediaSession Anda harus mengimplementasikan callback onPlay().
Implementasi MediaBrowserService
Setelah perangkat melakukan booting, sistem akan mencari lima aplikasi media yang terakhir digunakan, lalu menyediakan kontrol yang dapat digunakan untuk memulai ulang pemutaran dari setiap aplikasi.
Sistem akan mencoba menghubungi MediaBrowserService dengan koneksi dari
SystemUI. Aplikasi Anda harus mengizinkan koneksi semacam ini, karena jika tidak, aplikasi tidak dapat mendukung pelanjutan pemutaran.
Koneksi dari SystemUI dapat diidentifikasi dan diverifikasi menggunakan nama paket com.android.systemui dan tanda tangan. SystemUI ditandatangani dengan tanda tangan
platform. Lihat contoh cara memeriksa tanda tangan
platform di aplikasi UAMP.
Untuk mendukung pelanjutan pemutaran, MediaBrowserService harus
mengimplementasikan perilaku berikut:
onGetRoot()harus menampilkan root non-null dengan cepat. Logika kompleks lainnya harus ditangani dionLoadChildren()Saat
onLoadChildren()dipanggil pada ID media root, hasilnya harus memuat turunan FLAG_PLAYABLE.MediaBrowserServiceharus menampilkan item media yang terakhir diputar saat menerima kueri EXTRA_RECENT. Nilai yang dihasilkan harus berupa item media aktual, bukan fungsi generik.MediaBrowserServiceharus menyediakan MediaDescription yang sesuai, dengan judul dan subjudul yang tidak kosong. Class ini juga harus menetapkan URI ikon atau bitmap ikon.
Contoh kode berikut ini menggambarkan cara mengimplementasikan 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); }
Perilaku Sebelum Android 13
Untuk kompatibilitas mundur, UI Sistem terus menyediakan tata letak alternatif yang menggunakan tindakan notifikasi untuk aplikasi yang tidak diupdate untuk menargetkan Android 13, atau yang tidak menyertakan informasi PlaybackState. Tombol tindakan berasal dari daftar Notification.Action yang terlampir ke notifikasi MediaStyle. Sistem menampilkan hingga lima tindakan sesuai urutan penambahannya. Dalam mode ringkas, hingga tiga tombol ditampilkan, yang ditentukan oleh
nilai yang diteruskan ke
setShowActionsInCompactView().
Tindakan kustom ditempatkan sesuai urutan penambahannya ke PlaybackState.
Contoh kode berikut mengilustrasikan cara menambahkan tindakan ke notifikasi 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) // 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(new MediaStyleNotificationHelper.MediaStyle(mediaSession) .setShowActionsInCompactView(1 /* #1: pause button */)) .setContentTitle("Wonderful music") .setContentText("My Awesome Band") .setLargeIcon(albumArtBitmap) .build();