Przełącznik wyjścia

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ę.

Pobierz 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

Wymagania wstępne

  1. Przeprowadź migrację istniejącej aplikacji na Androida do AndroidaX.
  2. 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'
      ...
    }
  3. Aplikacja obsługuje powiadomienia o multimediach.
  4. 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:

Kotlin
// 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)
Java
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.

Kotlin
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()
)
Java
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.

Kotlin
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()
)
Java
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.

Kotlin
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)
        }
    }
}
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.

Kotlin
class CastOptionsProvider : OptionsProvider {
    fun getCastOptions(context: Context?): CastOptions {
        ...
        return Builder()
            ...
            .setRemoteToLocalEnabled(true)
            .build()
    }
}
Java
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:

  1. onTransferring
  2. onSessionEnding
  3. onSessionEnded
  4. 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.

Kotlin
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.
        }
    }
}
Java
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.

Kotlin
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.
        }
    }
}
Java
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.
    }
  }
}