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 emisores para Android.
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 de remitente hace referencia al objeto binario de la biblioteca de clases de Cast y a los recursos asociados presentes en el entorno de ejecución del remitente. La app emisora o app de Cast hace referencia a una app que también se ejecuta en el emisor. La app de receptor web hace referencia a la aplicación HTML que se ejecuta en el dispositivo compatible con Cast.
El framework de remitente 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 para Android emisora:
- El framework de Cast inicia automáticamente el descubrimiento de dispositivos
MediaRoutersegún el ciclo de vida deActivity. - 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 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 devoluciones de llamada en la app emisora para confirmar que se inició la app de Web Receiver.
- El framework crea un canal de comunicación entre las apps emisora y receptora de Web.
- El framework usa el canal de comunicación para cargar y controlar la reproducción de contenido multimedia en el receptor web.
- El framework sincroniza el estado de reproducción de contenido multimedia entre el emisor y el receptor web: cuando el usuario realiza acciones en la IU del emisor, el framework pasa esas solicitudes de control de contenido multimedia al receptor web, y cuando el receptor web envía actualizaciones de estado de contenido multimedia, el framework actualiza el estado de la IU del emisor.
- Cuando el usuario haga clic en el botón para transmitir para desconectarse del dispositivo de transmisión, el framework desconectará la app emisora del receptor web.
Para obtener una lista completa de todas las clases, los métodos y los eventos del SDK de Android de Google Cast, 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
En el archivo AndroidManifest.xml de tu app, debes configurar 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 nivel de API mínimo es 24 y el objetivo es 35.
<uses-sdk
android:minSdkVersion="24"
android:targetSdkVersion="35" />
android:theme
Establece el tema de tu app según 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 la app 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 interfaz OptionsProvider para 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 de ellos es el ID de aplicación del receptor web, que se usa para filtrar los resultados de descubrimiento y para iniciar la app de receptor web cuando se inicia una sesión de transmisión.
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 } }
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 de la OptionsProvider implementada 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().
class MyActivity : FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { val castContext = CastContext.getSharedInstance(this) } }
public class MyActivity extends FragmentActivity { @Override public void onCreate(Bundle savedInstanceState) { CastContext castContext = CastContext.getSharedInstance(this); } }
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 de introducción: 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, independientemente de la disponibilidad de los dispositivos de transmisión. Cuando el usuario hace clic por primera vez en el botón para transmitir, se muestra un diálogo de transmisión en el que se enumeran los dispositivos descubiertos. 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, el "botón para transmitir" se denomina "ícono para transmitir".
Controlador mini: Cuando el usuario transmite contenido y sale de la página de contenido actual o del controlador expandido para ir a otra pantalla de la app emisora, el controlador mini se muestra en la parte inferior de la pantalla para que el usuario pueda ver los metadatos del contenido multimedia que se está transmitiendo y controlar la reproducción.
Controlador expandido: Cuando el usuario transmite contenido, si hace clic en la notificación de contenido multimedia o en el minicontrolador, se inicia el controlador expandido, que muestra los metadatos del contenido multimedia que se está reproduciendo y proporciona varios botones para controlar la reproducción de contenido multimedia.
Notificación: Solo para Android. Cuando el usuario transmite contenido y sale de la app emisora, se muestra una notificación de contenido multimedia con los metadatos del contenido multimedia que se está transmitiendo y los controles de reproducción.
Pantalla de bloqueo: Solo para Android. Cuando el usuario transmite contenido y navega (o se 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 del contenido multimedia que se está 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 MediaRouter de Android están diseñadas para permitir la visualización y reproducció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 que los usuarios puedan seleccionar una ruta de acceso de contenido multimedia y reproducirlo 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" />
// 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 }
// 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>
// 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) }
// 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 Cómo personalizar el botón para transmitir.
Configura la detección de dispositivos
El descubrimiento de dispositivos lo administra por completo el CastContext.
Cuando se inicializa CastContext, la app emisora especifica el ID de aplicación del receptor web y, de manera opcional, puede solicitar el filtrado de espacios de nombres configurando supportedNamespaces en CastOptions.
CastContext contiene una referencia a MediaRouter de forma interna y comenzará el proceso de descubrimiento en las siguientes condiciones:
- Según un algoritmo diseñado para equilibrar la latencia de descubrimiento de dispositivos y el uso de la batería, el descubrimiento se iniciará automáticamente de vez en cuando cuando la app emisora pase al primer plano.
- Se abrió el diálogo de transmisión.
- El SDK de Cast está intentando recuperar una sesión de Cast.
El proceso de descubrimiento se detendrá cuando se cierre el diálogo de Cast o la app emisora pase a segundo plano.
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 } }
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 sesión de Cast, cuyo establecimiento combina los pasos para conectarse a un dispositivo, iniciar una app de receptor web (o unirse a ella), 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 del receptor web para obtener más información sobre las sesiones de Cast y el ciclo de vida del receptor web.
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 con subclases de la clase Session.
Por ejemplo,
CastSession
representa las sesiones con dispositivos de transmisión. Tu app puede acceder a la sesión de Cast activa en ese momento a través de SessionManager.getCurrentCastSession().
Tu app puede usar la clase SessionManagerListener para supervisar eventos de sesión, como la creación, la suspensión, la reanudación y la finalización. El framework intenta reanudar automáticamente la sesión desde una finalización anormal o abrupta mientras estaba activa.
Las sesiones se crean y se cierran automáticamente en respuesta a los gestos del usuario desde 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 la 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 se deben registrar como errores.
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 un Activity.
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) } }
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 (la fuente) y continúa en otro (el destino). Cualquier dispositivo Cast con el firmware más reciente puede funcionar como origen o destino en una transferencia de transmisión.
Para obtener el nuevo dispositivo de destino durante una transferencia de transmisión o expansió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 un
ReconnectionService
que la app emisora puede habilitar para controlar la reconexión en muchos casos límite
sutiles, como los siguientes:
- Recuperarse de una pérdida temporal de Wi-Fi
- Cómo recuperar el dispositivo del modo de suspensión
- Recuperación después de que la app se ejecuta en segundo plano
- Recuperación 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 con 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 multimedia y lo detendrá cuando esta finalice.
Cómo funcionan los controles multimedia
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 APIs más convenientes y evita tener que pasar un GoogleApiClient.
Cuando tu app establece una CastSession con una app de receptor web 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 de CastSession.
Todos los métodos de RemoteMediaClient que emiten solicitudes al receptor web devolverán un objeto PendingResult que se puede usar para hacer un seguimiento de esa solicitud.
Se espera que varias partes de tu app, y algunos componentes internos del framework, como los minicontroladores persistentes y el servicio de notificaciones, compartan la instancia de RemoteMediaClient.
Para ello, esta instancia admite el registro de varias instancias de RemoteMediaClient.Listener.
Configura los metadatos de contenido multimedia
La clase MediaMetadata representa la información sobre un elemento multimedia que quieres 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.
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))))
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 para obtener información sobre el uso de imágenes con metadatos de contenido multimedia.
Carga contenido multimedia
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 objeto 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 multimedia que se ejecuta en el receptor web.
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())
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 cómo usar pistas multimedia.
Formato de video 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 que estén en la misma red recibirán una notificación para que también puedan controlar la reproducción. Cualquier persona cuyo dispositivo reciba estas 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.
Cómo agregar un minicontrolador
Según la lista de tareas de diseño de Cast, una app emisora debe proporcionar un control persistente conocido como minicontrolador, que debería aparecer cuando el usuario sale de la página de contenido actual para ir a otra parte de la app emisora. El minicontrolador proporciona un recordatorio visible al usuario de la sesión de Cast actual. Si presiona el minicontrolador, el usuario puede volver a la vista del controlador expandido de pantalla completa 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 quieras mostrar el minicontrolador.
<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 video o audio, el SDK muestra automáticamente un botón de reproducción/detención en lugar del botón de reproducción/pausa en el minicontrolador.
Para establecer la apariencia del texto del título y el subtítulo de esta vista personalizada y elegir botones, consulta Cómo personalizar el minicontrolador.
Agrega un controlador 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 en pantalla completa del control mini.
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 de 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.
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 } }
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 de destino en tu nueva actividad:
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() }
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:
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() ) }
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 video o audio, 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 configurar la apariencia con temas, elegir qué botones mostrar y agregar botones personalizados, consulta Cómo personalizar el control expandido.
Control de volumen
El framework administra automáticamente el volumen de la app emisora y sincroniza las apps emisora y de Web Receiver para que la IU de la app emisora siempre informe el volumen especificado por el Web Receiver.
Control de volumen con botones físicos
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 use Jelly Bean o una versión posterior.
Control de volumen con botones físicos antes de Jelly Bean
Para usar las teclas de volumen físicas para controlar el volumen del dispositivo receptor web en dispositivos Android anteriores a Jelly Bean, la app emisora debe anular dispatchKeyEvent en sus actividades y llamar a CastContext.onDispatchVolumeKeyEventBeforeJellyBean():
class MyActivity : FragmentActivity() { override fun dispatchKeyEvent(event: KeyEvent): Boolean { return (CastContext.getSharedInstance(this) .onDispatchVolumeKeyEventBeforeJellyBean(event) || super.dispatchKeyEvent(event)) } }
class MyActivity extends FragmentActivity { @Override public boolean dispatchKeyEvent(KeyEvent event) { return CastContext.getSharedInstance(this) .onDispatchVolumeKeyEventBeforeJellyBean(event) || super.dispatchKeyEvent(event); } }
Agrega controles multimedia a la notificación y a la pantalla de bloqueo
Solo en Android, la lista de tareas de diseño de Google Cast requiere que una app emisora implemente controles multimedia en una notificación y en la pantalla de bloqueo, donde el emisor transmite, pero la app emisora no tiene el 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 transmite contenido y muestra una notificación con una miniatura de imagen y la información sobre el elemento que se transmite, un botón de reproducción/pausa y un botón de detención.
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 a través de NotificationOptions.
Tu app puede configurar qué botones de control se mostrarán en la notificación y qué Activity se 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.
// 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()
// 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 está activada siempre que la notificación esté activada.
// ... 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()
// ... 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 video o audio, 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 notificaciones, pero no en el control de la pantalla de bloqueo.
Nota: Para mostrar los controles de la pantalla de bloqueo en dispositivos anteriores a Lollipop, RemoteMediaClient solicitará automáticamente el foco 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 decidir cerrar la conexión con el receptor web.