Sélecteur de sortie

Le sélecteur de sortie est une fonctionnalité du SDK Cast qui permet de passer facilement de la lecture locale à la lecture à distance de contenu à partir d'Android 13. L'objectif est d'aider les applications émettrices à contrôler facilement et rapidement l'emplacement de lecture du contenu. Le sélecteur de sortie utilise la bibliothèque MediaRouter pour basculer la lecture de contenu entre le haut-parleur du téléphone, les appareils Bluetooth associés et les appareils distants compatibles Cast. Les cas d'utilisation peuvent être divisés en trois catégories:

Téléchargez et utilisez l'exemple ci-dessous pour savoir comment implémenter le commutateur de sortie dans votre application audio. Consultez le fichier README.md inclus pour découvrir comment exécuter l'exemple.

Télécharger un exemple

Le commutateur de sortie doit être activé pour prendre en charge l'authentification locale à distance et le commutateur local à distance à l'environnement local en suivant les étapes décrites dans ce guide. Aucune étape supplémentaire n'est nécessaire pour assurer le transfert entre les haut-parleurs des appareils locaux et les appareils Bluetooth associés.

Les applications audio sont des applications compatibles avec Google Cast pour l'audio dans les paramètres de l'application récepteur de la console pour les développeurs du SDK Google Cast.

UI du sélecteur de sortie

Le sélecteur de sortie affiche les appareils locaux et distants disponibles, ainsi que leur état actuel (y compris si l'appareil est sélectionné) se connecte, le niveau de volume actuel. S'il existe d'autres appareils en plus de l'appareil actuel, cliquer sur un autre appareil vous permet de transférer la lecture de contenus multimédias vers l'appareil sélectionné.

Problèmes connus

  • Les sessions multimédias créées pour la lecture locale seront ignorées et recréées lors du passage à la notification du SDK Cast.

Points d'entrée

Notification multimédia

Si une application publie une notification multimédia avec MediaSession pour une lecture locale (lecture locale), un chip de notification avec le nom de l'appareil (comme le haut-parleur du téléphone) sur lequel le contenu est en cours de lecture s'affiche dans l'angle supérieur droit de cette notification. Appuyer sur le chip de notification ouvre l'UI du système de boîte de dialogue du sélecteur de sortie.

Paramètres de volume

L'interface utilisateur du système de sélection du sélecteur de sortie peut également être déclenchée en cliquant sur les boutons de volume physiques de l'appareil, sur l'icône des paramètres en bas de l'écran, puis sur le texte "Lire <nom de l'application> sur <appareil Cast>".

Résumé des étapes

Prérequis

  1. Migrez votre application Android existante vers AndroidX.
  2. Mettez à jour le build.gradle de votre application afin d'utiliser la version minimale requise du SDK Android Sender pour le sélecteur de sortie :
    dependencies {
      ...
      implementation 'com.google.android.gms:play-services-cast-framework:21.2.0'
      ...
    }
  3. L'appli prend en charge les notifications multimédias.
  4. Appareil équipé d'Android 13.

Configurer les notifications multimédias

Pour utiliser le sélecteur de sortie, les applications audio et vidéo doivent créer une notification multimédia afin d'afficher l'état de la lecture et les commandes de lecture locale. Pour ce faire, vous devez créer un MediaSession, définir le MediaStyle avec le jeton de MediaSession et définir les commandes multimédias sur la notification.

Si vous n'utilisez pas actuellement MediaStyle ni MediaSession, l'extrait ci-dessous montre comment les configurer. Des guides sont également disponibles pour configurer les rappels de session multimédia pour les applications audio et vidéo:

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();
}

En outre, pour renseigner la notification avec les informations relatives à votre contenu multimédia, vous devez ajouter les métadonnées et l'état de lecture de votre contenu multimédia à MediaSession.

Pour ajouter des métadonnées à MediaSession, utilisez setMetaData() et fournissez toutes les constantes MediaMetadata pertinentes pour votre contenu multimédia dans MediaMetadataCompat.Builder().

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()
    );
}

Pour ajouter l'état de lecture à MediaSession, utilisez setPlaybackState() et fournissez toutes les constantes PlaybackStateCompat pertinentes pour votre contenu multimédia dans PlaybackStateCompat.Builder().

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()
    );
}

Comportement des notifications vidéo dans l'application

Les applications vidéo ou audio qui ne sont pas compatibles avec la lecture locale en arrière-plan doivent adopter un comportement spécifique pour les notifications multimédias afin d'éviter les problèmes d'envoi de commandes multimédias dans les cas où la lecture n'est pas prise en charge:

  • Publiez la notification multimédia lorsque vous lisez des contenus multimédias en local et que l'application est au premier plan.
  • Mettez en pause la lecture en local et ignorez la notification lorsque l'application est exécutée en arrière-plan.
  • Lorsque l'application revient au premier plan, la lecture en local doit reprendre et la notification doit être republiée.

Activer le sélecteur de sortie dans le fichier AndroidManifest.xml

Pour activer le sélecteur de sortie, vous devez ajouter MediaTransferReceiver au AndroidManifest.xml de l'application. Si ce n'est pas le cas, la fonctionnalité n'est pas activée et l'indicateur de fonctionnalité distante vers locale ne sera pas non plus valide.

<application>
    ...
    <receiver
         android:name="androidx.mediarouter.media.MediaTransferReceiver"
         android:exported="true">
    </receiver>
    ...
</application>

Le MediaTransferReceiver est un broadcast receiver qui permet le transfert de contenus multimédias entre les appareils dotés d'une UI système. Pour en savoir plus, consultez la documentation de référence de MediaTransferReceiver.

Communication locale à distance

Lorsque l'utilisateur passe de la lecture locale à la lecture à distance, le SDK Cast démarre automatiquement la session Cast. Toutefois, les applications doivent gérer le passage de l'environnement local à l'accès distant, par exemple arrêter la lecture locale et charger le contenu multimédia sur l'appareil Cast. Les applications doivent écouter l'objet Cast SessionManagerListener à l'aide des rappels onSessionStarted() et onSessionEnded(), et gérer l'action lors de la réception des rappels SessionManager Cast. Les applications doivent s'assurer que ces rappels sont toujours actifs lorsque la boîte de dialogue du sélecteur de sortie est ouverte et que l'application n'est pas au premier plan.

Mettre à jour SessionManagerListener pour la diffusion en arrière-plan

L'ancienne expérience Cast est déjà compatible avec le mode local vers le mode distant lorsque l'application est exécutée au premier plan. En général, une expérience Cast commence lorsque les utilisateurs cliquent sur l'icône Cast dans l'application et choisissent un appareil pour diffuser du contenu multimédia. Dans ce cas, l'application doit s'enregistrer auprès de SessionManagerListener dans onCreate() ou onStart(), et désenregistrer l'écouteur dans onStop() ou onDestroy() de l'activité de l'application.

Grâce à la nouvelle expérience de diffusion à l'aide du sélecteur de sortie, les applications peuvent commencer à caster du contenu en arrière-plan. Cela est particulièrement utile pour les applications audio qui publient des notifications lors de la lecture en arrière-plan. Les applications peuvent enregistrer les écouteurs SessionManager dans le onCreate() du service et se désinscrire dans l'onDestroy() du service. De cette manière, les applications doivent toujours recevoir les rappels de proximité à distance (tels que onSessionStarted) lorsqu'elles sont en arrière-plan.

Si l'application utilise l'MediaBrowserService, nous vous recommandons d'y enregistrer le 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);
    }
  }
}

Avec cette mise à jour, la diffusion locale vers la télécommande fonctionne de la même manière que la diffusion traditionnelle lorsque l'application est exécutée en arrière-plan. De plus, aucun travail supplémentaire n'est requis pour passer d'appareils Bluetooth à des appareils Cast.

Communication distante à locale

Le sélecteur de sortie permet de transférer la lecture à distance vers le haut-parleur du téléphone ou l'appareil Bluetooth local. Vous pouvez activer cette fonctionnalité en définissant l'option setRemoteToLocalEnabled sur true sur CastOptions.

Si l'appareil émetteur actuel rejoint une session existante avec plusieurs expéditeurs et que l'application doit vérifier si le contenu multimédia actuel est autorisé à être transféré localement, les applications doivent utiliser le rappel onTransferred de SessionTransferCallback pour vérifier le SessionState.

Définir l'indicateur setRemoteToLocalEnabled

CastOptions fournit un setRemoteToLocalEnabled pour afficher ou masquer le haut-parleur du téléphone et les appareils Bluetooth locaux en tant que cibles de transfert dans la boîte de dialogue du sélecteur de sortie lorsqu'une session de diffusion est active.

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()
  }
}

Continuer la lecture en local

Les applications compatibles avec la technologie distante vers locale doivent enregistrer le SessionTransferCallback pour recevoir une notification lorsque l'événement se produit. Elles peuvent ainsi vérifier si le contenu multimédia doit être autorisé à être transféré et à poursuivre la lecture localement.

CastContext#addSessionTransferCallback(SessionTransferCallback) permet à une application d'enregistrer son SessionTransferCallback et d'écouter les rappels onTransferred et onTransferFailed lorsqu'un expéditeur est transféré en lecture locale.

Une fois que l'application a annulé l'enregistrement de son SessionTransferCallback, elle ne reçoit plus de SessionTransferCallback.

SessionTransferCallback est une extension des rappels SessionManagerListener existants. Il se déclenche après le déclenchement de onSessionEnded. Par conséquent, l'ordre des rappels distants vers locaux est le suivant:

  1. onTransferring
  2. onSessionEnding
  3. onSessionEnded
  4. onTransferred

Étant donné que le sélecteur de sortie peut être ouvert par le chip de notification multimédia lorsque l'application est en arrière-plan et caste, les applications doivent gérer le transfert en local différemment selon qu'elles sont compatibles ou non avec la lecture en arrière-plan. En cas d'échec du transfert, onTransferFailed se déclenche à chaque fois que l'erreur se produit.

Applis avec lecture en arrière-plan

Pour les applications qui prennent en charge la lecture en arrière-plan (généralement les applications audio), il est recommandé d'utiliser un Service (par exemple, MediaBrowserService). Les services doivent écouter le rappel onTransferred et reprendre la lecture localement lorsque l'application est exécutée au premier plan ou en arrière-plan.

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

Applications incompatibles avec la lecture en arrière-plan

Pour les applications qui ne sont pas compatibles avec la lecture en arrière-plan (généralement les applications vidéo), il est recommandé d'écouter le rappel onTransferred et de reprendre la lecture localement si l'application est au premier plan.

Si l'application est exécutée en arrière-plan, elle doit suspendre la lecture et stocker les informations nécessaires à partir de SessionState (par exemple, les métadonnées multimédias et la position de lecture). Lorsque l'application est affichée au premier plan en arrière-plan, la lecture en local doit se poursuivre avec les informations stockées.

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