Pengalih Output adalah fitur SDK Cast yang memungkinkan transfer lancar
antara pemutaran konten lokal dan jarak jauh yang dimulai dengan Android 13. Tujuan
adalah untuk membantu aplikasi pengirim mengontrol tempat konten diputar dengan mudah dan cepat.
Pengalih Output menggunakan
MediaRouter
library ke
mengalihkan pemutaran konten di antara speaker ponsel, perangkat Bluetooth yang disambungkan,
dan perangkat yang kompatibel untuk Cast jarak jauh. Kasus penggunaan dapat dibagi menjadi
skenario:
Download dan gunakan contoh di bawah untuk referensi tentang cara mengimplementasikan Output Pengalih di aplikasi Audio. Lihat README.md yang disertakan untuk mendapatkan petunjuk mengenai cara menjalankan sampel.
Pengalih Output harus diaktifkan untuk mendukung lokal ke jarak jauh dan jarak jauh ke lokal menggunakan langkah-langkah yang tercakup dalam panduan ini. Tidak ada langkah tambahan yang diperlukan untuk mendukung transfer antara speaker perangkat lokal dan Bluetooth yang disambungkan perangkat.
Aplikasi audio adalah aplikasi yang mendukung Google Cast untuk Audio di Aplikasi Penerima setelan di Developer SDK Google Cast Konsol
UI Pengalih Output
Pengalih Output menampilkan perangkat lokal dan jarak jauh yang tersedia serta status perangkat saat ini, termasuk jika perangkat dipilih, terhubung, level volume saat ini. Jika ada perangkat lain selain ke perangkat saat ini, mengeklik perangkat lain memungkinkan Anda mentransfer media ke perangkat yang dipilih.
Masalah umum
- Sesi Media yang dibuat untuk pemutaran lokal akan ditutup dan dibuat ulang saat beralih ke notifikasi SDK Cast.
Titik entri
Notifikasi media
Jika aplikasi memposting notifikasi media dengan
MediaSession
untuk
pemutaran lokal (diputar secara lokal), di pojok kanan atas notifikasi media
menampilkan chip notifikasi dengan nama perangkat (seperti speaker ponsel) yang
konten yang sedang diputar. Mengetuk chip notifikasi akan terbuka
UI sistem dialog Pengalih Output.
Setelan volume
UI sistem dialog Pengalih Output juga dapat dipicu dengan mengklik tombol volume fisik di perangkat, mengetuk ikon pengaturan di bagian bawah, dan mengetuk "Play <App Name> di <Perangkat Cast>" teks.
Ringkasan langkah-langkah
- Memastikan prasyarat terpenuhi
- Mengaktifkan Pengalih Output di AndroidManifest.xml
- Mengupdate SessionManagerListener untuk transmisi latar belakang
- Menyetel tanda setRemoteToLocalEnabled
- Melanjutkan pemutaran secara lokal
Prasyarat
- Migrasikan aplikasi Android yang ada ke AndroidX.
- Update
build.gradle
aplikasi Anda untuk menggunakan Android Sender SDK versi minimum yang diperlukan untuk Pengalih Output:dependencies { ... implementation 'com.google.android.gms:play-services-cast-framework:21.2.0' ... }
- Aplikasi mendukung notifikasi media.
- Perangkat yang menjalankan Android 13.
Siapkan Notifikasi Media
Untuk menggunakan Pengalih Output,
audio dan
Aplikasi video
diperlukan untuk membuat notifikasi media
guna menampilkan status pemutaran dan
untuk media mereka untuk pemutaran lokal. Hal ini memerlukan pembuatan
MediaSession
,
menyetel
MediaStyle
dengan token MediaSession
, dan menyetel kontrol media pada
notifikasi.
Jika saat ini Anda tidak menggunakan MediaStyle
dan MediaSession
, cuplikan
di bawah ini menunjukkan cara menyiapkannya. Panduan juga tersedia untuk menyiapkan media
callback sesi untuk
audio dan
video
aplikasi:
// 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, guna mengisi notifikasi dengan informasi untuk media Anda,
Anda perlu menambahkan atribut
metadata dan status pemutaran
ke MediaSession
.
Untuk menambahkan metadata ke MediaSession
, gunakan
setMetaData()
dan menyediakan semua
informasi yang relevan
Konstanta MediaMetadata
untuk
media Anda dalam
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 menyediakan semua
informasi yang relevan
PlaybackStateCompat
untuk media Anda dalam
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 untuk notifikasi media untuk menghindari masalah dengan mengirimkan perintah media dalam situasi pemutaran tidak didukung:
- Posting notifikasi media saat memutar media secara lokal dan aplikasi dalam latar depan.
- Jeda pemutaran lokal dan tutup notifikasi saat aplikasi berada di latar belakang.
- Saat aplikasi kembali ke latar depan, pemutaran lokal akan dilanjutkan dan pemberitahuan harus diposting ulang.
Mengaktifkan Pengalih Output di AndroidManifest.xml
Untuk mengaktifkan Pengalih Output,
MediaTransferReceiver
perlu ditambahkan ke AndroidManifest.xml
aplikasi. Jika tidak, fitur tersebut
tidak akan diaktifkan dan tanda fitur jarak jauh ke lokal juga tidak akan valid.
<application>
...
<receiver
android:name="androidx.mediarouter.media.MediaTransferReceiver"
android:exported="true">
</receiver>
...
</application>
Tujuan
MediaTransferReceiver
adalah penerima siaran yang memungkinkan
transfer media antar perangkat dengan
UI. Lihat MediaTransferReceiver
referensi
untuk informasi selengkapnya.
Lokal ke jarak jauh
Saat pengguna mengalihkan pemutaran dari lokal ke jarak jauh, Cast SDK akan dimulai
sesi Transmisi secara otomatis. Namun, aplikasi perlu menangani peralihan dari
dari lokal ke jarak jauh, misalnya
menghentikan pemutaran lokal
dan memuat media di perangkat Transmisi. Aplikasi akan mendengarkan Transmisi
SessionManagerListener
,
menggunakan
onSessionStarted()
dan
onSessionEnded()
callback, dan menangani tindakan saat menerima Transmisi
SessionManager
callback. Aplikasi harus memastikan bahwa callback ini masih aktif saat
dialog Pengalih Output terbuka dan aplikasi tidak berada di latar depan.
Memperbarui SessionManagerListener untuk transmisi latar belakang
Pengalaman Cast lama sudah mendukung lokal-ke-jarak jauh saat aplikasi
di latar depan. Pengalaman Cast yang biasa dimulai saat pengguna mengklik Cast
di aplikasi dan pilih perangkat untuk {i>streaming <i}media. Dalam hal ini, aplikasi membutuhkan
untuk mendaftar ke
SessionManagerListener
,
dalam onCreate()
atau
onStart()
dan membatalkan pendaftaran pemroses
onStop()
atau
onDestroy()
aktivitas aplikasi.
Dengan pengalaman baru transmisi menggunakan Pengalih Output, aplikasi dapat memulai
melakukan transmisi saat
berada di latar belakang. Hal ini sangat berguna untuk audio
aplikasi yang memposting notifikasi saat diputar di latar belakang. Aplikasi dapat
mendaftarkan pemroses SessionManager
dalam onCreate()
layanan
dan membatalkan pendaftaran di onDestroy()
layanan. Dengan cara ini, aplikasi harus
selalu terima callback lokal ke jarak jauh (seperti onSessionStarted
) saat
aplikasi berada di latar belakang.
Jika aplikasi menggunakan
MediaBrowserService
,
Anda disarankan untuk mendaftarkan SessionManagerListener
di 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 pembaruan ini, {i>local-to-remote<i} bertindak sama seperti transmisi tradisional saat aplikasi ada di latar belakang dan tidak perlu pekerjaan tambahan untuk beralih dari Perangkat Bluetooth ke perangkat Transmisi.
Jarak jauh ke lokal
Pengalih Output menyediakan kemampuan untuk mentransfer dari pemutaran jarak jauh ke
speaker telepon atau perangkat Bluetooth lokal. Ini dapat diaktifkan dengan menyetel
setRemoteToLocalEnabled
untuk true
pada CastOptions
.
Untuk kasus ketika perangkat pengirim saat ini bergabung ke sesi yang ada dengan
beberapa pengirim dan aplikasi harus 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 telepon dan perangkat Bluetooth lokal sebagai target transfer-ke dalam Output
Dialog pengalih saat ada sesi Transmisi 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() } }
Melanjutkan pemutaran secara lokal
Aplikasi yang mendukung jarak jauh ke lokal harus mendaftarkan SessionTransferCallback
untuk mendapatkan pemberitahuan saat peristiwa terjadi sehingga mereka
dapat memeriksa apakah media harus
diizinkan untuk mentransfer dan melanjutkan pemutaran secara lokal.
CastContext#addSessionTransferCallback(SessionTransferCallback)
mengizinkan
aplikasi untuk mendaftarkan SessionTransferCallback
dan memproses onTransferred
serta
Callback onTransferFailed
saat pengirim ditransfer ke pemutaran lokal.
Setelah aplikasi membatalkan pendaftaran SessionTransferCallback
-nya, aplikasi tidak akan lagi
menerima SessionTransferCallback
.
SessionTransferCallback
merupakan ekstensi dari
SessionManagerListener
dan dipicu setelah onSessionEnded
dipicu. Oleh karena itu, urutan
callback jarak jauh ke lokal adalah:
onTransferring
onSessionEnding
onSessionEnded
onTransferred
Karena {i>Output Switcher<i} dapat
dibuka oleh {i>chip<i} notifikasi media saat
aplikasi berada di latar belakang dan melakukan transmisi, aplikasi harus menangani transfer ke
lokal secara berbeda tergantung pada apakah mereka
mendukung pemutaran di latar belakang atau tidak. Di beberapa
kasus transfer yang gagal, onTransferFailed
akan diaktifkan setiap saat
terjadi kesalahan.
Aplikasi yang mendukung pemutaran di 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 baik
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),
direkomendasikan untuk memproses callback onTransferred
dan melanjutkan pemutaran
secara lokal jika aplikasi berada di latar depan.
Jika berada di latar belakang, aplikasi harus menjeda pemutaran dan menyimpan
informasi yang diperlukan dari SessionState
(mis., metadata media dan 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. } } }