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 beragam untuk aplikasi yang memutar media, tombol tindakan pada kontrol media
berasal dari status Player
.
Dengan cara ini, Anda dapat menampilkan kumpulan kontrol media yang konsisten dan pengalaman kontrol media yang lebih baik di seluruh perangkat.
Gambar 1 menunjukkan contoh tampilannya 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 lainnya seperti Auto, Asisten, dan Wear OS.
Slot | Kriteria | Tindakan |
---|---|---|
1 |
playWhenReady
salah atau status
pemutaran saat ini adalah STATE_ENDED .
|
Putar |
playWhenReady bernilai benar dan status pemutaran saat ini adalah STATE_BUFFERING .
|
Memuat indikator lingkaran berputar | |
playWhenReady bernilai benar dan status pemutaran saat ini adalah STATE_READY . |
Jeda | |
2 | Perintah pemutar COMMAND_SEEK_TO_PREVIOUS atau COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM tersedia. |
Sebelumnya |
Perintah pemain COMMAND_SEEK_TO_PREVIOUS atau COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM tidak tersedia, dan perintah kustom dari tata letak kustom yang belum ditempatkan tersedia untuk mengisi slot. |
Kustom | |
Tambahan sesi menyertakan nilai boolean true untuk kunci EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV . |
Kosong | |
3 | Perintah pemutar COMMAND_SEEK_TO_NEXT atau COMMAND_SEEK_TO_NEXT_MEDIA_ITEM tersedia. |
Berikutnya |
Perintah pemain COMMAND_SEEK_TO_NEXT atau COMMAND_SEEK_TO_NEXT_MEDIA_ITEM tidak tersedia, dan perintah kustom dari tata letak kustom yang belum ditempatkan tersedia untuk mengisi slot. |
Kustom | |
Tambahan sesi menyertakan nilai boolean true untuk kunci EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT . |
Kosong | |
4 | Perintah kustom dari tata letak kustom yang belum ditempatkan tersedia untuk mengisi slot. | Kustom |
5 | Perintah kustom dari tata letak kustom yang belum ditempatkan tersedia untuk mengisi slot. | Kustom |
Perintah kustom ditempatkan sesuai urutan penambahannya ke tata letak kustom.
Menyesuaikan tombol perintah
Untuk menyesuaikan kontrol media sistem dengan Jetpack Media3,
Anda dapat menetapkan tata letak kustom sesi dan perintah pengontrol
yang tersedia, saat menerapkan MediaSessionService
:
Di
onCreate()
, buildMediaSession
dan tentukan tata letak kustom tombol perintah.Di
MediaSession.Callback.onConnect()
, izinkan 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() .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 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 ke klien lain.
Dengan Jetpack Media3, saat Anda menerapkan MediaSession
, PlaybackState
akan otomatis diperbarui dengan pemutar media. Demikian pula, saat Anda
menerapkan MediaSessionService
, library akan otomatis memublikasikan
notifikasi
MediaStyle
untuk Anda dan terus memperbaruinya.
Merespons tombol tindakan
Saat pengguna mengetuk tombol tindakan di kontrol media sistem, MediaController
sistem akan mengirim perintah pemutaran ke MediaSession
Anda. MediaSession
kemudian mendelegasikan perintah tersebut ke pemain. Perintah
yang ditentukan di antarmuka Player
Media3 ditangani secara otomatis oleh sesi
media.
Lihat Menambahkan perintah kustom untuk mendapatkan panduan tentang cara merespons perintah kustom.
Perilaku Pra-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 dilampirkan ke notifikasi
MediaStyle
. Sistem menampilkan hingga lima tindakan sesuai urutan
ditambahkannya. 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) .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();
Mendukung kelanjutan media
Lanjutan media memungkinkan pengguna memulai ulang sesi sebelumnya dari carousel tanpa harus memulai aplikasi. Saat pemutaran dimulai, pengguna berinteraksi dengan kontrol media seperti biasa.
Fitur melanjutkan pemutaran dapat diaktifkan dan dinonaktifkan menggunakan aplikasi Setelan, di bagian 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 untuk melanjutkan media. Lihat dokumentasi Lanjutan pemutaran dengan Media3 untuk mendapatkan panduan tentang cara menerapkan fitur ini.
Menggunakan API media lama
Bagian ini menjelaskan cara berintegrasi dengan kontrol media sistem menggunakan MediaCompat API lama.
Sistem mengambil informasi berikut dari
MediaMetadata
MediaSession
, lalu menampilkannya saat tersedia:
METADATA_KEY_ALBUM_ART_URI
METADATA_KEY_TITLE
METADATA_KEY_DISPLAY_TITLE
METADATA_KEY_ARTIST
METADATA_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
. Panel cari
memungkinkan pengguna mengubah posisi dan menampilkan waktu yang berlalu untuk item
media. Agar kolom pencari diaktifkan, Anda harus mengimplementasikan
PlaybackState.Builder#setActions
dan menyertakan ACTION_SEEK_TO
.
Slot | Tindakan | Kriteria |
---|---|---|
1 | Putar |
Status PlaybackState saat ini adalah salah satu dari berikut:
|
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 | Tindakan PlaybackState tidak menyertakan ACTION_SKIP_TO_PREVIOUS , dan tindakan kustom PlaybackState 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 | Tindakan PlaybackState tidak menyertakan ACTION_SKIP_TO_NEXT , dan tindakan kustom PlaybackState 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 | Tindakan kustom PlaybackState menyertakan tindakan kustom yang belum dilakukan. |
Menambahkan tindakan standar
Contoh kode berikut mengilustrasikan 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 ditampilkan di kontrol media, Anda dapat membuat
PlaybackStateCompat.CustomAction
dan menambahkannya ke PlaybackState
. 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 perlu 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); } } };
Melanjutkan 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.MediaBrowserService
harus menampilkan item media yang terakhir diputar saat menerima kueri EXTRA_RECENT. Nilai yang dihasilkan harus berupa item media aktual, bukan fungsi generik.MediaBrowserService
harus 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); }