Tornar um app Android TV compatível com Cast

1. Visão geral

Logotipo do Google Cast

Este codelab ensina a modificar um app Android TV para que ele ofereça suporte a transmissão e comunicação dos apps de envio do Google Cast.

O que é o Google Cast e a Cast Connect?

O Google Cast permite que os usuários transmitam conteúdo de um dispositivo móvel para uma TV. Uma sessão típica do Google Cast consiste em dois componentes: um aplicativo de transmissão e um receptor. Os aplicativos de transmissão, como um app para dispositivos móveis ou um site, como o YouTube.com, iniciam e controlam a reprodução de um aplicativo receptor do Cast. Os aplicativos receptores do Cast são apps HTML 5 executados em dispositivos Chromecast e Android TV.

Quase todo o estado de uma sessão do Google Cast é armazenado no app receptor. Quando o estado for atualizado, por exemplo, se um novo item de mídia for carregado, um status de mídia será transmitido para todos os apps de transmissão. Essas transmissões contêm o estado atual da sessão do Google Cast. Os aplicativos de transmissão usam esse status de mídia para exibir informações de reprodução na IU.

O Cast Connect expande essa infraestrutura, com seu app Android TV atuando como um receptor. A biblioteca Cast Connect permite que seu app Android TV receba mensagens e transmita o status de mídia como se fosse um aplicativo receptor.

O que vamos criar?

Ao concluir este codelab, você poderá usar os apps de transmissão do Google Cast para transmitir vídeos para um app Android TV, que também pode se comunicar com apps de transmissão com o protocolo do Cast.

O que você vai aprender

  • Como adicionar a biblioteca Cast Connect a um app ATV de amostra.
  • Como conectar um transmissor do Cast e iniciar o app ATV.
  • Como iniciar a reprodução de mídia no app ATV usando um app de transmissão do Google Cast.
  • Como enviar o status de mídia do app ATV para os apps de transmissão do Google Cast.

O que é necessário

2. Fazer o download do exemplo de código

Você pode fazer download de todo o exemplo de código para seu computador…

e descompactar o arquivo ZIP salvo.

3. Executar o app de amostra

Primeiro, vamos ver a aparência do app de amostra concluído. O app Android TV usa a IU do Leanback e um player de vídeo básico. O usuário pode selecionar um vídeo de uma lista para ser reproduzido na TV. Com o app de transmissão para dispositivos móveis associado, um usuário também pode transmitir um vídeo para o app Android TV.

Imagem de uma série de miniaturas de vídeo (uma delas está destacada) sobrepondo uma prévia em tela cheia de um vídeo; as palavras "Cast Connect" aparecem no canto superior direito

Registrar dispositivos de desenvolvedor

Para ativar os recursos da Cast Connect para o desenvolvimento de apps, é preciso registrar o número de série do Chromecast integrado do dispositivo Android TV que você vai usar no Console para desenvolvedores do Google Cast. Você pode encontrar o número de série acessando Configurações > Preferências do dispositivo > Chromecast integrado > Número de série no Android TV. Observe que ele é diferente do número de série do dispositivo físico e precisa ser verificado usando o método descrito acima.

Imagem de uma tela do Android TV mostrando a tela "Chromecast integrado", o número da versão e o número de série

Se você não registrar o dispositivo, a Cast Connect só funcionará para apps instalados da Google Play Store por motivos de segurança. Reinicie o dispositivo 15 minutos depois do registro.

Instalar o app Android de transmissão

Para testar o envio de solicitações de um dispositivo móvel, fornecemos um aplicativo de transmissão simples chamado "Transmitir vídeos como um arquivo mobile-sender-0629.apk" no arquivo ZIP do código-fonte. Usaremos o ADB para instalar o APK. Se você já tiver instalado uma versão diferente do Cast Videos, desinstale essa versão de todos os perfis no dispositivo antes de continuar.

  1. Ative as opções do desenvolvedor e a depuração USB no smartphone Android.
  2. Use um cabo de dados USB para conectar o smartphone Android ao computador de desenvolvimento.
  3. Instale o mobile-sender-0629.apk no smartphone Android.

Imagem de uma janela de terminal executando o comando adb install para instalar o arquivo mobile-sender.apk

  1. Encontre o app de transmissão Transmitir vídeos no smartphone Android. Ícone do app de transmissão Cast Videos

Imagem do app de transmissão Cast Videos em execução na tela de um smartphone Android

Instalar o app Android TV

As instruções a seguir descrevem como abrir e executar o app de amostra concluído no Android Studio:

  1. Selecione Import Project na tela de boas-vindas ou as opções File > New > Import Project… do menu.
  2. Selecione o diretório ícone da pastaapp-done na pasta de exemplo de código e clique em OK.
  3. Clique em File > Botão "Sync Project with Gradle" do Android App Studio Sync Project with Gradle Files.
  4. Ative as opções do desenvolvedor e a depuração USB no dispositivo Android TV.
  5. Faça a conexão adb com seu dispositivo Android TV. O dispositivo será exibido no Android Studio. Imagem mostrando o dispositivo Android TV aparecendo na barra de ferramentas do Android Studio
  6. Clique no botão Botão "Run" do Android Studio, um triângulo verde apontando para a direitaRun. O app ATV Cast Connect Codelab vai aparecer após alguns segundos.

Vamos usar a Cast Connect com o app ATV

  1. Acesse a tela inicial do Android TV.
  2. Abra o app de transmissão Cast Videos no smartphone Android. Clique no botão Transmitir Ícone do botão "Transmitir" e selecione o dispositivo ATV.
  3. O app ATV Cast Connect Codelab será iniciado no ATV, e o botão Transmitir no app de transmissão vai indicar que ele está conectado Ícone do botão Transmitir com cores invertidas.
  4. Selecione um vídeo no app ATV e ele começará a ser reproduzido no ATV.
  5. No smartphone, um minicontrolador estará visível na parte inferior do app de transmissão. Você pode usar o botão assistir/pausar para controlar a reprodução.
  6. Selecione um vídeo no smartphone para reproduzi-lo. O vídeo começará a ser reproduzido no ATV e o controlador expandido será exibido no transmissor móvel.
  7. Bloqueie o smartphone e, ao desbloqueá-lo, você verá uma notificação na tela de bloqueio para controlar a reprodução de mídia ou parar a transmissão.

Imagem de uma seção da tela de um smartphone Android com um miniplayer reproduzindo um vídeo

4. Preparar o projeto inicial

Agora que confirmamos a integração completa da Cast Connect no app, precisamos adicionar compatibilidade com a Cast Connect ao app inicial que você transferiu por download. Agora está tudo pronto para você criar com base no projeto inicial usando o Android Studio:

  1. Selecione Import Project na tela de boas-vindas ou as opções File > New > Import Project… do menu.
  2. Selecione o diretório ícone da pastaapp-start na pasta de exemplo de código e clique em "OK".
  3. Clique em File > Botão "Sync Project with Gradle" do Android Studio Sync Project with Gradle Files.
  4. Selecione um dispositivo ATV e clique no botão Botão "Run" do Android Studio, um triângulo verde apontando para a direitaRun para executar o app e conferir a interface. Barra de ferramentas do Android Studio mostrando o dispositivo Android TV selecionado

Imagem de uma série de miniaturas de vídeo (uma delas está destacada) sobrepondo uma prévia em tela cheia de um vídeo; as palavras "Cast Connect" aparecem no canto superior direito

Design do app

O app fornece uma lista de vídeos para os usuários navegarem. Os usuários podem selecionar um vídeo para assistir no Android TV. O app consiste em duas atividades principais: MainActivity e PlaybackActivity.

MainActivity

Esta atividade contém um fragmento (MainFragment). A lista de vídeos e os metadados associados são configurados na classe MovieList. O método setupMovies() é chamado para criar uma lista de objetos Movie.

Um objeto Movie representa uma entidade de vídeo com título, descrição, miniaturas e URL do vídeo. Cada objeto Movie está vinculado a um CardPresenter para exibir a miniatura do vídeo com o título e o estúdio, e é transmitido para o ArrayObjectAdapter.

Quando um item é selecionado, o objeto Movie correspondente é transmitido para a PlaybackActivity.

PlaybackActivity

Esta atividade contém um fragmento (PlaybackVideoFragment) que hospeda uma VideoView com ExoPlayer, alguns controles de mídia e uma área de texto para mostrar a descrição do vídeo selecionado e permite que o usuário reproduza o vídeo no Android TV. O usuário pode usar o controle remoto para assistir/pausar ou procurar na reprodução de vídeos.

Pré-requisitos da Cast Connect

A Cast Connect usa novas versões do Google Play Services que exigem que seu app ATV tenha sido atualizado para usar o namespace do AndroidX.

Para que haja compatibilidade com a Cast Connect no app Android TV, você precisa criar e oferecer compatibilidade com eventos de uma sessão de mídia. A biblioteca Cast Connect gera o status de mídia com base no status da sessão de mídia. A sessão de mídia também é usada pela biblioteca Cast Connect para sinalizar quando receber determinadas mensagens de um transmissor, como uma pausa.

5. Como configurar a compatibilidade com o Google Cast

Dependências

Atualize o arquivo build.gradle do app para incluir as dependências de biblioteca necessárias:

dependencies {
    ....

    // Cast Connect libraries
    implementation 'com.google.android.gms:play-services-cast-tv:20.0.0'
    implementation 'com.google.android.gms:play-services-cast:21.1.0'
}

Sincronize o projeto para confirmar se foi criado sem erros.

Inicialização

CastReceiverContext é um objeto Singleton para coordenar todas as interações com o Google Cast. Implemente a interface ReceiverOptionsProvider para fornecer CastReceiverOptions quando o CastReceiverContext for inicializado.

Crie o arquivo CastReceiverOptionsProvider.kt e adicione a seguinte classe ao projeto:

package com.google.sample.cast.castconnect

import android.content.Context
import com.google.android.gms.cast.tv.ReceiverOptionsProvider
import com.google.android.gms.cast.tv.CastReceiverOptions

class CastReceiverOptionsProvider : ReceiverOptionsProvider {
    override fun getOptions(context: Context): CastReceiverOptions {
        return CastReceiverOptions.Builder(context)
                .setStatusText("Cast Connect Codelab")
                .build()
    }
}

Em seguida, especifique o provedor de opções do receptor na tag <application> do arquivo AndroidManifest.xml do app:

<application>
  ...
  <meta-data
    android:name="com.google.android.gms.cast.tv.RECEIVER_OPTIONS_PROVIDER_CLASS_NAME"
    android:value="com.google.sample.cast.castconnect.CastReceiverOptionsProvider" />
</application>

Para se conectar com o app ATV de transmissão do Cast, selecione a atividade que você quer iniciar. Neste codelab, vamos iniciar a MainActivity do app quando uma sessão do Cast for iniciada. No arquivo AndroidManifest.xml, adicione o filtro de intent de inicialização na MainActivity.

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

Ciclo de vida do contexto do receptor do Cast

Inicie o CastReceiverContext junto do app e interrompa o CastReceiverContext quando o app for para o segundo plano. Recomendamos usar o LifecycleObserver da biblioteca androidx.lifecycle para gerenciar chamadas de CastReceiverContext.start() e CastReceiverContext.stop().

Abra o arquivo MyApplication.kt e inicialize o contexto de transmissão chamando initInstance() no método onCreate do aplicativo. Na classe AppLifeCycleObserver, use start() no CastReceiverContext quando o aplicativo for retomado e stop() quando ele for pausado:

package com.google.sample.cast.castconnect

import com.google.android.gms.cast.tv.CastReceiverContext
...

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        CastReceiverContext.initInstance(this)
        ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleObserver())
    }

    class AppLifecycleObserver : DefaultLifecycleObserver {
        override fun onResume(owner: LifecycleOwner) {
            Log.d(LOG_TAG, "onResume")
            CastReceiverContext.getInstance().start()
        }

        override fun onPause(owner: LifecycleOwner) {
            Log.d(LOG_TAG, "onPause")
            CastReceiverContext.getInstance().stop()
        }
    }
}

Como conectar a MediaSession ao MediaManager

MediaManager é uma propriedade do Singleton CastReceiverContext, que gerencia o status da mídia, processa a intent de carregamento, converte as mensagens do namespace de mídia dos transmissores em comandos de mídia e envia o status da mídia de volta aos transmissores.

Quando você cria uma MediaSession, também é necessário fornecer o token da MediaSession atual para MediaManager para que ele saiba para onde enviar os comandos e recuperar o estado da reprodução de mídia. No arquivo PlaybackVideoFragment.kt, verifique se a MediaSession foi inicializada antes de definir o token como MediaManager.

import com.google.android.gms.cast.tv.CastReceiverContext
import com.google.android.gms.cast.tv.media.MediaManager
...

class PlaybackVideoFragment : VideoSupportFragment() {
    private var castReceiverContext: CastReceiverContext? = null
    ...

    private fun initializePlayer() {
        if (mPlayer == null) {
            ...
            mMediaSession = MediaSessionCompat(getContext(), LOG_TAG)
            ...
            castReceiverContext = CastReceiverContext.getInstance()
            if (castReceiverContext != null) {
                val mediaManager: MediaManager = castReceiverContext!!.getMediaManager()
                mediaManager.setSessionCompatToken(mMediaSession!!.getSessionToken())
            }

        }
    }
}

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

private fun releasePlayer() {
    mMediaSession?.release()
    castReceiverContext?.mediaManager?.setSessionCompatToken(null)
    ...
}

Vamos executar o app de amostra

Clique no botão Botão &quot;Run&quot; do Android Studio, um triângulo verde apontando para a direitaRun para implantar o app no dispositivo ATV, feche-o e volte para a tela inicial do ATV. No transmissor, clique no botão Transmitir Ícone do botão &quot;Transmitir&quot; e selecione o dispositivo ATV. Você verá que o app ATV foi iniciado no dispositivo do ATV e o estado do botão Transmitir mostra que ele está conectado.

6. Carregando mídia

O comando de carregamento é enviado por uma intent com o nome do pacote definido no Developer Console. É necessário adicionar o seguinte filtro de intent pré-definido no app Android TV para especificar a atividade de destino que receberá a intent. No arquivo AndroidManifest.xml, adicione o filtro de intent de carregamento a PlayerActivity:

<activity android:name="com.google.sample.cast.castconnect.PlaybackActivity"
          android:launchMode="singleTask"
          android:exported="true">
  <intent-filter>
     <action android:name="com.google.android.gms.cast.tv.action.LOAD"/>
     <category android:name="android.intent.category.DEFAULT" />
  </intent-filter>
</activity>

Como processar solicitações de carregamento no Android TV

Agora que a atividade está configurada para receber essa intent contendo uma solicitação de carregamento, precisaremos processá-la.

O app chama o método privado processIntent quando a atividade é iniciada. Esse método contém a lógica para processar as intents recebidas. Para processar uma solicitação de carregamento, modificaremos esse método e enviaremos a intent para ser processada chamando o método onNewIntent da instância MediaManager. Se MediaManager detectar que a intent é uma solicitação de carregamento, ele extrairá o objeto MediaLoadRequestData da intent e invocará MediaLoadCommandCallback.onLoad(). Modifique o método processIntent no arquivo PlaybackVideoFragment.kt para processar a intent que contém a solicitação de carregamento:

fun processIntent(intent: Intent?) {
    val mediaManager: MediaManager = CastReceiverContext.getInstance().getMediaManager()
    // Pass intent to Cast SDK
    if (mediaManager.onNewIntent(intent)) {
        return
    }

    // Clears all overrides in the modifier.
    mediaManager.getMediaStatusModifier().clear()

    // If the SDK doesn't recognize the intent, handle the intent with your own logic.
    ...
}

Em seguida, estenderemos a classe abstrata MediaLoadCommandCallback que substituirá o método onLoad() chamado pelo MediaManager. Este método recebe os dados da solicitação de carregamento e os converte em um objeto Movie. Depois de convertido, o filme será reproduzido pelo player local. O MediaManager é atualizado com MediaLoadRequest e transmite o MediaStatus para os transmissores conectados. Crie uma classe particular aninhada com o nome MyMediaLoadCommandCallback no arquivo PlaybackVideoFragment.kt:

import com.google.android.gms.cast.MediaLoadRequestData
import com.google.android.gms.cast.MediaInfo
import com.google.android.gms.cast.MediaMetadata
import com.google.android.gms.cast.MediaError
import com.google.android.gms.cast.tv.media.MediaException
import com.google.android.gms.cast.tv.media.MediaCommandCallback
import com.google.android.gms.cast.tv.media.QueueUpdateRequestData
import com.google.android.gms.cast.tv.media.MediaLoadCommandCallback
import com.google.android.gms.tasks.Task
import com.google.android.gms.tasks.Tasks
import android.widget.Toast
...

private inner class MyMediaLoadCommandCallback :  MediaLoadCommandCallback() {
    override fun onLoad(
        senderId: String?, mediaLoadRequestData: MediaLoadRequestData): Task<MediaLoadRequestData> {
        Toast.makeText(activity, "onLoad()", Toast.LENGTH_SHORT).show()
        return if (mediaLoadRequestData == null) {
            // Throw MediaException to indicate load failure.
            Tasks.forException(MediaException(
                MediaError.Builder()
                    .setDetailedErrorCode(MediaError.DetailedErrorCode.LOAD_FAILED)
                    .setReason(MediaError.ERROR_REASON_INVALID_REQUEST)
                    .build()))
        } else Tasks.call {
            play(convertLoadRequestToMovie(mediaLoadRequestData)!!)
            // Update media metadata and state
            val mediaManager = castReceiverContext!!.mediaManager
            mediaManager.setDataFromLoad(mediaLoadRequestData)
            mediaLoadRequestData
        }
    }
}

private fun convertLoadRequestToMovie(mediaLoadRequestData: MediaLoadRequestData?): Movie? {
    if (mediaLoadRequestData == null) {
        return null
    }
    val mediaInfo: MediaInfo = mediaLoadRequestData.getMediaInfo() ?: return null
    var videoUrl: String = mediaInfo.getContentId()
    if (mediaInfo.getContentUrl() != null) {
        videoUrl = mediaInfo.getContentUrl()
    }
    val metadata: MediaMetadata = mediaInfo.getMetadata()
    val movie = Movie()
    movie.videoUrl = videoUrl
    movie.title = metadata?.getString(MediaMetadata.KEY_TITLE)
    movie.description = metadata?.getString(MediaMetadata.KEY_SUBTITLE)
    if(metadata?.hasImages() == true) {
        movie.cardImageUrl = metadata.images[0].url.toString()
    }
    return movie
}

Agora que o callback foi definido, registre-o no MediaManager. O callback precisa ser registrado antes de chamar MediaManager.onNewIntent(). Adicione setMediaLoadCommandCallback quando o player for inicializado:

private fun initializePlayer() {
    if (mPlayer == null) {
        ...
        mMediaSession = MediaSessionCompat(getContext(), LOG_TAG)
        ...
        castReceiverContext = CastReceiverContext.getInstance()
        if (castReceiverContext != null) {
            val mediaManager: MediaManager = castReceiverContext.getMediaManager()
            mediaManager.setSessionCompatToken(mMediaSession.getSessionToken())
            mediaManager.setMediaLoadCommandCallback(MyMediaLoadCommandCallback())
        }
    }
}

Vamos executar o app de amostra

Clique no botão Botão &quot;Run&quot; do Android Studio, um triângulo verde apontando para a direitaRun para implantar o app no dispositivo ATV. No transmissor, clique no botão Transmitir Ícone do botão Transmitir e selecione o dispositivo ATV. O app ATV será iniciado no dispositivo ATV. Selecione um vídeo no dispositivo móvel para começar a reprodução no ATV. Verifique se você recebeu uma notificação no smartphone com os controles de reprodução. Tente usar controles, como pausar. O vídeo no dispositivo do ATV precisa ser pausado.

7. Como oferecer compatibilidade com comandos de controle do Cast

Agora, o aplicativo atual é compatível com comandos básicos que são compatíveis com uma sessão de mídia, como reproduzir, pausar e procurar. No entanto, há alguns comandos de controle do Cast que não estão disponíveis na sessão de mídia. Você precisa registrar um MediaCommandCallback para oferecer compatibilidade com esses comandos de controle do Cast.

Adicione MyMediaCommandCallback à instância do MediaManager usando setMediaCommandCallback quando o player for inicializado:

private fun initializePlayer() {
    ...
    castReceiverContext = CastReceiverContext.getInstance()
    if (castReceiverContext != null) {
        val mediaManager = castReceiverContext!!.mediaManager
        ...
        mediaManager.setMediaCommandCallback(MyMediaCommandCallback())
    }
}

Crie a classe MyMediaCommandCallback para substituir os métodos, como onQueueUpdate(), para compatibilidade com esses comandos de controle do Cast:

private inner class MyMediaCommandCallback : MediaCommandCallback() {
    override fun onQueueUpdate(
        senderId: String?,
        queueUpdateRequestData: QueueUpdateRequestData
    ): Task<Void> {
        Toast.makeText(getActivity(), "onQueueUpdate()", Toast.LENGTH_SHORT).show()
        // Queue Prev / Next
        if (queueUpdateRequestData.getJump() != null) {
            Toast.makeText(
                getActivity(),
                "onQueueUpdate(): Jump = " + queueUpdateRequestData.getJump(),
                Toast.LENGTH_SHORT
            ).show()
        }
        return super.onQueueUpdate(senderId, queueUpdateRequestData)
    }
}

8. Como trabalhar com status de mídia

Como modificar o status da mídia

O Cast Connect recebe o status de mídia base da sessão de mídia. Para oferecer compatibilidade com recursos avançados, o app Android TV pode especificar e substituir outras propriedades de status com um MediaStatusModifier. MediaStatusModifier sempre funcionará na MediaSession que você definiu no CastReceiverContext.

Por exemplo, para especificar setMediaCommandSupported quando o callback onLoad for acionado:

import com.google.android.gms.cast.MediaStatus
...
private class MyMediaLoadCommandCallback : MediaLoadCommandCallback() {
    fun onLoad(
        senderId: String?,
        mediaLoadRequestData: MediaLoadRequestData
    ): Task<MediaLoadRequestData> {
        Toast.makeText(getActivity(), "onLoad()", Toast.LENGTH_SHORT).show()
        ...
        return Tasks.call({
            play(convertLoadRequestToMovie(mediaLoadRequestData)!!)
            ...
            // Use MediaStatusModifier to provide additional information for Cast senders.
            mediaManager.getMediaStatusModifier()
                .setMediaCommandSupported(MediaStatus.COMMAND_QUEUE_NEXT, true)
                .setIsPlayingAd(false)
            mediaManager.broadcastMediaStatus()
            // Return the resolved MediaLoadRequestData to indicate load success.
            mediaLoadRequestData
        })
    }
}

Como interceptar o MediaStatus antes da transmissão

Assim como o MessageInterceptor do SDK do receptor da Web, você pode especificar um MediaStatusWriter no seu MediaManager para realizar outras modificações no MediaStatus antes que ele seja transmitido para os transmissores conectados.

Por exemplo, você pode definir dados personalizados no MediaStatus antes de enviá-los para transmissores móveis:

import com.google.android.gms.cast.tv.media.MediaManager.MediaStatusInterceptor
import com.google.android.gms.cast.tv.media.MediaStatusWriter
import org.json.JSONObject
import org.json.JSONException
...

private fun initializePlayer() {
    if (mPlayer == null) {
        ...
        if (castReceiverContext != null) {
            ...
            val mediaManager: MediaManager = castReceiverContext.getMediaManager()
            ...
            // Use MediaStatusInterceptor to process the MediaStatus before sending out.
            mediaManager.setMediaStatusInterceptor(
                MediaStatusInterceptor { mediaStatusWriter: MediaStatusWriter ->
                    try {
                        mediaStatusWriter.setCustomData(JSONObject("{myData: 'CustomData'}"))
                    } catch (e: JSONException) {
                        Log.e(LOG_TAG,e.message,e);
                    }
            })
        }
    }
}        

9. Parabéns

Agora você já sabe como adicionar compatibilidade com o Google Cast a um app Android TV usando a biblioteca Cast Connect.

Consulte o guia para desenvolvedores para mais detalhes: /cast/docs/android_tv_receiver.