En esta guía para desarrolladores, se describe cómo agregar la compatibilidad con Google Cast a tu app de Android para remitentes mediante el SDK de Android Sender.
El dispositivo móvil o la laptop es 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 del remitente se refiere al objeto binario de la biblioteca de clases de Cast y a los recursos asociados presentes en el entorno 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 de 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 general de alto nivel de una app para Android de remitentes:
- El framework de Cast inicia automáticamente la detección de dispositivos
MediaRouter
según el ciclo de vida deActivity
. - 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 del 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 Receptor web.
- El framework 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 framework sincroniza el estado de reproducción de contenido multimedia entre el remitente y el receptor web: cuando el usuario realiza las acciones de IU del remitente, el framework pasa esas solicitudes de control de contenido multimedia al receptor web y cuando el receptor envía actualizaciones de estado multimedia, el framework actualiza el estado de la IU del remitente.
- Cuando el usuario haga clic en el botón para transmitir y se desconecte 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 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.
Cómo configurar 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 objetivo de la API 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 a una versión mínima del SDK de Android que sea anterior a la versión Lollipop.
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat" >
...
</application>
Inicializa el contexto de transmisión
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.
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 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()
.
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); } }
Los widgets de UX de Cast
El framework de Cast proporciona los widgets que cumplen con la lista de tareas de diseño de transmisión:
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 remitente 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 que es compatible con la app. Cuando el usuario hace clic por primera vez en ese botón, 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 le permite desconectarse del dispositivo de transmisión.
MiniController: Cuando el usuario transmite contenido y sale de la página de contenido actual o del control expandido a otra pantalla en la app emisora, se muestra el minicontrolador en la parte inferior de la pantalla de modo que el usuario pueda ver los metadatos multimedia que se están transmitiendo y controlar la reproducción.
Control expandido: Cuando el usuario transmite contenido, si hace clic en el minicontrol o la notificación multimedia, se inicia el control expandido, que muestra los metadatos del contenido multimedia en reproducción y proporciona varios botones para controlar la reproducción multimedia.
Notificación: Solo para Android. Cuando el usuario transmite contenido y sale de la app de remitente, se muestra una notificación multimedia con los metadatos y controles de reproducción de contenido multimedia actuales.
Pantalla de bloqueo: Solo para Android. Cuando el usuario transmite 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 del contenido multimedia y los controles de reproducción que se encuentran en ese momento.
En la siguiente guía, se incluyen descripciones para agregar estos widgets a tu app.
Agrega un botón para transmitir
Las APIs 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 seleccionen una ruta de contenido multimedia para reproducir contenido en un dispositivo secundario, como un dispositivo de transmisión.
El framework hace que agregar un MediaRouteButton
como Cast button
sea muy fácil. 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" />
// 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 el aspecto 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 la detección de dispositivos por completo.
Cuando se inicializa CastContext, la app emisora especifica el ID de aplicación del receptor web y, de forma opcional, puede solicitar el filtrado de espacios de nombres configurando supportedNamespaces
en CastOptions
.
CastContext
contiene una referencia a MediaRouter
de forma interna y comienza el proceso de descubrimiento cuando la app de remitente pasa al primer plano y se detiene cuando pasa.
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 una sesión de transmisión, cuyo establecimiento combina los pasos para conectarse a un dispositivo, iniciar (o unirse) una app receptora 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.
Las clases se administran mediante la clase SessionManager
, a la que tu app puede acceder mediante CastContext.getSessionManager()
.
Las sesiones individuales se representan con subclases de la clase Session
.
Por ejemplo, CastSession
representa sesiones con dispositivos de transmisión. La app puede acceder a la sesión de transmisión activa en ese momento 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 automáticamente reanudarse luego de 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
.
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 intencionales y no se deben registrar como error.
Si necesitas conocer los cambios de estado de la sesión, puedes implementar una 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 } 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 } }
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(); } @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, en la que los usuarios pueden mover transmisiones de audio y video existentes entre 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 funcionar 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 manejar la reconexión en muchos casos sutiles de la esquina, como los siguientes:
- Cómo recuperarte de una pérdida temporal de la conexión Wi-Fi
- Recuperar del estado de suspensión del dispositivo
- Cómo recuperar datos de la app en segundo plano
- Recuperar si falló la app
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 multimedia y lo detendrá cuando finalice.
Cómo funciona el control multimedia
El framework de Cast da 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 establezca un CastSession
con una app receptora web que admita el espacio de nombres multimedia, se creará automáticamente una instancia de RemoteMediaClient
en el framework. Para acceder a ella, se llamará al método getRemoteMediaClient()
en la instancia CastSession
.
Todos los métodos de RemoteMediaClient
que envían solicitudes al receptor web mostrarán un objeto PendingResult que se puede usar para realizar un seguimiento de esa solicitud.
Se espera que la instancia de RemoteMediaClient
se comparta con varias partes de tu app y, de hecho, algunos componentes internos del framework, como los minicontroladores persistentes y el servicio de notificaciones.
Para ello, esta instancia admite el registro de varias instancias de RemoteMediaClient.Listener
.
Configura metadatos de contenido multimedia
La clase MediaMetadata
representa la información de 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.
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 la selección de imágenes para obtener información sobre el uso de imágenes con metadatos multimedia.
Carga de 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 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.
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 el uso de pistas de contenido multimedia.
Formato de video 4K
Si quieres 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 HDR TV y la altura y el ancho en píxeles. Las variantes de formato 4K se indican con constantes HDR_TYPE_*
.
Notificaciones del control remoto para múltiples dispositivos
Cuando un usuario transmita contenido, otros dispositivos Android de la misma red recibirán una notificación para también controlar la reproducción. Cualquier usuario cuyo dispositivo reciba estas 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 app de Configuración). Para obtener más detalles, consulta Notificaciones del control remoto de transmisión.
Agregar minicontrol
Según la lista de tareas de diseño de Cast, una app emisora debe proporcionar un control persistente conocido como el minicontrolador que debe aparecer cuando el usuario sale de la página de contenido actual hacia otra parte de la app emisora. El minicontrolador proporciona un recordatorio visible al usuario de la sesión de transmisión actual. Si el usuario presiona el minicontrol, puede regresar a la vista expandida del control remoto de Cast en pantalla completa.
El framework proporciona una vista personalizada, MiniControllerFragment, que puedes agregar en la parte inferior del archivo de diseño de cada actividad en la que deseas 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 la app emisora está reproduciendo 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 definir el aspecto del texto del título y los subtítulos de esta vista personalizada, y seleccionar botones, consulta Cómo personalizar el minicontrol.
Agregar control expandido
La lista de tareas de diseño de Google Cast requiere que la app emisora proporcione un control expandido para el contenido multimedia que se está transmitiendo. El control expandido es una versión de pantalla completa del minicontrol.
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
.
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 CastOptionsProvider
y cambia NotificationOptions
y CastMediaOptions
para establecer la actividad objetivo como la nueva:
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 la app emisora está reproduciendo 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 establecer la apariencia con temas, elige los botones que deseas mostrar y agrega botones personalizados, consulta Cómo personalizar el controlador expandido.
Control de volumen
El framework administra automáticamente el volumen de la app emisora. El framework sincroniza automáticamente las apps del remitente y el receptor web a fin de que la IU del remitente siempre informe el volumen que especifica el receptor web.
Control del 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 para cualquier dispositivo que use Jelly Bean o una versión más reciente.
Control del volumen del botón físico antes de Jelly Bean
Para usar las teclas de volumen físicas a fin de controlar el volumen del dispositivo del 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 las notificaciones 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 remitente transmite contenido, 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 el objeto 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 explícitas, 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();
Los controles multimedia que se muestran en las notificaciones y la pantalla de bloqueo están activados de forma predeterminada y se pueden inhabilitar llamando a setNotificationOptions
con el valor nulo en CastMediaOptions.Builder
.
Actualmente, la función de pantalla de bloqueo se activará 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 la app emisora está reproduciendo una transmisión de video en vivo o audio, el SDK muestra automáticamente un botón de reproducción/detención en lugar del de reproducción/pausa en el control de notificaciones, pero no en el de la pantalla de bloqueo.
Nota: Para mostrar los controles de la pantalla de bloqueo en dispositivos con versiones anteriores a la versión Lollipop, RemoteMediaClient
solicitará automáticamente el enfoque de audio por ti.
Cómo solucionar 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 eliminar la conexión con el receptor web.