Pengalih Output adalah fitur Cast SDK yang memungkinkan transfer
yang lancar antara pemutaran konten lokal dan jarak jauh, mulai dari Android 13. Tujuannya adalah
untuk membantu aplikasi pengirim dengan mudah dan cepat mengontrol tempat konten diputar.
Pengalih Output menggunakan
library MediaRouter
untuk
beralih pemutaran konten di antara speaker ponsel, perangkat Bluetooth yang disambungkan,
dan perangkat yang kompatibel untuk Cast. Kasus penggunaan dapat diuraikan menjadi
skenario berikut:
Download dan gunakan contoh di bawah untuk referensi tentang cara menerapkan Pengalih Output di aplikasi Audio Anda. Lihat README.md yang disertakan untuk mendapatkan petunjuk terkait cara menjalankan contoh.
Pengalih Output harus diaktifkan untuk mendukung lokal ke jarak jauh dan dari jarak jauh ke lokal menggunakan langkah-langkah yang dijelaskan dalam panduan ini. Tidak diperlukan langkah tambahan untuk mendukung transfer antara speaker perangkat lokal dan perangkat Bluetooth yang disambungkan.
Aplikasi audio adalah aplikasi yang mendukung Google Cast untuk Audio di setelan Aplikasi Penerima di Google Cast SDK Developer Console
UI Pengalih Output
Pengalih Output menampilkan perangkat lokal dan jarak jauh yang tersedia serta status perangkat saat ini, termasuk apakah perangkat dipilih, terhubung, tingkat volume saat ini. Jika ada perangkat lain selain perangkat saat ini, mengklik perangkat lain memungkinkan Anda mentransfer pemutaran media ke perangkat yang dipilih.
Masalah umum
- Sesi Media yang dibuat untuk pemutaran lokal akan ditutup dan dibuat ulang saat beralih ke notifikasi Cast SDK.
Titik masuk
Notifikasi media
Jika aplikasi memposting notifikasi media dengan
MediaSession
untuk
pemutaran lokal (diputar secara lokal), pojok kanan atas notifikasi media
akan menampilkan chip notifikasi dengan nama perangkat (seperti speaker ponsel) yang
sedang memutar konten. Mengetuk chip notifikasi akan membuka
UI sistem dialog Pengalih Output.
Setelan volume
UI sistem dialog Output Switcher juga dapat dipicu dengan mengklik tombol volume fisik di perangkat, mengetuk ikon setelan di bagian bawah, dan mengetuk teks "Putar <Nama Aplikasi> di <Transmisikan Perangkat>".
Ringkasan langkah
- Pastikan prasyarat terpenuhi
- Mengaktifkan Output Switcher di AndroidManifest.xml
- Memperbarui SessionManagerListener untuk transmisi latar belakang
- Menetapkan flag setRemoteToLocalEnabled
- Melanjutkan pemutaran secara lokal
Prasyarat
- Memigrasikan aplikasi Android yang ada ke AndroidX.
- Update
build.gradle
aplikasi Anda untuk menggunakan versi minimum Android Sender SDK yang diperlukan untuk Output Switcher:dependencies { ... implementation 'com.google.android.gms:play-services-cast-framework:21.2.0' ... }
- Aplikasi mendukung notifikasi media.
- Perangkat yang menjalankan Android 13.
Menyiapkan Notifikasi Media
Untuk menggunakan Pengalih Output,
aplikasi audio dan
video
diperlukan untuk membuat notifikasi media guna menampilkan status dan
kontrol pemutaran media untuk pemutaran lokal. Tindakan ini mengharuskan Anda membuat
MediaSession
,
menyetel
MediaStyle
dengan token MediaSession
, dan menetapkan kontrol media pada
notifikasi.
Jika saat ini Anda tidak menggunakan MediaStyle
dan MediaSession
, cuplikan
di bawah ini menunjukkan cara menyiapkannya dan panduan tersedia untuk menyiapkan callback
sesi media untuk aplikasi
audio dan
video:
// Create a media session. NotificationCompat.MediaStyle // PlayerService is your own Service or Activity responsible for media playback. val mediaSession = MediaSessionCompat(this, "PlayerService") // Create a MediaStyle object and supply your media session token to it. val mediaStyle = Notification.MediaStyle().setMediaSession(mediaSession.sessionToken) // Create a Notification which is styled by your MediaStyle object. // This connects your media session to the media controls. // Don't forget to include a small icon. val notification = Notification.Builder(this@PlayerService, CHANNEL_ID) .setStyle(mediaStyle) .setSmallIcon(R.drawable.ic_app_logo) .build() // Specify any actions which your users can perform, such as pausing and skipping to the next track. val pauseAction: Notification.Action = Notification.Action.Builder( pauseIcon, "Pause", pauseIntent ).build() notification.addAction(pauseAction)
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { // Create a media session. NotificationCompat.MediaStyle // PlayerService is your own Service or Activity responsible for media playback. MediaSession mediaSession = new MediaSession(this, "PlayerService"); // Create a MediaStyle object and supply your media session token to it. Notification.MediaStyle mediaStyle = new Notification.MediaStyle().setMediaSession(mediaSession.getSessionToken()); // Specify any actions which your users can perform, such as pausing and skipping to the next track. Notification.Action pauseAction = Notification.Action.Builder(pauseIcon, "Pause", pauseIntent).build(); // Create a Notification which is styled by your MediaStyle object. // This connects your media session to the media controls. // Don't forget to include a small icon. String CHANNEL_ID = "CHANNEL_ID"; Notification notification = new Notification.Builder(this, CHANNEL_ID) .setStyle(mediaStyle) .setSmallIcon(R.drawable.ic_app_logo) .addAction(pauseAction) .build(); }
Selain itu, untuk mengisi notifikasi dengan informasi media,
Anda harus menambahkan
status metadata dan pemutaran
media ke MediaSession
.
Untuk menambahkan metadata ke MediaSession
, gunakan
setMetaData()
dan sediakan semua konstanta
MediaMetadata
yang relevan untuk
media Anda di
MediaMetadataCompat.Builder()
.
mediaSession.setMetadata(MediaMetadataCompat.Builder() // Title .putString(MediaMetadata.METADATA_KEY_TITLE, currentTrack.title) // Artist // Could also be the channel name or TV series. .putString(MediaMetadata.METADATA_KEY_ARTIST, currentTrack.artist) // Album art // Could also be a screenshot or hero image for video content // The URI scheme needs to be "content", "file", or "android.resource". .putString( MediaMetadata.METADATA_KEY_ALBUM_ART_URI, currentTrack.albumArtUri) ) // Duration // If duration isn't set, such as for live broadcasts, then the progress // indicator won't be shown on the seekbar. .putLong(MediaMetadata.METADATA_KEY_DURATION, currentTrack.duration) .build() )
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { mediaSession.setMetadata( new MediaMetadataCompat.Builder() // Title .putString(MediaMetadata.METADATA_KEY_TITLE, currentTrack.title) // Artist // Could also be the channel name or TV series. .putString(MediaMetadata.METADATA_KEY_ARTIST, currentTrack.artist) // Album art // Could also be a screenshot or hero image for video content // The URI scheme needs to be "content", "file", or "android.resource". .putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, currentTrack.albumArtUri) // Duration // If duration isn't set, such as for live broadcasts, then the progress // indicator won't be shown on the seekbar. .putLong(MediaMetadata.METADATA_KEY_DURATION, currentTrack.duration) .build() ); }
Untuk menambahkan status pemutaran ke MediaSession
, gunakan
setPlaybackState()
dan berikan semua konstanta
PlaybackStateCompat
yang relevan untuk media Anda di
PlaybackStateCompat.Builder()
.
mediaSession.setPlaybackState( PlaybackStateCompat.Builder() .setState( PlaybackStateCompat.STATE_PLAYING, // Playback position // Used to update the elapsed time and the progress bar. mediaPlayer.currentPosition.toLong(), // Playback speed // Determines the rate at which the elapsed time changes. playbackSpeed ) // isSeekable // Adding the SEEK_TO action indicates that seeking is supported // and makes the seekbar position marker draggable. If this is not // supplied seek will be disabled but progress will still be shown. .setActions(PlaybackStateCompat.ACTION_SEEK_TO) .build() )
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { mediaSession.setPlaybackState( new PlaybackStateCompat.Builder() .setState( PlaybackStateCompat.STATE_PLAYING, // Playback position // Used to update the elapsed time and the progress bar. mediaPlayer.currentPosition.toLong(), // Playback speed // Determines the rate at which the elapsed time changes. playbackSpeed ) // isSeekable // Adding the SEEK_TO action indicates that seeking is supported // and makes the seekbar position marker draggable. If this is not // supplied seek will be disabled but progress will still be shown. .setActions(PlaybackStateCompat.ACTION_SEEK_TO) .build() ); }
Perilaku notifikasi aplikasi video
Aplikasi video atau aplikasi audio yang tidak mendukung pemutaran lokal di latar belakang harus memiliki perilaku tertentu agar notifikasi media terhindar dari masalah terkait pengiriman perintah media dalam situasi pemutaran tidak didukung:
- Posting notifikasi media saat memutar media secara lokal dan aplikasi berada di latar depan.
- Jeda pemutaran lokal dan tutup notifikasi saat aplikasi berada di latar belakang.
- Saat aplikasi kembali ke latar depan, pemutaran lokal harus dilanjutkan dan notifikasi harus diposting ulang.
Mengaktifkan Pengalih Output di AndroidManifest.xml
Untuk mengaktifkan Pengalih Output, MediaTransferReceiver
harus ditambahkan ke AndroidManifest.xml
aplikasi. Jika tidak, fitur
tidak akan diaktifkan dan tombol fitur remote ke lokal juga tidak akan valid.
<application>
...
<receiver
android:name="androidx.mediarouter.media.MediaTransferReceiver"
android:exported="true">
</receiver>
...
</application>
MediaTransferReceiver
adalah penerima siaran yang memungkinkan transfer media antar-perangkat dengan UI
sistem. Lihat referensi
MediaTransferReceiver
untuk mengetahui informasi selengkapnya.
Lokal ke remote
Saat pengguna mengalihkan pemutaran dari lokal ke jarak jauh, Cast SDK akan memulai
sesi Cast secara otomatis. Namun, aplikasi perlu menangani peralihan dari
lokal ke jarak jauh, misalnya menghentikan pemutaran lokal
dan memuat media di perangkat Transmisi. Aplikasi harus memproses
SessionManagerListener
Cast,
menggunakan callback
onSessionStarted()
dan
onSessionEnded()
, serta menangani tindakan saat menerima callback
SessionManager
Transmisi. Aplikasi harus memastikan bahwa callback ini masih aktif saat
dialog Switcher Output dibuka dan aplikasi tidak ada di latar depan.
Mengupdate SessionManagerListener untuk transmisi latar belakang
Pengalaman Cast lama sudah mendukung lokal ke jarak jauh saat aplikasi
berada di latar depan. Pengalaman Cast biasa dimulai saat pengguna mengklik ikon Cast
di aplikasi dan memilih perangkat untuk melakukan streaming media. Dalam hal ini, aplikasi perlu
mendaftar ke
SessionManagerListener
,
di onCreate()
atau
onStart()
dan membatalkan pendaftaran pemroses di
onStop()
atau
onDestroy()
aktivitas aplikasi.
Dengan pengalaman baru transmisi menggunakan Pengalih Output, aplikasi dapat mulai
melakukan transmisi saat berada di latar belakang. Hal ini sangat berguna untuk aplikasi
audio yang memposting notifikasi saat diputar di latar belakang. Aplikasi dapat
mendaftarkan pemroses SessionManager
di onCreate()
layanan
dan membatalkan pendaftaran di onDestroy()
layanan. Dengan cara ini, aplikasi harus
selalu menerima callback lokal ke jarak jauh (seperti onSessionStarted
) saat
aplikasi berada di latar belakang.
Jika aplikasi menggunakan
MediaBrowserService
,
sebaiknya daftarkan SessionManagerListener
ke sana.
class MyService : Service() { private var castContext: CastContext? = null protected fun onCreate() { castContext = CastContext.getSharedInstance(this) castContext .getSessionManager() .addSessionManagerListener(sessionManagerListener, CastSession::class.java) } protected fun onDestroy() { if (castContext != null) { castContext .getSessionManager() .removeSessionManagerListener(sessionManagerListener, CastSession::class.java) } } }
public class MyService extends Service { private CastContext castContext; @Override protected void onCreate() { castContext = CastContext.getSharedInstance(this); castContext .getSessionManager() .addSessionManagerListener(sessionManagerListener, CastSession.class); } @Override protected void onDestroy() { if (castContext != null) { castContext .getSessionManager() .removeSessionManagerListener(sessionManagerListener, CastSession.class); } } }
Dengan update ini, tindakan lokal ke jarak jauh berfungsi sama seperti transmisi tradisional saat aplikasi berada di latar belakang dan pekerjaan tambahan tidak diperlukan untuk beralih dari perangkat Bluetooth ke perangkat Cast.
Jarak jauh ke lokal
Pengalih Output memberikan kemampuan untuk mentransfer dari pemutaran jarak jauh ke
speaker ponsel atau perangkat Bluetooth lokal. Hal ini dapat diaktifkan dengan menetapkan
tanda setRemoteToLocalEnabled
ke true
pada CastOptions
.
Untuk kasus ketika perangkat pengirim saat ini bergabung dengan sesi yang ada dengan beberapa pengirim dan aplikasi perlu memeriksa apakah media saat ini diizinkan untuk ditransfer secara lokal, aplikasi harus menggunakan callback onTransferred
dari SessionTransferCallback
untuk memeriksa SessionState
.
Menyetel tanda setRemoteToLocalEnabled
CastOptions
menyediakan setRemoteToLocalEnabled
untuk menampilkan atau menyembunyikan
speaker ponsel dan perangkat Bluetooth lokal sebagai target transfer ke dalam dialog
Pengalih Output saat ada sesi Cast yang aktif.
class CastOptionsProvider : OptionsProvider { fun getCastOptions(context: Context?): CastOptions { ... return Builder() ... .setRemoteToLocalEnabled(true) .build() } }
public class CastOptionsProvider implements OptionsProvider { @Override public CastOptions getCastOptions(Context context) { ... return new CastOptions.Builder() ... .setRemoteToLocalEnabled(true) .build() } }
Lanjutkan pemutaran secara lokal
Aplikasi yang mendukung remote ke lokal harus mendaftarkan SessionTransferCallback
untuk mendapatkan notifikasi saat peristiwa terjadi sehingga dapat memeriksa apakah media harus
ditransfer dan melanjutkan pemutaran secara lokal.
CastContext#addSessionTransferCallback(SessionTransferCallback)
memungkinkan
aplikasi mendaftarkan SessionTransferCallback
dan memproses callback onTransferred
dan
onTransferFailed
saat pengirim ditransfer ke pemutaran lokal.
Setelah membatalkan pendaftaran SessionTransferCallback
, aplikasi tidak akan lagi
menerima SessionTransferCallback
.
SessionTransferCallback
adalah ekstensi dari callback
SessionManagerListener
yang ada dan dipicu setelah onSessionEnded
dipicu. Oleh karena itu, urutan
callback jarak jauh ke lokal adalah:
onTransferring
onSessionEnding
onSessionEnded
onTransferred
Karena Pengalih Output dapat dibuka oleh chip notifikasi media saat
aplikasi berada di latar belakang dan melakukan transmisi, aplikasi perlu menangani transfer ke
lokal secara berbeda bergantung pada apakah aplikasi mendukung pemutaran latar belakang atau tidak. Jika transfer gagal, onTransferFailed
akan diaktifkan setiap kali terjadi error.
Aplikasi yang mendukung pemutaran latar belakang
Untuk aplikasi yang mendukung pemutaran di latar belakang (biasanya aplikasi audio), sebaiknya
gunakan Service
(misalnya MediaBrowserService
). Layanan
harus memproses callback onTransferred
dan melanjutkan pemutaran secara lokal
saat aplikasi berada di latar depan atau latar belakang.
class MyService : Service() { private var castContext: CastContext? = null private var sessionTransferCallback: SessionTransferCallback? = null protected fun onCreate() { castContext = CastContext.getSharedInstance(this) castContext.getSessionManager() .addSessionManagerListener(sessionManagerListener, CastSession::class.java) sessionTransferCallback = MySessionTransferCallback() castContext.addSessionTransferCallback(sessionTransferCallback) } protected fun onDestroy() { if (castContext != null) { castContext.getSessionManager() .removeSessionManagerListener(sessionManagerListener, CastSession::class.java) if (sessionTransferCallback != null) { castContext.removeSessionTransferCallback(sessionTransferCallback) } } } class MySessionTransferCallback : SessionTransferCallback() { fun onTransferring(@SessionTransferCallback.TransferType transferType: Int) { // Perform necessary steps prior to onTransferred } fun onTransferred(@SessionTransferCallback.TransferType transferType: Int, sessionState: SessionState?) { if (transferType == SessionTransferCallback.TRANSFER_TYPE_FROM_REMOTE_TO_LOCAL) { // Remote stream is transferred to the local device. // Retrieve information from the SessionState to continue playback on the local player. } } fun onTransferFailed(@SessionTransferCallback.TransferType transferType: Int, @SessionTransferCallback.TransferFailedReason transferFailedReason: Int) { // Handle transfer failure. } } }
public class MyService extends Service { private CastContext castContext; private SessionTransferCallback sessionTransferCallback; @Override protected void onCreate() { castContext = CastContext.getSharedInstance(this); castContext.getSessionManager() .addSessionManagerListener(sessionManagerListener, CastSession.class); sessionTransferCallback = new MySessionTransferCallback(); castContext.addSessionTransferCallback(sessionTransferCallback); } @Override protected void onDestroy() { if (castContext != null) { castContext.getSessionManager() .removeSessionManagerListener(sessionManagerListener, CastSession.class); if (sessionTransferCallback != null) { castContext.removeSessionTransferCallback(sessionTransferCallback); } } } public static class MySessionTransferCallback extends SessionTransferCallback { public MySessionTransferCallback() {} @Override public void onTransferring(@SessionTransferCallback.TransferType int transferType) { // Perform necessary steps prior to onTransferred } @Override public void onTransferred(@SessionTransferCallback.TransferType int transferType, SessionState sessionState) { if (transferType==SessionTransferCallback.TRANSFER_TYPE_FROM_REMOTE_TO_LOCAL) { // Remote stream is transferred to the local device. // Retrieve information from the SessionState to continue playback on the local player. } } @Override public void onTransferFailed(@SessionTransferCallback.TransferType int transferType, @SessionTransferCallback.TransferFailedReason int transferFailedReason) { // Handle transfer failure. } } }
Aplikasi yang tidak mendukung pemutaran di latar belakang
Untuk aplikasi yang tidak mendukung pemutaran di latar belakang (biasanya aplikasi video), sebaiknya
dengarkan callback onTransferred
dan lanjutkan pemutaran secara lokal
jika aplikasi berada di latar depan.
Jika berada di latar belakang, aplikasi harus menjeda pemutaran dan harus menyimpan
informasi yang diperlukan dari SessionState
(misalnya, metadata media dan posisi
pemutaran). Saat aplikasi berada di latar depan dari latar belakang, pemutaran lokal
harus melanjutkan dengan informasi yang disimpan.
class MyActivity : AppCompatActivity() { private var castContext: CastContext? = null private var sessionTransferCallback: SessionTransferCallback? = null protected fun onCreate() { castContext = CastContext.getSharedInstance(this) castContext.getSessionManager() .addSessionManagerListener(sessionManagerListener, CastSession::class.java) sessionTransferCallback = MySessionTransferCallback() castContext.addSessionTransferCallback(sessionTransferCallback) } protected fun onDestroy() { if (castContext != null) { castContext.getSessionManager() .removeSessionManagerListener(sessionManagerListener, CastSession::class.java) if (sessionTransferCallback != null) { castContext.removeSessionTransferCallback(sessionTransferCallback) } } } class MySessionTransferCallback : SessionTransferCallback() { fun onTransferring(@SessionTransferCallback.TransferType transferType: Int) { // Perform necessary steps prior to onTransferred } fun onTransferred(@SessionTransferCallback.TransferType transferType: Int, sessionState: SessionState?) { if (transferType == SessionTransferCallback.TRANSFER_TYPE_FROM_REMOTE_TO_LOCAL) { // Remote stream is transferred to the local device. // Retrieve information from the SessionState to continue playback on the local player. } } fun onTransferFailed(@SessionTransferCallback.TransferType transferType: Int, @SessionTransferCallback.TransferFailedReason transferFailedReason: Int) { // Handle transfer failure. } } }
public class MyActivity extends AppCompatActivity { private CastContext castContext; private SessionTransferCallback sessionTransferCallback; @Override protected void onCreate() { castContext = CastContext.getSharedInstance(this); castContext .getSessionManager() .addSessionManagerListener(sessionManagerListener, CastSession.class); sessionTransferCallback = new MySessionTransferCallback(); castContext.addSessionTransferCallback(sessionTransferCallback); } @Override protected void onDestroy() { if (castContext != null) { castContext .getSessionManager() .removeSessionManagerListener(sessionManagerListener, CastSession.class); if (sessionTransferCallback != null) { castContext.removeSessionTransferCallback(sessionTransferCallback); } } } public static class MySessionTransferCallback extends SessionTransferCallback { public MySessionTransferCallback() {} @Override public void onTransferring(@SessionTransferCallback.TransferType int transferType) { // Perform necessary steps prior to onTransferred } @Override public void onTransferred(@SessionTransferCallback.TransferType int transferType, SessionState sessionState) { if (transferType==SessionTransferCallback.TRANSFER_TYPE_FROM_REMOTE_TO_LOCAL) { // Remote stream is transferred to the local device. // Retrieve information from the SessionState to continue playback on the local player. } } @Override public void onTransferFailed(@SessionTransferCallback.TransferType int transferType, @SessionTransferCallback.TransferFailedReason int transferFailedReason) { // Handle transfer failure. } } }