Przełącznik danych wyjściowych to funkcja pakietu SDK Cast, która umożliwia bezproblemowe przełączanie się między lokalnym a zdalnym odtwarzaniem treści od wersji Androida 13. Ma to na celu ułatwienie aplikacjom dla nadawców łatwego i szybkiego kontrolowania, gdzie są odtwarzane treści.
Przełącznik wyjścia używa biblioteki MediaRouter
do przełączania odtwarzania treści między głośnikiem telefonu, sparowanymi urządzeniami Bluetooth i zdalnymi urządzeniami obsługującymi Cast. Przypadki użycia można podzielić na następujące scenariusze:
Pobierz ten przykładowy kod i użyj go, aby dowiedzieć się, jak wdrożyć przełącznik wyjścia w aplikacji audio. Zapoznaj się z załączonym plikiem README.md, aby dowiedzieć się, jak uruchomić próbkę.
Przełącznik wyjścia powinien być włączony, aby zapewnić obsługę połączeń między urządzeniami lokalnym a zdalnym i zdalnym na lokalny, wykonując czynności opisane w tym przewodniku. Przesyłanie danych między głośnikami urządzenia lokalnego a sparowanymi urządzeniami Bluetooth nie wymaga żadnych dodatkowych czynności.
Aplikacje audio to takie, które obsługują Google Cast dla dźwięku w ustawieniach aplikacji odbiorcy w Konsoli programisty Google Cast SDK.
Interfejs przełącznika wyjścia
Przełącznik wyjścia pokazuje dostępne urządzenia lokalne i zdalne, a także aktualne stany urządzenia (jeśli urządzenie jest wybrane) i bieżący poziom głośności. Jeśli oprócz bieżącego urządzenia są dostępne inne urządzenia, kliknięcie innego urządzenia umożliwi przeniesienie odtwarzania multimediów na wybrane urządzenie.
Znane problemy
- Sesje multimediów utworzone na potrzeby odtwarzania lokalnego zostaną odrzucone i odtworzone po przejściu na powiadomienie z pakietu SDK Cast.
Punkty wejścia
Powiadomienie o multimediach
Jeśli aplikacja opublikuje powiadomienie multimedialne przy użyciu MediaSession
na potrzeby odtwarzania lokalnego (odtwarzane lokalnie), w prawym górnym rogu powiadomienia o multimediach pojawi się element powiadomienia z nazwą urządzenia (np. głośnika telefonu), na którym treści są aktualnie odtwarzane. Dotykając elementu powiadomienia,
otwiera się systemowy interfejs przełącznika danych wyjściowych.
Ustawienia głośności
Interfejs systemowy przełącznika wyjścia można też wyświetlić, klikając fizyczne przyciski głośności na urządzeniu, ikonę ustawień na dole i tekst „Włącz <nazwa aplikacji> na <urządzeniu przesyłającym>”.
Podsumowanie kroków
- Sprawdzanie, czy zostały spełnione wymagania wstępne
- Włącz przełącznik danych wyjściowych w pliku AndroidManifest.xml
- Aktualizowanie funkcji SessionManagerListener do przesyłania w tle
- Ustawianie flagi setRemoteToLocalEnabled
- Odtwarzaj dalej lokalnie
Wymagania wstępne
- Przeprowadź migrację istniejącej aplikacji na Androida do AndroidaX.
- Zaktualizuj plik
build.gradle
w aplikacji, aby używać minimalnej wymaganej wersji pakietu Android Sender SDK na potrzeby przełącznika danych wyjściowych:dependencies { ... implementation 'com.google.android.gms:play-services-cast-framework:21.2.0' ... }
- Aplikacja obsługuje powiadomienia o multimediach.
- Urządzenie z Androidem 13.
Skonfiguruj powiadomienia o multimediach
Aby korzystać z przełącznika wyjścia, aplikacje audio i wideo muszą utworzyć powiadomienie o multimediach, które będą wyświetlać stan odtwarzania i elementy sterujące multimediami do lokalnego odtwarzania. Wymaga to utworzenia MediaSession
, ustawienia zdarzenia MediaStyle
za pomocą tokena MediaSession
oraz ustawienia opcji sterowania multimediami w powiadomieniu.
Jeśli nie korzystasz obecnie z MediaStyle
ani MediaSession
, z poniższego fragmentu kodu dowiesz się, jak je skonfigurować. Dostępne są też przewodniki konfiguracji wywołań zwrotnych sesji multimediów w aplikacjach audio i wideo:
// 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(); }
Dodatkowo, aby wypełnić powiadomienie informacjami o multimediach, musisz dodać do parametru MediaSession
metadane i stan odtwarzania multimediów.
Aby dodać metadane do obiektu MediaSession
, użyj setMetaData()
i wpisz w MediaMetadataCompat.Builder()
wszystkie odpowiednie stałe MediaMetadata
dla multimediów.
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() ); }
Aby dodać stan odtwarzania do: MediaSession
, użyj kodu setPlaybackState()
i w PlaybackStateCompat.Builder()
podaj wszystkie stałe PlaybackStateCompat
dotyczące multimediów.
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() ); }
Działanie powiadomień wideo w aplikacji
Aplikacje wideo i aplikacje audio, które nie obsługują lokalnego odtwarzania w tle, powinny w określony sposób działać w przypadku powiadomień o multimediach. Pozwoli to uniknąć problemów z wysyłaniem poleceń multimedialnych w sytuacjach, gdy odtwarzanie nie jest obsługiwane:
- Publikuj powiadomienie o multimediach podczas odtwarzania multimediów lokalnie, gdy aplikacja działa na pierwszym planie.
- Wstrzymaj odtwarzanie lokalne i zamknij powiadomienie, gdy aplikacja działa w tle.
- Gdy aplikacja wróci na pierwszy plan, odtwarzanie lokalne powinno zostać wznowione, a powiadomienie powinno zostać ponownie opublikowane.
Włącz przełącznik danych wyjściowych w pliku AndroidManifest.xml
Aby włączyć przełącznik wyjścia, musisz dodać MediaTransferReceiver
do AndroidManifest.xml
aplikacji. Jeśli nie, funkcja nie zostanie włączona, a flaga funkcji zdalnej na lokalnej również będzie nieprawidłowa.
<application>
...
<receiver
android:name="androidx.mediarouter.media.MediaTransferReceiver"
android:exported="true">
</receiver>
...
</application>
MediaTransferReceiver
to odbiornik transmisji, który umożliwia przesyłanie multimediów między urządzeniami z interfejsem systemu. Więcej informacji znajdziesz w dokumentacji komponentu MediaTransferReceivedr.
Lokalne – zdalne
Gdy użytkownik przełączy odtwarzanie ze zdalnego na zdalny, pakiet SDK Cast automatycznie rozpocznie sesję przesyłania. Aplikacje muszą jednak obsługiwać przełączanie się z lokalnego na zdalne sterowanie, np. zatrzymywanie lokalnego odtwarzania i wczytywanie multimediów na urządzenie przesyłające. Aplikacje powinny nasłuchiwać funkcji Przesyłaj
SessionManagerListener
za pomocą wywołań zwrotnych
onSessionStarted()
i onSessionEnded()
i wykonywać działania podczas odbierania wywołań Cast
SessionManager
. Aplikacje powinny sprawdzać, czy te wywołania zwrotne są nadal aktywne, gdy otwarte jest okno Przełącznik danych wyjściowych, a aplikacja nie działa na pierwszym planie.
Zaktualizuj parametr SessionManagerListener na potrzeby przesyłania w tle
Starsze środowisko przesyłania obsługuje już przesyłanie programów lokalnych na zdalne, gdy aplikacja działa na pierwszym planie. Przesyłanie rozpoczyna się zwykle, gdy użytkownik kliknie ikonę Cast w aplikacji i wybierzesz urządzenie, na którym ma być przesyłane strumieniowo treści multimedialne. W tym przypadku aplikacja musi zarejestrować się w SessionManagerListener
, onCreate()
lub onStart()
i wyrejestrować detektor w onStop()
lub onDestroy()
aktywności w aplikacji.
Dzięki nowemu interfejsowi przesyłania za pomocą przełącznika wyjścia aplikacje mogą rozpocząć przesyłanie, gdy działają w tle. Jest to szczególnie przydatne w przypadku aplikacji audio,
które publikują powiadomienia podczas odtwarzania w tle. Aplikacje mogą zarejestrować detektory SessionManager
w elemencie onCreate()
usługi i wyrejestrować się w jej onDestroy()
. Dzięki temu aplikacje zawsze powinny otrzymywać wywołania zwrotne „lokalnie na zdalne” (np. onSessionStarted
), gdy działają w tle.
Jeśli aplikacja używa MediaBrowserService
, warto zarejestrować w niej SessionManagerListener
.
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); } } }
Dzięki tej aktualizacji przesyłanie z lokalnego na pilot działa tak samo jak tradycyjne przesyłanie, gdy aplikacja działa w tle, a przechodzenie z urządzeń Bluetooth na urządzenia przesyłające nie wymaga dodatkowej pracy.
Ze zdalnej lokalizacji na lokalny
Przełącznik wyjścia umożliwia przenoszenie dźwięku ze zdalnego odtwarzania do głośnika telefonu lub lokalnego urządzenia Bluetooth. Aby to zrobić, ustaw flagę setRemoteToLocalEnabled
na true
w: CastOptions
.
Jeśli bieżące urządzenie nadawcy dołącza do istniejącej sesji z wieloma nadawcami, a aplikacja musi sprawdzić, czy można przenieść bieżące multimedia lokalnie, aplikacje powinny użyć wywołania zwrotnego onTransferred
SessionTransferCallback
, aby sprawdzić SessionState
.
Ustaw flagę setRemoteToLocalEnabled
CastOptions
udostępnia element setRemoteToLocalEnabled
, który umożliwia wyświetlenie lub ukrycie głośnika telefonu i lokalnych urządzeń Bluetooth jako elementów docelowych w oknie przełączania wyjścia w przypadku aktywnej sesji przesyłania.
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() } }
Kontynuuj odtwarzanie lokalnie
Aplikacje, które obsługują przesyłanie zdalne do lokalnych, powinny zarejestrować SessionTransferCallback
, aby otrzymywać powiadomienia o zdarzeniu, co pozwoli im sprawdzić, czy można przesyłać multimedia i kontynuować ich odtwarzanie lokalnie.
CastContext#addSessionTransferCallback(SessionTransferCallback)
pozwala aplikacji zarejestrować swój parametr SessionTransferCallback
i nasłuchiwać wywołań zwrotnych onTransferred
oraz onTransferFailed
, gdy nadawca zostanie przekierowany do odtwarzania lokalnego.
Gdy aplikacja wyrejestruje swoje SessionTransferCallback
, nie będzie już otrzymywać sygnałów SessionTransferCallback
.
Element SessionTransferCallback
jest rozszerzeniem dotychczasowych wywołań zwrotnych SessionManagerListener
i jest wyzwalany po wywołaniu onSessionEnded
. Kolejność wywołań zwrotnych między zdalnymi a lokalnymi jest następująca:
onTransferring
onSessionEnding
onSessionEnded
onTransferred
Ponieważ można otworzyć przełącznik wyjścia za pomocą elementu powiadomienia o multimediach, gdy aplikacja działa w tle i przesyła treści, aplikacje muszą obsługiwać przesyłanie lokalnie w różny sposób zależnie od tego, czy obsługują odtwarzanie w tle. W przypadku niepowodzenia transferu onTransferFailed
będzie uruchamiany przy każdym wystąpieniu błędu.
Aplikacje obsługujące odtwarzanie w tle
W przypadku aplikacji, które obsługują odtwarzanie w tle (zwykle jest to aplikacje audio), zalecamy użycie Service
(np. MediaBrowserService
). Usługi powinny nasłuchiwać wywołania zwrotnego onTransferred
i wznawiać odtwarzanie lokalnie, zarówno wtedy, gdy aplikacja działa na pierwszym planie, jak i w tle.
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. } } }
Aplikacje, które nie obsługują odtwarzania w tle
W przypadku aplikacji, które nie obsługują odtwarzania w tle (zwykle są to aplikacje wideo), zalecamy odsłuchiwanie wywołania zwrotnego onTransferred
i wznowienie odtwarzania lokalnie, jeśli aplikacja działa na pierwszym planie.
Jeśli aplikacja działa w tle, powinna wstrzymać odtwarzanie i zachować niezbędne informacje z usługi SessionState
(np. metadane multimediów oraz pozycję odtwarzania). Gdy aplikacja działa w tle, lokalne odtwarzanie powinno być kontynuowane z wykorzystaniem zapisanych informacji.
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. } } }