Cómo habilitar la transmisión de contenido en una app para Android

1. Descripción general

Logotipo de Google Cast

En este codelab, aprenderás a modificar una app de video para Android existente a fin de transmitir contenido en un dispositivo compatible con Google Cast.

¿Qué es Google Cast?

Google Cast permite a los usuarios transmitir contenido desde un dispositivo móvil a una TV. De esa manera, los usuarios pueden usar su dispositivo móvil como control remoto de modo que se reproduzca contenido multimedia en la TV.

El SDK de Google Cast posibilita que extiendas tu app para controlar una TV o un sistema de sonido. Este SDK te permite agregar los componentes de IU necesarios según la lista de tareas de diseño de Google Cast.

Te proporcionamos la lista de tareas de diseño de Google Cast con el fin de que la experiencia del usuario de Cast resulte sencilla y predecible en todas las plataformas compatibles.

¿Qué compilaremos?

Cuando completes este codelab, tendrás una app de video de Android que podrá transmitir videos a un dispositivo compatible con Google Cast.

Qué aprenderás

  • Cómo agregar el SDK de Google Cast a una app de video de muestra
  • Cómo agregar el botón para transmitir a fin de seleccionar un dispositivo Google Cast
  • Cómo conectarse a un dispositivo de transmisión e iniciar un receptor de contenido multimedia
  • Cómo transmitir un video
  • Cómo agregar un minicontrolador de transmisión a tu app
  • Cómo admitir las notificaciones de contenido multimedia y los controles de pantalla de bloqueo
  • Cómo agregar un control expandido
  • Cómo proporcionar una superposición introductoria
  • Cómo personalizar los widgets de Cast
  • Cómo realizar la integración con Cast Connect

Requisitos

  • El SDK de Android más reciente
  • Android Studio versión 3.2 o superior
  • Un dispositivo móvil con Android 4.1 Jelly Bean (nivel de API 16) o una versión posterior
  • Un cable de datos USB para conectar tu dispositivo móvil a la computadora de desarrollo
  • Un dispositivo Google Cast, como Chromecast o Android TV, que esté configurado con acceso a Internet
  • Una TV o un monitor con entrada HDMI
  • Se requiere un Chromecast con Google TV para probar la integración de Cast Connect, pero es opcional para el resto del codelab. Si no tienes una, puedes omitir el paso Agregar compatibilidad con Cast Connect al final de este instructivo.

Experiencia

  • Deberás tener conocimientos previos sobre desarrollo de Kotlin y Android.
  • También deberás tener experiencia como usuario de TV :)

¿Cómo usarás este instructivo?

Ler Leer y completar los ejercicios

¿Cómo calificarías tu experiencia cuando compilas apps para Android?

Principiante Intermedio Avanzado

¿Cómo calificarías tu experiencia cuando miras TV?

Principiante Intermedio Avanzado

2. Obtén el código de muestra

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

y descomprimir el archivo ZIP que se descargó.

3. Ejecuta la app de muestra

ícono de un par de brújulas

Primero, veamos el aspecto de la app de muestra completa. La app es un reproductor de video básico. El usuario podrá seleccionar un video de una lista y, a continuación, reproducirlo en un dispositivo local o transmitirlo a uno compatible con Google Cast.

Una vez que hayas descargado el código, podrás seguir estas instrucciones para abrir y ejecutar la app de ejemplo completa en Android Studio:

Selecciona Import Project en la pantalla de bienvenida o las opciones del menú File > New > Import Project....

Selecciona el directorio ícono de carpetaapp-done de la carpeta del código de muestra y haz clic en OK.

Haz clic en File > Botón "Sync Project with Gradle" de Android Studio Sync Project with Gradle Files.

Habilita la depuración por USB en tu dispositivo Android. En Android 4.2 y versiones posteriores, la pantalla de Opciones para desarrolladores está oculta de forma predeterminada. Para poder visualizarla, dirígete a Configuración > Acerca del dispositivo y presiona Número de compilación siete veces. Regresa a la pantalla anterior, ve a Sistema > Avanzado y presiona Opciones para desarrolladores cerca de la parte inferior. Luego, presiona Depuración por USB para activarla.

Conecta tu dispositivo Android y haz clic en el botón Botón Run de Android Studio, un triángulo verde que apunta a la derechaRun en Android Studio. Después de unos segundos, debería aparecer la app de video llamada Cast Videos.

Haz clic en el botón para transmitir de la app de video y selecciona tu dispositivo Google Cast.

Selecciona un video y haz clic en el botón de reproducción.

El video comenzará a reproducirse en tu dispositivo Google Cast.

Se mostrará el control expandido. Puedes usar el botón de reproducción/pausa para controlar la reproducción.

Regresa a la lista de videos.

Ahora, verás un minicontrolador en la parte inferior de la pantalla. Ilustración de un teléfono Android que ejecuta la app "Transmitir videos" con el minicontrol en la parte inferior de la pantalla

Haz clic en el botón de pausa del minicontrolador a fin de pausar el video en el receptor. Haz clic en el botón de reproducción del minicontrolador para reanudar la reproducción del video.

Haz clic en el botón de inicio del dispositivo móvil. Desliza hacia abajo las notificaciones de modo que veas una notificación para la sesión de transmisión.

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.

Regresa a la app de video y haz clic en el botón para transmitir a fin de dejar de transmitir en el dispositivo Google Cast.

Preguntas frecuentes

4. Prepara el proyecto inicial

Ilustración de un teléfono Android que ejecuta la app "Cast Videos"

Debemos agregar compatibilidad con Google Cast a la app inicial que descargaste. A continuación, se detalla la terminología de Google Cast que usaremos en este codelab:

  • una app emisora se ejecuta en un dispositivo móvil o una laptop.
  • una app receptora se ejecuta en el dispositivo Google Cast.

Ahora está todo listo para compilar sobre el proyecto inicial usando Android Studio:

  1. Selecciona el directorio ícono de carpetaapp-start de la descarga del código de muestra (selecciona Import Project en la pantalla de bienvenida o la opción del menú File > New > Import Project...).
  2. Haz clic en el botón Botón "Sync Project with Gradle" de Android Studio Sync Project with Gradle Files.
  3. Haz clic en el botón Botón Run de Android Studio, un triángulo verde que apunta a la derechaRun para ejecutar la app y explorar la IU.

Diseño de apps

La app recuperará una lista de videos de un servidor web remoto y proporcionará una lista para que el usuario explore. Los usuarios podrán seleccionar un video de forma que vean los detalles o reproducirlo localmente en el dispositivo móvil.

La app consta de dos actividades principales: VideoBrowserActivity y LocalPlayerActivity. Para integrar la funcionalidad de Google Cast, las Actividades deben heredar de AppCompatActivity o de su elemento superior FragmentActivity. Esta limitación existe, ya que deberíamos agregar el MediaRouteButton (proporcionado en la biblioteca de compatibilidad de MediaRouter) como un MediaRouteActionProvider, lo que solo funcionará si hereda la actividad de las clases antes mencionadas. La biblioteca de compatibilidad de MediaRouter depende de la biblioteca de compatibilidad de AppCompat, que proporciona las clases necesarias.

VideoBrowserActivity

Esta actividad contiene un Fragment (VideoBrowserFragment). Esta lista está respaldada por un ArrayAdapter (VideoListAdapter). La lista de videos y los metadatos asociados se alojan en un servidor remoto como un archivo JSON. Un AsyncTaskLoader (VideoItemLoader) recupera este JSON y lo procesa para compilar una lista de objetos MediaItem.

Un objeto MediaItem modelará un video y los metadatos asociados, como el título, la descripción, la URL de la transmisión, la URL de las imágenes complementarias y las pistas de texto asociadas (para los subtítulos) si las hubiera. El objeto MediaItem se pasa entre actividades, por lo que MediaItem tiene métodos de utilidad para convertirlo en Bundle y viceversa.

Cuando el cargador compila la lista de MediaItems y la pasará al VideoListAdapter. Luego, presentará la lista de MediaItems en el VideoBrowserFragment. Se le mostrará al usuario una lista de miniaturas de video con una descripción breve de cada uno. Cuando se seleccione un elemento, el MediaItem correspondiente se convertirá en Bundle y se pasará a LocalPlayerActivity.

LocalPlayerActivity

Esta actividad muestra los metadatos de un video específico y permite que el usuario reproduzca el video de forma local en el dispositivo móvil.

La actividad aloja un VideoView, algunos controles multimedia y un área de texto para mostrar la descripción del video seleccionado. El reproductor cubre la parte superior de la pantalla y deja espacio para la descripción detallada del video que se encuentra a continuación. El usuario puede reproducir, pausar o saltar la reproducción local de los videos.

Dependencias

Como estamos usando AppCompatActivity, necesitamos la biblioteca de compatibilidad de AppCompat. Para administrar la lista de videos y obtener de forma asíncrona las imágenes de la lista, usamos la biblioteca de Volley.

Preguntas frecuentes

5. Agrega el botón para transmitir

Ilustración de la parte superior de un teléfono Android con la app de Cast Video en ejecución; el botón para transmitir aparece en la esquina superior derecha de la pantalla

Una app compatible con Cast muestra el botón para transmitir en cada una de sus actividades. Al hacer clic en ese botón, se mostrará la lista de dispositivos de transmisión que un usuario puede seleccionar. Si el usuario estaba reproduciendo contenido de forma local en el dispositivo emisor, al seleccionar un dispositivo de transmisión podrá iniciar o reanudar la reproducción en ese dispositivo. En cualquier momento de una sesión de transmisión, el usuario podrá hacer clic en el botón para transmitir y dejar de transmitir tu aplicación al dispositivo de transmisión. El usuario debe poder conectarse al dispositivo de transmisión o desconectarse de él mientras esté en cualquier actividad de tu aplicación, como se describe en la Lista de tareas de diseño de Google Cast.

Dependencias

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

dependencies {
    implementation 'androidx.appcompat:appcompat:1.5.0'
    implementation 'androidx.mediarouter:mediarouter:1.3.1'
    implementation 'androidx.recyclerview:recyclerview:1.2.1'
    implementation 'com.google.android.gms:play-services-cast-framework:21.1.0'
    implementation 'com.android.volley:volley:1.2.1'
    implementation "androidx.core:core-ktx:1.8.0"
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
}

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

Inicialización

El framework de Cast tiene un objeto singleton global, el CastContext, que coordina todas las interacciones de Cast.

Debes implementar la interfaz OptionsProvider para proporcionar las CastOptions necesarias para inicializar el singleton CastContext. La opción más importante es el ID de aplicación receptora, que se utiliza para filtrar los resultados de detección de dispositivos de transmisión y activar la aplicación receptora cuando se inicia una sesión de transmisión.

Cuando desarrolles tu propia app compatible con Cast, tendrás que registrarte como desarrollador de Cast y, luego, obtener el ID de aplicación correspondiente a tu app. Para este codelab, usaremos un ID de app de muestra.

Agrega el siguiente archivo CastOptionsProvider.kt nuevo al paquete com.google.sample.cast.refplayer del proyecto:

package com.google.sample.cast.refplayer

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

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

    override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? {
        return null
    }
}

Ahora, declara el OptionsProvider en la etiqueta "application" del archivo AndroidManifest.xml de la app:

<meta-data
    android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
    android:value="com.google.sample.cast.refplayer.CastOptionsProvider" />

Inicia el CastContext de manera diferida en el método onCreate VideoBrowserActivity:

import com.google.android.gms.cast.framework.CastContext

private var mCastContext: CastContext? = null

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.video_browser)
    setupActionBar()

    mCastContext = CastContext.getSharedInstance(this)
}

Agrega la misma lógica de inicialización a la LocalPlayerActivity.

Botón para transmitir

Ahora que se inicializó CastContext, debemos agregar el botón para transmitir a fin de permitir que el usuario seleccione un dispositivo de transmisión. El MediaRouteButton implementa el botón para transmitir desde la biblioteca de compatibilidad de MediaRouter. Al igual que con cualquier ícono de acción que puedes agregar a tu actividad (con una ActionBar o una Toolbar), primero debes agregar el elemento de menú correspondiente a tu menú.

Edita el archivo res/menu/browse.xml y agrega el elemento MediaRouteActionProvider en el menú antes del elemento de configuración:

<item
    android:id="@+id/media_route_menu_item"
    android:title="@string/media_route_menu_title"
    app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
    app:showAsAction="always"/>

Anula el método onCreateOptionsMenu() de VideoBrowserActivity con CastButtonFactory para conectar MediaRouteButton al framework de Cast:

import com.google.android.gms.cast.framework.CastButtonFactory

private var mediaRouteMenuItem: MenuItem? = null

override fun onCreateOptionsMenu(menu: Menu): Boolean {
     super.onCreateOptionsMenu(menu)
     menuInflater.inflate(R.menu.browse, menu)
     mediaRouteMenuItem = CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), menu,
                R.id.media_route_menu_item)
     return true
}

Anula onCreateOptionsMenu de LocalPlayerActivity de una manera similar.

Haz clic en el botón Botón Run de Android Studio, un triángulo verde que apunta a la derechaRun para ejecutar la app en tu dispositivo móvil. Deberías ver un botón para transmitir en la barra de acciones de la app. Cuando hagas clic en él, se mostrará una lista de los dispositivos de transmisión en tu red local. CastContext administra la detección de dispositivos automáticamente. Selecciona tu dispositivo de transmisión. La app receptora de muestra se cargará en él. Puedes navegar entre la actividad de navegación y la actividad del reproductor local. El estado del botón para transmitir se mantendrá sincronizado.

Aún no se incluyó ningún tipo de compatibilidad para reproducir contenido multimedia, por lo que aún no podrás reproducir videos en el dispositivo de transmisión. Haz clic en el botón para transmitir a fin de desconectarte.

6. Transmite contenido de video

Ilustración de un teléfono Android que ejecuta la app &quot;Cast Videos&quot;

Extenderemos la app de muestra de modo que también reproduzca videos de forma remota en un dispositivo de transmisión. Para ello, tenemos que escuchar los diferentes eventos generados por el framework de Cast.

Transmisión de contenido multimedia

A grandes rasgos, si deseas reproducir contenido multimedia en un dispositivo de transmisión, deberás realizar las siguientes acciones:

  1. Crea un objeto MediaInfo que modele un elemento multimedia.
  2. Conectarte al dispositivo de transmisión e iniciar la aplicación receptora
  3. Cargar el objeto MediaInfo en tu receptor y reproducir el contenido
  4. Realizar un seguimiento del estado del contenido multimedia
  5. Enviar comandos de reproducción al receptor según las interacciones del usuario

Ya realizamos el paso 2 en la sección anterior. Con el framework de Cast, el paso 3 resulta sencillo. El paso 1 equivale a mapear un objeto con otro. MediaInfo es algo que el framework de Cast interpreta, y MediaItem es el encapsulamiento de nuestra app para un elemento multimedia. Podemos mapear fácilmente un MediaItem con una MediaInfo.

La app de muestra LocalPlayerActivity ya distingue entre la reproducción local y la remota a través de esta enumeración:

private var mLocation: PlaybackLocation? = null

enum class PlaybackLocation {
    LOCAL, REMOTE
}

enum class PlaybackState {
    PLAYING, PAUSED, BUFFERING, IDLE
}

En este codelab, no es importante que comprendas exactamente cómo funciona toda la lógica del reproductor de muestra. Sí resulta importante que comprendas que el reproductor multimedia de tu app deberá modificarse para que tenga en cuenta las dos ubicaciones de reproducción de manera similar.

Por el momento, el reproductor local siempre estará en el estado de reproducción local porque aún no conoce los estados de transmisión. Deberemos actualizar la IU en función de las transiciones de estado que ocurran en el framework de Cast. Por ejemplo, si comenzamos a transmitir, deberemos detener la reproducción local e inhabilitar algunos controles. Análogamente, si dejamos de transmitir contenido cuando estemos en esta actividad, deberemos hacer la transición a la reproducción local. Para ello, tenemos que escuchar los diferentes eventos generados por el framework de Cast.

Administración de sesiones de transmisión

En el framework de Cast, una sesión de transmisión combina los pasos para conectarse a un dispositivo, iniciar (o unirse a) una aplicación receptora y conectarse a ella e inicializar un canal de control de contenido multimedia, si corresponde. Este canal es la forma en la que el framework de Cast envía y recibe mensajes del reproductor multimedia del receptor.

La sesión de transmisión se iniciará automáticamente cuando el usuario seleccione un dispositivo desde el botón para transmitir y se detendrá automáticamente cuando el usuario se desconecte. El SDK de Cast también administra automáticamente la reconexión a una sesión del receptor a causa de problemas de red.

Agreguemos un SessionManagerListener a la LocalPlayerActivity:

import com.google.android.gms.cast.framework.CastSession
import com.google.android.gms.cast.framework.SessionManagerListener
...

private var mSessionManagerListener: SessionManagerListener<CastSession>? = null
private var mCastSession: CastSession? = null
...

private fun setupCastListener() {
    mSessionManagerListener = object : SessionManagerListener<CastSession> {
        override fun onSessionEnded(session: CastSession, error: Int) {
            onApplicationDisconnected()
        }

        override fun onSessionResumed(session: CastSession, wasSuspended: Boolean) {
            onApplicationConnected(session)
        }

        override fun onSessionResumeFailed(session: CastSession, error: Int) {
            onApplicationDisconnected()
        }

        override fun onSessionStarted(session: CastSession, sessionId: String) {
            onApplicationConnected(session)
        }

        override fun onSessionStartFailed(session: CastSession, error: Int) {
            onApplicationDisconnected()
        }

        override fun onSessionStarting(session: CastSession) {}
        override fun onSessionEnding(session: CastSession) {}
        override fun onSessionResuming(session: CastSession, sessionId: String) {}
        override fun onSessionSuspended(session: CastSession, reason: Int) {}
        private fun onApplicationConnected(castSession: CastSession) {
            mCastSession = castSession
            if (null != mSelectedMedia) {
                if (mPlaybackState == PlaybackState.PLAYING) {
                    mVideoView!!.pause()
                    loadRemoteMedia(mSeekbar!!.progress, true)
                    return
                } else {
                    mPlaybackState = PlaybackState.IDLE
                    updatePlaybackLocation(PlaybackLocation.REMOTE)
                }
            }
            updatePlayButton(mPlaybackState)
            invalidateOptionsMenu()
        }

        private fun onApplicationDisconnected() {
            updatePlaybackLocation(PlaybackLocation.LOCAL)
            mPlaybackState = PlaybackState.IDLE
            mLocation = PlaybackLocation.LOCAL
            updatePlayButton(mPlaybackState)
            invalidateOptionsMenu()
       }
   }
}

En la actividad LocalPlayerActivity, nos interesa recibir un aviso cuando nos conectemos al dispositivo de transmisión o nos desconectemos de él, de manera que podamos alternar entre el reproductor local y otro. Ten en cuenta que no solo la instancia de tu aplicación que se ejecute en tu dispositivo móvil interrumpirá la conectividad, sino que también puede hacerlo otra instancia de tu aplicación (o bien otra) que se ejecute en un dispositivo móvil diferente.

Se puede acceder a la sesión activa en ese momento como SessionManager.getCurrentSession(). Las sesiones se crean y destruyen automáticamente en respuesta a las interacciones del usuario con los diálogos de Cast.

Deberemos registrar el objeto de escucha de nuestra sesión e inicializar algunas variables que usaremos en la actividad. Cambia el método onCreate de LocalPlayerActivity a:

import com.google.android.gms.cast.framework.CastContext
...

private var mCastContext: CastContext? = null
...

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    mCastContext = CastContext.getSharedInstance(this)
    mCastSession = mCastContext!!.sessionManager.currentCastSession
    setupCastListener()
    ...
    loadViews()
    ...
    val bundle = intent.extras
    if (bundle != null) {
        ....
        if (shouldStartPlayback) {
              ....

        } else {
            if (mCastSession != null && mCastSession!!.isConnected()) {
                updatePlaybackLocation(PlaybackLocation.REMOTE)
            } else {
                updatePlaybackLocation(PlaybackLocation.LOCAL)
            }
            mPlaybackState = PlaybackState.IDLE
            updatePlayButton(mPlaybackState)
        }
    }
    ...
}

Carga de contenido multimedia

En el SDK de Cast, el RemoteMediaClient proporciona un conjunto de APIs convenientes para administrar la reproducción de contenido multimedia remoto en la app receptora. Para una CastSession compatible con la reproducción de contenido multimedia, el SDK creará automáticamente una instancia de RemoteMediaClient. Se puede acceder a ella llamando al método getRemoteMediaClient() en la instancia CastSession. Agrega los siguientes métodos a LocalPlayerActivity para cargar el video que esté seleccionado en el receptor:

import com.google.android.gms.cast.framework.media.RemoteMediaClient
import com.google.android.gms.cast.MediaInfo
import com.google.android.gms.cast.MediaLoadOptions
import com.google.android.gms.cast.MediaMetadata
import com.google.android.gms.common.images.WebImage
import com.google.android.gms.cast.MediaLoadRequestData

private fun loadRemoteMedia(position: Int, autoPlay: Boolean) {
    if (mCastSession == null) {
        return
    }
    val remoteMediaClient = mCastSession!!.remoteMediaClient ?: return
    remoteMediaClient.load( MediaLoadRequestData.Builder()
                .setMediaInfo(buildMediaInfo())
                .setAutoplay(autoPlay)
                .setCurrentTime(position.toLong()).build())
}

private fun buildMediaInfo(): MediaInfo? {
    val movieMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE)
    mSelectedMedia?.studio?.let { movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, it) }
    mSelectedMedia?.title?.let { movieMetadata.putString(MediaMetadata.KEY_TITLE, it) }
    movieMetadata.addImage(WebImage(Uri.parse(mSelectedMedia!!.getImage(0))))
    movieMetadata.addImage(WebImage(Uri.parse(mSelectedMedia!!.getImage(1))))
    return mSelectedMedia!!.url?.let {
        MediaInfo.Builder(it)
            .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
            .setContentType("videos/mp4")
            .setMetadata(movieMetadata)
            .setStreamDuration((mSelectedMedia!!.duration * 1000).toLong())
            .build()
    }
}

A continuación, actualiza varios métodos existentes a fin de usar la lógica de la sesión de transmisión y admitir la reproducción remota:

private fun play(position: Int) {
    startControllersTimer()
    when (mLocation) {
        PlaybackLocation.LOCAL -> {
            mVideoView!!.seekTo(position)
            mVideoView!!.start()
        }
        PlaybackLocation.REMOTE -> {
            mPlaybackState = PlaybackState.BUFFERING
            updatePlayButton(mPlaybackState)
            //seek to a new position within the current media item's new position 
            //which is in milliseconds from the beginning of the stream
            mCastSession!!.remoteMediaClient?.seek(position.toLong())
        }
        else -> {}
    }
    restartTrickplayTimer()
}
private fun togglePlayback() {
    ...
    PlaybackState.IDLE -> when (mLocation) {
        ...
        PlaybackLocation.REMOTE -> {
            if (mCastSession != null && mCastSession!!.isConnected) {
                loadRemoteMedia(mSeekbar!!.progress, true)
            }
        }
        else -> {}
    }
    ...
}
override fun onPause() {
    ...
    mCastContext!!.sessionManager.removeSessionManagerListener(
                mSessionManagerListener!!, CastSession::class.java)
}
override fun onResume() {
    Log.d(TAG, "onResume() was called")
    mCastContext!!.sessionManager.addSessionManagerListener(
            mSessionManagerListener!!, CastSession::class.java)
    if (mCastSession != null && mCastSession!!.isConnected) {
        updatePlaybackLocation(PlaybackLocation.REMOTE)
    } else {
        updatePlaybackLocation(PlaybackLocation.LOCAL)
    }
    super.onResume()
}

Para el método updatePlayButton, cambia el valor de la variable isConnected:

private fun updatePlayButton(state: PlaybackState?) {
    ...
    val isConnected = (mCastSession != null
                && (mCastSession!!.isConnected || mCastSession!!.isConnecting))
    ...
}

Ahora, haz clic en el botón Botón Run de Android Studio, un triángulo verde que apunta a la derechaRun para ejecutar la app en tu dispositivo móvil. Conéctate a tu dispositivo de transmisión y comienza a reproducir un video. Deberías ver que el video se reproduce en el receptor.

7. Minicontrolador

La lista de tareas para el diseño de Cast requiere que todas las apps de Cast proporcionen un minicontrol que aparezca cuando el usuario salga de la página de contenido actual. El minicontrol proporciona acceso instantáneo y un recordatorio visible para la sesión de transmisión en curso.

Ilustración de la parte inferior del teléfono Android que muestra el reproductor en miniatura en la app Cast Videos

El SDK de Cast proporciona una vista personalizada, MiniControllerFragment, que se puede agregar al archivo de diseño de la app de las actividades en las que quieres mostrar el minicontrol.

Agrega el siguiente fragmento de definición a la parte inferior de res/layout/player_activity.xml y res/layout/video_browser.xml:

<fragment
    android:id="@+id/castMiniController"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:visibility="gone"
    class="com.google.android.gms.cast.framework.media.widget.MiniControllerFragment"/>

Haz clic en el botón Botón Run de Android Studio, un triángulo verde que apunta a la derechaRun para ejecutar la app y transmitir un video. Cuando se inicie la reproducción en el receptor, deberá aparecer el minicontrolador en la parte inferior de cada actividad. Podrás controlar la reproducción remota con el minicontrolador. Si navegas entre la actividad de navegación y la actividad del reproductor local, el estado del minicontrolador debería mantenerse sincronizado con el estado de reproducción de contenido multimedia del receptor.

8. Notificaciones y pantalla de bloqueo

La lista de tareas de diseño de Google Cast requiere que una app emisora implemente controles multimedia desde una notificación y la pantalla de bloqueo.

Ilustración de un teléfono Android que muestra los controles multimedia en el área de notificaciones

El SDK de Cast proporciona un MediaNotificationService para ayudar a la app emisora a compilar controles multimedia para la notificación y la pantalla de bloqueo. El servicio se combina automáticamente en el manifiesto de tu app mediante Gradle.

El MediaNotificationService se ejecutará en segundo plano cuando la emisora esté transmitiendo y mostrará una notificación con una miniatura de imagen y metadatos sobre el elemento que se esté transmitiendo, un botón de reproducción/pausa y un botón para detener.

Los controles de notificación y pantalla de bloqueo se pueden habilitar con las CastOptions cuando inicialices el CastContext. Estos controles estarán activados de forma predeterminada. La función de pantalla de bloqueo se activará siempre que la notificación esté activada.

Edita el CastOptionsProvider y cambia la implementación de getCastOptions de modo que coincida con este código:

import com.google.android.gms.cast.framework.media.CastMediaOptions
import com.google.android.gms.cast.framework.media.NotificationOptions

override fun getCastOptions(context: Context): CastOptions {
   val notificationOptions = NotificationOptions.Builder()
            .setTargetActivityClassName(VideoBrowserActivity::class.java.name)
            .build()
    val mediaOptions = CastMediaOptions.Builder()
            .setNotificationOptions(notificationOptions)
            .build()
   return CastOptions.Builder()
                .setReceiverApplicationId(context.getString(R.string.app_id))
                .setCastMediaOptions(mediaOptions)
                .build()
}

Haz clic en el botón Botón Run de Android Studio, un triángulo verde que apunta a la derechaRun para ejecutar la app en tu dispositivo móvil. Transmite un video y sal de la app de muestra. Deberías ver una notificación sobre el video que se está reproduciendo en el receptor. Bloquea el dispositivo móvil. La pantalla de bloqueo debería mostrar los controles de la reproducción de contenido multimedia en el dispositivo de transmisión.

Ilustración de un teléfono Android que muestra controles multimedia en la pantalla de bloqueo

9. Superposición introductoria

La lista de tareas de diseño de Google Cast requiere que las apps emisoras presenten el botón para transmitir a los usuarios existentes para informarles que ahora admiten la transmisión y que ayudan a los usuarios nuevos de Google Cast.

Ilustración que muestra la superposición introductoria de Cast alrededor del botón para transmitir en la app de Cast Videos para Android

El SDK de Cast proporciona una vista personalizada, IntroductoryOverlay, que se puede usar a fin de destacar el botón para transmitir cuando se muestra por primera vez a los usuarios. Agrega el siguiente código a VideoBrowserActivity:

import com.google.android.gms.cast.framework.IntroductoryOverlay
import android.os.Looper

private var mIntroductoryOverlay: IntroductoryOverlay? = null

private fun showIntroductoryOverlay() {
    mIntroductoryOverlay?.remove()
    if (mediaRouteMenuItem?.isVisible == true) {
       Looper.myLooper().run {
           mIntroductoryOverlay = com.google.android.gms.cast.framework.IntroductoryOverlay.Builder(
                    this@VideoBrowserActivity, mediaRouteMenuItem!!)
                   .setTitleText("Introducing Cast")
                   .setSingleTime()
                   .setOnOverlayDismissedListener(
                           object : IntroductoryOverlay.OnOverlayDismissedListener {
                               override fun onOverlayDismissed() {
                                   mIntroductoryOverlay = null
                               }
                          })
                   .build()
          mIntroductoryOverlay!!.show()
        }
    }
}

Ahora, agrega un CastStateListener y llama al método showIntroductoryOverlay cuando haya un dispositivo de transmisión disponible. Para ello, modifica el método onCreate y anula los métodos onResume y onPause para que coincidan con lo siguiente:

import com.google.android.gms.cast.framework.CastState
import com.google.android.gms.cast.framework.CastStateListener

private var mCastStateListener: CastStateListener? = null

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.video_browser)
    setupActionBar()
    mCastStateListener = object : CastStateListener {
            override fun onCastStateChanged(newState: Int) {
                if (newState != CastState.NO_DEVICES_AVAILABLE) {
                    showIntroductoryOverlay()
                }
            }
        }
    mCastContext = CastContext.getSharedInstance(this)
}

override fun onResume() {
    super.onResume()
    mCastContext?.addCastStateListener(mCastStateListener!!)
}

override fun onPause() {
    super.onPause()
    mCastContext?.removeCastStateListener(mCastStateListener!!)
}

Borra los datos de la app o quítala de tu dispositivo. Luego, haz clic en el botón Botón Run de Android Studio, un triángulo verde que apunta a la derechaRun para ejecutar la app en tu dispositivo móvil. Deberías ver la superposición introductoria (borra los datos de app si no se muestra la superposición).

10. Control expandido

La lista de tareas de diseño de Google Cast requiere que la app emisora proporcione un control expandido para el contenido multimedia que se transmite. Este control es una versión de pantalla completa del minicontrolador.

Ilustración de un video que se reproduce en un teléfono Android con un control expandido superpuesto

El SDK de Cast proporciona un widget llamado ExpandedControllerActivity para el control expandido. Esta es una clase abstracta para la que debes crear una subclase a fin de agregar un botón para transmitir.

Primero, crea un nuevo archivo de recursos del menú, llamado expanded_controller.xml, de modo que el control expandido proporcione el botón para transmitir:

<?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/media_route_menu_title"
            app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
            app:showAsAction="always"/>

</menu>

Crea un paquete nuevo expandedcontrols en el paquete com.google.sample.cast.refplayer. A continuación, crea un archivo nuevo llamado ExpandedControlsActivity.kt en el paquete com.google.sample.cast.refplayer.expandedcontrols.

package com.google.sample.cast.refplayer.expandedcontrols

import android.view.Menu
import com.google.android.gms.cast.framework.media.widget.ExpandedControllerActivity
import com.google.sample.cast.refplayer.R
import com.google.android.gms.cast.framework.CastButtonFactory

class ExpandedControlsActivity : ExpandedControllerActivity() {
    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        super.onCreateOptionsMenu(menu)
        menuInflater.inflate(R.menu.expanded_controller, menu)
        CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item)
        return true
    }
}

Ahora, declara el ExpandedControlsActivity en AndroidManifest.xml dentro de la etiqueta application encima de OPTIONS_PROVIDER_CLASS_NAME:

<application>
    ...
    <activity
        android:name="com.google.sample.cast.refplayer.expandedcontrols.ExpandedControlsActivity"
        android:label="@string/app_name"
        android:launchMode="singleTask"
        android:theme="@style/Theme.CastVideosDark"
        android:screenOrientation="portrait"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.MAIN"/>
        </intent-filter>
        <meta-data
            android:name="android.support.PARENT_ACTIVITY"
            android:value="com.google.sample.cast.refplayer.VideoBrowserActivity"/>
    </activity>
    ...
</application>

Edita el CastOptionsProvider y cambia NotificationOptions y CastMediaOptions a fin de establecer la actividad objetivo en ExpandedControlsActivity:

import com.google.sample.cast.refplayer.expandedcontrols.ExpandedControlsActivity

override fun getCastOptions(context: Context): CastOptions {
    val notificationOptions = NotificationOptions.Builder()
            .setTargetActivityClassName(ExpandedControlsActivity::class.java.name)
            .build()
    val mediaOptions = CastMediaOptions.Builder()
            .setNotificationOptions(notificationOptions)
            .setExpandedControllerActivityClassName(ExpandedControlsActivity::class.java.name)
            .build()
    return CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .setCastMediaOptions(mediaOptions)
            .build()
}

Actualiza el método LocalPlayerActivity de loadRemoteMedia para mostrar la ExpandedControlsActivity cuando se cargue el contenido multimedia remoto:

import com.google.sample.cast.refplayer.expandedcontrols.ExpandedControlsActivity

private fun loadRemoteMedia(position: Int, autoPlay: Boolean) {
    if (mCastSession == null) {
        return
    }
    val remoteMediaClient = mCastSession!!.remoteMediaClient ?: return
    remoteMediaClient.registerCallback(object : RemoteMediaClient.Callback() {
        override fun onStatusUpdated() {
            val intent = Intent(this@LocalPlayerActivity, ExpandedControlsActivity::class.java)
            startActivity(intent)
            remoteMediaClient.unregisterCallback(this)
        }
    })
    remoteMediaClient.load(MediaLoadRequestData.Builder()
                .setMediaInfo(buildMediaInfo())
                .setAutoplay(autoPlay)
                .setCurrentTime(position.toLong()).build())
}

Haz clic en el botón Botón Run de Android Studio, un triángulo verde que apunta a la derechaRun para ejecutar la app en tu dispositivo móvil y transmitir un video. Deberías ver el control expandido. Vuelve a la lista de videos. Cuando hagas clic en el minicontrolador, se volverá a cargar el control expandido. Navega fuera de la app para ver la notificación. Haz clic en la imagen de la notificación a fin de cargar el control expandido.

11. Cómo agregar compatibilidad con Cast Connect

La biblioteca de Cast Connect permite que las aplicaciones emisoras existentes se comuniquen con aplicaciones de Android TV a través del protocolo de transmisión. Cast Connect se basa en la infraestructura de Cast, y tu app de Android TV actúa como receptor.

Dependencias

Nota: Para implementar Cast Connect, la play-services-cast-framework debe ser 19.0.0 o superior.

LaunchOptions

Para iniciar la aplicación de Android TV, también conocida como receptor de Android, debemos establecer la marca setAndroidReceiverCompatible como verdadera en el objeto LaunchOptions. Este objeto LaunchOptions determina cómo se inicia el receptor y se pasa al CastOptions que muestra la clase CastOptionsProvider. Si estableces la marca mencionada anteriormente en false, se iniciará el receptor web para el ID de app definido en la Consola para desarrolladores de Cast.

En el archivo CastOptionsProvider.kt, agrega lo siguiente al método getCastOptions:

import com.google.android.gms.cast.LaunchOptions
...
val launchOptions = LaunchOptions.Builder()
            .setAndroidReceiverCompatible(true)
            .build()
return new CastOptions.Builder()
        .setLaunchOptions(launchOptions)
        ...
        .build()

Establece credenciales de inicio

Del lado del remitente, puedes especificar CredentialsData para que represente quién se une a la sesión. credentials es una cadena que puede definir el usuario, siempre que tu app de ATV pueda comprenderla. El CredentialsData solo se pasa a tu app de Android TV durante el inicio o la hora de unirse a la app. Si la vuelves a configurar mientras tienes conexión, no se pasará a la app de Android TV.

Para establecer las credenciales de lanzamiento, se debe definir CredentialsData y pasar al objeto LaunchOptions. Agrega el siguiente código al método getCastOptions en tu archivo CastOptionsProvider.kt:

import com.google.android.gms.cast.CredentialsData
...

val credentialsData = CredentialsData.Builder()
        .setCredentials("{\"userId\": \"abc\"}")
        .build()
val launchOptions = LaunchOptions.Builder()
       ...
       .setCredentialsData(credentialsData)
       .build()

Configurar credenciales en LoadRequest

En caso de que la app de Web Receiver y la app de Android TV manejen credentials de manera diferente, es posible que debas definir credentials independientes para cada una. Para solucionarlo, agrega el siguiente código en tu archivo LocalPlayerActivity.kt, en la función loadRemoteMedia:

remoteMediaClient.load(MediaLoadRequestData.Builder()
       ...
       .setCredentials("user-credentials")
       .setAtvCredentials("atv-user-credentials")
       .build())

Según la app receptora a la que tu remitente esté transmitiendo, ahora el SDK controlará automáticamente qué credenciales usar para la sesión actual.

Cómo probar Cast Connect

Pasos para instalar el APK de Android TV en Chromecast con Google TV

  1. Busca la dirección IP del dispositivo Android TV. Por lo general, está disponible en Configuración > Internet y redes > (Nombre de la red a la que está conectado tu dispositivo). A la derecha, mostrará los detalles y la IP de tu dispositivo en la red.
  2. Usa la dirección IP de tu dispositivo para conectarte a ella a través de ADB con el terminal:
$ adb connect <device_ip_address>:5555
  1. Desde la ventana de la terminal, navega a la carpeta de nivel superior de las muestras del codelab que descargaste al comienzo. Por ejemplo:
$ cd Desktop/android_codelab_src
  1. Instala el archivo .apk de esta carpeta en tu Android TV. Para ello, ejecuta el siguiente comando:
$ adb -s <device_ip_address>:5555 install android-tv-app.apk
  1. Ahora deberías poder ver una app con el nombre Transmitir videos en el menú Tus apps del dispositivo Android TV.
  2. Regresa a tu proyecto de Android Studio y haz clic en el botón Ejecutar para instalar y ejecutar la app emisora en tu dispositivo móvil físico. En la esquina superior derecha, haz clic en el ícono de transmisión y selecciona tu dispositivo Android TV entre las opciones disponibles. Deberías ver que la app de Android TV se inició en el dispositivo Android TV y deberías poder controlar la reproducción de video con el control remoto de Android TV.

12. Personaliza los widgets de Cast

Para personalizar los widgets de Cast, configura los colores, el estilo de los botones, el texto y la apariencia de la miniatura, y elige los tipos de botones que quieres mostrar.

Actualiza res/values/styles_castvideo.xml

<style name="Theme.CastVideosTheme" parent="Theme.AppCompat.Light.NoActionBar">
    ...
    <item name="mediaRouteTheme">@style/CustomMediaRouterTheme</item>
    <item name="castIntroOverlayStyle">@style/CustomCastIntroOverlay</item>
    <item name="castMiniControllerStyle">@style/CustomCastMiniController</item>
    <item name="castExpandedControllerStyle">@style/CustomCastExpandedController</item>
    <item name="castExpandedControllerToolbarStyle">
        @style/ThemeOverlay.AppCompat.ActionBar
    </item>
    ...
</style>

Declara los siguientes temas personalizados:

<!-- Customize Cast Button -->
<style name="CustomMediaRouterTheme" parent="Theme.MediaRouter">
    <item name="mediaRouteButtonStyle">@style/CustomMediaRouteButtonStyle</item>
</style>
<style name="CustomMediaRouteButtonStyle" parent="Widget.MediaRouter.Light.MediaRouteButton">
    <item name="mediaRouteButtonTint">#EEFF41</item>
</style>

<!-- Customize Introductory Overlay -->
<style name="CustomCastIntroOverlay" parent="CastIntroOverlay">
    <item name="castButtonTextAppearance">@style/TextAppearance.CustomCastIntroOverlay.Button</item>
    <item name="castTitleTextAppearance">@style/TextAppearance.CustomCastIntroOverlay.Title</item>
</style>
<style name="TextAppearance.CustomCastIntroOverlay.Button" parent="android:style/TextAppearance">
    <item name="android:textColor">#FFFFFF</item>
</style>
<style name="TextAppearance.CustomCastIntroOverlay.Title" parent="android:style/TextAppearance.Large">
    <item name="android:textColor">#FFFFFF</item>
</style>

<!-- Customize Mini Controller -->
<style name="CustomCastMiniController" parent="CastMiniController">
    <item name="castShowImageThumbnail">true</item>
    <item name="castTitleTextAppearance">@style/TextAppearance.AppCompat.Subhead</item>
    <item name="castSubtitleTextAppearance">@style/TextAppearance.AppCompat.Caption</item>
    <item name="castBackground">@color/accent</item>
    <item name="castProgressBarColor">@color/orange</item>
</style>

<!-- Customize Expanded Controller -->
<style name="CustomCastExpandedController" parent="CastExpandedController">
    <item name="castButtonColor">#FFFFFF</item>
    <item name="castPlayButtonDrawable">@drawable/cast_ic_expanded_controller_play</item>
    <item name="castPauseButtonDrawable">@drawable/cast_ic_expanded_controller_pause</item>
    <item name="castStopButtonDrawable">@drawable/cast_ic_expanded_controller_stop</item>
</style>

13. Felicitaciones

Ahora sabes cómo habilitar la transmisión de contenido en una app de video con los widgets del SDK de Cast en Android.

Para obtener más información, consulta la guía para desarrolladores de Android Sender.