
Dans la rue, j'écoute souvent des livres audio et des podcasts à partir d'un smartphone. Une fois à la maison, je souhaite continuer à les écouter sur Android TV ou Google Home. Mais toutes les applications ne prennent pas en charge Chromecast. Et ce serait pratique.
Selon les statistiques de Google au cours des 3 dernières années, le nombre d'appareils sur Android TV a quadruplé et le nombre de partenaires industriels a déjà dépassé la centaine: téléviseurs «intelligents», haut-parleurs, décodeurs. Tous prennent en charge Chromecast. Mais il existe encore de nombreuses applications sur le marché qui manquent évidemment d'intégration avec lui.
Dans cet article, je souhaite partager mon expérience d'intégration de Chromecast dans une application Android pour lire du contenu multimédia.
Comment ça marche
Si c'est la première fois que vous entendez le mot "Chromecast", je vais essayer de vous le dire brièvement. En termes d'utilisation, cela ressemble à ceci:
- L'utilisateur écoute de la musique ou regarde des vidéos via une application ou un site Web.
- Un appareil Chromecast apparaît sur le réseau local.
- Un bouton correspondant devrait apparaître dans l'interface du lecteur.
- En cliquant dessus, l'utilisateur sélectionne l'appareil souhaité dans la liste. Il peut s'agir d'un Nexus Player, d'Android TV ou d'un haut-parleur intelligent.
- La lecture continue avec cet appareil.

Techniquement, quelque chose comme ce qui suit se produit:
- Les services Google surveillent la présence d'appareils Chromecast sur le réseau local via la diffusion.
- Si MediaRouter est connecté à votre application, vous recevrez un événement à ce sujet.
- Lorsqu'un utilisateur sélectionne un appareil Cast et s'y connecte, une nouvelle session multimédia (CastSession) s'ouvre.
- Déjà dans la session créée, nous transférerons le contenu pour la lecture.
Cela semble très simple.
Intégration
Google possède son propre SDK Chromecast, mais il est mal couvert dans la documentation et son code est obscurci. Par conséquent, beaucoup de choses devaient être vérifiées en tapant. Mettons tout en ordre.
Initialisation
Nous devons d'abord connecter Cast Application Framework et MediaRouter:
implementation "com.google.android.gms:play-services-cast-framework:16.1.0" implementation "androidx.mediarouter:mediarouter:1.0.0"
Ensuite, Cast Framework devrait obtenir l'identifiant de l'application (plus à ce sujet plus tard) et les types de contenu multimédia pris en charge. Autrement dit, si notre application ne lit que de la vidéo, la diffusion dans la colonne Google Home sera impossible et elle ne figurera pas dans la liste des appareils. Pour ce faire, créez une implémentation d'OptionsProvider:
class CastOptionsProvider: OptionsProvider { override fun getCastOptions(context: Context): CastOptions { return CastOptions.Builder() .setReceiverApplicationId(BuildConfig.CHROMECAST_APP_ID) .build() } override fun getAdditionalSessionProviders(context: Context): MutableList<SessionProvider>? { return null } }
Et déclarez-le dans le manifeste:
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME" android:value="your.app.package.CastOptionsProvider" />
Enregistrez l'application
Pour que Chromecast fonctionne avec notre application, elle doit être enregistrée dans la console des développeurs du SDK Google Cast . Cela nécessite un compte développeur Chromecast (à ne pas confondre avec un compte développeur Google Play). Lors de votre inscription, vous devrez payer des frais uniques de 5 $. Après avoir publié l'application ChromeCast, vous devez attendre un peu.
Dans la console, vous pouvez modifier l' apparence du lecteur Cast pour les appareils dotés d'un écran et voir les analyses de diffusion dans l'application.
MediaRouteFramework est un mécanisme qui vous permet de trouver tous les périphériques de lecture à distance à proximité de l'utilisateur. Il peut s'agir non seulement de Chromecast, mais également d'écrans et de haut-parleurs distants utilisant des protocoles tiers. Mais ce qui nous intéresse, c'est Chromecast.

MediaRouteFramework a une vue qui reflète l'état du scooter multimédia. Il existe deux façons de le connecter:
1) Via le menu:
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> ... <item android:id="@+id/menu_media_route" android:title="@string/cast" app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider" app:showAsAction="always"/> ... </menu>
2) Via la mise en page:
<androidx.mediarouter.app.MediaRouteButton android:id="@+id/mediaRouteButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:mediaRouteTypes="user"/>
Et à partir du code, il vous suffit d'enregistrer le bouton dans CastButtonFactory. alors l'état actuel du scooter multimédia y sera jeté:
CastButtonFactory.setUpMediaRouteButton(applicationContext, view.mediaRouteButton)
Maintenant que l'application est enregistrée et que MediaRouter est configuré, vous pouvez vous connecter aux appareils ChromeCast et y ouvrir des sessions.
ChromeCast prend en charge trois principaux types de contenu:
Selon les paramètres du lecteur, tels que le contenu multimédia et l'appareil de diffusion, l'interface du lecteur peut varier.
Castsession
Ainsi, l'utilisateur a sélectionné le périphérique souhaité, CastFramework a ouvert une nouvelle session. Maintenant, notre tâche est de répondre à cela et de transmettre des informations à l'appareil pour la lecture.
Pour connaître l'état actuel de la session et vous inscrire pour mettre à jour cet état, utilisez l'objet SessionManager :
private val mediaSessionListener = object : SessionManagerListener<CastSession> { override fun onSessionStarted(session: CastSession, sessionId: String) { currentSession = session
Et nous pouvons également savoir s'il y a une session ouverte en ce moment:
val currentSession: CastSession? = sessionManager.currentCastSession
Nous avons deux conditions principales dans lesquelles nous pouvons commencer le casting:
- La session est déjà ouverte.
- Il y a du contenu pour le casting.
À chacun de ces deux événements, nous pouvons vérifier l'état, et si tout est en ordre, puis lancer le casting.
Casting
Maintenant que nous avons quoi lancer et où lancer, nous pouvons passer à la chose la plus importante. Entre autres choses, CastSession a un objet RemoteMediaClient qui est responsable de l'état de lecture du contenu multimédia. Nous travaillerons avec lui.
Créons MediaMetadata , où les informations sur l'auteur, l'album, etc. seront stockées. C'est très similaire à ce que nous transférons à MediaSession lorsque nous démarrons la lecture locale.
val mediaMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MUSIC_TRACK ).apply { putString(MediaMetadata.KEY_TITLE, “In C”) putString(MediaMetadata.KEY_ARTIST, “Terry Riley”) mediaContent?.metadata?.posterUrl?.let { poster -> addImage(WebImage(Uri.parse(“https:
MediaMetadata a beaucoup de paramètres, et il est préférable de les voir dans la documentation. J'ai été agréablement surpris que vous puissiez ajouter une image non pas via bitmap, mais simplement par un lien à l'intérieur de WebImage.
L'objet MediaInfo contient des informations sur les métadonnées de contenu et expliquera d'où vient le contenu multimédia, de quel type il s'agit, comment le lire:
val mediaInfo = MediaInfo.Builder(“https:
Permettez-moi de vous rappeler que contentType est un type de contenu selon la spécification MIME .
Vous pouvez également transférer des encarts publicitaires vers MediaInfo:
- setAdBreakClips - accepte une liste de publicités AdBreakClipInfo avec des liens vers le contenu, le titre, le moment et le temps pendant lequel l'annonce est ignorée.
- setAdBreaks - informations sur la mise en page et le calendrier des insertions publicitaires.
Dans MediaLoadOptions, nous décrivons comment nous traiterons le flux multimédia (vitesse, position de départ). La documentation indique également que via setCredentials, vous pouvez passer l'en-tête de demande d'autorisation, mais mes demandes de Chromecast n'incluaient pas les champs d'autorisation demandés.
val mediaLoadOptions = MediaLoadOptions.Builder() .setPlayPosition(position!!) .setAutoplay(true) .setPlaybackRate(playbackSpeed) .setCredentials(context.getString(R.string.bearer_token, authGateway.authState.accessToken!!)) .setCredentialsType(context.getString(R.string.authorization_header_key)) .build()
Une fois que tout est prêt, nous pouvons donner toutes les données à RemoteMediaClient et Chromecast commencera à jouer. Il est important de suspendre la lecture locale.
val remoteMediaClient = currentSession!!.remoteMediaClient remoteMediaClient.load(mediaInfo, mediaLoadOptions)
Gestion des événements
La vidéo a commencé à jouer, et puis quoi? Que faire si l'utilisateur met le téléviseur en pause? Pour en savoir plus sur les événements du côté de Chromecast, RemoteMediaClient a des rappels:
private val castStatusCallback = object : RemoteMediaClient.Callback() { override fun onStatusUpdated() {
Connaître les progrès en cours est également simple:
val periodMills = 1000L remoteMediaClient.addProgressListener( RemoteMediaClient.ProgressListener { progressMills, durationMills ->
Expérience d'intégration avec un joueur existant
L'application sur laquelle je travaillais avait déjà un lecteur multimédia prêt à l'emploi. L'objectif était d'y intégrer le support Chromecast. Le lecteur multimédia était basé sur State Machine, et la première pensée a été d'ajouter un nouvel état: «CastingState». Mais cette idée a été immédiatement rejetée, car chaque état du lecteur reflète l'état de lecture, et peu importe ce qui sert d'implémentation d'ExoPlayer ou de ChromeCast.
L'idée est alors venue de créer un certain système de délégués avec hiérarchisation et traitement du «cycle de vie» du joueur. Tous les délégués peuvent recevoir des événements de statut de joueur: Play, Pause, etc. - mais seul le délégué principal jouera le contenu multimédia.

Nous avons quelque chose comme cette interface de lecteur:
interface Player { val isPlaying: Boolean val isReleased: Boolean val duration: Long var positionInMillis: Long var speed: Float var volume: Float var loop: Boolean fun addListener(listener: PlayerCallback) fun removeListener(listener: PlayerCallback): Boolean fun getListeners(): MutableSet<PlayerCallback> fun prepare(mediaContent: MediaContent) fun play() fun pause() fun release() interface PlayerCallback { fun onPlaying(currentPosition: Long) fun onPaused(currentPosition: Long) fun onPreparing() fun onPrepared() fun onLoadingChanged(isLoading: Boolean) fun onDurationChanged(duration: Long) fun onSetSpeed(speed: Float) fun onSeekTo(fromTimeInMillis: Long, toTimeInMillis: Long) fun onWaitingForNetwork() fun onError(error: String?) fun onReleased() fun onPlayerProgress(currentPosition: Long) } }
À l'intérieur, il y aura une machine d'état avec autant d'états:
- Vide - état initial avant l'initialisation.
- Préparation - le lecteur lance la lecture du contenu multimédia.
- Préparé - Le média est téléchargé et prêt à jouer.
- Jouer
- En pause
- Réseau en attente
- Erreur

Auparavant, chaque état lors de l'initialisation émettait une commande dans ExoPlayer. Maintenant, il enverra la commande à la liste des délégués de lecture, et le délégué «Lead» pourra la traiter. Étant donné que le délégué implémente toutes les fonctions du lecteur, il peut également être hérité de l'interface du lecteur et utilisé séparément si nécessaire. Ensuite, le délégué abstrait ressemblera à ceci:
abstract class PlayingDelegate( protected val playerCallback: Player.PlayerCallback, var isLeading: Boolean = false ) : Player { fun setIsLeading(isLeading: Boolean, positionMills: Long, isPlaying: Boolean) { this.isLeading = isLeading if (isLeading) { onLeading(positionMills, isPlaying) } else { onDormant() } } final override fun addListener(listener: Player.PlayerCallback) {
Par exemple, j'ai simplifié les interfaces. En réalité, il y a un peu plus d'événements.
Il peut y avoir autant de délégués qu'il y a de sources de reproduction. Un délégué Chromecast pourrait ressembler à ceci:
ChromeCastDelegate.kt class ChromeCastDelegate( private val context: Context, private val castCallback: ChromeCastListener, playerCallback: Player.PlayerCallback ) : PlayingDelegate(playerCallback) { companion object { private const val CONTENT_TYPE_VIDEO = "videos/mp4" private const val CONTENT_TYPE_AUDIO = "audio/mp3" private const val PROGRESS_DELAY_MILLS = 500L } interface ChromeCastListener { fun onCastStarted() fun onCastStopped() } private var sessionManager: SessionManager? = null private var currentSession: CastSession? = null private var mediaContent: MediaContent? = null private var currentPosition: Long = 0 private val mediaSessionListener = object : SessionManagerListener<CastSession> { override fun onSessionStarted(session: CastSession, sessionId: String) { currentSession = session castCallback.onCastStarted() } override fun onSessionEnding(session: CastSession) { currentPosition = session.remoteMediaClient?.approximateStreamPosition ?: currentPosition stopCasting() } override fun onSessionResumed(session: CastSession, wasSuspended: Boolean) { currentSession = session castCallback.onCastStarted() } override fun onSessionStartFailed(session: CastSession, p1: Int) { stopCasting() } override fun onSessionEnded(session: CastSession, p1: Int) {
Avant de donner une commande sur la reproduction, nous devons décider du délégué principal. Pour ce faire, ils sont ajoutés par ordre de priorité au joueur, et chacun d'eux peut donner son état de préparation dans la méthode readyForLeading (). L'exemple de code complet peut être vu sur GitHub .
Y a-t-il de la vie après ChromeCast

Après avoir intégré le support Chromecast dans l'application, j'ai été plus heureux de rentrer à la maison et de profiter des livres audio non seulement via les écouteurs, mais aussi via Google Home. Quant à l'architecture, l'implémentation des acteurs dans différentes applications peut varier, donc cette approche ne sera pas appropriée partout. Mais pour notre architecture, c'est venu. J'espère que cet article vous aura été utile, et dans un futur proche il y aura plus d'applications pouvant s'intégrer à l'environnement numérique!