Adicionar os principais recursos ao receptor do Android TV

Esta página contém snippets de código e descrições dos recursos disponíveis para personalizar um app receptor do Android TV.

Como configurar bibliotecas

Para disponibilizar as APIs Cast Connect no app Android TV:

Android
  1. Abra o arquivo build.gradle dentro do diretório do módulo do aplicativo.
  2. Verifique se google() está incluído no repositories listado.
      repositories {
        google()
      }
  3. Dependendo do tipo de dispositivo de destino do app, adicione as versões mais recentes das bibliotecas às dependências:
    • Para o app receptor do Android:
        dependencies {
          implementation 'com.google.android.gms:play-services-cast-tv:21.0.1'
          implementation 'com.google.android.gms:play-services-cast:21.4.0'
        }
    • Para o app Android Sender:
        dependencies {
          implementation 'com.google.android.gms:play-services-cast:21.0.1'
          implementation 'com.google.android.gms:play-services-cast-framework:21.4.0'
        }
    Atualize esse número de versão sempre que os serviços forem atualizados.
  4. Salve as mudanças e clique em Sync Project with Gradle Files na barra de ferramentas.
iOS
  1. Verifique se o Podfile segmenta google-cast-sdk 4.8.1 ou mais recente.
  2. Segmente o iOS 14 ou mais recente. Consulte as Notas de lançamento para mais detalhes.
      platform: ios, '14'
    
      def target_pods
         pod 'google-cast-sdk', '~>4.8.1'
      end
Web
  1. Exige o navegador Chromium versão M87 ou mais recente.
  2. Adicione a biblioteca da API Web Sender ao seu projeto
      <script src="//www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1"></script>

Requisito do AndroidX

As novas versões do Google Play Services exigem que um app seja atualizado para usar o namespace androidx. Siga as instruções para migrar para o AndroidX.

App Android TV: pré-requisitos

Para oferecer suporte à Cast Connect no seu app Android TV, você precisa criar e oferecer suporte a eventos de uma sessão de mídia. Os dados fornecidos pela sua sessão de mídia fornecem as informações básicas (por exemplo, posição, estado de reprodução etc.) para o status da sua mídia. Sua sessão de mídia também é usada pela biblioteca Cast Connect para sinalizar quando recebeu determinadas mensagens de um remetente, como uma pausa.

Para saber mais sobre sessões de mídia e como inicializar uma sessão de mídia, consulte o guia sobre como trabalhar com uma sessão de mídia.

Ciclo de vida da sessão de mídia

Seu app precisa criar uma sessão de mídia quando a reprodução começar e soltá-la quando não puder mais ser controlado. Por exemplo, se o app for um app de vídeo, você precisará liberar a sessão quando o usuário sair da atividade de reprodução, selecionando "Voltar" para navegar por outro conteúdo ou colocando o app em segundo plano. Se o app for de música, solte-o quando o app não estiver mais reproduzindo mídia.

Atualizando o status da sessão

Os dados da sua sessão de mídia precisam estar atualizados com o status do player. Por exemplo, quando a reprodução é pausada, é necessário atualizar o estado da reprodução, bem como as ações com suporte. Nas tabelas a seguir, listamos os estados que você é responsável por atualizar.

MediaMetadataCompat

Campo de metadados Descrição
METADATA_KEY_TITLE (obrigatório) O título da mídia.
METADATA_KEY_DISPLAY_SUBTITLE O subtítulo.
METADATA_KEY_DISPLAY_ICON_URI O URL do ícone.
METADATA_KEY_DURATION (obrigatório) Duração da mídia.
METADATA_KEY_MEDIA_URI O Content ID.
METADATA_KEY_ARTIST O artista.
METADATA_KEY_ALBUM O álbum.

PlaybackStateCompat

Método obrigatório Descrição
setActions() Define os comandos de mídia compatíveis.
setState() Define o estado de reprodução e a posição atual.

MediaSessionCompat

Método obrigatório Descrição
setRepeatMode() Define o modo de repetição.
setShuffleMode() Define o modo de ordem aleatória.
setMetadata() Define os metadados de mídia.
setPlaybackState() Define o estado da reprodução.
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);
}

Como gerenciar o controle de transporte

Seu app precisa implementar um callback de controle de transporte da sessão de mídia. A tabela a seguir mostra quais ações de controle de transporte eles precisam processar:

MediaSessionCompat.Callback

Ações Descrição
onPlay() Retomar
onPause() Pausar
onSeekTo() (link em inglês) Procurar uma posição
onStop() Parar a mídia atual
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());

Como configurar a compatibilidade com o Cast

Quando uma solicitação de inicialização é enviada por um aplicativo remetente, uma intent é criada com um namespace de aplicativo. O aplicativo será responsável por processá-lo e criar uma instância do objeto CastReceiverContext quando o app de TV for iniciado. O objeto CastReceiverContext é necessário para interagir com o Cast enquanto o app de TV estiver em execução. Esse objeto permite que o app de TV aceite mensagens de mídia do Google Cast provenientes de qualquer remetente conectado.

Configuração do Android TV

Como adicionar um filtro de intent de inicialização

Adicione um novo filtro de intent à atividade que você quer que processe a intent de inicialização do app remetente:

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

Especificar o provedor de opções de receptor

É necessário implementar um ReceiverOptionsProvider para fornecer 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();
  }
}

Em seguida, especifique o provedor de opções no AndroidManifest:

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

O ReceiverOptionsProvider é usado para fornecer o CastReceiverOptions quando CastReceiverContext é inicializado.

Contexto do receptor de transmissão

Inicialize o CastReceiverContext quando o app for criado:

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

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

  ...
}

Inicie a CastReceiverContext quando o app for para o primeiro plano:

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

Chame stop() no CastReceiverContext depois que o app ficar em segundo plano para apps de vídeo ou que não têm suporte para a reprodução em segundo plano:

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

Além disso, se o app oferecer suporte à reprodução em segundo plano, chame stop() no CastReceiverContext quando a reprodução for interrompida em segundo plano.

Recomendamos que você use o LifecycleObserver da biblioteca androidx.lifecycle para gerenciar chamadas de CastReceiverContext.start() e CastReceiverContext.stop(), principalmente se o app nativo tiver várias atividades. Isso evita disputas quando você chama start() e stop() em atividades diferentes.

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

Como conectar a MediaSession ao MediaManager

Ao criar um MediaSession, você também precisa fornecer o token atual do MediaSession para CastReceiverContext para que ele saiba para onde enviar os comandos e recuperar o estado de reprodução de mídia:

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

Quando você liberar a MediaSession devido à reprodução inativa, defina um token nulo em MediaManager:

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

Se o app oferecer suporte à reprodução de mídia enquanto estiver em segundo plano, em vez de chamar CastReceiverContext.stop() quando o app for enviado para o segundo plano, chame-o somente quando o app estiver em segundo plano e não estiver mais tocando mídia. Exemplo:

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();
  }
}

Como usar o Exoplayer com o Cast Connect

Se você estiver usando Exoplayer, poderá usar o MediaSessionConnector para manter automaticamente a sessão e todas as informações relacionadas, incluindo o estado de reprodução, em vez de rastrear as mudanças manualmente.

MediaSessionConnector.MediaButtonEventHandler pode ser usado para processar eventos do MediaButton chamando setMediaButtonEventHandler(MediaButtonEventHandler), que, de outra forma, é processado por MediaSessionCompat.Callback por padrão.

Para integrar MediaSessionConnector ao app, adicione o seguinte à sua classe de atividade do player ou ao local onde você gerencia a sessão de mídia:

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);
    ...
  }
}

Configuração do app do remetente

Ativar o suporte ao Cast Connect

Depois de atualizar o app remetente com suporte ao Cast Connect, é possível declarar a prontidão dele definindo a flag androidReceiverCompatible em LaunchOptions como "true".

Android

Requer o play-services-cast-framework versão 19.0.0 ou mais recente.

A sinalização androidReceiverCompatible é definida em LaunchOptions (que faz 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

Requer google-cast-sdk versão v4.4.8 ou mais recente.

A sinalização androidReceiverCompatible é definida em GCKLaunchOptions (que faz parte de GCKCastOptions):

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

Requer a versão M87 ou mais recente do navegador Chromium.

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

Configuração do Play Console do Google Cast

Configurar o app Android TV

Adicione o nome do pacote do seu app para Android TV no Play Console do Google Cast para associá-lo ao ID do app Cast.

Registrar dispositivos de desenvolvedor

Registre o número de série do dispositivo Android TV que você vai usar para desenvolvimento no Play Console do Google Cast.

Sem registro, o Cast Connect só funcionará para apps instalados da Google Play Store por motivos de segurança.

Para mais informações sobre como registrar um dispositivo Cast ou Android TV para o desenvolvimento do Google Cast, consulte a página de registro.

Como carregar mídia

Se você já implementou o suporte a links diretos no app Android TV, precisa ter uma definição semelhante configurada no manifesto do 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>

Carregar por entidade no remetente

Nos remetentes, é possível transmitir o link direto definindo o entity nas informações de mídia da solicitação de carregamento:

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

Requer a versão M87 ou mais recente do 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);

O comando de carregamento é enviado por uma intent com seu link direto e o nome do pacote definido no console para desenvolvedores.

Como configurar as credenciais do ATV no remetente

É possível que o app receptor da Web e o app Android TV ofereçam suporte a links diretos e credentials diferentes. Por exemplo, se você processa a autenticação de maneira diferente nas duas plataformas. Para resolver isso, você pode fornecer entity e credentials alternativos para o 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

Requer a versão M87 ou mais recente do 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);

Se o app receptor da Web for iniciado, ele usará entity e credentials na solicitação de carregamento. No entanto, se o app Android TV for iniciado, o SDK vai substituir entity e credentials pelos seus atvEntity e atvCredentials, se especificados.

Carregar por Content ID ou MediaQueueData

Se você não estiver usando entity ou atvEntity, mas estiver usando o Content ID ou o URL de conteúdo nas suas informações de mídia ou se usar dados de solicitação de carregamento de mídia mais detalhados, adicione o filtro de intent predefinido abaixo no app 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>

No lado do remetente, semelhante a carregar por entidade, é possível criar uma solicitação de carregamento com suas informações de conteúdo e chamar 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

Requer a versão M87 ou mais recente do 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);

Processamento de solicitações de carregamento

Na sua atividade, para processar essas solicitações de carregamento, é necessário processar as intents nos callbacks do ciclo de vida da atividade:

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

Se o MediaManager detectar que a intent é de carregamento, ele extrairá um objeto MediaLoadRequestData da intent e invocará MediaLoadCommandCallback.onLoad(). Você precisa substituir esse método para lidar com a solicitação de carregamento. O callback precisa ser registrado antes que MediaManager.onNewIntent() seja chamado. É recomendável usar um método onCreate() de atividade ou aplicativo.

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 processar a intent de carregamento, você pode analisá-la nas estruturas de dados que definimos (MediaLoadRequestData para solicitações de carregamento).

Compatibilidade com comandos de mídia

Suporte básico ao controle de mídia

Os comandos básicos de integração incluem os comandos compatíveis com a sessão de mídia. Esses comandos são notificados por callbacks da sessão de mídia. Você precisa registrar um callback para a sessão de mídia para oferecer suporte a isso (talvez você já esteja fazendo isso).

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());

Compatibilidade com comandos de controle do Cast

Há alguns comandos do Cast que não estão disponíveis em MediaSession, como skipAd() ou setActiveMediaTracks(). Além disso, alguns comandos de fila precisam ser implementados aqui, porque a fila de transmissão não é totalmente compatível com a fila MediaSession.

Kotlin
class MyMediaCommandCallback : MediaCommandCallback() {
    override fun onSkipAd(requestData: RequestData?): Task {
        // 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());

Especificar comandos de mídia compatíveis

Assim como acontece com o receptor do Cast, o app para Android TV precisa especificar quais comandos são compatíveis. Assim, os remetentes podem ativar ou desativar determinados controles de interface. Para comandos que fazem parte de MediaSession, especifique os comandos em PlaybackStateCompat. Outros comandos precisam ser especificados no 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);

Ocultar botões incompatíveis

Se o app Android TV oferecer suporte apenas a controle básico de mídia, mas o app receptor da Web for compatível com controles mais avançados, verifique se o app remetente se comporta corretamente ao transmitir para o app Android TV. Por exemplo, se o app Android TV não oferecer suporte à mudança da taxa de reprodução enquanto o app receptor da Web é, defina as ações compatíveis corretamente em cada plataforma e confira se o app remetente renderiza a interface corretamente.

Como modificar o MediaStatus

Para oferecer suporte a recursos avançados, como faixas, anúncios, transmissões ao vivo e filas, o app para Android TV precisa fornecer outras informações que não podem ser verificadas usando MediaSession.

Para isso, fornecemos a classe MediaStatusModifier. MediaStatusModifier sempre vai funcionar no MediaSession que você definiu em CastReceiverContext.

Para criar e transmitir MediaStatus:

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();

Nossa biblioteca de cliente vai receber a MediaStatus base de MediaSession. Seu app para Android TV pode especificar outro status e substituir o status usando um modificador MediaStatus.

Alguns estados e metadados podem ser definidos em MediaSession e MediaStatusModifier. É altamente recomendável que você as defina apenas em MediaSession. Você ainda pode usar o modificador para substituir os estados em MediaSession. Isso não é recomendado, porque o status no modificador sempre tem uma prioridade maior do que os valores fornecidos por MediaSession.

Como interceptar o MediaStatus antes do envio

Assim como no SDK do receptor da Web, se você quiser fazer alguns retoques finais antes de enviar, especifique um MediaStatusInterceptor para processar o MediaStatus a ser enviado. Passamos um MediaStatusWriter para manipular o MediaStatus antes que ele seja enviado.

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\"}"));
    }
});

Como processar credenciais do usuário

O app Android TV pode permitir que apenas determinados usuários iniciem ou participem da sessão do app. Por exemplo, permita que um remetente inicie ou participe apenas se:

  • O app do remetente está conectado à mesma conta e ao mesmo perfil do app ATV.
  • O app do remetente está conectado à mesma conta, mas em um perfil diferente do app ATV.

Se o app puder processar vários usuários ou usuários anônimos, você poderá permitir que mais usuários participem da sessão do ATV. Se o usuário fornecer credenciais, seu app ATV vai precisar processar as credenciais para que o progresso e outros dados do usuário possam ser rastreados corretamente.

Quando o app remetente é iniciado ou entra no seu app Android TV, o app remetente precisa fornecer as credenciais que representam quem está participando da sessão.

Antes de um remetente iniciar e entrar no app Android TV, você pode especificar um verificador de inicialização para conferir se as credenciais do remetente são permitidas. Caso contrário, o SDK do Cast Connect vai voltar para a inicialização do seu receptor da Web.

Dados das credenciais de inicialização do app do remetente

No lado do remetente, é possível especificar CredentialsData para representar quem está entrando na sessão.

O credentials é uma string que pode ser definida pelo usuário, desde que o app ATV possa compreendê-la. O credentialsType define de qual plataforma o CredentialsData vem ou pode ser um valor personalizado. Por padrão, ele é definido como a plataforma da qual está sendo enviado.

O CredentialsData só é transmitido para o app Android TV durante a inicialização ou o horário de entrada. Se você configurá-lo novamente enquanto estiver conectado, ele não será transmitido para o app Android TV. Se o remetente mudar o perfil enquanto estiver conectado, você poderá permanecer na sessão ou chamar SessionManager.endCurrentCastSession(boolean stopCasting) se achar que o novo perfil é incompatível com a sessão.

O CredentialsData de cada remetente pode ser recuperado usando getSenders no CastReceiverContext para acessar o SenderInfo, o getCastLaunchRequest() para acessar o CastLaunchRequest e, em seguida, getCredentialsData().

Android

Requer o play-services-cast-framework versão 19.0.0 ou mais recente.

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

Requer google-cast-sdk versão v4.8.1 ou mais recente.

Pode ser chamado a qualquer momento depois que as opções forem definidas: GCKCastContext.setSharedInstanceWith(options).

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

Requer a versão M87 ou mais recente do navegador Chromium.

Pode ser chamado a qualquer momento depois que as opções forem definidas: cast.framework.CastContext.getInstance().setOptions(options);.

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

Como implementar o verificador de solicitação de inicialização do ATV

O CredentialsData é transmitido para o app Android TV quando um remetente tenta iniciar ou participar. É possível implementar um LaunchRequestChecker. para permitir ou recusar essa solicitação.

Se uma solicitação for rejeitada, o Web Receiver será carregado em vez de ser iniciado de forma nativa no app ATV. Recuse uma solicitação se o ATV não conseguir processar o usuário que está pedindo para iniciar ou participar. Por exemplo, quando um usuário diferente está conectado ao app ATV e o app não consegue alternar credenciais ou não há um usuário conectado ao app ATV no momento.

Se uma solicitação for permitida, o app ATV será iniciado. É possível personalizar esse comportamento dependendo se o app oferece suporte ao envio de solicitações de carregamento quando um usuário não está conectado ao app ATV ou se há uma incompatibilidade de usuário. Esse comportamento é totalmente personalizável no LaunchRequestChecker.

Crie uma classe implementando a interface 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;
}

Em seguida, defina-o em 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 no LaunchRequestChecker inicia o app ATV e false inicia seu app receptor da Web.

Como enviar e receber mensagens personalizadas

O protocolo de transmissão permite o envio de mensagens de string personalizadas entre remetentes e seu aplicativo receptor. É necessário registrar um namespace (canal) para enviar mensagens antes de inicializar o CastReceiverContext.

Android TV: especificar namespace personalizado

É necessário especificar os namespaces com suporte no CastReceiverOptions durante a configuração:

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: como enviar mensagens

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: receber mensagens de namespace personalizadas

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());