Cast Connect y la app de ATV

googlecastnew500.png

En este codelab, aprenderás a modificar una app de Android TV existente para que las apps emisoras compatibles con Cast puedan transmitir contenido y comunicarse con la app de Android TV.

¿Qué son Google Cast y Cast Connect?

Google Cast permite a los usuarios transmitir contenido desde un dispositivo móvil a una TV. Una sesión típica de Google Cast consta de dos componentes: una aplicación emisora y una receptora. Las aplicaciones emisoras, como una app para dispositivos móviles o un sitio web tal como YouTube.com, inician y controlan la reproducción de una aplicación receptora compatible con Cast. Estas últimas son apps HTML 5 que se ejecutan en dispositivos Chromecast y Android TV.

Casi todo el estado de una sesión de Cast se almacena en la aplicación receptora. Cuando el estado se actualiza (por ejemplo, si se carga un nuevo elemento multimedia), se transmite un estado de contenido multimedia a todas las emisoras. Estas transmisiones contienen el estado actual de la sesión de Cast. Las aplicaciones emisoras usan este estado de contenido multimedia a fin de mostrar la información de reproducción en su IU.

Cast Connect compilará sobre esta infraestructura, y tu app de Android TV actuará como receptora. La biblioteca de Cast Connect permite que la app de Android TV reciba mensajes y transmita el estado del contenido multimedia como si fuera una aplicación receptora de transmisión.

¿Qué compilaremos?

Cuando completes este codelab, podrás usar las apps emisoras de Cast para transmitir videos a una app de Android TV. Esta app también puede comunicarse con las apps emisoras mediante el protocolo de Cast.

Qué aprenderás

  • Cómo agregar la biblioteca de Cast Connect a una app de ATV de muestra
  • Cómo conectar una emisora de Cast e iniciar la app de ATV
  • Cómo iniciar la reproducción de contenido multimedia en la app de ATV desde una app emisora de Cast
  • Cómo enviar el estado del contenido multimedia desde la app de ATV a las apps emisoras de Cast

Requisitos

Puedes descargar el código de muestra completo a tu computadora…

Descargar código fuente

y descomprimir el archivo ZIP que se descargó.

Primero, veamos el aspecto de la app de muestra completa. La app de Android TV usa la IU de Leanback y un reproductor de video básico. El usuario puede seleccionar un video de una lista y, a continuación, reproducirlo en la TV. Con la app emisora para dispositivos móviles complementaria, un usuario también podrá transmitir un video a la app de Android TV.

f9f98aa234e84bae.png

Cómo registrar los dispositivos de desarrollador

A fin de habilitar las capacidades de Cast Connect para el desarrollo de aplicaciones, debes registrar el número de serie del Chromecast integrado en el dispositivo Android TV que usarás en la Play Console de Cast. Puedes encontrar el número de serie en Settings > Device Preferences > Chromecast built-in > Serial number, en tu Android TV. Ten en cuenta que este número de serie es diferente del correspondiente al dispositivo físico, y debe obtenerse del método descrito anteriormente.

f74dfe19bc459b76.png

Por razones de seguridad, sin el registro, Cast Connect solo funcionará para apps instaladas desde Google Play Store. Reinicia el dispositivo después de 15 minutos desde que se inició el proceso de registro.

Cómo instalar la app emisora de Android

A fin de probar el envío de solicitudes desde un dispositivo móvil, proporcionamos una aplicación emisora sencilla llamada Cast Videos. Aprovecharemos ADB para instalar el APK. Si ya instalaste una versión distinta de Cast Videos, antes de continuar, desinstala esa versión de los perfiles del dispositivo.

  1. Habilita las opciones para desarrolladores y la depuración por USB en tu teléfono Android.
  2. Conecta un cable de datos USB de modo que conectes tu teléfono Android a tu computadora de desarrollo.
  3. Instala mobile-sender.apk en tu teléfono Android.

93e35a0f0332f290.png

  1. Podrás encontrar la app emisora Cast Videos en tu teléfono Android. e29d89df484d9661.png

d6a0435ec3bac0af.png

Cómo instalar la app de Android TV

Las siguientes instrucciones describen cómo abrir y ejecutar la app de muestra completa en Android Studio:

  1. Selecciona Import Project en la pantalla de bienvenida o las opciones del menú File > New > Import Project….
  2. Selecciona el directorio android_studio_folder.pngapp-done en la carpeta del código de muestra y haz clic en Aceptar.
  3. Haz clic en File > 1791b5212a8947d.png Sync Project with Gradle Files.
  4. Habilita las opciones para desarrolladores y la depuración por USB en tu dispositivo Android TV.
  5. ADB se conectará con tu dispositivo Android TV, y este último debería aparecer en Android Studio. 7bcf00bfb6877ad5.png
  6. Haz clic en el botón execute.pngRun. La app de ATV llamada Cast Connect Codelab aparecerá después de unos segundos.

Reproduzcamos Cast Connect con la app de ATV

  1. Ve a la pantalla principal de Android TV.
  2. Abre la app emisora Cast Videos en tu teléfono Android. Haz clic en el botón para transmitir f77992b2cf0422a2.png y selecciona tu dispositivo ATV.
  3. La app Cast Connect Codelab de ATV se iniciará en tu ATV, y el botón para transmitir en la emisora indicará que está conectada 303287388679d79b.png.
  4. Selecciona un video de la app de ATV. Este comenzará a reproducirse en tu ATV.
  5. En tu teléfono celular, ahora aparecerá un minicontrolador en la parte inferior de la app emisora. Puedes usar el botón de reproducción/pausa a los efectos de controlar la reproducción.
  6. Selecciona un video del teléfono celular y presiona botón de reproducción. El video comenzará a reproducirse en tu ATV, y el control expandido se mostrará en el dispositivo móvil emisor.
  7. Bloquea el teléfono. Cuando lo desbloquees, deberías ver una notificación en la pantalla de bloqueo que permita controlar la reproducción de contenido multimedia o detener la transmisión.

a20257e816c913a.png

Ahora que verificamos la integración con Cast Connect de la app completa, debemos agregar compatibilidad con Cast Connect a la app de inicio que descargaste. Ahora está todo listo para compilar sobre el proyecto inicial usando Android Studio:

  1. Selecciona Import Project en la pantalla de bienvenida o las opciones del menú File > New > Import Project….
  2. Selecciona el directorio android_studio_folder.pngapp-start en la carpeta del código de muestra y haz clic en Aceptar.
  3. Haz clic en File > 1791b5212a8947d.png Sync Project with Gradle Files.
  4. Selecciona un dispositivo ATV y haz clic en el botón execute.pngRun a fin de ejecutar la app y explorar la IU. 7bcf00bfb6877ad5.png

f9f98aa234e84bae.png

Diseño de apps

La app proporciona una lista de videos que el usuario puede explorar. Los usuarios pueden seleccionar un video y reproducirlo en Android TV. La app consta de dos actividades principales: MainActivity y PlaybackActivity.

MainActivity

Esta actividad contiene un Fragmento (MainFragment). La configuración de la lista de videos y sus metadatos asociados se encuentran en la clase MovieList, y el método setupMovies() se llama para compilar una lista de objetos Movie.

Un objeto Movie representa una entidad de video con un título, una descripción, miniaturas de imágenes y la URL del video. Cada objeto Movie está vinculado a un CardPresenter a fin de presentar la miniatura del video con el título y estudio, y se pasa a ArrayObjectAdapter.

Cuando se seleccione un elemento, el objeto Movie correspondiente se pasará a la PlaybackActivity.

PlaybackActivity

Esta actividad contiene un Fragmento (PlaybackVideoFragment) que aloja una VideoView con ExoPlayer, algunos controles de contenido multimedia y un área de texto que muestra la descripción del video seleccionado y le permite al usuario reproducirlo en Android TV. El usuario puede utilizar el control remoto para reproducir, pausar o saltar la reproducción de los videos.

Requisitos previos de Cast Connect

Cast Connect usa versiones nuevas de los Servicios de Google Play que requieren que se actualice tu app de ATV a fin de usar el espacio de nombres AndroidX.

A los efectos de que tu app de Android TV sea compatible con Cast Connect, deberás crear y admitir eventos de una sesión multimedia. La biblioteca de Cast Connect generará el estado de contenido multimedia en función del estado de la sesión multimedia. La biblioteca de Cast Connect también usará esta sesión para indicar que recibió determinados mensajes de una emisora, como un evento de pausa.

Dependencias

Actualiza el archivo build.gradle de la app para incluir las dependencias de biblioteca necesarias:

dependencies {
    ....

    // Cast Connect libraries and dependencies
    implementation 'com.google.android.gms:play-services-cast-tv:17.0.0'
    implementation 'com.google.android.gms:play-services-cast:19.0.0'
}

Sincroniza el proyecto a fin de confirmar que se compile sin errores.

Inicialización

CastReceiverContext es un objeto singleton que coordina todas las interacciones de Cast. Debes implementar la interfaz de ReceiverOptionsProvider de modo que proporciones las CastReceiverOptions cuando se inicialice CastReceiverContext.

Crea el archivo CastReceiverOptionsProvider.java y agrega la siguiente clase al proyecto:

package com.google.sample.cast.castconnect;

import android.content.Context;
import com.google.android.gms.cast.tv.CastReceiverOptions;
import com.google.android.gms.cast.tv.ReceiverOptionsProvider;

public class CastReceiverOptionsProvider implements ReceiverOptionsProvider {
    @Override
    public CastReceiverOptions getOptions(Context context) {
        return new CastReceiverOptions.Builder(context)
                .setStatusText("Cast Connect Codelab")
                .build();
    }
}

Luego, especifica el proveedor de opciones de la receptora en la etiqueta <application> del archivo AndroidManifest.xml de la app:

<application>
  ...

  <meta-data
    android:name="com.google.android.gms.cast.tv.RECEIVER_OPTIONS_PROVIDER_CLASS_NAME"
    android:value="com.google.sample.cast.castconnect.CastReceiverOptionsProvider" />
</application>

Para conectarte con la app de ATV desde la emisora de Cast, selecciona la actividad que quieras iniciar. En este codelab, iniciaremos la MainActivity de la app cuando se inicie una sesión de Cast. En el archivo AndroidManifest.xml, agrega el filtro de intents de inicio en la MainActivity.

<activity android:name=".MainActivity">
  ...
  <intent-filter>
    <action android:name="com.google.android.gms.cast.tv.action.LAUNCH" />
    <category android:name="android.intent.category.DEFAULT" />
  </intent-filter>
</activity>

Ciclo de vida del contexto del receptor de transmisión

Debes iniciar el CastReceiverContext cuando se inicie tu app. Debes detener el CastReceiverContext cuando la app pase a segundo plano. Te recomendamos que uses el LifecycleObserver de la biblioteca de androidx.lifecycle a fin de administrar las llamadas a CastReceiverContext.start() y CastReceiverContext.stop().

Abre MyApplication.java, inicializa el contexto de transmisión mediante una llamada a initInstance() en el método onCreate de la aplicación. En la clase AppLifeCycleObserver, ejecuta start() en relación con el CastReceiverContext cuando se reanude la aplicación y stop() cuando se detenga:

package com.google.sample.cast.castconnect;

...

import com.google.android.gms.cast.tv.CastReceiverContext;

public class MyApplication extends Application {

    private static final String LOG_TAG = "MyApplication";

    @Override
    public void onCreate() {
        super.onCreate();
        CastReceiverContext.initInstance(this);
        ProcessLifecycleOwner.get().getLifecycle().addObserver(new AppLifecycleObserver());
    }

    public static class AppLifecycleObserver implements DefaultLifecycleObserver {
        @Override
        public void onResume(@NonNull LifecycleOwner owner) {
            Log.d(LOG_TAG, "onResume");
            CastReceiverContext.getInstance().start();
        }

        @Override
        public void onPause(@NonNull LifecycleOwner owner) {
            Log.d(LOG_TAG, "onPause");
            CastReceiverContext.getInstance().stop();
        }
    }
}

Cómo conectar MediaSession a MediaManager

MediaManager es una propiedad del singleton CastReceiverContext, que administra el estado de contenido multimedia, controla el intent de carga, convierte los mensajes del espacio de nombres del contenido multimedia de las emisoras en comandos multimedia y envía el estado de dicho contenido a las emisoras.

Cuando creas una MediaSession, también debes proporcionar el token de MediaSession actual a MediaManager de manera que sepa a dónde enviar los comandos y recuperar el estado de reproducción del contenido multimedia. Asegúrate de que se inicialice MediaSession antes de establecer el token en MediaManager.

import com.google.android.gms.cast.tv.CastReceiverContext;
...

public class PlaybackVideoFragment extends VideoSupportFragment {

    private CastReceiverContext castReceiverContext;
    ...

    private void initializePlayer() {
        if (mPlayer == null) {
            ...
            mMediaSession = new MediaSessionCompat(getContext(), LOG_TAG);
            ...

            castReceiverContext = CastReceiverContext.getInstance();
            if (castReceiverContext != null) {
                MediaManager mediaManager = castReceiverContext.getMediaManager();
                mediaManager.setSessionCompatToken(mMediaSession.getSessionToken());
            }
        }
    }
}

Cuando liberes tu MediaSession por tener una reproducción inactiva, deberás establecer un token nulo en MediaManager:

private void releasePlayer() {
    if (mMediaSession != null) {
        mMediaSession.release();
    }
    if (castReceiverContext != null) {
        MediaManager mediaManager = castReceiverContext.getMediaManager();
        mediaManager.setSessionCompatToken(null);
    }
    ...
}

Ejecutemos la app de muestra

Haz clic en el botón execute.pngRun para implementar la app en tu dispositivo ATV, cierra la app y vuelve a la pantalla principal de ATV. En la emisora, haz clic en el botón para transmitir f77992b2cf0422a2.png y selecciona tu dispositivo ATV. Verás que la app de ATV se inició en el dispositivo ATV y que el estado del botón para transmitir es el de conectado.

El comando de carga se enviará mediante un intent con el nombre de paquete que definiste en la Play Console. Deberás agregar el siguiente filtro de intents predefinido en tu app de Android TV a los efectos de especificar la actividad de destino que recibirá este intent. En el archivo AndroidManifest.xml, agrega el filtro de intents de carga a PlayerActivity:

<activity android:name="com.google.sample.cast.castconnect.PlaybackActivity"
          android:launchMode="singleTask">
  <intent-filter>
     <action android:name="com.google.android.gms.cast.tv.action.LOAD"/>
     <category android:name="android.intent.category.DEFAULT" />
  </intent-filter>
</activity>

Cómo administrar las solicitudes de carga en Android TV

Ahora que la actividad está configurada para recibir este intent que contiene una solicitud de carga, necesitaremos administrarla.

La app llamará a un método privado llamado processIntent cuando se inicie la actividad. Este método contiene la lógica para procesar intents entrantes. A fin de controlar una solicitud de carga, modificaremos este método y enviaremos el intent para que se procese llamando al método onNewIntent de la instancia de MediaManager. Si MediaManager detecta que el intent es una solicitud de carga, extraerá el objeto MediaLoadRequestData del intent e invocará a MediaLoadCommandCallback.onLoad(). Modifica el método processIntent en PlaybackVideoFragment a los efectos de controlar el intent que contiene la solicitud de carga:

public void processIntent(Intent intent) {
    MediaManager mediaManager = CastReceiverContext.getInstance().getMediaManager();
    // Pass intent to Cast SDK
    if (mediaManager.onNewIntent(intent)) {
        return;
    }

    // Clears all overrides in the modifier.
    mediaManager.getMediaStatusModifier().clear();

    // If the SDK doesn't recognize the intent, handle the intent with your own logic.
    ...
}

A continuación, extenderemos la clase abstracta MediaLoadCommandCallback, que anulará el método onLoad() que MediaManager llama. Este método recibirá los datos de la solicitud de carga y los convertirá en un objeto Movie. Una vez convertida, la película se reproducirá localmente. Luego, el MediaManager se actualizará con la MediaLoadRequest y transmitirá el MediaStatus a las emisoras conectadas. Crea una clase privada anidada llamada MyMediaLoadCommandCallback en PlaybackVideoFragment:

private class MyMediaLoadCommandCallback extends MediaLoadCommandCallback {
    @Override
    public Task<MediaLoadRequestData> onLoad(String senderId, MediaLoadRequestData mediaLoadRequestData) {
        Toast.makeText(getActivity(), "onLoad()", Toast.LENGTH_SHORT).show();

        if (mediaLoadRequestData == null) {
            // Throw MediaException to indicate load failure.
            return Tasks.forException(new MediaException(
                    new MediaError.Builder()
                            .setDetailedErrorCode(MediaError.DetailedErrorCode.LOAD_FAILED)
                            .setReason(MediaError.ERROR_REASON_INVALID_REQUEST)
                            .build()));
        }

        return Tasks.call(() -> {
            play(convertLoadRequestToMovie(mediaLoadRequestData));

            // Update media metadata and state
            MediaManager mediaManager = castReceiverContext.getMediaManager();
            mediaManager.setDataFromLoad(mediaLoadRequestData);
            mediaManager.broadcastMediaStatus();

            // Return the resolved MediaLoadRequestData to indicate load success.
            return mediaLoadRequestData;
        });
    }
}

private Movie convertLoadRequestToMovie(MediaLoadRequestData mediaLoadRequestData) {
    if (mediaLoadRequestData == null) {
        return null;
    }
    MediaInfo mediaInfo = mediaLoadRequestData.getMediaInfo();
    if (mediaInfo == null) {
        return null;
    }

    String videoUrl = mediaInfo.getContentId();
    if (mediaInfo.getContentUrl() != null) {
        videoUrl = mediaInfo.getContentUrl();
    }

    MediaMetadata metadata = mediaInfo.getMetadata();
    Movie movie = new Movie();
    movie.setVideoUrl(videoUrl);
    if (metadata != null) {
        movie.setTitle(metadata.getString(MediaMetadata.KEY_TITLE));
        movie.setDescription(metadata.getString(MediaMetadata.KEY_SUBTITLE));
        movie.setCardImageUrl(metadata.getImages().get(0).getUrl().toString());
    }
    return movie;
}

Ahora que se definió la devolución de llamada, deberemos registrarla en el MediaManager. Se deberá realizar este registro antes de llamar a MediaManager.onNewIntent(). Agrega setMediaLoadCommandCallback cuando se inicialice el reproductor:

private void initializePlayer() {
    if (mPlayer == null) {
        ...
        mMediaSession = new MediaSessionCompat(getContext(), LOG_TAG);
        ...

        castReceiverContext = CastReceiverContext.getInstance();
        if (castReceiverContext != null) {
            MediaManager mediaManager = castReceiverContext.getMediaManager();
            mediaManager.setSessionCompatToken(mMediaSession.getSessionToken());
            mediaManager.setMediaLoadCommandCallback(new MyMediaLoadCommandCallback());
        }
    }
}

Ejecutemos la app de muestra

Haz clic en el botón execute.pngRun a fin de implementar la app en tu dispositivo ATV. En la emisora, haz clic en el botón para transmitir f77992b2cf0422a2.png y selecciona tu dispositivo ATV. La app de ATV se iniciará en el dispositivo ATV. Selecciona un video en el dispositivo móvil. Este comenzará a reproducirse en ATV. Comprueba si recibes una notificación en el teléfono en la que tengas controles de reproducción. Usa los controles, por ejemplo, la pausa: el video deberá pausarse en el dispositivo ATV.

La aplicación actual ahora admite comandos básicos compatibles con una sesión multimedia, como reproducir, pausar y saltar. Sin embargo, hay algunos comandos de control de Cast que no están disponibles en la sesión multimedia. Debes registrar una MediaCommandCallback para admitir esos comandos.

Agrega MyMediaCommandCallback a la instancia de MediaManager con setMediaCommandCallback cuando se inicialice el reproductor:

private void initializePlayer() {
        ...

        castReceiverContext = CastReceiverContext.getInstance();
        if (castReceiverContext != null) {
            MediaManager mediaManager = castReceiverContext.getMediaManager();
            ...
            mediaManager.setMediaCommandCallback(new MyMediaCommandCallback());
        }
}

Crea la clase MyMediaCommandCallback de modo que se anulen los métodos, como onQueueUpdate() a fin de admitir esos comandos de control de Cast:

private class MyMediaCommandCallback extends MediaCommandCallback {
        @Override
        public Task<Void> onQueueUpdate(String senderId, QueueUpdateRequestData queueUpdateRequestData) {
            Toast.makeText(getActivity(), "onQueueUpdate()", Toast.LENGTH_SHORT).show();

            // Queue Prev / Next
            if (queueUpdateRequestData.getJump() != null) {
                Toast.makeText(getActivity(),
                        "onQueueUpdate(): Jump = " + queueUpdateRequestData.getJump(),
                        Toast.LENGTH_SHORT).show();
            }

            return super.onQueueUpdate(senderId, queueUpdateRequestData);
        }
}

Cómo modificar el estado del contenido multimedia

Cast Connect obtiene el estado básico del contenido de la sesión multimedia. Para admitir funciones avanzadas, tu app de Android TV puede especificar y anular propiedades de estado adicionales mediante un MediaStatusModifier. MediaStatusModifier siempre funcionará en la MediaSession que configuraste en CastReceiverContext.

Por ejemplo, si quieres especificar setMediaCommandSupported cuando se active la devolución de llamada onLoad, incluye lo siguiente:

private class MyMediaLoadCommandCallback extends MediaLoadCommandCallback {
    @Override
    public Task<MediaLoadRequestData> onLoad(String senderId, MediaLoadRequestData mediaLoadRequestData) {
        Toast.makeText(getActivity(), "onLoad()", Toast.LENGTH_SHORT).show();

        ...

        return Tasks.call(() -> {
            play(convertLoadRequestToMovie(mediaLoadRequestData));

            // Update media metadata and state
            MediaManager mediaManager = castReceiverContext.getMediaManager();
            mediaManager.setDataFromLoad(mediaLoadRequestData);

            // Use MediaStatusModifier to provide additional information for Cast senders.
            mediaManager.getMediaStatusModifier()
                        .setMediaCommandSupported(MediaStatus.COMMAND_QUEUE_NEXT, true)
                        .setIsPlayingAd(false);

            mediaManager.broadcastMediaStatus();

            // Return the resolved MediaLoadRequestData to indicate load success.
            return mediaLoadRequestData;
        });
    }
}

Cómo interceptar el MediaStatus antes de su envío

Al igual que con el MessageInterceptor del SDK del receptor web, puedes especificar un MediaStatusWriter en tu MediaManager a fin de realizar modificaciones adicionales en tu MediaStatus antes de que se transmita a las emisoras conectadas.

Por ejemplo, puedes configurar datos personalizados en el MediaStatus antes de enviarlos a emisoras móviles:

MediaManager mediaManager = castReceiverContext.getMediaManager();
...

// Use MediaStatusInterceptor to process the MediaStatus before sending out.
mediaManager.setMediaStatusInterceptor(mediaStatusWriter -> {
    try {
        mediaStatusWriter.setCustomData(new JSONObject("{myData: 'CustomData'}"));
    } catch (JSONException e) {
        e.printStackTrace();
    }
});

Ahora sabes cómo habilitar la transmisión de contenido en una app de Android TV con la biblioteca de Cast Connect.

Si deseas obtener información detallada, consulta la guía para desarrolladores: https://developers.google.com/cast/docs/android_tv_receiver