Kontrol media di Android terdapat di dekat Setelan Cepat. Sesi dari beberapa aplikasi disusun dalam carousel yang dapat digeser. Carousel mencantumkan sesi dalam urutan ini:
- 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
serangkaian kontrol media 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 di seluruh perangkat.
Gambar 1 menunjukkan contoh tampilannya di perangkat ponsel dan tablet, secara berurutan.
Sistem menampilkan hingga lima tombol tindakan berdasarkan status Player
sebagai
dijelaskan dalam tabel berikut. Dalam mode rapat, hanya tiga tindakan pertama
slot yang ditampilkan. Hal ini sejalan dengan cara kontrol media dirender di
Platform Android seperti Auto, Asisten, dan Wear OS.
Slot | Kriteria | Tindakan |
---|---|---|
1 |
playWhenReady
salah atau pemutaran saat ini
statusadalah STATE_ENDED .
|
Putar |
playWhenReady benar dan status pemutaran saat ini adalah STATE_BUFFERING .
|
Memuat indikator lingkaran berputar | |
playWhenReady 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 pemutar COMMAND_SEEK_TO_PREVIOUS maupun COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM tidak tersedia, dan perintah kustom dari tata letak kustom yang belum ditempatkan tersedia untuk mengisi slot. |
Kustom | |
(belum didukung dengan Media3) tambahan PlaybackState 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 pemutar COMMAND_SEEK_TO_NEXT maupun COMMAND_SEEK_TO_NEXT_MEDIA_ITEM tidak tersedia, dan perintah kustom dari tata letak kustom yang belum ditempatkan tersedia untuk mengisi slot. |
Kustom | |
(belum didukung dengan Media3) tambahan PlaybackState 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 dan tata letak kustom.
Menyesuaikan tombol perintah
Untuk menyesuaikan kontrol media sistem dengan Jetpack Media3,
Anda dapat mengatur tata letak khusus
sesi dan perintah yang tersedia dari
yang sesuai, saat mengimplementasikan MediaSessionService
:
Di
onCreate()
, buatMediaSession
dan menentukan tata letak kustom tombol perintah.Di
MediaSession.Callback.onConnect()
, memberikan otorisasi kepada pengontrol dengan menentukan perintah yang tersedia, termasuk perintah khusus, diConnectionResult
.Di
MediaSession.Callback.onCustomCommand()
, merespons perintah{i> <i}khusus 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 menyukai
terhubung ke aplikasi media Anda, lihat
Berikan kontrol ke klien lain.
Dengan Jetpack Media3, saat Anda menerapkan MediaSession
, PlaybackState
Anda
terus diperbarui secara otomatis pada pemutar media. Demikian pula, ketika Anda
mengimplementasikan MediaSessionService
, library akan otomatis memublikasikan
Notifikasi MediaStyle
untuk Anda dan tetap {i>up-to-date<i}.
Merespons tombol tindakan
Saat pengguna mengetuk tombol tindakan di kontrol media sistem,
MediaController
mengirim perintah pemutaran ke MediaSession
Anda. Tujuan
MediaSession
kemudian mendelegasikan perintah tersebut ke pemutar. Perintah
yang ditentukan dalam Player
Media3
secara otomatis ditangani oleh media
sesi.
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 adalah
berasal dari daftar Notification.Action
yang dilampirkan pada MediaStyle
notifikasi. Sistem menampilkan hingga lima tindakan dalam urutan
ditambahkan. Dalam mode rapat, hingga tiga tombol ditampilkan, ditentukan oleh
nilai yang diteruskan ke setShowActionsInCompactView()
.
Tindakan kustom ditempatkan sesuai urutan penambahannya ke
PlaybackState
.
Contoh kode berikut mengilustrasikan cara menambahkan tindakan ke MediaStyle notifikasi :
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 melanjutkan media
Dengan melanjutkan media, pengguna dapat memulai ulang sesi sebelumnya dari carousel tanpa harus memulai aplikasi. Saat pemutaran dimulai, pengguna berinteraksi dengan mengontrol media seperti biasa.
Fitur lanjutkan pemutaran dapat diaktifkan dan dinonaktifkan menggunakan aplikasi Setelan, di bagian Sound > Opsi media. Pengguna juga dapat mengakses Setelan dengan mengetuk ikon roda gigi yang muncul setelah menggeser pada carousel yang diperluas.
Media3 menawarkan API untuk mempermudah dukungan melanjutkan media. Lihat Melanjutkan pemutaran dengan Media3 untuk mendapatkan panduan menerapkan fitur ini.
Menggunakan API media lama
Bagian ini menjelaskan cara mengintegrasikan 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 disetel, bilah geser tidak tampilkan kemajuan)
Untuk memastikan Anda memiliki notifikasi kontrol media yang valid dan akurat,
tetapkan nilai METADATA_KEY_TITLE
atau METADATA_KEY_DISPLAY_TITLE
metadata 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 media yang sedang diputar, beserta
bilah geser yang dipetakan ke MediaSession
PlaybackState
. Bilah geser
memungkinkan pengguna mengubah posisi dan menampilkan waktu yang berlalu untuk media
yang bermanfaat. Agar bilah geser 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 menggambarkan cara menambahkan PlaybackState
standar dan
tindakan kustom Anda.
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 informasi ekstra 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 di
urutannya ditambahkan.
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 akan menggunakan
MediaController.TransportControls
untuk mengirim perintah kembali ke MediaSession
. Anda harus mendaftarkan callback
yang dapat merespons kejadian ini
dengan baik.
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 menerapkan 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); }