Cast Connect com app ATV

googlecastnew500.png

Este codelab ensinará você a modificar um app Android TV já existente para que seus apps de transmissão do Google Cast possam transmitir conteúdo e se comunicar com o app Android TV.

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

Pré-requisitos

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

Fazer download do código-fonte

e descompactar o arquivo ZIP salvo.

Primeiro, vamos ver como será o 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.

f9f98aa234e84bae.png

Registrar dispositivos de desenvolvedor

Para ativar os recursos da Cast Connect para o desenvolvimento de aplicativos, é preciso registrar o número de série do Chromecast integrado do dispositivo Android TV que você usará no Developer Console do 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.

f74dfe19bc459b76.png

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 Cast Videos. 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.apk no smartphone Android.

93e35a0f0332f290.png

  1. Você poderá encontrar o app de transmissão Cast Videos no smartphone Android. e29d89df484d9661.png

d6a0435ec3bac0af.png

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 nas opções de menu File > New > Import Project....
  2. Selecione o diretório android_studio_folder.pngapp-done na pasta do exemplo de código e clique em OK.
  3. Clique em File > 1791b5212a8947d.png Sync Project with Gradle Files.
  4. Ative as opções do desenvolvedor e a depuração USB no dispositivo Android TV.
  5. Conecte ao dispositivo Android TV usando o ADB. O dispositivo será exibido no Android Studio. 7bcf00bfb6877ad5.png
  6. Clique no botão execute.pngRun. O app ATV Cast Connect Codelab será exibido 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 f77992b2cf0422a2.png 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 indicará que ele está conectado 303287388679d79b.png.
  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 seu 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.

a20257e816c913a.png

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 você está pronto para usar o Android Studio para expandir o projeto inicial:

  1. Selecione Import Project na tela de boas-vindas ou nas opções de menu File > New > Import Project....
  2. Selecione o diretório android_studio_folder.pngapp-start na pasta do exemplo de código e clique em OK.
  3. Clique em File > 1791b5212a8947d.png Sync Project with Gradle Files.
  4. Selecione um dispositivo ATV e clique no botão execute.pngRun para executar o app e explorar a IU. 7bcf00bfb6877ad5.png

f9f98aa234e84bae.png

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.

Dependências

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

dependencies {
    ....

    // Cast Connect libraries and dependencies
    implementation 'com.google.android.gms:play-services-cast-tv:17.0.0'
    implementation 'com.google.android.gms:play-services-cast:19.0.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.java e adicione a seguinte classe ao projeto:

package com.google.sample.cast.castconnect;

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

public class CastReceiverOptionsProvider implements ReceiverOptionsProvider {
    @Override
    public CastReceiverOptions getOptions(Context context) {
        return new 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, iniciaremos 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 MyApplication.java 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;

public class MyApplication extends Application {

    private static final String LOG_TAG = "MyApplication";

    @Override
    public void onCreate() {
        super.onCreate();
        CastReceiverContext.initInstance(this);
        ProcessLifecycleOwner.get().getLifecycle().addObserver(new AppLifecycleObserver());
    }

    public static class AppLifecycleObserver implements DefaultLifecycleObserver {
        @Override
        public void onResume(@NonNull LifecycleOwner owner) {
            Log.d(LOG_TAG, "onResume");
            CastReceiverContext.getInstance().start();
        }

        @Override
        public void onPause(@NonNull LifecycleOwner owner) {
            Log.d(LOG_TAG, "onPause");
            CastReceiverContext.getInstance().stop();
        }
    }
}

Conectando 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. Verifique se a MediaSession foi inicializada antes de definir o token como MediaManager.

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

public class PlaybackVideoFragment extends VideoSupportFragment {

    private CastReceiverContext castReceiverContext;
    ...

    private void initializePlayer() {
        if (mPlayer == null) {
            ...
            mMediaSession = new MediaSessionCompat(getContext(), LOG_TAG);
            ...

            castReceiverContext = CastReceiverContext.getInstance();
            if (castReceiverContext != null) {
                MediaManager mediaManager = castReceiverContext.getMediaManager();
                mediaManager.setSessionCompatToken(mMediaSession.getSessionToken());
            }
        }
    }
}

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

private void releasePlayer() {
    if (mMediaSession != null) {
        mMediaSession.release();
    }
    if (castReceiverContext != null) {
        MediaManager mediaManager = castReceiverContext.getMediaManager();
        mediaManager.setSessionCompatToken(null);
    }
    ...
}

Vamos executar o app de amostra

Clique no botão execute.pngRun para implantar o app no seu dispositivo ATV e feche-o novamente para voltar à tela inicial do ATV. No transmissor, clique no botão Transmitir f77992b2cf0422a2.png 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.

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">
  <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 em PlaybackVideoFragment para manipular a intent que contém a solicitação de carregamento:

public void processIntent(Intent intent) {
    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 privada aninhada com o nome MyMediaLoadCommandCallback no PlaybackVideoFragment:

private class MyMediaLoadCommandCallback extends MediaLoadCommandCallback {
    @Override
    public Task<MediaLoadRequestData> onLoad(String senderId, MediaLoadRequestData mediaLoadRequestData) {
        Toast.makeText(getActivity(), "onLoad()", Toast.LENGTH_SHORT).show();

        if (mediaLoadRequestData == null) {
            // Throw MediaException to indicate load failure.
            return Tasks.forException(new MediaException(
                    new MediaError.Builder()
                            .setDetailedErrorCode(MediaError.DetailedErrorCode.LOAD_FAILED)
                            .setReason(MediaError.ERROR_REASON_INVALID_REQUEST)
                            .build()));
        }

        return Tasks.call(() -> {
            play(convertLoadRequestToMovie(mediaLoadRequestData));

            // Update media metadata and state
            MediaManager mediaManager = castReceiverContext.getMediaManager();
            mediaManager.setDataFromLoad(mediaLoadRequestData);
            mediaManager.broadcastMediaStatus();

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

private Movie convertLoadRequestToMovie(MediaLoadRequestData mediaLoadRequestData) {
    if (mediaLoadRequestData == null) {
        return null;
    }
    MediaInfo mediaInfo = mediaLoadRequestData.getMediaInfo();
    if (mediaInfo == null) {
        return null;
    }

    String videoUrl = mediaInfo.getContentId();
    if (mediaInfo.getContentUrl() != null) {
        videoUrl = mediaInfo.getContentUrl();
    }

    MediaMetadata metadata = mediaInfo.getMetadata();
    Movie movie = new Movie();
    movie.setVideoUrl(videoUrl);
    if (metadata != null) {
        movie.setTitle(metadata.getString(MediaMetadata.KEY_TITLE));
        movie.setDescription(metadata.getString(MediaMetadata.KEY_SUBTITLE));
        movie.setCardImageUrl(metadata.getImages().get(0).getUrl().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 void initializePlayer() {
    if (mPlayer == null) {
        ...
        mMediaSession = new MediaSessionCompat(getContext(), LOG_TAG);
        ...

        castReceiverContext = CastReceiverContext.getInstance();
        if (castReceiverContext != null) {
            MediaManager mediaManager = castReceiverContext.getMediaManager();
            mediaManager.setSessionCompatToken(mMediaSession.getSessionToken());
            mediaManager.setMediaLoadCommandCallback(new MyMediaLoadCommandCallback());
        }
    }
}

Vamos executar o app de amostra

Clique no botão execute.pngRun para implantar o app no dispositivo ATV. No transmissor, clique no botão Transmitir f77992b2cf0422a2.png 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.

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 void initializePlayer() {
        ...

        castReceiverContext = CastReceiverContext.getInstance();
        if (castReceiverContext != null) {
            MediaManager mediaManager = castReceiverContext.getMediaManager();
            ...
            mediaManager.setMediaCommandCallback(new MyMediaCommandCallback());
        }
}

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

private class MyMediaCommandCallback extends MediaCommandCallback {
        @Override
        public Task<Void> onQueueUpdate(String senderId, QueueUpdateRequestData queueUpdateRequestData) {
            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);
        }
}

Como modificar o status de 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:

private class MyMediaLoadCommandCallback extends MediaLoadCommandCallback {
    @Override
    public Task<MediaLoadRequestData> onLoad(String senderId, MediaLoadRequestData mediaLoadRequestData) {
        Toast.makeText(getActivity(), "onLoad()", Toast.LENGTH_SHORT).show();

        ...

        return Tasks.call(() -> {
            play(convertLoadRequestToMovie(mediaLoadRequestData));

            // Update media metadata and state
            MediaManager mediaManager = castReceiverContext.getMediaManager();
            mediaManager.setDataFromLoad(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.
            return 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:

MediaManager mediaManager = castReceiverContext.getMediaManager();
...

// Use MediaStatusInterceptor to process the MediaStatus before sending out.
mediaManager.setMediaStatusInterceptor(mediaStatusWriter -> {
    try {
        mediaStatusWriter.setCustomData(new JSONObject("{myData: 'CustomData'}"));
    } catch (JSONException e) {
        e.printStackTrace();
    }
});

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

Dê uma olhada no guia do desenvolvedor para mais detalhes: https://developers.google.com/cast/docs/android_tv_receiver