Cet article fait partie de ceux dont j’ai écrit l’original sur Medium.

Dans cet article, je vais faire une brève histoire du produit Chromecast et après, j’expliquerai de manière simple (mais détaillée) comment rendre une application Android qui n’est pas une application “media” (player video, streaming musical, …) compatible avec Chromecast, et plus globalement Cast ready (TV Cast, Box Cast,…).

🕰 Un peu d’histoire

🔙 De retour en 2013

La première Chromecast est sortie en 2013 aux États-Unis (en 2014 en France).

C’était il y a 5 ans (au moment où j’écris ces lignes) et pourtant on dirait que ça en fait 10.

D’ailleurs, elle ressemblait à ça

Chromecast v2

Dès le départ, Google a développé la technologie de la Chromecast pour être ouverte aux apps externes.

Comme à leur habitude, ils fournissent même un SDK pour développer dessus. 👍

Et pourtant, depuis, je trouve que ça n’a pas beaucoup bougé, pas été très utilisé.

Sérieux! Je veux dire, on peut techniquement balancer n’importe quoi sur notre télé maintenant ! Et pourtant … relativement peu d’apps sont compatibles.😢

Ce n’est pas si déconnant pour une app de tous les jours, mais des jeux, des quizzs etc ? Et pourtant les rares apps compatibles sont beaucoup d’apps de streaming (logique !!) et quelques jeux.

Pour les curieux, une liste d’apps compatibles est disponible sur le site store de google et sur celui de présentation de Chromecast.

Bref, tout ça, Google l’a bien compris et les dernières versions du SDK sont très optimisées en ce sens (streaming multimédia: vidéo, musique , photo).

⚙️ Comment ça marche en fait tout ça ?

Le point important dans la technologie Google cast, c’est que, généralement, les données ne sont pas envoyées de votre smartphone ou navigateur vers la Chromecast (ou l’appareil Cast) Elles passent par un site web. 🤯

Wait What !? Allez un petit schéma pour illustrer tout ça ⤵️

La technologie cast schématisée

Mais qu’est-ce que ça veut dire ?

Déjà, quand vous castez, vous le faites depuis ce qu’on va appeler une Sender app 📱(app mobile, player web,…).

Généralement, ça se passe en cliquant sur une icône.

Elle est d’ailleurs dispo sur https://developers.google.com/cast/docs/developers#icons

l'icône cast

En réalité, en faisant ça, vous demandez à votre appareil Cast d’afficher un site web. Enfin, une app web (on est 2.0 t’as vu).

Cette app web c’est la Receiver app 📺. Et ça, bah ça implique que cette receiver app, elle soit hébergée sur le net et accessible 🌐! (Comme n’importe quel autre site en fait, sinon l’appareil peut pas l’afficher ).

Pour finir, une fois que tout ça est chargé des 2 côtés, les deux applications communiquent via… on va dire une sorte de sockets.

Si c’est plus clair tant mieux, sinon accrochez-vous parce qu’on va rentrer dans la pratique 🥳 (Perso, je comprends toujours mieux avec un exemple concret).

🗂 Notre application à nous !

Les différentes étapes pour créer une application compatible Cast sont les suivantes.

Les 2 premières étapes ne sont à faire qu’une fois (sauf pour le point 2 si vous voulez tester sur un autre appareil).

Ensuite, pour chaque nouvelle app il faudra:

Allez, perdons pas d’temps, on y va (si vous avez déjà un compte développeur Cast, vous pouvez sauter ces 2 étapes).

👨‍💻 Créer un compte développeur Cast

Avant toute chose, vous aurez besoin:

Pour pouvoir développer sur Chromecast, rendez-vous sur la console pour enregistrer votre compte.

Comme dans la vie rien n’est gratuit , au moment où j’écris ces lignes, on nous demande de payer un one-shot fee de $5 💸.

Je vous laisse être guidé par Google ici, rien de bien compliqué et revenez quand c’est fait.

Moi j’bouge pas d’ici.

Et TADA! Vous avez maintenant accès à cette superbe (et je pèse mes mots) console de dev Cast.

console google cast

Elle est pas belle notre console?

Bon, on se moque un peu 🙄 mais de toute façon, pour être franc, cette console, elle sert pas à grand chose (à l’inverse de celle du Play Store pour ceux qui connaissent).

On voit rapidement qu’il n’y a que 2 catégories:

Pas besoin d’un doctorat pour comprendre que tout ce qu’on va faire ici c’est associer des devices et déclarer des apps. D’ailleurs…

🔗 Associer votre Cast device dans votre compte de dev

En parlant de ça, associons notre premier appareil à la console.

Cliquez sur le bouton ADD NEW DEVICE.

Une modale va apparaître et vous demander des informations sur votre appareil (Chromecast dans notre cas).

Modale d'ajout de cast receiver

Le serial number se trouve sur votre Chromecast (ou si box/télé je ne sais où) ! Pratique.

Si comme moi, l’appareil est déjà branché, ou que votre télé/box est l’appareil… vous commencez à paniquer à l’idée de devoir tout débrancher 🥺

Mais ne soyons pas mauvaises langues, Google a prévu le coup 🧠(ils sont bons quad même)

Je vous le résume en 2 secondes:

  1. Avec un navigateur compatible avec l’option Cast (genre Chrome, FireFox, etc)
  2. Rendez-vous sur la console cast (si vous n’y êtes pas déjà)
  3. Allumez votre appareil qui cast (télé, chromecast, etc)
  4. Castez le site (oui, oui, castez la console! – pas le blog !)

Et hop, le numéro de série devrait apparaître _(et être dicté). _

Je vous laisse donc remplir le formulaire, ça peut prendre une 15aine de minutes pour s’activer.

La description c’est pour vous, ça se retient mieux qu’un numéro de série.

Le temps d’une pause⏳, d’un café ☕️, ce que vous voulez… avant la suite 😋

📝 Déclarer une application

Bon, maintenant que l’appareil est prêt, on passe aux choses sérieuses.

Le première chose à savoir, c’est que, comme je l’expliquais plus tôt, vous aurez besoin ici d’un hébergement web (publique) !

Bah oui, votre receiver app, c’est une app web, donc faut l’héberger quelque part pour que le device y accède ! Donc commencez par obtenir une *url accessible publiquement *où vous allez servir votre receiver.

Cela étant fait, allons y, cliquez sur ADD NEW APPLICATION.

Première action, premier choix.

Comme expliqué au début de l’article, on le fait pour une app qui ne fera pas de streaming (sous-entendu pas de media).

Ca donne une indication sur ce qu’on va choisir ici…

… 🥁🥁🥁🥁 Custom Receiver yes !

Créer un custom receiver

Ha, je n’vous avez pas mytho ?! On doit remplir un Name, bon, ok, rentrez ce que vous voulez Mais Receiver Application URL c’est là qu’on rentre notre fameuse url.

Même si on n’a encore, ni écrit, ni déployé la receiver app, mettez le hosting web qui vous servira par la suite. On SAVE tout ça et on note l’Application ID. On va en avoir besoin.

Une fois de retour à la console, on va tout de suite Editer notre app (en fin de ligne)

On va retrouver 3 grandes parties BASICS, SENDER DETAILS et LISTING DETAILS

BASICS

C’est ce qu’on vient de faire, nom, url, type. That’s it (en même temps c’est basics quoi)

SENDER DETAILS

Ca commence à devenir intéressant. Ici, on rentre les informations concernant la sender app. Cette app peut être iOS, Android et/ou Web. En fonction de ce que vous choisissez, vous ne devrez pas rentrer les mêmes infos.

LISTING DETAILS Et enfin, les listing details, l’idée est de gérer la catégorisation de l’application si vous voulez la référencer sur chromecast.com/apps

Remplissons les SENDER DETAILS.

Sender details

Comme on fait une app Android, on doit remplir le Package Name, classique.

A noter que par Package Name, Google entend applicationId. Généralement, c’est le même, mais si chez vous il diffère, ne vous trompez pas. (Ne mettez rien en Intent to Join URI)

Et on zappe la partie LISTING DETAILS. On sauvegarde ça et on peut quitter la console.

📺 Créer la receiver app

A ce moment-là, vous vous dites que vous en avez marre de faire du clic clic. C’est vrai qui! En cliquant sur l’article vous pensiez voir du code…

Naïfs 🙄 Patience petit scarabée, ça vient!

On va commencer simple, avec ce qu’on appelle le CAF SDK.

Comme expliqué par Google, voici ce que fait un receiver:

Fournit une interface pour afficher le contenu de l’application sur le téléviseur.

Gère les messages de la sender app pour contrôler le contenu distant.

Gère les messages personnalisés de la sender app qui sont spécifiques à l’application.

En gros, ça gère l’affichage sur la télé, les updates d’affichages et les communications, envoyées par la sender app, qui demandent une update (par exemple en streaming musical: play, pause, etc).

Et pour faire ça, on a juste à créer un fichier index.html, voilà le nôtre (avec commentaires)

<!doctype html>
<html>
  <head>
    <title>Estimate It</title>
    <link href='https://fonts.googleapis.com/css?family=Roboto' rel='stylesheet' type='text/css'>

    <style>
    body {
      background: white;
      color: black;
    }

    #container {
      width: 100vw;
      height: 100vh;
      display: flex;
      justify-content: center;
      align-items: center;
    }

    #content {
      font-family: 'Roboto', sans-serif;
      font-size: 6em;
    }
    </style>
  </head>
  <body>

    <!-- 
        C'est dans ce div qu'on va faire apparaître notre contenu ! 🧙
    -->
    <div id="container">
      <span id="content">Waiting...</span>
    </div>

    <!-- 
        Ca, c'est le fameux CAF SDK dont je parlais plus haut, c'est tout la logique de Google Cast en 1 ligne.
    -->
    <script src="//www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js"></script>
    
    <!--
        Et là, c'est notre receiver
    -->
    <script type="text/javascript">
        window.onload = function () {
          
          /*
             Le CastReceiverContext. 
             En gros c'est notre point de liaison avec le CAF Framework, c'est à lui qu'on demande de bosser
             Il gère les sessions, les app senders, l'envoie et réception de messages, et les event globaux. 
          */
          var ctx = cast.framework.CastReceiverContext.getInstance();
          /*
              Ca c'est nos settings/options. (Les namespaces, timeouts etc)
              Pour en savoir plus: https://developers.google.com/cast/docs/reference/caf_receiver/cast.framework.CastReceiverOptions
          */
          var options = new cast.framework.CastReceiverOptions();
          /*
              Dans une app cast, le sender et le receiver communiquent, pour faire ça, ils passent par des canaux.
              Il peut y en avoir plusieurs et de types différents.
              Dans notre exemple, la sender app enverra des messages à la receiver app.
              Ca sera fait via un Channel dont l’identifiant est écrit ci-dessous. Le type de messages est fixé plus bas.
              Vous pouvez mettre ce que vous voulez en nom.
              NDA: Si vous n'avez qu'un seul channel, la pratique courante est d'utiliser l’applicationId comme nom.
              C'est ce que je fais ici
          */
          var CHANNEL = 'urn:x-cast:com.github.quentin7b.estimateit';

          /*
             Ici, on va définir les namespaces, notamment notre channel de communication qui sera en JSON
             Comme vous le voyez, c'est un tableau, on peut donc en avoir plus d'un.
             On ne le fera pas ici, mais pour plus d'info: https://developers.google.com/cast/docs/caf_receiver/core_features#custom_messages
          */
          options.customNamespaces = Object.assign({});
          options.customNamespaces[CHANNEL] = cast.framework.system.MessageType.JSON;

          /*
              Finalement, ici, on va écouter les messages.
              Depuis le context, on ajoute un custom listener sur le CHANNEL qu'on a créé et qui va lire nos messages.
              Le but ici, afficher le contenu du message dans la <div id="content"><div> plus haut
          */
          ctx.addCustomMessageListener(CHANNEL, function(customEvent) {
            var message = customEvent.data
            console.log("Message received from " + 
                        "[" +  customEvent.senderId +  "] " +
                        ": " + message);
            document.getElementById("content").innerHTML = message;
          });

          /*
              On démarre tout ça et hop
          */
          ctx.start(options);
        }
    </script>
  </body>
</html>

On déploie ce fichier sur notre hébergement web, à l’adresse qu’on a indiqué dans les BASICS de notre receiver app et on va pouvoir passer à la suite.

📱 Créer la sender app

Tout ce qui est écrit ci-dessous vient de la doc officielle … *ou pas ! * Au moment où j’écris ces lignes c’est toujours du appcompat (pas androidX) et du java (alors que toutes les docs de maintenant sont en kotlin).

Mais si jamais vous voulez y jeter un oeil 👁 https://developers.google.com/cast/docs/android_sender/integrate

Bref, allons-y. Ouvrez votre Android Studio 🛠 préféré, nouvelle app sans Activity, AndroidX, Kotlin, applicationID très important, mettez celui qu’on a déclaré plus haut.

Une fois les 30 minutes (☕️⌛️) de synchronisation Gradle terminées , on ouvre le build.gradle de notre application et on y ajoute les dépendances suivantes:

implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.mediarouter:mediarouter:1.1.0'
implementation 'com.google.android.gms:play-services-cast-framework:17.1.0'

Et c’est tout, votre build.gradle devrait donc ressembler à ça

apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.github.quentin7b.estimateit"
        minSdkVersion 21
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.mediarouter:mediarouter:1.1.0'
    implementation 'com.google.android.gms:play-services-cast-framework:17.1.0'
}

Et maintenant le code 👨‍💻

Une sender app sur Android, c’est principalement 2 choses

Commençons par l’OptionProvider, créons une classe CastOptionsProvider qui hérite d’OptionProvider

package com.github.quentin7b.estimateit

import android.content.Context
import com.google.android.gms.cast.framework.CastOptions
import com.google.android.gms.cast.framework.OptionsProvider

class CastOptionsProvider : OptionsProvider {
  override fun getCastOptions(ctx: Context): CastOptions {
    return CastOptions
             .Builder()
             .setReceiverApplicationId(
               ctx.getString(R.string.receiver_id)
             )
             .build()
  }

  override fun getAdditionalSessionProviders(ctx: Context) = null
}

Dans la fonction getCastOptions on va renvoyer la “configuration” de l’app. Pour nous rien de particulier, juste son ID (qui est dans la console). Je l’ai mis dans le fichier strings.xml sous le nom receiver_id.

Pour ce qui est du getAdditionalSessionProviders, pas besoin ici, on renvoie null.

Une fois nos options faites, il faut préciser à notre application où les trouver. C’est très simple et ça se passe dans le manifeste.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.github.quentin7b.estimateit">

  <application
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name">

      <meta-data
        android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
        android:value="com.github.quentin7b.estimateit.CastOptionsProvider" />
  </application>

</manifest>

Et maintenant, on passe à l’étape finale. L’Activity !! 👏 (Avouez, vous n’en pouvez plus. Vous le savez, je le sais. Moi même, je craque en écrivant tout ça)

L’Activity va faire 3 choses:

Accrochez-vous, c’est le dernier et plus gros morceau. 🍰 L’avantage, c’est que c’est très linéaire à lire 📖

package com.github.quentin7b.estimateit

import android.os.Bundle
import android.util.Log
import android.view.Menu
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.MenuItemCompat
import androidx.mediarouter.app.MediaRouteActionProvider
import androidx.mediarouter.media.MediaControlIntent
import androidx.mediarouter.media.MediaRouteSelector
import androidx.mediarouter.media.MediaRouter
import com.google.android.gms.cast.CastMediaControlIntent
import com.google.android.gms.cast.framework.CastButtonFactory
import com.google.android.gms.cast.framework.CastContext
import com.google.android.gms.cast.framework.CastSession
import kotlinx.android.synthetic.main.activity_estimate.*
import java.util.*


class MainActivity : AppCompatActivity() {

    /**
     * Contient notre session actuelle avec l'appareil Cast
     */
    private var castSession: CastSession? = null
    /**
     * Le context, un peu comme sur le sdk permet d'initier les échanges.
     * Il permet d'obtenir une session par la suite
     */
    private val castContext by lazy { CastContext.getSharedInstance(baseContext) }
    /**
     * Notre namespace pour échanger des messages.
     * Sa valeur, comme dans le receiver est urn:x-cast:com.github.quentin7b.estimateit
     */
    private val nameSpace by lazy { baseContext.getString(R.string.namespace) }

    /**
     * Le selector est utilisé par le MediaRouter pour le controle du bouton
     */
    private var mediaSelector: MediaRouteSelector? = null

    /**
     * Le session listener permet d'initialiser notre castSession, il est automatiquement appelé par
     * le castContext quand il ouvre une session et nous permet de stocker sa ref.
     *
     * A la base, l'interface est `SessionManagerListener<CastSession>` mais pour plus de lisibilité,
     * j'ai fait une première impkémentation qui vire toutes les méthodes dont on ne se sert pas.
     * Pour en savoir plus: https://developers.google.com/android/reference/com/google/android/gms/cast/framework/SessionManagerListener
     */
    private val sessionManagerListener = object : EstimateSessionManagerListener {

        override fun onSessionStarted(pCastSession: CastSession?, p1: String?) =
                this@MainActivity.run {
                    castSession = pCastSession
                    invalidateOptionsMenu()
                }

        override fun onSessionResumed(pCastSession: CastSession?, wasSuspended: Boolean) =
                this@MainActivity.run {
                    castSession = pCastSession
                    invalidateOptionsMenu()
                }

        override fun onSessionEnded(pCastSession: CastSession, error: Int) =
                this@MainActivity.run {
                    if (pCastSession == castSession) {
                        cleanup()
                    }
                    invalidateOptionsMenu()
                }
    }

    /**
     * Cette callback ne sert à rien ici si ce n'est permettre la découverte des devices plus tard
     */
    private val mediaRouterCallback = object : MediaRouter.Callback() {
    }


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Log.i("MainActivity", "onCreate")

        /*
        un layout basique avec 1 gros btn au milieu
         */
        setContentView(R.layout.activity_estimate)

        /*
        Dans l'idée, quand on clique sur le btn, on va envoyer un message à la session active
        Pour cela, on spécifie le namespace et la valeur du message (ici un entier entre 0 et 5)
         */
        btn_estimate_doit.setOnClickListener {
            castSession?.sendMessage(nameSpace, Random().nextInt(5).toString())
        }

        // On créé le mediaSelector
        mediaSelector = MediaRouteSelector.Builder()
                // On ajoute les intents auxquels on va répondre, ici c'est uniquement systemr
                .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
                // On spécifie notre app id
                .addControlCategory(CastMediaControlIntent.categoryForCast(baseContext.getString(R.string.receiver_id)))
                .build()
    }

    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        super.onCreateOptionsMenu(menu)
        Log.i("MainActivity", "onCreateOptionsMenu")

        /*
        Ce menu contient le cast btn.
        <?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/media_route_menu_item"
                    android:title="@string/cast"
                    app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
                    app:showAsAction="always" />
            </menu>
         */
        menuInflater.inflate(R.menu.estimate, menu)
        /*
        Ici, on initialise le bouton pour que ça lance la session quand on clique dessus.
        Rien de fou, le SDK fait tout pour nous
         */
        val mediaRouteMenuItem = CastButtonFactory.setUpMediaRouteButton(baseContext, menu, R.id.media_route_menu_item)
        /*
         *On affecte ce menu à notre mediaSelector
         */
        val mediaRouteActionProvider =
                MenuItemCompat.getActionProvider(mediaRouteMenuItem) as MediaRouteActionProvider
        mediaSelector?.also(mediaRouteActionProvider::setRouteSelector)
        /*
        On filtre les devices pour ne faire apparaitre que les Chromecast (pas les Google Home par ex)
         */
        mediaRouteActionProvider.dialogFactory = ChromecastRouteDialogFactory()
        return true
    }

    override fun onStart() {
        /*
        On lance la découverte des appareils
         */
        mediaSelector?.also { selector ->
            MediaRouter.getInstance(baseContext)?.addCallback(selector, mediaRouterCallback,
                    MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY)
        }
        super.onStart()
    }

    override fun onResume() {
        super.onResume()
        Log.i("MainActivity", "onResume")

        /*
        Ici on demande au context de nous alerter (avec la callback) quand une CastSession est activée
        Ca nous permet de la stocker pour pouvoir jouer avec (envoyer des messages par exemple)
         */
        castContext.sessionManager.addSessionManagerListener(sessionManagerListener, CastSession::class.java)
        if (castSession == null) {
            /*
            Comme on résume l'histoire, c'est possible qu'il y avait une session en cours !
            Si tel est le cas, on tente de la réaffecter
             */
            castSession = castContext.sessionManager.currentCastSession
        }
    }

    override fun onPause() {
        Log.i("MainActivity", "onPause")

        /*
        Classique, on pause alors on se désabonne
         */
        castContext.sessionManager.removeSessionManagerListener(sessionManagerListener, CastSession::class.java)
        super.onPause()
    }

    override fun onStop() {
        cleanup()
        super.onStop()
    }

    private fun cleanup() {
        castSession = null
        MediaRouter.getInstance(baseContext)?.removeCallback(mediaRouterCallback)
    }

}

Lancez l’application, allumez votre télé, le bouton devrait apparaître ! Cliquez dessus, connectez-vous ! Et hop.

Sur mobile 📱

Sur Cast (TV) 📺

Voilà, j’espère que cette introduction / tuto sur la Cast technologie de Google vous a permis d’y voir plus clair et été utile. 😸 Perso je continue d’y croire et de me dire qu’elle est largement sous-exploitée aujourd’hui … Affaire à suivre donc ! 🚀

👻 Bonus

Il est possible de débuguer une application receiver. Si vous avez Chrome, rendez-vous sur chrome://inspect et vous y verrez votre chromecast, que vous pourrez débuguer !🎉

💚 Liens utiles

Bon, ça peut surprendre, mais je n’ai pas découvert tout ça en explorant le SDK. Alors voici la liste des liens que j’ai utilisé pour mon article !

Et les images

Et enfin, au cas où ça vous intéresse, le dépôt est disponible sur Github

https://github.com/quentin7b/android-estimateit