Cómo agregar funciones principales a tu app receptora de Android TV

En esta página, se incluyen fragmentos de código y descripciones de las funciones disponibles para personalizar una app de receptor de Android TV.

Configura bibliotecas

Para que las APIs de Cast Connect estén disponibles en tu app de Android TV, haz lo siguiente:

Android
  1. Abre el archivo build.gradle dentro del directorio del módulo de tu aplicación.
  2. Verifica que google() esté incluido en el repositories que aparece en la lista.
      repositories {
        google()
      }
  3. Según el tipo de dispositivo objetivo de tu app, agrega las versiones más recientes de las bibliotecas a tus dependencias:
    • Para la app de Android Receiver, haz lo siguiente:
        dependencies {
          implementation 'com.google.android.gms:play-services-cast-tv:21.1.1'
          implementation 'com.google.android.gms:play-services-cast:22.1.0'
        }
    • En el caso de la app emisora de Android, haz lo siguiente:
        dependencies {
          implementation 'com.google.android.gms:play-services-cast:21.1.1'
          implementation 'com.google.android.gms:play-services-cast-framework:22.1.0'
        }
    Asegúrate de actualizar este número de versión cada vez que se actualicen los servicios.
  4. Guarda los cambios y haz clic en Sync Project with Gradle Files en la barra de herramientas.
iOS
  1. Asegúrate de que tu Podfile se oriente a google-cast-sdk 4.8.3 o una versión posterior.
  2. Segmenta para iOS 14 o versiones posteriores. Consulta las notas de la versión para obtener más detalles.
      platform: ios, '14'
    
      def target_pods
         pod 'google-cast-sdk', '~>4.8.3'
      end
Web
  1. Requiere la versión M87 o posterior del navegador Chromium.
  2. Agrega la biblioteca de la API de Web Sender a tu proyecto
      <script src="//www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1"></script>

Requisito de AndroidX

Las versiones nuevas de los Servicios de Google Play requieren que se actualice una app para usar el espacio de nombres androidx. Sigue las instrucciones para migrar a AndroidX.

App para Android TV: Requisitos previos

Para que tu app de Android TV sea compatible con Cast Connect, deberás crear y admitir eventos de una sesión multimedia. Los datos que proporciona tu sesión de medios brindan la información básica (por ejemplo, la posición, el estado de reproducción, etcétera) para el estado de tus medios. La biblioteca de Cast Connect también usa tu sesión multimedia para indicar que recibió determinados mensajes de una emisora, como un evento de pausa.

Para obtener más información sobre la sesión multimedia y cómo inicializarla, consulta la guía para trabajar con una sesión multimedia.

Ciclo de vida de la sesión multimedia

Tu app debe crear una sesión multimedia cuando comience la reproducción y liberarla cuando ya no se pueda controlar. Por ejemplo, si tu app es de video, debes liberar la sesión cuando el usuario salga de la actividad de reproducción, ya sea seleccionando "Atrás" para explorar otro contenido o ejecutando la app en segundo plano. Si tu app es de música, debes liberar la sesión cuando la app ya no reproduzca contenido multimedia.

Actualización del estado de la sesión

Los datos de tu sesión multimedia deben mantenerse actualizados con el estado de tu reproductor. Por ejemplo, cuando se pausa la reproducción, debes actualizar el estado de reproducción y las acciones admitidas. En las siguientes tablas, se enumeran los estados que debes mantener actualizados.

MediaMetadataCompat

Campo de metadatos Descripción
METADATA_KEY_TITLE (obligatorio) Es el título del contenido multimedia.
METADATA_KEY_DISPLAY_SUBTITLE Es el subtítulo.
METADATA_KEY_DISPLAY_ICON_URI Es la URL del ícono.
METADATA_KEY_DURATION (obligatorio) Es la duración del contenido multimedia.
METADATA_KEY_MEDIA_URI Es el ID de contenido.
METADATA_KEY_ARTIST El artista
METADATA_KEY_ALBUM El álbum

PlaybackStateCompat

Método obligatorio Descripción
setActions() Establece los comandos de contenido multimedia admitidos.
setState() Establece el estado de reproducción y la posición actual.

MediaSessionCompat

Método obligatorio Descripción
setRepeatMode() Establece el modo de repetición.
setShuffleMode() Establece el modo de reproducción aleatoria.
setMetadata() Establece los metadatos de contenido multimedia.
setPlaybackState() Establece el estado de reproducción.
Kotlin
private fun updateMediaSession() {
    val metadata = MediaMetadataCompat.Builder()
         .putString(MediaMetadataCompat.METADATA_KEY_TITLE, "title")
         .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, "subtitle")
         .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, mMovie.getCardImageUrl())
         .build()

    val playbackState = PlaybackStateCompat.Builder()
         .setState(
             PlaybackStateCompat.STATE_PLAYING,
             player.getPosition(),
             player.getPlaybackSpeed(),
             System.currentTimeMillis()
        )
         .build()

    mediaSession.setMetadata(metadata)
    mediaSession.setPlaybackState(playbackState)
}
Java
private void updateMediaSession() {
  MediaMetadataCompat metadata =
      new MediaMetadataCompat.Builder()
          .putString(MediaMetadataCompat.METADATA_KEY_TITLE, "title")
          .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, "subtitle")
          .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI,mMovie.getCardImageUrl())
          .build();

  PlaybackStateCompat playbackState =
      new PlaybackStateCompat.Builder()
          .setState(
               PlaybackStateCompat.STATE_PLAYING,
               player.getPosition(),
               player.getPlaybackSpeed(),
               System.currentTimeMillis())
          .build();

  mediaSession.setMetadata(metadata);
  mediaSession.setPlaybackState(playbackState);
}

Cómo controlar el transporte

Tu app debe implementar la devolución de llamada del control de transporte de la sesión multimedia. En la siguiente tabla, se muestran las acciones de control de transporte que deben controlar:

MediaSessionCompat.Callback

Acciones Descripción
onPlay() Reanudar
onPause() Pausar
onSeekTo() Búsqueda de una posición
onStop() Detener el contenido multimedia actual
Kotlin
class MyMediaSessionCallback : MediaSessionCompat.Callback() {
  override fun onPause() {
    // Pause the player and update the play state.
    ...
  }

  override fun onPlay() {
    // Resume the player and update the play state.
    ...
  }

  override fun onSeekTo (long pos) {
    // Seek and update the play state.
    ...
  }
  ...
}

mediaSession.setCallback( MyMediaSessionCallback() );
Java
public MyMediaSessionCallback extends MediaSessionCompat.Callback {
  public void onPause() {
    // Pause the player and update the play state.
    ...
  }

  public void onPlay() {
    // Resume the player and update the play state.
    ...
  }

  public void onSeekTo (long pos) {
    // Seek and update the play state.
    ...
  }
  ...
}

mediaSession.setCallback(new MyMediaSessionCallback());

Cómo configurar la compatibilidad con Cast

Cuando una aplicación emisora envía una solicitud de inicio, se crea un intent con un espacio de nombres de la aplicación. Tu aplicación es responsable de controlarlo y crear una instancia del objeto CastReceiverContext cuando se inicia la app para TV. Se necesita el objeto CastReceiverContext para interactuar con Cast mientras se ejecuta la app para TV. Este objeto permite que tu aplicación para TVs acepte mensajes multimedia de Cast provenientes de cualquier remitente conectado.

Configuración de Android TV

Cómo agregar un filtro de intents de inicio

Agrega un nuevo filtro de intents a la actividad que deseas que controle el intent de inicio desde tu app de la emisora:

<activity android:name="com.example.activity">
  <intent-filter>
      <action android:name="com.google.android.gms.cast.tv.action.LAUNCH" />
      <category android:name="android.intent.category.DEFAULT" />
  </intent-filter>
</activity>

Especifica el proveedor de opciones del receptor

Debes implementar un ReceiverOptionsProvider para proporcionar CastReceiverOptions:

Kotlin
class MyReceiverOptionsProvider : ReceiverOptionsProvider {
  override fun getOptions(context: Context?): CastReceiverOptions {
    return CastReceiverOptions.Builder(context)
          .setStatusText("My App")
          .build()
    }
}
Java
public class MyReceiverOptionsProvider implements ReceiverOptionsProvider {
  @Override
  public CastReceiverOptions getOptions(Context context) {
    return new CastReceiverOptions.Builder(context)
        .setStatusText("My App")
        .build();
  }
}

Luego, especifica el proveedor de opciones en tu AndroidManifest:

 <meta-data
    android:name="com.google.android.gms.cast.tv.RECEIVER_OPTIONS_PROVIDER_CLASS_NAME"
    android:value="com.example.mysimpleatvapplication.MyReceiverOptionsProvider" />

Se usa ReceiverOptionsProvider para proporcionar CastReceiverOptions cuando se inicializa CastReceiverContext.

Contexto del receptor de transmisiones

Inicializa CastReceiverContext cuando se cree tu app:

Kotlin
override fun onCreate() {
  CastReceiverContext.initInstance(this)

  ...
}
Java
@Override
public void onCreate() {
  CastReceiverContext.initInstance(this);

  ...
}

Inicia el CastReceiverContext cuando tu app pase a primer plano:

Kotlin
CastReceiverContext.getInstance().start()
Java
CastReceiverContext.getInstance().start();

Llama a stop() en CastReceiverContext después de que la app pase a segundo plano para las apps de video o las apps que no admiten la reproducción en segundo plano:

Kotlin
// Player has stopped.
CastReceiverContext.getInstance().stop()
Java
// Player has stopped.
CastReceiverContext.getInstance().stop();

Además, si tu app admite la reproducción en segundo plano, llama a stop() en el CastReceiverContext cuando deje de reproducirse en segundo plano.

Te recomendamos que uses LifecycleObserver de la biblioteca androidx.lifecycle para administrar las llamadas a CastReceiverContext.start() y CastReceiverContext.stop(), en especial si tu app nativa tiene varias actividades. Esto evita las condiciones de carrera cuando llamas a start() y stop() desde diferentes actividades.

Kotlin
// Create a LifecycleObserver class.
class MyLifecycleObserver : DefaultLifecycleObserver {
  override fun onStart(owner: LifecycleOwner) {
    // App prepares to enter foreground.
    CastReceiverContext.getInstance().start()
  }

  override fun onStop(owner: LifecycleOwner) {
    // App has moved to the background or has terminated.
    CastReceiverContext.getInstance().stop()
  }
}

// Add the observer when your application is being created.
class MyApplication : Application() {
  fun onCreate() {
    super.onCreate()

    // Initialize CastReceiverContext.
    CastReceiverContext.initInstance(this /* android.content.Context */)

    // Register LifecycleObserver
    ProcessLifecycleOwner.get().lifecycle.addObserver(
        MyLifecycleObserver())
  }
}
Java
// Create a LifecycleObserver class.
public class MyLifecycleObserver implements DefaultLifecycleObserver {
  @Override
  public void onStart(LifecycleOwner owner) {
    // App prepares to enter foreground.
    CastReceiverContext.getInstance().start();
  }

  @Override
  public void onStop(LifecycleOwner owner) {
    // App has moved to the background or has terminated.
    CastReceiverContext.getInstance().stop();
  }
}

// Add the observer when your application is being created.
public class MyApplication extends Application {
  @Override
  public void onCreate() {
    super.onCreate();

    // Initialize CastReceiverContext.
    CastReceiverContext.initInstance(this /* android.content.Context */);

    // Register LifecycleObserver
    ProcessLifecycleOwner.get().getLifecycle().addObserver(
        new MyLifecycleObserver());
  }
}
// In AndroidManifest.xml set MyApplication as the application class
<application
    ...
    android:name=".MyApplication">

Cómo conectar MediaSession a MediaManager

Cuando creas un objeto MediaSession, también debes proporcionar el token de MediaSession actual a CastReceiverContext para que sepa a dónde enviar los comandos y recuperar el estado de reproducción del contenido multimedia:

Kotlin
val mediaManager: MediaManager = receiverContext.getMediaManager()
mediaManager.setSessionCompatToken(currentMediaSession.getSessionToken())
Java
MediaManager mediaManager = receiverContext.getMediaManager();
mediaManager.setSessionCompatToken(currentMediaSession.getSessionToken());

Cuando liberes tu MediaSession por tener una reproducción inactiva, deberás establecer un token nulo en MediaManager:

Kotlin
myPlayer.stop()
mediaSession.release()
mediaManager.setSessionCompatToken(null)
Java
myPlayer.stop();
mediaSession.release();
mediaManager.setSessionCompatToken(null);

Si tu app admite la reproducción de contenido multimedia en segundo plano, en lugar de llamar a CastReceiverContext.stop() cuando se envía la app a segundo plano, debes llamarlo solo cuando la app esté en segundo plano y ya no reproduzca contenido multimedia. Por ejemplo:

Kotlin
class MyLifecycleObserver : DefaultLifecycleObserver {
  ...
  // App has moved to the background.
  override fun onPause(owner: LifecycleOwner) {
    mIsBackground = true
    myStopCastReceiverContextIfNeeded()
  }
}

// Stop playback on the player.
private fun myStopPlayback() {
  myPlayer.stop()

  myStopCastReceiverContextIfNeeded()
}

// Stop the CastReceiverContext when both the player has
// stopped and the app has moved to the background.
private fun myStopCastReceiverContextIfNeeded() {
  if (mIsBackground && myPlayer.isStopped()) {
    CastReceiverContext.getInstance().stop()
  }
}
Java
public class MyLifecycleObserver implements DefaultLifecycleObserver {
  ...
  // App has moved to the background.
  @Override
  public void onPause(LifecycleOwner owner) {
    mIsBackground = true;

    myStopCastReceiverContextIfNeeded();
  }
}

// Stop playback on the player.
private void myStopPlayback() {
  myPlayer.stop();

  myStopCastReceiverContextIfNeeded();
}

// Stop the CastReceiverContext when both the player has
// stopped and the app has moved to the background.
private void myStopCastReceiverContextIfNeeded() {
  if (mIsBackground && myPlayer.isStopped()) {
    CastReceiverContext.getInstance().stop();
  }
}

Cómo usar ExoPlayer con Cast Connect

Si usas Exoplayer, puedes usar MediaSessionConnector para mantener automáticamente la sesión y toda la información relacionada, incluido el estado de reproducción, en lugar de hacer un seguimiento manual de los cambios.

MediaSessionConnector.MediaButtonEventHandler se puede usar para controlar eventos de MediaButton llamando a setMediaButtonEventHandler(MediaButtonEventHandler), que, de lo contrario, se controla con MediaSessionCompat.Callback de forma predeterminada.

Para integrar MediaSessionConnector en tu app, agrega lo siguiente a la clase de actividad del reproductor o a donde administres tu sesión multimedia:

Kotlin
class PlayerActivity : Activity() {
  private var mMediaSession: MediaSessionCompat? = null
  private var mMediaSessionConnector: MediaSessionConnector? = null
  private var mMediaManager: MediaManager? = null

  override fun onCreate(savedInstanceState: Bundle?) {
    ...
    mMediaSession = MediaSessionCompat(this, LOG_TAG)
    mMediaSessionConnector = MediaSessionConnector(mMediaSession!!)
    ...
  }

  override fun onStart() {
    ...
    mMediaManager = receiverContext.getMediaManager()
    mMediaManager!!.setSessionCompatToken(currentMediaSession.getSessionToken())
    mMediaSessionConnector!!.setPlayer(mExoPlayer)
    mMediaSessionConnector!!.setMediaMetadataProvider(mMediaMetadataProvider)
    mMediaSession!!.isActive = true
    ...
  }

  override fun onStop() {
    ...
    mMediaSessionConnector!!.setPlayer(null)
    mMediaSession!!.release()
    mMediaManager!!.setSessionCompatToken(null)
    ...
  }
}
Java
public class PlayerActivity extends Activity {
  private MediaSessionCompat mMediaSession;
  private MediaSessionConnector mMediaSessionConnector;
  private MediaManager mMediaManager;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    ...
    mMediaSession = new MediaSessionCompat(this, LOG_TAG);
    mMediaSessionConnector = new MediaSessionConnector(mMediaSession);
    ...
  }

  @Override
  protected void onStart() {
    ...
    mMediaManager = receiverContext.getMediaManager();
    mMediaManager.setSessionCompatToken(currentMediaSession.getSessionToken());

    mMediaSessionConnector.setPlayer(mExoPlayer);
    mMediaSessionConnector.setMediaMetadataProvider(mMediaMetadataProvider);
    mMediaSession.setActive(true);
    ...
  }

  @Override
  protected void onStop() {
    ...
    mMediaSessionConnector.setPlayer(null);
    mMediaSession.release();
    mMediaManager.setSessionCompatToken(null);
    ...
  }
}

Configuración de la app del remitente

Habilita la compatibilidad con Cast Connect

Una vez que actualices tu app del remitente para que admita Cast Connect, puedes declarar que está lista configurando la marca androidReceiverCompatible en LaunchOptions como verdadera.

Android

Se requiere la versión play-services-cast-framework 19.0.0 o posterior.

La marca androidReceiverCompatible se establece en LaunchOptions (que forma parte de CastOptions):

Kotlin
class CastOptionsProvider : OptionsProvider {
  override fun getCastOptions(context: Context?): CastOptions {
    val launchOptions: LaunchOptions = Builder()
          .setAndroidReceiverCompatible(true)
          .build()
    return CastOptions.Builder()
          .setLaunchOptions(launchOptions)
          ...
          .build()
    }
}
Java
public class CastOptionsProvider implements OptionsProvider {
  @Override
  public CastOptions getCastOptions(Context context) {
    LaunchOptions launchOptions = new LaunchOptions.Builder()
              .setAndroidReceiverCompatible(true)
              .build();
    return new CastOptions.Builder()
        .setLaunchOptions(launchOptions)
        ...
        .build();
  }
}
iOS

Se requiere la versión google-cast-sdk v4.4.8 o una posterior.

La marca androidReceiverCompatible se establece en GCKLaunchOptions (que forma parte de GCKCastOptions):

let options = GCKCastOptions(discoveryCriteria: GCKDiscoveryCriteria(applicationID: kReceiverAppID))
...
let launchOptions = GCKLaunchOptions()
launchOptions.androidReceiverCompatible = true
options.launchOptions = launchOptions
GCKCastContext.setSharedInstanceWith(options)
Web

Se requiere la versión M87 o posterior del navegador Chromium.

const context = cast.framework.CastContext.getInstance();
const castOptions = new cast.framework.CastOptions();
castOptions.receiverApplicationId = kReceiverAppID;
castOptions.androidReceiverCompatible = true;
context.setOptions(castOptions);

Configuración de la Consola para desarrolladores de Cast

Configura la app de Android TV

Agrega el nombre del paquete de tu app de Android TV en Play Console de Cast para asociarlo con tu ID de app de Cast.

Cómo registrar los dispositivos de desarrollador

Registra el número de serie del dispositivo Android TV que usarás para el desarrollo en la Play Console de Cast.

Por razones de seguridad, sin el registro, Cast Connect solo funcionará para apps instaladas desde Google Play Store.

Para obtener más información sobre cómo registrar un dispositivo Cast o Android TV para el desarrollo de Cast, consulta la página de registro.

Carga de contenido multimedia

Si ya implementaste la compatibilidad con vínculos directos en tu app para Android TV, deberías tener una definición similar configurada en tu manifiesto de Android TV:

<activity android:name="com.example.activity">
  <intent-filter>
     <action android:name="android.intent.action.VIEW" />
     <category android:name="android.intent.category.DEFAULT" />
     <data android:scheme="https"/>
     <data android:host="www.example.com"/>
     <data android:pathPattern=".*"/>
  </intent-filter>
</activity>

Carga por entidad en el remitente

En los remitentes, puedes pasar el vínculo directo configurando entity en la información de medios de la solicitud de carga:

Kotlin
val mediaToLoad = MediaInfo.Builder("some-id")
    .setEntity("https://example.com/watch/some-id")
    ...
    .build()
val loadRequest = MediaLoadRequestData.Builder()
    .setMediaInfo(mediaToLoad)
    .setCredentials("user-credentials")
    ...
    .build()
remoteMediaClient.load(loadRequest)
Android
Java
MediaInfo mediaToLoad =
    new MediaInfo.Builder("some-id")
        .setEntity("https://example.com/watch/some-id")
        ...
        .build();
MediaLoadRequestData loadRequest =
    new MediaLoadRequestData.Builder()
        .setMediaInfo(mediaToLoad)
        .setCredentials("user-credentials")
        ...
        .build();
remoteMediaClient.load(loadRequest);
iOS
let mediaInfoBuilder = GCKMediaInformationBuilder(entity: "https://example.com/watch/some-id")
...
mediaInformation = mediaInfoBuilder.build()

let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder()
mediaLoadRequestDataBuilder.mediaInformation = mediaInformation
mediaLoadRequestDataBuilder.credentials = "user-credentials"
...
let mediaLoadRequestData = mediaLoadRequestDataBuilder.build()

remoteMediaClient?.loadMedia(with: mediaLoadRequestData)
Web

Se requiere la versión M87 o posterior del navegador Chromium.

let mediaInfo = new chrome.cast.media.MediaInfo('some-id"', 'video/mp4');
mediaInfo.entity = 'https://example.com/watch/some-id';
...

let request = new chrome.cast.media.LoadRequest(mediaInfo);
request.credentials = 'user-credentials';
...

cast.framework.CastContext.getInstance().getCurrentSession().loadMedia(request);

El comando de carga se envía a través de un intent con tu vínculo directo y el nombre del paquete que definiste en Play Console.

Cómo configurar las credenciales de ATV en el remitente

Es posible que tu app de Web Receiver y tu app para Android TV admitan diferentes vínculos directos y credentials (por ejemplo, si manejas la autenticación de manera diferente en las dos plataformas). Para solucionar este problema, puedes proporcionar valores alternativos para entity y credentials en Android TV:

Android
Kotlin
val mediaToLoad = MediaInfo.Builder("some-id")
        .setEntity("https://example.com/watch/some-id")
        .setAtvEntity("myscheme://example.com/atv/some-id")
        ...
        .build()
val loadRequest = MediaLoadRequestData.Builder()
        .setMediaInfo(mediaToLoad)
        .setCredentials("user-credentials")
        .setAtvCredentials("atv-user-credentials")
        ...
        .build()
remoteMediaClient.load(loadRequest)
Java
MediaInfo mediaToLoad =
    new MediaInfo.Builder("some-id")
        .setEntity("https://example.com/watch/some-id")
        .setAtvEntity("myscheme://example.com/atv/some-id")
        ...
        .build();
MediaLoadRequestData loadRequest =
    new MediaLoadRequestData.Builder()
        .setMediaInfo(mediaToLoad)
        .setCredentials("user-credentials")
        .setAtvCredentials("atv-user-credentials")
        ...
        .build();
remoteMediaClient.load(loadRequest);
iOS
let mediaInfoBuilder = GCKMediaInformationBuilder(entity: "https://example.com/watch/some-id")
mediaInfoBuilder.atvEntity = "myscheme://example.com/atv/some-id"
...
mediaInformation = mediaInfoBuilder.build()

let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder()
mediaLoadRequestDataBuilder.mediaInformation = mediaInformation
mediaLoadRequestDataBuilder.credentials = "user-credentials"
mediaLoadRequestDataBuilder.atvCredentials = "atv-user-credentials"
...
let mediaLoadRequestData = mediaLoadRequestDataBuilder.build()

remoteMediaClient?.loadMedia(with: mediaLoadRequestData)
Web

Se requiere la versión M87 o posterior del navegador Chromium.

let mediaInfo = new chrome.cast.media.MediaInfo('some-id"', 'video/mp4');
mediaInfo.entity = 'https://example.com/watch/some-id';
mediaInfo.atvEntity = 'myscheme://example.com/atv/some-id';
...

let request = new chrome.cast.media.LoadRequest(mediaInfo);
request.credentials = 'user-credentials';
request.atvCredentials = 'atv-user-credentials';
...

cast.framework.CastContext.getInstance().getCurrentSession().loadMedia(request);

Si se inicia la app de Web Receiver, esta usa entity y credentials en la solicitud de carga. Sin embargo, si se inicia tu app para Android TV, el SDK anula entity y credentials con tus atvEntity y atvCredentials (si se especifican).

Carga por Content ID o MediaQueueData

Si no usas entity ni atvEntity, y usas el ID de contenido o la URL de contenido en la información de medios, o bien usas los datos de la solicitud de carga de medios más detallados, debes agregar el siguiente filtro de intents predefinido en tu app para Android TV:

<activity android:name="com.example.activity">
  <intent-filter>
     <action android:name="com.google.android.gms.cast.tv.action.LOAD"/>
     <category android:name="android.intent.category.DEFAULT" />
  </intent-filter>
</activity>

Del lado del remitente, de manera similar a load by entity, puedes crear una solicitud de carga con la información de tu contenido y llamar a load().

Android
Kotlin
val mediaToLoad = MediaInfo.Builder("some-id").build()
val loadRequest = MediaLoadRequestData.Builder()
    .setMediaInfo(mediaToLoad)
    .setCredentials("user-credentials")
    ...
    .build()
remoteMediaClient.load(loadRequest)
Java
MediaInfo mediaToLoad =
    new MediaInfo.Builder("some-id").build();
MediaLoadRequestData loadRequest =
    new MediaLoadRequestData.Builder()
        .setMediaInfo(mediaToLoad)
        .setCredentials("user-credentials")
        ...
        .build();
remoteMediaClient.load(loadRequest);
iOS
let mediaInfoBuilder = GCKMediaInformationBuilder(contentId: "some-id")
...
mediaInformation = mediaInfoBuilder.build()

let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder()
mediaLoadRequestDataBuilder.mediaInformation = mediaInformation
mediaLoadRequestDataBuilder.credentials = "user-credentials"
...
let mediaLoadRequestData = mediaLoadRequestDataBuilder.build()

remoteMediaClient?.loadMedia(with: mediaLoadRequestData)
Web

Se requiere la versión M87 o posterior del navegador Chromium.

let mediaInfo = new chrome.cast.media.MediaInfo('some-id"', 'video/mp4');
...

let request = new chrome.cast.media.LoadRequest(mediaInfo);
...

cast.framework.CastContext.getInstance().getCurrentSession().loadMedia(request);

Cómo controlar las solicitudes de carga

En tu actividad, para controlar estas solicitudes de carga, debes controlar los intents en las devoluciones de llamada del ciclo de vida de tu actividad:

Kotlin
class MyActivity : Activity() {
  override fun onStart() {
    super.onStart()
    val mediaManager = CastReceiverContext.getInstance().getMediaManager()
    // Pass the intent to the SDK. You can also do this in onCreate().
    if (mediaManager.onNewIntent(intent)) {
        // If the SDK recognizes the intent, you should early return.
        return
    }
    // If the SDK doesn't recognize the intent, you can handle the intent with
    // your own logic.
    ...
  }

  // For some cases, a new load intent triggers onNewIntent() instead of
  // onStart().
  override fun onNewIntent(intent: Intent) {
    val mediaManager = CastReceiverContext.getInstance().getMediaManager()
    // Pass the intent to the SDK. You can also do this in onCreate().
    if (mediaManager.onNewIntent(intent)) {
        // If the SDK recognizes the intent, you should early return.
        return
    }
    // If the SDK doesn't recognize the intent, you can handle the intent with
    // your own logic.
    ...
  }
}
Java
public class MyActivity extends Activity {
  @Override
  protected void onStart() {
    super.onStart();
    MediaManager mediaManager =
        CastReceiverContext.getInstance().getMediaManager();
    // Pass the intent to the SDK. You can also do this in onCreate().
    if (mediaManager.onNewIntent(getIntent())) {
      // If the SDK recognizes the intent, you should early return.
      return;
    }
    // If the SDK doesn't recognize the intent, you can handle the intent with
    // your own logic.
    ...
  }

  // For some cases, a new load intent triggers onNewIntent() instead of
  // onStart().
  @Override
  protected void onNewIntent(Intent intent) {
    MediaManager mediaManager =
        CastReceiverContext.getInstance().getMediaManager();
    // Pass the intent to the SDK. You can also do this in onCreate().
    if (mediaManager.onNewIntent(intent)) {
      // If the SDK recognizes the intent, you should early return.
      return;
    }
    // If the SDK doesn't recognize the intent, you can handle the intent with
    // your own logic.
    ...
  }
}

Si MediaManager detecta que el intent es de carga, extraerá un objeto MediaLoadRequestData del intent y, luego, invocará MediaLoadCommandCallback.onLoad(). Debes anular este método para controlar la solicitud de carga. La devolución de llamada debe registrarse antes de que se llame a MediaManager.onNewIntent() (se recomienda que esté en un método onCreate() de Activity o Application).

Kotlin
class MyActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val mediaManager = CastReceiverContext.getInstance().getMediaManager()
        mediaManager.setMediaLoadCommandCallback(MyMediaLoadCommandCallback())
    }
}

class MyMediaLoadCommandCallback : MediaLoadCommandCallback() {
  override fun onLoad(
        senderId: String?,
        loadRequestData: MediaLoadRequestData
  ): Task {
      return Tasks.call {
        // Resolve the entity into your data structure and load media.
        val mediaInfo = loadRequestData.getMediaInfo()
        if (!checkMediaInfoSupported(mediaInfo)) {
            // Throw MediaException to indicate load failure.
            throw MediaException(
                MediaError.Builder()
                    .setDetailedErrorCode(DetailedErrorCode.LOAD_FAILED)
                    .setReason(MediaError.ERROR_REASON_INVALID_REQUEST)
                    .build()
            )
        }
        myFillMediaInfo(MediaInfoWriter(mediaInfo))
        myPlayerLoad(mediaInfo.getContentUrl())

        // Update media metadata and state (this clears all previous status
        // overrides).
        castReceiverContext.getMediaManager()
            .setDataFromLoad(loadRequestData)
        ...
        castReceiverContext.getMediaManager().broadcastMediaStatus()

        // Return the resolved MediaLoadRequestData to indicate load success.
        return loadRequestData
     }
  }

  private fun myPlayerLoad(contentURL: String) {
    myPlayer.load(contentURL)

    // Update the MediaSession state.
    val playbackState: PlaybackStateCompat = Builder()
        .setState(
            player.getState(), player.getPosition(), System.currentTimeMillis()
        )
        ...
        .build()
    mediaSession.setPlaybackState(playbackState)
  }
Java
public class MyActivity extends Activity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    MediaManager mediaManager =
        CastReceiverContext.getInstance().getMediaManager();
    mediaManager.setMediaLoadCommandCallback(new MyMediaLoadCommandCallback());
  }
}

public class MyMediaLoadCommandCallback extends MediaLoadCommandCallback {
  @Override
  public Task onLoad(String senderId, MediaLoadRequestData loadRequestData) {
    return Tasks.call(() -> {
        // Resolve the entity into your data structure and load media.
        MediaInfo mediaInfo = loadRequestData.getMediaInfo();
        if (!checkMediaInfoSupported(mediaInfo)) {
          // Throw MediaException to indicate load failure.
          throw new MediaException(
              new MediaError.Builder()
                  .setDetailedErrorCode(DetailedErrorCode.LOAD_FAILED)
                  .setReason(MediaError.ERROR_REASON_INVALID_REQUEST)
                  .build());
        }
        myFillMediaInfo(new MediaInfoWriter(mediaInfo));
        myPlayerLoad(mediaInfo.getContentUrl());

        // Update media metadata and state (this clears all previous status
        // overrides).
        castReceiverContext.getMediaManager()
            .setDataFromLoad(loadRequestData);
        ...
        castReceiverContext.getMediaManager().broadcastMediaStatus();

        // Return the resolved MediaLoadRequestData to indicate load success.
        return loadRequestData;
    });
}

private void myPlayerLoad(String contentURL) {
  myPlayer.load(contentURL);

  // Update the MediaSession state.
  PlaybackStateCompat playbackState =
      new PlaybackStateCompat.Builder()
          .setState(
              player.getState(), player.getPosition(), System.currentTimeMillis())
          ...
          .build();
  mediaSession.setPlaybackState(playbackState);
}

Para procesar la intención de carga, puedes analizarla en las estructuras de datos que definimos (MediaLoadRequestData para las solicitudes de carga).

Compatibilidad con comandos de contenido multimedia

Compatibilidad básica con los controles de reproducción

Los comandos de integración básicos incluyen los comandos que son compatibles con la sesión multimedia. Estos comandos se notifican a través de devoluciones de llamada de la sesión multimedia. Debes registrar una devolución de llamada en la sesión de medios para admitir esta función (es posible que ya lo estés haciendo).

Kotlin
private class MyMediaSessionCallback : MediaSessionCompat.Callback() {
  override fun onPause() {
    // Pause the player and update the play state.
    myPlayer.pause()
  }

  override fun onPlay() {
    // Resume the player and update the play state.
    myPlayer.play()
  }

  override fun onSeekTo(pos: Long) {
    // Seek and update the play state.
    myPlayer.seekTo(pos)
  }
    ...
 }

mediaSession.setCallback(MyMediaSessionCallback())
Java
private class MyMediaSessionCallback extends MediaSessionCompat.Callback {
  @Override
  public void onPause() {
    // Pause the player and update the play state.
    myPlayer.pause();
  }
  @Override
  public void onPlay() {
    // Resume the player and update the play state.
    myPlayer.play();
  }
  @Override
  public void onSeekTo(long pos) {
    // Seek and update the play state.
    myPlayer.seekTo(pos);
  }

  ...
}

mediaSession.setCallback(new MyMediaSessionCallback());

Cómo agregar compatibilidad con los comandos de control de Cast

Hay algunos comandos de Cast que no están disponibles en MediaSession, como skipAd() o setActiveMediaTracks(). Además, aquí se deben implementar algunos comandos de la fila porque la fila de Cast no es totalmente compatible con la fila de MediaSession.

Kotlin
class MyMediaCommandCallback : MediaCommandCallback() {
    override fun onSkipAd(requestData: RequestData?): Task<Void?> {
        // Skip your ad
        ...
        return Tasks.forResult(null)
    }
}

val mediaManager = CastReceiverContext.getInstance().getMediaManager()
mediaManager.setMediaCommandCallback(MyMediaCommandCallback())
Java
public class MyMediaCommandCallback extends MediaCommandCallback {
  @Override
  public Task onSkipAd(RequestData requestData) {
    // Skip your ad
    ...
    return Tasks.forResult(null);
  }
}

MediaManager mediaManager =
    CastReceiverContext.getInstance().getMediaManager();
mediaManager.setMediaCommandCallback(new MyMediaCommandCallback());

Cómo especificar los comandos de contenido multimedia compatibles

Al igual que con tu receptor de Cast, tu app para Android TV debe especificar qué comandos se admiten para que los emisores puedan habilitar o inhabilitar ciertos controles de la IU. Para los comandos que forman parte de MediaSession, especifícalos en PlaybackStateCompat. Los comandos adicionales se deben especificar en MediaStatusModifier.

Kotlin
// Set media session supported commands
val playbackState: PlaybackStateCompat = PlaybackStateCompat.Builder()
    .setActions(PlaybackStateCompat.ACTION_PLAY or PlaybackStateCompat.ACTION_PAUSE)
    .setState(PlaybackStateCompat.STATE_PLAYING)
    .build()

mediaSession.setPlaybackState(playbackState)

// Set additional commands in MediaStatusModifier
val mediaManager = CastReceiverContext.getInstance().getMediaManager()
mediaManager.getMediaStatusModifier()
    .setMediaCommandSupported(MediaStatus.COMMAND_QUEUE_NEXT)
Java
// Set media session supported commands
PlaybackStateCompat playbackState =
    new PlaybackStateCompat.Builder()
        .setActions(PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE)
        .setState(PlaybackStateCompat.STATE_PLAYING)
        .build();

mediaSession.setPlaybackState(playbackState);

// Set additional commands in MediaStatusModifier
MediaManager mediaManager = CastReceiverContext.getInstance().getMediaManager();
mediaManager.getMediaStatusModifier()
            .setMediaCommandSupported(MediaStatus.COMMAND_QUEUE_NEXT);

Oculta los botones no compatibles

Si tu app para Android TV solo admite controles multimedia básicos, pero tu app de Web Receiver admite controles más avanzados, debes asegurarte de que tu app del remitente se comporte correctamente cuando transmita contenido a la app para Android TV. Por ejemplo, si tu app para Android TV no admite el cambio de la velocidad de reproducción, pero tu app de Web Receiver sí, debes configurar las acciones admitidas correctamente en cada plataforma y asegurarte de que tu app del remitente renderice la IU correctamente.

Cómo modificar MediaStatus

Para admitir funciones avanzadas, como pistas, anuncios, transmisiones en vivo y filas, tu app para Android TV debe proporcionar información adicional que no se puede determinar a través de MediaSession.

Para lograrlo, te proporcionamos la clase MediaStatusModifier. MediaStatusModifier siempre funcionará en la MediaSession que configuraste en CastReceiverContext.

Para crear y transmitir MediaStatus, haz lo siguiente:

Kotlin
val mediaManager: MediaManager = castReceiverContext.getMediaManager()
val statusModifier: MediaStatusModifier = mediaManager.getMediaStatusModifier()

statusModifier
    .setLiveSeekableRange(seekableRange)
    .setAdBreakStatus(adBreakStatus)
    .setCustomData(customData)

mediaManager.broadcastMediaStatus()
Java
MediaManager mediaManager = castReceiverContext.getMediaManager();
MediaStatusModifier statusModifier = mediaManager.getMediaStatusModifier();

statusModifier
    .setLiveSeekableRange(seekableRange)
    .setAdBreakStatus(adBreakStatus)
    .setCustomData(customData);

mediaManager.broadcastMediaStatus();

Nuestra biblioteca cliente obtendrá el MediaStatus base de MediaSession. Tu app de Android TV puede especificar un estado adicional y anular el estado a través de un modificador MediaStatus.

Algunos estados y metadatos se pueden establecer tanto en MediaSession como en MediaStatusModifier. Te recomendamos que solo los configures en MediaSession. Aun así, puedes usar el modificador para anular los estados en MediaSession. Sin embargo, esto no se recomienda porque el estado en el modificador siempre tiene una prioridad más alta que los valores proporcionados por MediaSession.

Cómo interceptar el MediaStatus antes de su envío

Al igual que con el SDK del receptor web, si deseas realizar algunos retoques finales antes de enviar, puedes especificar un MediaStatusInterceptor para procesar el MediaStatus que se enviará. Pasamos un MediaStatusWriter para manipular el MediaStatus antes de que se envíe.

Kotlin
mediaManager.setMediaStatusInterceptor(object : MediaStatusInterceptor {
    override fun intercept(mediaStatusWriter: MediaStatusWriter) {
      // Perform customization.
        mediaStatusWriter.setCustomData(JSONObject("{data: \"my Hello\"}"))
    }
})
Java
mediaManager.setMediaStatusInterceptor(new MediaStatusInterceptor() {
    @Override
    public void intercept(MediaStatusWriter mediaStatusWriter) {
        // Perform customization.
        mediaStatusWriter.setCustomData(new JSONObject("{data: \"my Hello\"}"));
    }
});

Cómo controlar las credenciales del usuario

Es posible que tu app para Android TV solo permita que ciertos usuarios inicien o se unan a la sesión de la app. Por ejemplo, solo permite que un remitente inicie o se una si se cumple alguna de las siguientes condiciones:

  • La app del remitente accedió a la misma cuenta y perfil que la app de ATV.
  • La app del remitente accedió a la misma cuenta, pero a un perfil diferente al de la app de ATV.

Si tu app puede controlar varios usuarios o usuarios anónimos, puedes permitir que cualquier usuario adicional se una a la sesión de ATV. Si el usuario proporciona credenciales, tu app para ATV debe controlarlas para que se pueda hacer un seguimiento adecuado de su progreso y otros datos del usuario.

Cuando tu app emisora se inicia o se une a tu app de Android TV, debe proporcionar las credenciales que representan a quién se une a la sesión.

Antes de que un remitente inicie y se una a tu app para Android TV, puedes especificar un verificador de inicio para ver si se permiten las credenciales del remitente. De lo contrario, el SDK de Cast Connect recurre al inicio de tu Web Receiver.

Datos de credenciales de inicio de la app del remitente

En el lado del remitente, puedes especificar el CredentialsData para representar a quién se une a la sesión.

El credentials es una cadena que puede definir el usuario, siempre y cuando tu app para ATV pueda comprenderla. El credentialsType define desde qué plataforma proviene el CredentialsData o puede ser un valor personalizado. De forma predeterminada, se establece en la plataforma desde la que se envía.

El objeto CredentialsData solo se pasa a tu app para Android TV durante el inicio o el momento de unirse. Si lo vuelves a configurar mientras estás conectado, no se pasará a tu app para Android TV. Si el remitente cambia el perfil mientras está conectado, puedes permanecer en la sesión o llamar a SessionManager.endCurrentCastSession(boolean stopCasting) si crees que el nuevo perfil es incompatible con la sesión.

El CredentialsData de cada remitente se puede recuperar con getSenders en CastReceiverContext para obtener el SenderInfo, getCastLaunchRequest() para obtener el CastLaunchRequest y, luego, getCredentialsData().

Android

Se requiere la versión play-services-cast-framework 19.0.0 o posterior.

Kotlin
CastContext.getSharedInstance().setLaunchCredentialsData(
    CredentialsData.Builder()
        .setCredentials("{\"userId\": \"abc\"}")
        .build()
)
Java
CastContext.getSharedInstance().setLaunchCredentialsData(
    new CredentialsData.Builder()
        .setCredentials("{\"userId\": \"abc\"}")
        .build());
iOS

Se requiere la versión google-cast-sdk v4.8.3 o una posterior.

Se puede llamar en cualquier momento después de que se establecen las opciones: GCKCastContext.setSharedInstanceWith(options).

GCKCastContext.sharedInstance().setLaunch(
    GCKCredentialsData(credentials: "{\"userId\": \"abc\"}")
Web

Se requiere la versión M87 o posterior del navegador Chromium.

Se puede llamar en cualquier momento después de que se establecen las opciones: cast.framework.CastContext.getInstance().setOptions(options);.

let credentialsData =
    new chrome.cast.CredentialsData("{\"userId\": \"abc\"}");
cast.framework.CastContext.getInstance().setLaunchCredentialsData(credentialsData);

Implementación del verificador de solicitudes de inicio de ATV

El objeto CredentialsData se pasa a tu app para Android TV cuando un remitente intenta iniciar o unirse. Puedes implementar un LaunchRequestChecker. para permitir o rechazar esta solicitud.

Si se rechaza una solicitud, se carga el Web Receiver en lugar de iniciarse de forma nativa en la app para ATV. Debes rechazar una solicitud si tu ATV no puede controlar la solicitud del usuario para iniciar o unirse. Por ejemplo, es posible que un usuario diferente haya accedido a la app para ATV del que realiza la solicitud, y tu app no pueda controlar el cambio de credenciales, o bien que no haya ningún usuario que haya accedido a la app para ATV.

Si se permite una solicitud, se inicia la app de ATV. Puedes personalizar este comportamiento según si tu app admite el envío de solicitudes de carga cuando un usuario no accedió a la app para ATV o si hay una discrepancia de usuario. Este comportamiento se puede personalizar por completo en LaunchRequestChecker.

Crea una clase que implemente la interfaz CastReceiverOptions.LaunchRequestChecker:

Kotlin
class MyLaunchRequestChecker : LaunchRequestChecker {
  override fun checkLaunchRequestSupported(launchRequest: CastLaunchRequest): Task {
    return Tasks.call {
      myCheckLaunchRequest(
           launchRequest
      )
    }
  }
}

private fun myCheckLaunchRequest(launchRequest: CastLaunchRequest): Boolean {
  val credentialsData = launchRequest.getCredentialsData()
     ?: return false // or true if you allow anonymous users to join.

  // The request comes from a mobile device, e.g. checking user match.
  return if (credentialsData.credentialsType == CredentialsData.CREDENTIALS_TYPE_ANDROID) {
     myCheckMobileCredentialsAllowed(credentialsData.getCredentials())
  } else false // Unrecognized credentials type.
}
Java
public class MyLaunchRequestChecker
    implements CastReceiverOptions.LaunchRequestChecker {
  @Override
  public Task checkLaunchRequestSupported(CastLaunchRequest launchRequest) {
    return Tasks.call(() -> myCheckLaunchRequest(launchRequest));
  }
}

private boolean myCheckLaunchRequest(CastLaunchRequest launchRequest) {
  CredentialsData credentialsData = launchRequest.getCredentialsData();
  if (credentialsData == null) {
    return false;  // or true if you allow anonymous users to join.
  }

  // The request comes from a mobile device, e.g. checking user match.
  if (credentialsData.getCredentialsType().equals(CredentialsData.CREDENTIALS_TYPE_ANDROID)) {
    return myCheckMobileCredentialsAllowed(credentialsData.getCredentials());
  }

  // Unrecognized credentials type.
  return false;
}

Luego, configúralo en tu ReceiverOptionsProvider:

Kotlin
class MyReceiverOptionsProvider : ReceiverOptionsProvider {
  override fun getOptions(context: Context?): CastReceiverOptions {
    return CastReceiverOptions.Builder(context)
        ...
        .setLaunchRequestChecker(MyLaunchRequestChecker())
        .build()
  }
}
Java
public class MyReceiverOptionsProvider implements ReceiverOptionsProvider {
  @Override
  public CastReceiverOptions getOptions(Context context) {
    return new CastReceiverOptions.Builder(context)
        ...
        .setLaunchRequestChecker(new MyLaunchRequestChecker())
        .build();
  }
}

Resolver true en LaunchRequestChecker inicia la app de ATV y false inicia tu app de Web Receiver.

Envío y recepción de mensajes personalizados

El protocolo de Cast te permite enviar mensajes de cadena personalizados entre los emisores y tu aplicación receptora. Debes registrar un espacio de nombres (canal) para enviar mensajes antes de inicializar tu CastReceiverContext.

Android TV: Especifica un espacio de nombres personalizado

Debes especificar los espacios de nombres admitidos en tu CastReceiverOptions durante la configuración:

Kotlin
class MyReceiverOptionsProvider : ReceiverOptionsProvider {
  override fun getOptions(context: Context?): CastReceiverOptions {
    return CastReceiverOptions.Builder(context)
        .setCustomNamespaces(
            Arrays.asList("urn:x-cast:com.example.cast.mynamespace")
        )
        .build()
  }
}
Java
public class MyReceiverOptionsProvider implements ReceiverOptionsProvider {
  @Override
  public CastReceiverOptions getOptions(Context context) {
    return new CastReceiverOptions.Builder(context)
        .setCustomNamespaces(
              Arrays.asList("urn:x-cast:com.example.cast.mynamespace"))
        .build();
  }
}

Android TV: Envío de mensajes

Kotlin
// If senderId is null, then the message is broadcasted to all senders.
CastReceiverContext.getInstance().sendMessage(
    "urn:x-cast:com.example.cast.mynamespace", senderId, customString)
Java
// If senderId is null, then the message is broadcasted to all senders.
CastReceiverContext.getInstance().sendMessage(
    "urn:x-cast:com.example.cast.mynamespace", senderId, customString);

Android TV: Receive Custom Namespace Messages

Kotlin
class MyCustomMessageListener : MessageReceivedListener {
    override fun onMessageReceived(
        namespace: String, senderId: String?, message: String ) {
        ...
    }
}

CastReceiverContext.getInstance().setMessageReceivedListener(
    "urn:x-cast:com.example.cast.mynamespace", new MyCustomMessageListener());
Java
class MyCustomMessageListener implements CastReceiverContext.MessageReceivedListener {
  @Override
  public void onMessageReceived(
      String namespace, String senderId, String message) {
    ...
  }
}

CastReceiverContext.getInstance().setMessageReceivedListener(
    "urn:x-cast:com.example.cast.mynamespace", new MyCustomMessageListener());