
En la calle, a menudo escucho audiolibros y podcasts desde un teléfono inteligente. Cuando llegue a casa, quiero seguir escuchándolos en Android TV o Google Home. Pero no todas las aplicaciones admiten Chromecast. Y sería conveniente.
Según las estadísticas de Google en los últimos 3 años, el número de dispositivos en Android TV ha aumentado 4 veces, y el número de socios de fabricación ya ha superado el centenar: televisores "inteligentes", altavoces, decodificadores. Todos ellos son compatibles con Chromecast. Pero todavía hay muchas aplicaciones en el mercado que obviamente carecen de integración con él.
En este artículo, quiero compartir mi experiencia al integrar Chromecast en una aplicación de Android para reproducir contenido multimedia.
Como funciona
Si es la primera vez que escuchas la palabra "Chromecast", intentaré decírtelo brevemente. En términos de uso, se ve más o menos así:
- El usuario escucha música o mira videos a través de una aplicación o sitio web.
- Aparece un dispositivo Chromecast en la red local.
- Debería aparecer un botón correspondiente en la interfaz del jugador.
- Al hacer clic en él, el usuario selecciona el dispositivo deseado de la lista. Puede ser un Nexus Player, Android TV o un altavoz inteligente.
- La reproducción adicional continúa con este dispositivo.

Técnicamente, sucede algo como lo siguiente:
- Google Services monitorea la presencia de dispositivos Chromecast en la red local a través de Broadcasting.
- Si MediaRouter está conectado a su aplicación, recibirá un evento al respecto.
- Cuando un usuario selecciona un dispositivo Cast y se conecta a él, se abre una nueva sesión de medios (CastSession).
- Ya en la sesión creada, transferiremos contenido para su reproducción.
Suena muy simple.
Integración
Google tiene su propio SDK de Chromecast, pero está mal cubierto en la documentación y su código está ofuscado. Por lo tanto, muchas cosas tuvieron que verificarse escribiendo. Pongamos todo en orden.
Inicialización
Primero necesitamos conectar Cast Application Framework y MediaRouter:
implementation "com.google.android.gms:play-services-cast-framework:16.1.0" implementation "androidx.mediarouter:mediarouter:1.0.0"
Luego, Cast Framework debería obtener el identificador de la aplicación (más sobre eso más adelante) y los tipos de contenido multimedia compatible. Es decir, si nuestra aplicación solo reproduce video, será imposible enviar contenido a la columna Google Home y no estará en la lista de dispositivos. Para hacer esto, cree una implementación de OptionsProvider:
class CastOptionsProvider: OptionsProvider { override fun getCastOptions(context: Context): CastOptions { return CastOptions.Builder() .setReceiverApplicationId(BuildConfig.CHROMECAST_APP_ID) .build() } override fun getAdditionalSessionProviders(context: Context): MutableList<SessionProvider>? { return null } }
Y declararlo en Manifiesto:
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME" android:value="your.app.package.CastOptionsProvider" />
Registrar la solicitud
Para que Chromecast funcione con nuestra aplicación, debe estar registrada en la Consola de desarrolladores de SDK de Google Cast . Esto requiere una cuenta de desarrollador de Chromecast (que no debe confundirse con una cuenta de desarrollador de Google Play). Al registrarse, deberá pagar una tarifa única de $ 5. Después de publicar la aplicación ChromeCast, debe esperar un poco.
En la consola, puede cambiar la apariencia del reproductor Cast para dispositivos con pantalla y ver los análisis de transmisión dentro de la aplicación.
MediaRouteFramework es un mecanismo que le permite encontrar todos los dispositivos de reproducción remota cerca del usuario. Esto no solo puede ser Chromecast, sino también pantallas remotas y altavoces que utilizan protocolos de terceros. Pero lo que nos interesa es Chromecast.

MediaRouteFramework tiene una Vista que refleja el estado del scooter multimedia. Hay dos formas de conectarlo:
1) A través del menú:
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> ... <item android:id="@+id/menu_media_route" android:title="@string/cast" app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider" app:showAsAction="always"/> ... </menu>
2) Vía diseño:
<androidx.mediarouter.app.MediaRouteButton android:id="@+id/mediaRouteButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:mediaRouteTypes="user"/>
Y desde el código solo necesita registrar el botón en CastButtonFactory. entonces se mostrará el estado actual del scooter de medios:
CastButtonFactory.setUpMediaRouteButton(applicationContext, view.mediaRouteButton)
Ahora que la aplicación está registrada y MediaRouter está configurado, puede conectarse a dispositivos ChromeCast y abrir sesiones para ellos.
ChromeCast admite tres tipos principales de contenido:
Dependiendo de la configuración del reproductor, como el contenido multimedia y el dispositivo de transmisión, la interfaz del reproductor puede variar.
Castsession
Entonces, el usuario seleccionó el dispositivo deseado, CastFramework abrió una nueva sesión. Ahora nuestra tarea es responder a esto y pasar información al dispositivo para su reproducción.
Para averiguar el estado actual de la sesión e inscribirse para actualizar este estado, use el objeto SessionManager :
private val mediaSessionListener = object : SessionManagerListener<CastSession> { override fun onSessionStarted(session: CastSession, sessionId: String) { currentSession = session
Y también podemos averiguar si hay una sesión abierta en este momento:
val currentSession: CastSession? = sessionManager.currentCastSession
Tenemos dos condiciones principales en las que podemos comenzar a lanzar:
- La sesión ya está abierta.
- Hay contenido para el casting.
En cada uno de estos dos eventos, podemos verificar el estado y, si todo está en orden, comenzar a emitir.
Fundición
Ahora que tenemos qué lanzar y dónde lanzar, podemos pasar a lo más importante. Entre otras cosas, CastSession tiene un objeto RemoteMediaClient que es responsable del estado de reproducción del contenido multimedia. Trabajaremos con él.
Creemos MediaMetadata , donde se almacenará la información sobre el autor, el álbum, etc. Es muy similar a lo que transferimos a MediaSession cuando comenzamos la reproducción local.
val mediaMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MUSIC_TRACK ).apply { putString(MediaMetadata.KEY_TITLE, “In C”) putString(MediaMetadata.KEY_ARTIST, “Terry Riley”) mediaContent?.metadata?.posterUrl?.let { poster -> addImage(WebImage(Uri.parse(“https:
MediaMetadata tiene muchos parámetros, y es mejor verlos en la documentación. Me sorprendió gratamente que pueda agregar una imagen no a través de un mapa de bits, sino simplemente mediante un enlace dentro de WebImage.
El objeto MediaInfo contiene información sobre los metadatos del contenido y hablará de dónde proviene el contenido multimedia, de qué tipo es, cómo reproducirlo:
val mediaInfo = MediaInfo.Builder(“https:
Permítame recordarle que contentType es un tipo de contenido de acuerdo con la especificación MIME .
También puede transferir inserciones publicitarias a MediaInfo:
- setAdBreakClips: acepta una lista de comerciales de AdBreakClipInfo con enlaces a contenido, título, tiempo y el tiempo durante el cual se omite el anuncio.
- setAdBreaks: información sobre el diseño y el momento de las inserciones publicitarias.
En MediaLoadOptions describimos cómo procesaremos el flujo de medios (velocidad, posición inicial). La documentación también dice que a través de setCredentials puede pasar el encabezado de la solicitud de autorización, pero mis solicitudes de Chromecast no incluyeron los campos de autorización solicitados.
val mediaLoadOptions = MediaLoadOptions.Builder() .setPlayPosition(position!!) .setAutoplay(true) .setPlaybackRate(playbackSpeed) .setCredentials(context.getString(R.string.bearer_token, authGateway.authState.accessToken!!)) .setCredentialsType(context.getString(R.string.authorization_header_key)) .build()
Una vez que todo esté listo, podemos entregar todos los datos a RemoteMediaClient y Chromecast comenzará a reproducirse. Es importante pausar la reproducción local.
val remoteMediaClient = currentSession!!.remoteMediaClient remoteMediaClient.load(mediaInfo, mediaLoadOptions)
Manejo de eventos
El video comenzó a reproducirse, ¿y luego qué? ¿Qué pasa si el usuario detiene el televisor? Para obtener información sobre eventos desde Chromecast, RemoteMediaClient tiene devoluciones de llamada:
private val castStatusCallback = object : RemoteMediaClient.Callback() { override fun onStatusUpdated() {
Conocer el progreso actual también es simple:
val periodMills = 1000L remoteMediaClient.addProgressListener( RemoteMediaClient.ProgressListener { progressMills, durationMills ->
Experiencia integrando con un jugador existente
La aplicación en la que estaba trabajando ya tenía un reproductor multimedia listo para usar. El objetivo era integrar el soporte de Chromecast en él. El reproductor multimedia se basó en la máquina de estado, y la primera idea fue agregar un nuevo estado: "CastingState". Pero esta idea fue rechazada de inmediato, porque cada estado del reproductor refleja el estado de reproducción, y no importa lo que sirva como implementación de ExoPlayer o ChromeCast.
Luego surgió la idea de crear un cierto sistema de delegados con priorización y procesamiento del "ciclo de vida" del jugador. Todos los delegados pueden recibir eventos de estado del jugador: Jugar, Pausa, etc. - pero solo el delegado principal reproducirá el contenido multimedia.

Tenemos algo como esta interfaz de jugador:
interface Player { val isPlaying: Boolean val isReleased: Boolean val duration: Long var positionInMillis: Long var speed: Float var volume: Float var loop: Boolean fun addListener(listener: PlayerCallback) fun removeListener(listener: PlayerCallback): Boolean fun getListeners(): MutableSet<PlayerCallback> fun prepare(mediaContent: MediaContent) fun play() fun pause() fun release() interface PlayerCallback { fun onPlaying(currentPosition: Long) fun onPaused(currentPosition: Long) fun onPreparing() fun onPrepared() fun onLoadingChanged(isLoading: Boolean) fun onDurationChanged(duration: Long) fun onSetSpeed(speed: Float) fun onSeekTo(fromTimeInMillis: Long, toTimeInMillis: Long) fun onWaitingForNetwork() fun onError(error: String?) fun onReleased() fun onPlayerProgress(currentPosition: Long) } }
Dentro habrá una máquina de estado con tantos estados:
- Vacío: estado inicial antes de la inicialización.
- Preparación: el reproductor inicia la reproducción de contenido multimedia.
- Preparado: los medios están cargados y listos para reproducir.
- Jugando
- Pausado
- Esperando red
- Error

Anteriormente, cada estado durante la inicialización emitía un comando en ExoPlayer. Ahora emitirá el comando a la lista de delegados Jugadores, y el delegado "Líder" podrá procesarlo. Dado que el delegado implementa todas las funciones del reproductor, también se puede heredar de la interfaz del jugador y usarse por separado si es necesario. Entonces el delegado abstracto se verá así:
abstract class PlayingDelegate( protected val playerCallback: Player.PlayerCallback, var isLeading: Boolean = false ) : Player { fun setIsLeading(isLeading: Boolean, positionMills: Long, isPlaying: Boolean) { this.isLeading = isLeading if (isLeading) { onLeading(positionMills, isPlaying) } else { onDormant() } } final override fun addListener(listener: Player.PlayerCallback) {
Por ejemplo, simplifiqué las interfaces. En realidad, hay un poco más de eventos.
Puede haber tantos delegados como fuentes de reproducción. Un delegado de Chromecast podría verse así:
ChromeCastDelegate.kt class ChromeCastDelegate( private val context: Context, private val castCallback: ChromeCastListener, playerCallback: Player.PlayerCallback ) : PlayingDelegate(playerCallback) { companion object { private const val CONTENT_TYPE_VIDEO = "videos/mp4" private const val CONTENT_TYPE_AUDIO = "audio/mp3" private const val PROGRESS_DELAY_MILLS = 500L } interface ChromeCastListener { fun onCastStarted() fun onCastStopped() } private var sessionManager: SessionManager? = null private var currentSession: CastSession? = null private var mediaContent: MediaContent? = null private var currentPosition: Long = 0 private val mediaSessionListener = object : SessionManagerListener<CastSession> { override fun onSessionStarted(session: CastSession, sessionId: String) { currentSession = session castCallback.onCastStarted() } override fun onSessionEnding(session: CastSession) { currentPosition = session.remoteMediaClient?.approximateStreamPosition ?: currentPosition stopCasting() } override fun onSessionResumed(session: CastSession, wasSuspended: Boolean) { currentSession = session castCallback.onCastStarted() } override fun onSessionStartFailed(session: CastSession, p1: Int) { stopCasting() } override fun onSessionEnded(session: CastSession, p1: Int) {
Antes de dar un comando sobre la reproducción, debemos decidir sobre el delegado principal. Para hacer esto, se agregan en orden de prioridad para el jugador, y cada uno de ellos puede dar su estado de preparación en el método readyForLeading (). El código de muestra completo se puede ver en GitHub .
¿Hay vida después de ChromeCast?

Después de integrar el soporte de Chromecast en la aplicación, estaba más contento de volver a casa y disfrutar de audiolibros no solo a través de los auriculares, sino también a través de Google Home. En cuanto a la arquitectura, la implementación de jugadores en diferentes aplicaciones puede variar, por lo que este enfoque no será apropiado en todas partes. Pero para nuestra arquitectura, surgió. ¡Espero que este artículo haya sido útil, y en el futuro cercano habrá más aplicaciones que puedan integrarse con el entorno digital!