Integra Cast en tu app para Android

En esta guía para desarrolladores, se describe cómo agregar compatibilidad con Google Cast a tu app emisora para Android con el SDK de Android Sender.

El dispositivo móvil o la laptop es el emisor que controla la reproducción, y el dispositivo Google Cast es el receptor que muestra el contenido en la TV.

El framework del emisor se refiere al objeto binario de la biblioteca de clases de Cast y a los recursos asociados presentes en el tiempo de ejecución en el emisor. La app emisora o app de Cast se refiere a una app que también se ejecuta en el emisor. La app Web Receiver se refiere a la aplicación HTML que se ejecuta en el dispositivo compatible con Cast.

El framework del emisor usa un diseño de devolución de llamada asíncrono para informar a la app emisora sobre los eventos y realizar la transición entre varios estados del ciclo de vida de la app de Cast.

Flujo de la app

En los siguientes pasos, se describe el flujo de ejecución típico de alto nivel para una app emisora para Android:

  • El framework de Cast inicia automáticamente la detección de dispositivos MediaRouter en función del ciclo de vida Activity.
  • Cuando el usuario hace clic en el botón para transmitir, el framework presenta el diálogo de Cast con la lista de dispositivos de transmisión detectados.
  • Cuando el usuario selecciona un dispositivo de transmisión, el framework intenta iniciar la app Web Receiver en el dispositivo de transmisión.
  • El framework invoca devoluciones de llamada en la app emisora para confirmar que se inició la app Web Receiver.
  • El framework crea un canal de comunicación entre las apps emisora y Web Receiver.
  • El framework usa el canal de comunicación para cargar y controlar la reproducción de contenido multimedia en Web Receiver.
  • El framework sincroniza el estado de reproducción de contenido multimedia entre el emisor y Web Receiver: cuando el usuario realiza acciones de la IU del emisor, el framework pasa esas solicitudes de control de contenido multimedia a Web Receiver y, cuando Web Receiver envía actualizaciones de estado de contenido multimedia, el framework actualiza el estado de la IU del emisor.
  • Cuando el usuario hace clic en el botón para transmitir para desconectarse del dispositivo de transmisión, el framework desconectará la app emisora de Web Receiver.

Para obtener una lista completa de todas las clases, los métodos y los eventos del SDK de Google Cast para Android, consulta la Referencia de la API de Google Cast Sender para Android. En las siguientes secciones, se describen los pasos para agregar Cast a tu app para Android.

Configura el manifiesto de Android

El archivo AndroidManifest.xml de tu app requiere que configures los siguientes elementos para el SDK de Cast:

uses-sdk

Establece los niveles de API de Android mínimo y objetivo que admite el SDK de Cast. Actualmente, el mínimo es el nivel de API 23 y el objetivo es el nivel de API 34.

<uses-sdk
        android:minSdkVersion="23"
        android:targetSdkVersion="34" />

android:theme

Establece el tema de tu app en función de la versión mínima del SDK de Android. Por ejemplo, si no implementas tu propio tema, debes usar una variante de Theme.AppCompat cuando te orientes a una versión mínima del SDK de Android anterior a Lollipop.

<application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/Theme.AppCompat" >
       ...
</application>

Inicializa el contexto de Cast

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

Tu app debe implementar la OptionsProvider interfaz para proporcionar las opciones necesarias para inicializar el CastContext singleton. OptionsProvider proporciona una instancia de CastOptions que contiene opciones que afectan el comportamiento del framework. La más importante es el ID de la aplicación Web Receiver, que se usa para filtrar los resultados de la detección y para iniciar la app Web Receiver cuando comienza una sesión de Cast.

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

    override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? {
        return null
    }
}
Java
public class CastOptionsProvider implements OptionsProvider {
    @Override
    public CastOptions getCastOptions(Context context) {
        CastOptions castOptions = new CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .build();
        return castOptions;
    }
    @Override
    public List<SessionProvider> getAdditionalSessionProviders(Context context) {
        return null;
    }
}

Debes declarar el nombre completamente calificado del OptionsProvider implementado como un campo de metadatos en el archivo AndroidManifest.xml de la app emisora:

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

CastContext se inicializa de forma diferida cuando se llama a CastContext.getSharedInstance().

Kotlin
class MyActivity : FragmentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        val castContext = CastContext.getSharedInstance(this)
    }
}
Java
public class MyActivity extends FragmentActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        CastContext castContext = CastContext.getSharedInstance(this);
    }
}

Los widgets de UX de Cast

El framework de Cast proporciona los widgets que cumplen con la Lista de tareas de diseño de Cast:

  • Superposición introductoria: El framework proporciona una vista personalizada, IntroductoryOverlay, que se muestra al usuario para llamar la atención sobre el botón para transmitir la primera vez que hay un receptor disponible. La app emisora puede personalizar el texto y la posición del texto del título.

  • Botón para transmitir: El botón para transmitir es visible independientemente de la disponibilidad de los dispositivos de transmisión. Cuando el usuario hace clic en el botón para transmitir por primera vez, se muestra un diálogo de Cast que muestra los dispositivos detectados. Cuando el usuario hace clic en el botón para transmitir mientras el dispositivo está conectado, se muestran los metadatos de contenido multimedia actuales (como el título, el nombre del estudio de grabación y una imagen en miniatura) o se permite que el usuario se desconecte del dispositivo de transmisión. A veces, se hace referencia al "botón para transmitir" como el "ícono para transmitir".

  • Minicontrol: Cuando el usuario transmite contenido y abandona la página de contenido actual o el control expandido a otra pantalla de la app emisora, el minicontrol se muestra en la parte inferior de la pantalla para permitir que el usuario vea los metadatos de contenido multimedia que se están transmitiendo y controle la reproducción.

  • Control expandido: Cuando el usuario transmite contenido, si hace clic en la notificación de contenido multimedia o en el minicontrol, se inicia el control expandido, que muestra los metadatos de contenido multimedia que se están reproduciendo y proporciona varios botones para controlar la reproducción de contenido multimedia.

  • Notificación: Solo para Android. Cuando el usuario transmite contenido y abandona la app emisora, se muestra una notificación de contenido multimedia que muestra los metadatos de contenido multimedia que se están transmitiendo y los controles de reproducción.

  • Pantalla de bloqueo: Solo para Android. Cuando el usuario transmite contenido y navega (o el dispositivo agota el tiempo de espera) a la pantalla de bloqueo, se muestra un control de pantalla de bloqueo de contenido multimedia que muestra los metadatos de contenido multimedia que se están transmitiendo y los controles de reproducción.

En la siguiente guía, se incluyen descripciones de cómo agregar estos widgets a tu app.

Agrega un botón para transmitir

Las APIs de Android MediaRouter están diseñadas para habilitar la reproducción y la visualización de contenido multimedia en dispositivos secundarios. Las apps para Android que usan la API de MediaRouter deben incluir un botón para transmitir como parte de su interfaz de usuario para permitir que los usuarios seleccionen una ruta de contenido multimedia para reproducir contenido multimedia en un dispositivo secundario, como un dispositivo de transmisión.

El framework facilita mucho agregar un MediaRouteButton como un Cast button. Primero, debes agregar un elemento de menú o un MediaRouteButton en el archivo XML que define tu menú y usar CastButtonFactory para conectarlo con el framework.

// To add a Cast button, add the following snippet.
// menu.xml
<item
    android:id="@+id/media_route_menu_item"
    android:title="@string/media_route_menu_title"
    app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
    app:showAsAction="always" />
Kotlin
// Then override the onCreateOptionMenu() for each of your activities.
// MyActivity.kt
override fun onCreateOptionsMenu(menu: Menu): Boolean {
    super.onCreateOptionsMenu(menu)
    menuInflater.inflate(R.menu.main, menu)
    CastButtonFactory.setUpMediaRouteButton(
        applicationContext,
        menu,
        R.id.media_route_menu_item
    )
    return true
}
Java
// Then override the onCreateOptionMenu() for each of your activities.
// MyActivity.java
@Override public boolean onCreateOptionsMenu(Menu menu) {
    super.onCreateOptionsMenu(menu);
    getMenuInflater().inflate(R.menu.main, menu);
    CastButtonFactory.setUpMediaRouteButton(getApplicationContext(),
                                            menu,
                                            R.id.media_route_menu_item);
    return true;
}

Luego, si tu Activity hereda de FragmentActivity, puedes agregar un MediaRouteButton a tu diseño.

// activity_layout.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:gravity="center_vertical"
   android:orientation="horizontal" >

   <androidx.mediarouter.app.MediaRouteButton
       android:id="@+id/media_route_button"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_weight="1"
       android:mediaRouteTypes="user"
       android:visibility="gone" />

</LinearLayout>
Kotlin
// MyActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_layout)

    mMediaRouteButton = findViewById<View>(R.id.media_route_button) as MediaRouteButton
    CastButtonFactory.setUpMediaRouteButton(applicationContext, mMediaRouteButton)

    mCastContext = CastContext.getSharedInstance(this)
}
Java
// MyActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_layout);

   mMediaRouteButton = (MediaRouteButton) findViewById(R.id.media_route_button);
   CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), mMediaRouteButton);

   mCastContext = CastContext.getSharedInstance(this);
}

Para establecer la apariencia del botón para transmitir con un tema, consulta Personaliza el botón para transmitir.

Configura la detección de dispositivos

La detección de dispositivos es administrada por completo por el CastContext. Cuando se inicializa CastContext, la app emisora especifica el ID de la aplicación Web Receiver y, de manera opcional, puede solicitar el filtrado del espacio de nombres estableciendo supportedNamespaces en CastOptions. CastContext contiene una referencia a MediaRouter de forma interna y comenzará el proceso de detección en las siguientes condiciones:

  • Según un algoritmo diseñado para equilibrar la latencia de detección de dispositivos y el uso de la batería, la detección se iniciará automáticamente de forma ocasional cuando la app emisora pase a primer plano.
  • El diálogo de Cast está abierto.
  • El SDK de Cast intenta recuperar una sesión de Cast.

El proceso de detección se detendrá cuando se cierre el diálogo de Cast o la app emisora pase a segundo plano.

Kotlin
class CastOptionsProvider : OptionsProvider {
    companion object {
        const val CUSTOM_NAMESPACE = "urn:x-cast:custom_namespace"
    }

    override fun getCastOptions(appContext: Context): CastOptions {
        val supportedNamespaces: MutableList<String> = ArrayList()
        supportedNamespaces.add(CUSTOM_NAMESPACE)

        return CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .setSupportedNamespaces(supportedNamespaces)
            .build()
    }

    override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? {
        return null
    }
}
Java
class CastOptionsProvider implements OptionsProvider {
    public static final String CUSTOM_NAMESPACE = "urn:x-cast:custom_namespace";

    @Override
    public CastOptions getCastOptions(Context appContext) {
        List<String> supportedNamespaces = new ArrayList<>();
        supportedNamespaces.add(CUSTOM_NAMESPACE);

        CastOptions castOptions = new CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .setSupportedNamespaces(supportedNamespaces)
            .build();
        return castOptions;
    }

    @Override
    public List<SessionProvider> getAdditionalSessionProviders(Context context) {
        return null;
    }
}

Cómo funciona la administración de sesiones

El SDK de Cast presenta el concepto de una sesión de Cast, cuyo establecimiento combina los pasos para conectarse a un dispositivo, iniciar (o unirse a) una app Web Receiver, conectarse a esa app y, luego, inicializar un canal de control de contenido multimedia. Consulta la guía del ciclo de vida de la aplicación Web Receiver para obtener más información sobre las sesiones de Cast y el ciclo de vida de Web Receiver.

Las sesiones se administran con la clase SessionManager, a la que tu app puede acceder a través de CastContext.getSessionManager(). Las sesiones individuales se representan mediante subclases de la clase Session. Por ejemplo, CastSession representa sesiones con dispositivos de transmisión. Tu app puede acceder a la sesión de Cast activa actualmente a través de SessionManager.getCurrentCastSession().

Tu app puede usar la SessionManagerListener clase para supervisar los eventos de sesión, como crear, suspender, reanudar y finalizar. El framework intenta reanudar automáticamente una finalización anormal o abrupta mientras una sesión estaba activa.

Las sesiones se crean y desconectan automáticamente en respuesta a los gestos del usuario en los diálogos de MediaRouter.

Para comprender mejor los errores de inicio de Cast, las apps pueden usar CastContext#getCastReasonCodeForCastStatusCode(int) para convertir el error de inicio de sesión en CastReasonCodes. Ten en cuenta que algunos errores de inicio de sesión (p.ej., CastReasonCodes#CAST_CANCELLED) son un comportamiento previsto y no deben registrarse como un error.

Si necesitas conocer los cambios de estado de la sesión, puedes implementar un SessionManagerListener. En este ejemplo, se escucha la disponibilidad de un CastSession en una Activity.

Kotlin
class MyActivity : Activity() {
    private var mCastSession: CastSession? = null
    private lateinit var mCastContext: CastContext
    private lateinit var mSessionManager: SessionManager
    private val mSessionManagerListener: SessionManagerListener<CastSession> =
        SessionManagerListenerImpl()

    private inner class SessionManagerListenerImpl : SessionManagerListener<CastSession?> {
        override fun onSessionStarting(session: CastSession?) {}

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

        override fun onSessionStartFailed(session: CastSession?, error: Int) {
            val castReasonCode = mCastContext.getCastReasonCodeForCastStatusCode(error)
            // Handle error
        }

        override fun onSessionSuspended(session: CastSession?, reason Int) {}

        override fun onSessionResuming(session: CastSession?, sessionId: String) {}

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

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

        override fun onSessionEnding(session: CastSession?) {}

        override fun onSessionEnded(session: CastSession?, error: Int) {
            finish()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mCastContext = CastContext.getSharedInstance(this)
        mSessionManager = mCastContext.sessionManager
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession::class.java)
    }

    override fun onResume() {
        super.onResume()
        mCastSession = mSessionManager.currentCastSession
    }

    override fun onDestroy() {
        super.onDestroy()
        mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession::class.java)
    }
}
Java
public class MyActivity extends Activity {
    private CastContext mCastContext;
    private CastSession mCastSession;
    private SessionManager mSessionManager;
    private SessionManagerListener<CastSession> mSessionManagerListener =
            new SessionManagerListenerImpl();

    private class SessionManagerListenerImpl implements SessionManagerListener<CastSession> {
        @Override
        public void onSessionStarting(CastSession session) {}
        @Override
        public void onSessionStarted(CastSession session, String sessionId) {
            invalidateOptionsMenu();
        }
        @Override
        public void onSessionStartFailed(CastSession session, int error) {
            int castReasonCode = mCastContext.getCastReasonCodeForCastStatusCode(error);
            // Handle error
        }
        @Override
        public void onSessionSuspended(CastSession session, int reason) {}
        @Override
        public void onSessionResuming(CastSession session, String sessionId) {}
        @Override
        public void onSessionResumed(CastSession session, boolean wasSuspended) {
            invalidateOptionsMenu();
        }
        @Override
        public void onSessionResumeFailed(CastSession session, int error) {}
        @Override
        public void onSessionEnding(CastSession session) {}
        @Override
        public void onSessionEnded(CastSession session, int error) {
            finish();
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mCastContext = CastContext.getSharedInstance(this);
        mSessionManager = mCastContext.getSessionManager();
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession.class);
    }

    @Override
    protected void onResume() {
        super.onResume();
        mCastSession = mSessionManager.getCurrentCastSession();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession.class);
    }
}

Transferencia de transmisión

La preservación del estado de la sesión es la base de la transferencia de transmisión, en la que los usuarios pueden mover transmisiones de audio y video existentes entre dispositivos con comandos por voz, la app de Google Home o pantallas inteligentes. El contenido multimedia deja de reproducirse en un dispositivo (el origen) y continúa en otro (el destino). Cualquier dispositivo de transmisión con el firmware más reciente puede servir como origen o destino en una transferencia de transmisión.

Para obtener el nuevo dispositivo de destino durante una transferencia o expansión de transmisión, registra un Cast.Listener con CastSession#addCastListener. Luego, llama a CastSession#getCastDevice() durante la onDeviceNameChanged devolución de llamada.

Consulta Transferencia de transmisión en Web Receiver para obtener más información.

Reconexión automática

El framework proporciona un ReconnectionService que la app emisora puede habilitar para controlar la reconexión en muchos casos extremos sutiles, como los siguientes:

  • Recuperación ante una pérdida temporal de Wi-Fi
  • Recuperación ante el modo de suspensión del dispositivo
  • Recuperación ante el paso de la app a segundo plano
  • Recuperación en caso de que la app falle

Este servicio está activado de forma predeterminada y se puede desactivar en CastOptions.Builder.

Este servicio se puede combinar automáticamente en el manifiesto de tu app si la combinación automática está habilitada en tu archivo de Gradle.

El framework iniciará el servicio cuando haya una sesión de contenido multimedia y lo detendrá cuando finalice la sesión de contenido multimedia.

Cómo funciona el control de contenido multimedia

El framework de Cast da de baja la RemoteMediaPlayer clase de Cast 2.x en favor de una nueva clase RemoteMediaClient, que proporciona la misma funcionalidad en un conjunto de APIs más convenientes y evita tener que pasar un GoogleApiClient.

Cuando tu app establece un CastSession con una app Web Receiver que admite el espacio de nombres de contenido multimedia, el framework creará automáticamente una instancia de RemoteMediaClient. Tu app puede acceder a ella llamando al método getRemoteMediaClient() en la instancia CastSession .

Todos los métodos de RemoteMediaClient que emiten solicitudes a Web Receiver mostrarán un objeto PendingResult que se puede usar para hacer un seguimiento de esa solicitud.

Se espera que la instancia de RemoteMediaClient pueda ser compartida por varias partes de tu app y, de hecho, algunos componentes internos del framework, como los minicontroles persistentes y el servicio de notificaciones. Para ello, esta instancia admite el registro de varias instancias de RemoteMediaClient.Listener.

Establece metadatos de contenido multimedia

La MediaMetadata clase representa la información sobre un elemento de contenido multimedia que deseas transmitir. En el siguiente ejemplo, se crea una instancia nueva de MediaMetadata de una película y se establecen el título, el subtítulo y dos imágenes.

Kotlin
val movieMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE)

movieMetadata.putString(MediaMetadata.KEY_TITLE, mSelectedMedia.getTitle())
movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, mSelectedMedia.getStudio())
movieMetadata.addImage(WebImage(Uri.parse(mSelectedMedia.getImage(0))))
movieMetadata.addImage(WebImage(Uri.parse(mSelectedMedia.getImage(1))))
Java
MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);

movieMetadata.putString(MediaMetadata.KEY_TITLE, mSelectedMedia.getTitle());
movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, mSelectedMedia.getStudio());
movieMetadata.addImage(new WebImage(Uri.parse(mSelectedMedia.getImage(0))));
movieMetadata.addImage(new WebImage(Uri.parse(mSelectedMedia.getImage(1))));

Consulta Selección de imágenes sobre el uso de imágenes con metadatos de contenido multimedia.

Carga contenido multimedia

Tu app puede cargar un elemento de contenido multimedia, como se muestra en el siguiente código. Primero, usa MediaInfo.Builder con los metadatos del contenido multimedia para compilar una instancia de MediaInfo. Obtén el RemoteMediaClient del CastSession actual y, luego, carga el MediaInfo en ese RemoteMediaClient. Usa RemoteMediaClient para reproducir, pausar y controlar de otras formas una app de reproductor de contenido multimedia que se ejecuta en Web Receiver.

Kotlin
val mediaInfo = MediaInfo.Builder(mSelectedMedia.getUrl())
    .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
    .setContentType("videos/mp4")
    .setMetadata(movieMetadata)
    .setStreamDuration(mSelectedMedia.getDuration() * 1000)
    .build()
val remoteMediaClient = mCastSession.getRemoteMediaClient()
remoteMediaClient.load(MediaLoadRequestData.Builder().setMediaInfo(mediaInfo).build())
Java
MediaInfo mediaInfo = new MediaInfo.Builder(mSelectedMedia.getUrl())
        .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
        .setContentType("videos/mp4")
        .setMetadata(movieMetadata)
        .setStreamDuration(mSelectedMedia.getDuration() * 1000)
        .build();
RemoteMediaClient remoteMediaClient = mCastSession.getRemoteMediaClient();
remoteMediaClient.load(new MediaLoadRequestData.Builder().setMediaInfo(mediaInfo).build());

Consulta también la sección sobre el uso de pistas de contenido multimedia.

Formato de video en 4K

Para verificar el formato de video de tu contenido multimedia, usa getVideoInfo() en MediaStatus para obtener la instancia actual de VideoInfo. Esta instancia contiene el tipo de formato de TV HDR y la altura y el ancho de la pantalla en píxeles. Las variantes del formato 4K se indican con las constantes HDR_TYPE_*.

Notificaciones de control remoto a varios dispositivos

Cuando un usuario transmite contenido, otros dispositivos Android en la misma red recibirán una notificación para permitirles controlar la reproducción. Cualquier persona cuyo dispositivo reciba esas notificaciones puede desactivarlas para ese dispositivo en la app de Configuración en Google > Google Cast > Mostrar notificaciones de control remoto. (Las notificaciones incluyen un acceso directo a la app de Configuración). Para obtener más detalles, consulta Notificaciones de control remoto de Cast.

Agrega un minicontrol

Según la Lista de tareas de diseño de Cast, una app emisora debe proporcionar un control persistente conocido como minicontrol que debe aparecer cuando el usuario abandona la página de contenido actual a otra parte de la app emisora. El minicontrol proporciona un recordatorio visible al usuario de la sesión de Cast actual. Si presiona el minicontrol, el usuario puede volver a la vista de control expandido de Cast en pantalla completa.

El framework proporciona una vista personalizada, MiniControllerFragment, que puedes agregar a la parte inferior del archivo de diseño de cada actividad en la que quieras mostrar el minicontrol.

<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" />

Cuando tu app emisora reproduce una transmisión en vivo de audio o video, el SDK muestra automáticamente un botón de reproducción/detención en lugar del botón de reproducción/pausa en el minicontrol.

Para establecer la apariencia del texto del título y el subtítulo de esta vista personalizada, y para elegir botones, consulta Personaliza el minicontrol.

Agrega un control expandido

La Lista de tareas de diseño de Google Cast requiere que una app emisora proporcione un control expandido para el contenido multimedia que se está transmitiendo. Este control expandido es una versión de pantalla completa del minicontrol.

El SDK de Cast proporciona un widget para el control expandido llamado ExpandedControllerActivity. 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ú para que el control expandido proporcione el botón para transmitir:

<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 una clase nueva que extienda ExpandedControllerActivity.

Kotlin
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
    }
}
Java
public class ExpandedControlsActivity extends ExpandedControllerActivity {
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        getMenuInflater().inflate(R.menu.expanded_controller, menu);
        CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item);
        return true;
    }
}

Ahora declara tu nueva actividad en el manifiesto de la app, en la etiqueta application:

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

Edita el CastOptionsProvider y cambia NotificationOptions y CastMediaOptions para establecer la actividad objetivo en tu nueva actividad:

Kotlin
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()
}
Java
public CastOptions getCastOptions(Context context) {
    NotificationOptions notificationOptions = new NotificationOptions.Builder()
            .setTargetActivityClassName(ExpandedControlsActivity.class.getName())
            .build();
    CastMediaOptions mediaOptions = new CastMediaOptions.Builder()
            .setNotificationOptions(notificationOptions)
            .setExpandedControllerActivityClassName(ExpandedControlsActivity.class.getName())
            .build();

    return new CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .setCastMediaOptions(mediaOptions)
            .build();
}

Actualiza el método loadRemoteMedia de LocalPlayerActivity para mostrar tu nueva actividad cuando se cargue el contenido multimedia remoto:

Kotlin
private fun loadRemoteMedia(position: Int, autoPlay: Boolean) {
    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(mSelectedMedia)
            .setAutoplay(autoPlay)
            .setCurrentTime(position.toLong()).build()
    )
}
Java
private void loadRemoteMedia(int position, boolean autoPlay) {
    if (mCastSession == null) {
        return;
    }
    final RemoteMediaClient remoteMediaClient = mCastSession.getRemoteMediaClient();
    if (remoteMediaClient == null) {
        return;
    }
    remoteMediaClient.registerCallback(new RemoteMediaClient.Callback() {
        @Override
        public void onStatusUpdated() {
            Intent intent = new Intent(LocalPlayerActivity.this, ExpandedControlsActivity.class);
            startActivity(intent);
            remoteMediaClient.unregisterCallback(this);
        }
    });
    remoteMediaClient.load(new MediaLoadRequestData.Builder()
            .setMediaInfo(mSelectedMedia)
            .setAutoplay(autoPlay)
            .setCurrentTime(position).build());
}

Cuando tu app emisora reproduce una transmisión en vivo de audio o video, el SDK muestra automáticamente un botón de reproducción/detención en lugar del botón de reproducción/pausa en el control expandido.

Para establecer la apariencia con temas, elegir qué botones mostrar, y agregar botones personalizados, consulta Personaliza el control expandido.

Control de volumen

El framework administra automáticamente el volumen de la app emisora. El framework sincroniza automáticamente las apps emisora y Web Receiver para que la IU del emisor siempre informe el volumen especificado por Web Receiver.

Control de volumen del botón físico

En Android, los botones físicos del dispositivo emisor se pueden usar para cambiar el volumen de la sesión de Cast en Web Receiver de forma predeterminada para cualquier dispositivo que use Jelly Bean o versiones posteriores.

Control de volumen del botón físico antes de Jelly Bean

Para usar las teclas físicas de volumen para controlar el volumen del dispositivo Web Receiver en dispositivos Android anteriores a Jelly Bean, la app emisora debe anular dispatchKeyEvent en sus actividades y llamar a CastContext.onDispatchVolumeKeyEventBeforeJellyBean():

Kotlin
class MyActivity : FragmentActivity() {
    override fun dispatchKeyEvent(event: KeyEvent): Boolean {
        return (CastContext.getSharedInstance(this)
            .onDispatchVolumeKeyEventBeforeJellyBean(event)
                || super.dispatchKeyEvent(event))
    }
}
Java
class MyActivity extends FragmentActivity {
    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        return CastContext.getSharedInstance(this)
            .onDispatchVolumeKeyEventBeforeJellyBean(event)
            || super.dispatchKeyEvent(event);
    }
}

Agrega controles de contenido multimedia a la notificación y la pantalla de bloqueo

Solo en Android, la Lista de tareas de diseño de Google Cast requiere que una app emisora implemente controles de contenido multimedia en una notificación y en la pantalla de bloqueo, en la que el emisor está transmitiendo, pero la app emisora no tiene el enfoque. El framework proporciona MediaNotificationService y MediaIntentReceiver para ayudar a la app emisora a compilar controles de contenido multimedia en una notificación y en la pantalla de bloqueo.

MediaNotificationService se ejecuta cuando el emisor está transmitiendo y mostrará una notificación con una miniatura de imagen y información sobre el elemento que se esté transmitiendo, un botón de reproducción/pausa y un botón para detener.

MediaIntentReceiver es un BroadcastReceiver que controla las acciones del usuario desde la notificación.

Tu app puede configurar la notificación y el control de contenido multimedia desde la pantalla de bloqueo a través de NotificationOptions. Tu app puede configurar qué botones de control mostrar en la notificación y qué Activity abrir cuando el usuario presione la notificación. Si no se proporcionan acciones de forma explícita, se usarán los valores predeterminados, MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK y MediaIntentReceiver.ACTION_STOP_CASTING.

Kotlin
// Example showing 4 buttons: "rewind", "play/pause", "forward" and "stop casting".
val buttonActions: MutableList<String> = ArrayList()
buttonActions.add(MediaIntentReceiver.ACTION_REWIND)
buttonActions.add(MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK)
buttonActions.add(MediaIntentReceiver.ACTION_FORWARD)
buttonActions.add(MediaIntentReceiver.ACTION_STOP_CASTING)

// Showing "play/pause" and "stop casting" in the compat view of the notification.
val compatButtonActionsIndices = intArrayOf(1, 3)

// Builds a notification with the above actions. Each tap on the "rewind" and "forward" buttons skips 30 seconds.
// Tapping on the notification opens an Activity with class VideoBrowserActivity.
val notificationOptions = NotificationOptions.Builder()
    .setActions(buttonActions, compatButtonActionsIndices)
    .setSkipStepMs(30 * DateUtils.SECOND_IN_MILLIS)
    .setTargetActivityClassName(VideoBrowserActivity::class.java.name)
    .build()
Java
// Example showing 4 buttons: "rewind", "play/pause", "forward" and "stop casting".
List<String> buttonActions = new ArrayList<>();
buttonActions.add(MediaIntentReceiver.ACTION_REWIND);
buttonActions.add(MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK);
buttonActions.add(MediaIntentReceiver.ACTION_FORWARD);
buttonActions.add(MediaIntentReceiver.ACTION_STOP_CASTING);

// Showing "play/pause" and "stop casting" in the compat view of the notification.
int[] compatButtonActionsIndices = new int[]{1, 3};

// Builds a notification with the above actions. Each tap on the "rewind" and "forward" buttons skips 30 seconds.
// Tapping on the notification opens an Activity with class VideoBrowserActivity.
NotificationOptions notificationOptions = new NotificationOptions.Builder()
    .setActions(buttonActions, compatButtonActionsIndices)
    .setSkipStepMs(30 * DateUtils.SECOND_IN_MILLIS)
    .setTargetActivityClassName(VideoBrowserActivity.class.getName())
    .build();

La visualización de los controles de contenido multimedia desde la notificación y la pantalla de bloqueo está activada de forma predeterminada y se puede inhabilitar llamando a setNotificationOptions con un valor nulo en CastMediaOptions.Builder. Actualmente, la función de pantalla de bloqueo se activará siempre que la notificación esté activada.

Kotlin
// ... continue with the NotificationOptions built above
val mediaOptions = CastMediaOptions.Builder()
    .setNotificationOptions(notificationOptions)
    .build()
val castOptions: CastOptions = Builder()
    .setReceiverApplicationId(context.getString(R.string.app_id))
    .setCastMediaOptions(mediaOptions)
    .build()
Java
// ... continue with the NotificationOptions built above
CastMediaOptions mediaOptions = new CastMediaOptions.Builder()
        .setNotificationOptions(notificationOptions)
        .build();
CastOptions castOptions = new CastOptions.Builder()
        .setReceiverApplicationId(context.getString(R.string.app_id))
        .setCastMediaOptions(mediaOptions)
        .build();

Cuando tu app emisora reproduce una transmisión en vivo de audio o video, el SDK muestra automáticamente un botón de reproducción/detención en lugar del botón de reproducción/pausa en el control de notificación, pero no en el control de pantalla de bloqueo.

Nota: Para mostrar los controles de pantalla de bloqueo en dispositivos anteriores a Lollipop, RemoteMediaClient solicitará automáticamente el enfoque de audio en tu nombre.

Soluciona errores

Es muy importante que las apps emisoras controlen todas las devoluciones de llamada de error y decidan la mejor respuesta para cada etapa del ciclo de vida de Cast. La app puede mostrar diálogos de error al usuario o puede decidir desconectar la conexión a Web Receiver.