Cómo integrar Cast a tu app para Android

Organiza tus páginas con colecciones Guarda y categoriza el contenido según tus preferencias.

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

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

El marco de trabajo de remitente se refiere al objeto binario de la biblioteca de clases de Cast y a los recursos asociados presentes durante el tiempo de ejecución en el remitente. La app de remitente o la app de Cast hacen referencia a una app que también se ejecuta en el remitente. La app receptora web hace referencia a la aplicación HTML que se ejecuta en el dispositivo compatible con Cast.

El framework del remitente usa un diseño de devolución de llamada asíncrono para informar a la app emisora sobre los eventos y hacer 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 de una app emisora para Android:

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

Para obtener una lista completa de todas las clases, los métodos y eventos en el SDK de Google Cast para Android, consulta la Referencia de la API del remitente de Google Cast 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 la app requiere que configures los siguientes elementos para el SDK de Cast:

uses-sdk

Establece los niveles mínimos y de la API de destino de Android que admite el SDK de Cast. Actualmente, el mínimo es el nivel de API 19 y el objetivo es el nivel de API 28.

<uses-sdk
        android:minSdkVersion="19"
        android:targetSdkVersion="28" />

android:tema

Configura 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 orientes una versión mínima del SDK de Android que sea 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 interfaz OptionsProvider a fin de proporcionar las opciones necesarias para inicializar el singleton CastContext. OptionsProvider proporciona una instancia de CastOptions que contiene opciones que afectan el comportamiento del framework. El más importante es el ID de aplicación del receptor web, que se usa para filtrar los resultados de descubrimiento y para iniciar la app del receptor web cuando se inicia una sesión de transmisión.

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 un receptor está 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 cuando se detecta un receptor compatible con tu app. Cuando el usuario hace clic por primera vez en el botón para transmitir, se muestra un diálogo para transmitir en el que se enumeran los dispositivos detectados. Cuando el usuario hace clic en el botón para transmitir mientras el dispositivo está conectado, muestra los metadatos multimedia actuales (como el título, el nombre del estudio de grabación y una imagen en miniatura) o permite que el usuario se desconecte del dispositivo de transmisión.

  • Control mínimo: Cuando el usuario está transmitiendo contenido y se fue de la página de contenido actual o se expandió a otra pantalla en la app emisora, el minicontrolador se muestra en la parte inferior de la pantalla para permitir que el usuario vea los metadatos de la transmisión en curso y controle la reproducción.

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

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

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

En la siguiente guía, se incluyen descripciones para agregar estos widgets a tu app.

Agregar un botón para transmitir

Las API de MediaRouter de Android 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, a fin de 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 hace que sea muy fácil agregar una MediaRouteButton como Cast button. Primero, debes agregar un elemento de menú o un MediaRouteButton en el archivo en formato 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 una 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 configurar la apariencia del botón para transmitir con un tema, consulta Cómo personalizar el botón para transmitir.

Configura la detección de dispositivos

CastContext administra por completo el descubrimiento de dispositivos. Cuando se inicializa CastContext, la app emisora especifica el ID de aplicación del receptor web y, de manera opcional, puede solicitar el filtrado del espacio de nombres mediante la configuración de supportedNamespaces en CastOptions. CastContext contiene una referencia al MediaRouter de forma interna y comienza el proceso de descubrimiento cuando la app emisora pasa a primer plano y se detiene cuando la app emisora pasa 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 transmisión, cuyo establecimiento combina los pasos para conectarse a un dispositivo, iniciar (o unirse) a una app de receptor web, conectarse a esa app e inicializar un canal de control de contenido multimedia. Consulta la Guía del ciclo de vida de la aplicación del receptor web para obtener más información sobre las sesiones de transmisión y el ciclo de vida del receptor web.

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

Tu app puede usar la clase SessionManagerListener para supervisar eventos de sesión, como creación, suspensión, reanudación y finalización. El framework intenta reanudarse automáticamente tras una finalización anormal o abrupta mientras una sesión estaba activa.

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

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

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

    private inner class SessionManagerListenerImpl : SessionManagerListener<CastSession?> {
        override fun onSessionStarted(session: CastSession?, sessionId: String) {
            invalidateOptionsMenu()
        }

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

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

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mSessionManager = CastContext.getSharedInstance(this).sessionManager
    }

    override fun onResume() {
        super.onResume()
        mCastSession = mSessionManager.currentCastSession
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession::class.java)
    }

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

    private class SessionManagerListenerImpl implements SessionManagerListener<CastSession> {
        @Override
        public void onSessionStarted(CastSession session, String sessionId) {
            invalidateOptionsMenu();
        }
        @Override
        public void onSessionResumed(CastSession session, boolean wasSuspended) {
            invalidateOptionsMenu();
        }
        @Override
        public void onSessionEnded(CastSession session, int error) {
            finish();
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mSessionManager = CastContext.getSharedInstance(this).getSessionManager();
    }
    @Override
    protected void onResume() {
        super.onResume();
        mCastSession = mSessionManager.getCurrentCastSession();
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession.class);
    }
    @Override
    protected void onPause() {
        super.onPause();
        mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession.class);
        mCastSession = null;
    }
}

Transferencia de transmisión

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

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

Consulta Transferencia de transmisión en el receptor web para obtener más información.

Reconexión automática

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

  • Cómo recuperarte de una pérdida temporal de Wi-Fi
  • Recuperar dispositivo suspendido
  • Recuperación de apps en segundo plano
  • Cómo recuperar si la app falló

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 está habilitada la fusión automática en el archivo de Gradle.

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

Cómo funciona el control de medios

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

Cuando tu app establece un CastSession con una app receptora web que admite el espacio de nombres de medios, el framework creará una instancia de RemoteMediaClient de forma automática. Para acceder a ella, se llamará al método getRemoteMediaClient() en la instancia CastSession.

Todos los métodos de RemoteMediaClient que emiten solicitudes al receptor web mostrarán un objeto PendingResult que se puede usar para realizar un seguimiento de esa solicitud.

Se espera que varias instancias de tu app compartan la instancia de RemoteMediaClient y, de hecho, algunos componentes internos del framework, como los minicontroladores persistentes y el servicio de notificaciones. Con ese fin, esta instancia admite el registro de varias instancias de RemoteMediaClient.Listener.

Establecer metadatos de medios

La clase MediaMetadata representa la información sobre un elemento multimedia que deseas transmitir. En el siguiente ejemplo, se crea una nueva instancia 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 la selección de imágenes sobre el uso de imágenes con metadatos multimedia.

Cargar medios

Tu app puede cargar un elemento 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 otra manera una app de reproducción multimedia que se ejecute en el receptor web.

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 multimedia.

Formato de video 4K

Para verificar el formato del video, usa getVideoInfo() en MediaStatus a fin de 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 de formato 4K se indican mediante constantes HDR_TYPE_*.

Notificaciones del control remoto para varios dispositivos

Cuando un usuario esté transmitiendo, otros dispositivos Android de la misma red recibirán una notificación para también permitirle controlar la reproducción. Cualquier persona que reciba este tipo de notificaciones puede desactivarlas para ese dispositivo en la app de Configuración en Google > Google Cast > Mostrar notificaciones del control remoto. (Las notificaciones incluyen un acceso directo a la aplicación Configuración). Para obtener más detalles, consulta Notificaciones sobre el control remoto de Cast.

Agregar minicontrolador

De acuerdo con la lista de tareas de diseño de Cast, una app emisora debe proporcionar un control persistente conocido como el minicontrolador que debería aparecer cuando el usuario salga de la página de contenido actual hacia otra parte de la app emisora. El minicontrolador proporciona un recordatorio visible para el usuario de la sesión de transmisión actual. Cuando el usuario presiona el minicontrolador, puede volver a la vista expandida del control remoto de Cast.

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 deseas mostrar el controlador mini.

<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 la app emisora reproduce una transmisión en vivo de audio o video, el SDK muestra automáticamente un botón de reproducción/pausa en lugar del botón de reproducción/pausa del minicontrolador.

Para configurar la apariencia del texto del título y el subtítulo de esta vista personalizada, y para elegir los botones, consulta Personalizar el minicontrolador.

Agregar control expandido

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

El SDK de Cast proporciona un widget para el controlador 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 archivo de recursos de menú nuevo para que el controlador 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 dentro de 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 LocalPlayerActivity loadRemoteMedia 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 la app emisora reproduce una transmisión en vivo de audio o video, el SDK muestra automáticamente un botón de reproducción/pausa en lugar del botón de reproducción/pausa del control expandido.

Para configurar la apariencia mediante temas, elige los botones que quieres mostrar y agrega botones personalizados, consulta Cómo personalizar el control expandido.

Control de volumen

El framework administra de forma automática el volumen de la app emisora. El framework sincroniza de manera automática las apps del remitente y del receptor web para que la IU del remitente siempre informe el volumen especificado por el receptor web.

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 transmisión en el receptor web de forma predeterminada en cualquier dispositivo que utilice Jelly Bean o una versión más reciente.

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

Si quieres usar las teclas de volumen físicas para controlar el volumen del dispositivo del receptor web en dispositivos Android anteriores a Jelly Bean, la app emisora debe anular dispatchKeyEvent en su actividad 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);
    }
}

Cómo agregar controles multimedia a las notificaciones y la pantalla bloqueada

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

MediaNotificationService se ejecuta cuando el remitente está transmitiendo y mostrará una notificación con la miniatura de la imagen y la información sobre el elemento de transmisión actual, 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 las notificaciones y el control multimedia desde la pantalla de bloqueo hasta NotificationOptions. Tu app puede configurar qué botones de control mostrar en la notificación y qué Activity abrir cuando el usuario presiona 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 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 la app emisora reproduce una transmisión en vivo de audio o video, el SDK muestra automáticamente un botón de reproducción o detención en lugar del botón de reproducción/pausa en el control de notificaciones, pero no en el de bloqueo de pantalla.

Nota: Para mostrar los controles de pantalla de bloqueo en dispositivos anteriores a la versión Lollipop, RemoteMediaClient solicitará automáticamente el foco de audio por ti.

Cómo solucionar errores

Es muy importante que las apps emisoras manejen 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 eliminar la conexión con el receptor web.