Integrar o Google Cast ao seu app Android

Este guia para desenvolvedores descreve como adicionar suporte ao Google Cast ao seu app remetente Android usando o SDK do remetente do Android.

O dispositivo móvel ou laptop é o remetente que controla a reprodução, e o dispositivo de transmissão Google Cast é o receptor que mostra o conteúdo na TV.

O framework do remetente se refere ao binário da biblioteca de classes do Cast e aos recursos associados presentes no tempo de execução no remetente. O app remetente ou app do Cast se refere a um app que também está em execução no remetente. O app receptor da Web se refere ao aplicativo HTML em execução no dispositivo compatível com o Cast.

O framework do remetente usa um design de callback assíncrono para informar o app remetente sobre eventos e fazer a transição entre vários estados do ciclo de vida do app do Cast.

Fluxo de aplicativos

As etapas a seguir descrevem o fluxo de execução típico de alto nível para um app Android remetente:

  • O framework do Cast inicia automaticamente a descoberta de dispositivos MediaRouter com base no Activity ciclo de vida.
  • Quando o usuário clica no botão "Transmitir", o framework apresenta a caixa de diálogo do Cast com a lista de dispositivos de transmissão descobertos.
  • Quando o usuário seleciona um dispositivo de transmissão, o framework tenta iniciar o app receptor da Web no dispositivo de transmissão.
  • O framework invoca callbacks no app remetente para confirmar que o app receptor da Web foi iniciado.
  • O framework cria um canal de comunicação entre os apps remetente e receptor da Web.
  • O framework usa o canal de comunicação para carregar e controlar a reprodução de mídia no receptor da Web.
  • O framework sincroniza o estado de reprodução de mídia entre o remetente e o receptor da Web: quando o usuário faz ações de interface do remetente, o framework transmite essas solicitações de controle de mídia para o receptor da Web. Quando o receptor da Web envia atualizações de status de mídia, o framework atualiza o estado da interface do remetente.
  • Quando o usuário clica no botão "Transmitir" para se desconectar do dispositivo de transmissão, o framework desconecta o app remetente do receptor da Web.

Para uma lista completa de todas as classes, métodos e eventos no SDK do Android do Google Cast, consulte a Referência da API do remetente do Google Cast para Android. As seções a seguir abrangem as etapas para adicionar o Cast ao seu app Android.

Configurar o manifesto do Android

O arquivo AndroidManifest.xml do seu app exige que você configure os seguintes elementos para o SDK do Cast:

uses-sdk

Defina os níveis mínimo e de destino da API Android que o SDK do Cast oferece suporte. Atualmente, o mínimo é o nível 23 da API e o destino é o nível 34 da API.

<uses-sdk
        android:minSdkVersion="23"
        android:targetSdkVersion="34" />

android:theme

Defina o tema do seu app com base na versão mínima do SDK do Android. Por exemplo, se você não estiver implementando seu próprio tema, use uma variante de Theme.AppCompat ao segmentar uma versão mínima do SDK do Android que seja anterior ao Lollipop.

<application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/Theme.AppCompat" >
       ...
</application>

Inicializar o contexto do Cast

O framework tem um objeto Singleton global, o CastContext, que coordena todas as interações do framework.

Seu app precisa implementar a OptionsProvider interface para fornecer as opções necessárias para inicializar o CastContext Singleton. OptionsProvider fornece uma instância de CastOptions que contém opções que afetam o comportamento do framework. A mais importante delas é o ID do aplicativo receptor da Web, que é usado para filtrar os resultados da descoberta e iniciar o app receptor da Web quando uma sessão de transmissão é iniciada.

Kotlin
class CastOptionsProvider : OptionsProvider {
    override fun getCastOptions(context: Context): CastOptions {
        return Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .build()
    }

    override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? {
        return null
    }
}
Java
public class CastOptionsProvider implements OptionsProvider {
    @Override
    public CastOptions getCastOptions(Context context) {
        CastOptions castOptions = new CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .build();
        return castOptions;
    }
    @Override
    public List<SessionProvider> getAdditionalSessionProviders(Context context) {
        return null;
    }
}

Você precisa declarar o nome totalmente qualificado do OptionsProvider implementado como um campo de metadados no arquivo AndroidManifest.xml do app remetente:

<application>
    ...
    <meta-data
        android:name=
            "com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
        android:value="com.foo.CastOptionsProvider" />
</application>

CastContext é inicializado lentamente quando o CastContext.getSharedInstance() é chamado.

Kotlin
class MyActivity : FragmentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        val castContext = CastContext.getSharedInstance(this)
    }
}
Java
public class MyActivity extends FragmentActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        CastContext castContext = CastContext.getSharedInstance(this);
    }
}

Widgets de UX do Cast

O framework do Cast fornece os widgets que estão em conformidade com a lista de verificação de design do Cast:

  • Sobreposição introdutória: O framework fornece uma visualização personalizada, IntroductoryOverlay, que é mostrada ao usuário para chamar a atenção para o botão "Transmitir" na primeira vez que um receptor está disponível. O app remetente pode personalizar o texto e a posição do texto do título.

  • Botão "Transmitir": O botão "Transmitir" fica visível, independentemente da disponibilidade de dispositivos de transmissão. Quando o usuário clica no botão "Transmitir" pela primeira vez, uma caixa de diálogo do Cast é mostrada com a lista de dispositivos descobertos. Quando o usuário clica no botão "Transmitir" enquanto o dispositivo está conectado, ele mostra os metadados de mídia atuais (como título, nome do estúdio de gravação e uma imagem em miniatura) ou permite que o usuário se desconecte do dispositivo de transmissão. O "botão Transmitir" às vezes é chamado de "ícone Transmitir".

  • Minicontrole: quando o usuário está transmitindo conteúdo e saiu da página de conteúdo atual ou do controle expandido para outra tela no app remetente, o minicontrole é mostrado na parte de baixo da tela para permitir que o usuário veja os metadados de mídia transmitidos no momento e controle a reprodução.

  • Controle expandido: Quando o usuário está transmitindo conteúdo, se ele clicar na notificação de mídia ou no minicontrole, o controle expandido será iniciado, mostrando os metadados de mídia em reprodução no momento e fornecendo vários botões para controlar a reprodução de mídia.

  • Notificação: somente Android. Quando o usuário está transmitindo conteúdo e sai do app remetente, uma notificação de mídia é mostrada com os metadados de mídia transmitidos no momento e os controles de reprodução.

  • Tela de bloqueio: somente Android. Quando o usuário está transmitindo conteúdo e navega (ou o dispositivo expira) para a tela de bloqueio, um controle de mídia da tela de bloqueio é mostrado com os metadados de mídia transmitidos no momento e os controles de reprodução.

O guia a seguir inclui descrições de como adicionar esses widgets ao seu app.

Adicionar um botão "Transmitir"

As APIs MediaRouter do Android foram projetadas para ativar a exibição de mídia e a reprodução em dispositivos secundários. Os apps Android que usam a API MediaRouter precisam incluir um botão "Transmitir" como parte da interface do usuário para permitir que os usuários selecionem um caminho de mídia para reproduzir mídia em um dispositivo secundário, como um dispositivo de transmissão.

O framework facilita muito a adição de um MediaRouteButton como um Cast button. Primeiro, adicione um item de menu ou um MediaRouteButton no arquivo XML que define seu menu e use CastButtonFactory para conectá-lo ao framework.

// To add a Cast button, add the following snippet.
// menu.xml
<item
    android:id="@+id/media_route_menu_item"
    android:title="@string/media_route_menu_title"
    app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
    app:showAsAction="always" />
Kotlin
// Then override the onCreateOptionMenu() for each of your activities.
// MyActivity.kt
override fun onCreateOptionsMenu(menu: Menu): Boolean {
    super.onCreateOptionsMenu(menu)
    menuInflater.inflate(R.menu.main, menu)
    CastButtonFactory.setUpMediaRouteButton(
        applicationContext,
        menu,
        R.id.media_route_menu_item
    )
    return true
}
Java
// Then override the onCreateOptionMenu() for each of your activities.
// MyActivity.java
@Override public boolean onCreateOptionsMenu(Menu menu) {
    super.onCreateOptionsMenu(menu);
    getMenuInflater().inflate(R.menu.main, menu);
    CastButtonFactory.setUpMediaRouteButton(getApplicationContext(),
                                            menu,
                                            R.id.media_route_menu_item);
    return true;
}

Em seguida, se o Activity herdar de FragmentActivity, você poderá adicionar um MediaRouteButton ao layout.

// activity_layout.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:gravity="center_vertical"
   android:orientation="horizontal" >

   <androidx.mediarouter.app.MediaRouteButton
       android:id="@+id/media_route_button"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_weight="1"
       android:mediaRouteTypes="user"
       android:visibility="gone" />

</LinearLayout>
Kotlin
// MyActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_layout)

    mMediaRouteButton = findViewById<View>(R.id.media_route_button) as MediaRouteButton
    CastButtonFactory.setUpMediaRouteButton(applicationContext, mMediaRouteButton)

    mCastContext = CastContext.getSharedInstance(this)
}
Java
// MyActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_layout);

   mMediaRouteButton = (MediaRouteButton) findViewById(R.id.media_route_button);
   CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), mMediaRouteButton);

   mCastContext = CastContext.getSharedInstance(this);
}

Para definir a aparência do botão "Transmitir" usando um tema, consulte Personalizar o botão "Transmitir".

Configurar a descoberta de dispositivos

A descoberta de dispositivos é totalmente gerenciada pelo CastContext. Ao inicializar o CastContext, o app remetente especifica o ID do aplicativo receptor da Web e, opcionalmente, pode solicitar a filtragem de namespace definindo supportedNamespaces em CastOptions. CastContext mantém uma referência ao MediaRouter internamente e inicia o processo de descoberta nas seguintes condições:

  • Com base em um algoritmo projetado para equilibrar a latência de descoberta de dispositivos e o uso da bateria, a descoberta será iniciada automaticamente quando o app remetente entrar em primeiro plano.
  • A caixa de diálogo do Cast está aberta.
  • O SDK do Cast está tentando recuperar uma sessão de transmissão.

O processo de descoberta será interrompido quando a caixa de diálogo do Cast for fechada ou o app remetente entrar em segundo plano.

Kotlin
class CastOptionsProvider : OptionsProvider {
    companion object {
        const val CUSTOM_NAMESPACE = "urn:x-cast:custom_namespace"
    }

    override fun getCastOptions(appContext: Context): CastOptions {
        val supportedNamespaces: MutableList<String> = ArrayList()
        supportedNamespaces.add(CUSTOM_NAMESPACE)

        return CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .setSupportedNamespaces(supportedNamespaces)
            .build()
    }

    override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? {
        return null
    }
}
Java
class CastOptionsProvider implements OptionsProvider {
    public static final String CUSTOM_NAMESPACE = "urn:x-cast:custom_namespace";

    @Override
    public CastOptions getCastOptions(Context appContext) {
        List<String> supportedNamespaces = new ArrayList<>();
        supportedNamespaces.add(CUSTOM_NAMESPACE);

        CastOptions castOptions = new CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .setSupportedNamespaces(supportedNamespaces)
            .build();
        return castOptions;
    }

    @Override
    public List<SessionProvider> getAdditionalSessionProviders(Context context) {
        return null;
    }
}

Como o gerenciamento de sessões funciona

O SDK do Cast apresenta o conceito de uma sessão de transmissão, cuja criação combina as etapas de conexão a um dispositivo, inicialização (ou participação) de um app receptor da Web, conexão a esse app e inicialização de um canal de controle de mídia. Consulte o guia Ciclo de vida do aplicativo receptor da Web para mais informações sobre sessões de transmissão e o ciclo de vida do receptor da Web.

As sessões são gerenciadas pela classe SessionManager, que pode ser acessada pelo seu app via CastContext.getSessionManager(). As sessões individuais são representadas por subclasses da classe Session. Por exemplo, CastSession representa sessões com dispositivos de transmissão. Seu app pode acessar a sessão de transmissão ativa no momento via SessionManager.getCurrentCastSession().

Seu app pode usar a SessionManagerListener classe para monitorar eventos de sessão, como criação, suspensão, retomada e encerramento. O framework tenta retomar automaticamente de um encerramento anormal/abrupto enquanto uma sessão estava ativa.

As sessões são criadas e eliminadas automaticamente em resposta aos gestos do usuário nas caixas de diálogo MediaRouter.

Para entender melhor os erros de inicialização do Cast, os apps podem usar CastContext#getCastReasonCodeForCastStatusCode(int) para converter o erro de inicialização da sessão em CastReasonCodes. Alguns erros de inicialização de sessão (por exemplo, CastReasonCodes#CAST_CANCELLED) são comportamentos intencionais e não devem ser registrados como um erro.

Se você precisar estar ciente das mudanças de estado da sessão, implemente um SessionManagerListener. Este exemplo detecta a disponibilidade de uma CastSession em uma Activity.

Kotlin
class MyActivity : Activity() {
    private var mCastSession: CastSession? = null
    private lateinit var mCastContext: CastContext
    private lateinit var mSessionManager: SessionManager
    private val mSessionManagerListener: SessionManagerListener<CastSession> =
        SessionManagerListenerImpl()

    private inner class SessionManagerListenerImpl : SessionManagerListener<CastSession?> {
        override fun onSessionStarting(session: CastSession?) {}

        override fun onSessionStarted(session: CastSession?, sessionId: String) {
            invalidateOptionsMenu()
        }

        override fun onSessionStartFailed(session: CastSession?, error: Int) {
            val castReasonCode = mCastContext.getCastReasonCodeForCastStatusCode(error)
            // Handle error
        }

        override fun onSessionSuspended(session: CastSession?, reason Int) {}

        override fun onSessionResuming(session: CastSession?, sessionId: String) {}

        override fun onSessionResumed(session: CastSession?, wasSuspended: Boolean) {
            invalidateOptionsMenu()
        }

        override fun onSessionResumeFailed(session: CastSession?, error: Int) {}

        override fun onSessionEnding(session: CastSession?) {}

        override fun onSessionEnded(session: CastSession?, error: Int) {
            finish()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mCastContext = CastContext.getSharedInstance(this)
        mSessionManager = mCastContext.sessionManager
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession::class.java)
    }

    override fun onResume() {
        super.onResume()
        mCastSession = mSessionManager.currentCastSession
    }

    override fun onDestroy() {
        super.onDestroy()
        mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession::class.java)
    }
}
Java
public class MyActivity extends Activity {
    private CastContext mCastContext;
    private CastSession mCastSession;
    private SessionManager mSessionManager;
    private SessionManagerListener<CastSession> mSessionManagerListener =
            new SessionManagerListenerImpl();

    private class SessionManagerListenerImpl implements SessionManagerListener<CastSession> {
        @Override
        public void onSessionStarting(CastSession session) {}
        @Override
        public void onSessionStarted(CastSession session, String sessionId) {
            invalidateOptionsMenu();
        }
        @Override
        public void onSessionStartFailed(CastSession session, int error) {
            int castReasonCode = mCastContext.getCastReasonCodeForCastStatusCode(error);
            // Handle error
        }
        @Override
        public void onSessionSuspended(CastSession session, int reason) {}
        @Override
        public void onSessionResuming(CastSession session, String sessionId) {}
        @Override
        public void onSessionResumed(CastSession session, boolean wasSuspended) {
            invalidateOptionsMenu();
        }
        @Override
        public void onSessionResumeFailed(CastSession session, int error) {}
        @Override
        public void onSessionEnding(CastSession session) {}
        @Override
        public void onSessionEnded(CastSession session, int error) {
            finish();
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mCastContext = CastContext.getSharedInstance(this);
        mSessionManager = mCastContext.getSessionManager();
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession.class);
    }

    @Override
    protected void onResume() {
        super.onResume();
        mCastSession = mSessionManager.getCurrentCastSession();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession.class);
    }
}

Transferência de stream

A preservação do estado da sessão é a base da transferência de stream, em que os usuários podem mover streams de áudio e vídeo existentes entre dispositivos usando comandos de voz, o app Google Home ou Smart Displays. A mídia para de ser reproduzida em um dispositivo (a origem) e continua em outro (o destino). Qualquer dispositivo de transmissão com o firmware mais recente pode servir como origem ou destino em uma transferência de stream.

Para receber o novo dispositivo de destino durante uma transferência de stream ou expansão, registre um Cast.Listener usando o CastSession#addCastListener. Em seguida, chame CastSession#getCastDevice() durante o callback onDeviceNameChanged.

Consulte Transferência de stream no receptor da Web para mais informações.

Reconexão automática

O framework fornece um ReconnectionService que pode ser ativado pelo app remetente para lidar com a reconexão em muitos casos sutis sutis, como:

  • Recuperar de uma perda temporária de Wi-Fi
  • Recuperar do modo de espera do dispositivo
  • Recuperar do app em segundo plano
  • Recuperar se o app falhou

Esse serviço fica ativado por padrão e pode ser desativado em CastOptions.Builder.

Esse serviço pode ser mesclado automaticamente ao manifesto do seu app se a mesclagem automática estiver ativada no arquivo do Gradle.

O framework vai iniciar o serviço quando houver uma sessão de mídia e interrompê-lo quando a sessão de mídia terminar.

Como o controle de mídia funciona

O framework do Cast descontinua a RemoteMediaPlayer classe do Cast 2.x em favor de uma nova classe RemoteMediaClient, que fornece a mesma funcionalidade em um conjunto de APIs mais convenientes e evita a necessidade de transmitir um GoogleApiClient.

Quando o app estabelece um CastSession com um app receptor da Web que oferece suporte ao namespace de mídia, uma instância de RemoteMediaClient é criada automaticamente pelo framework. Seu app pode acessá-la chamando o método getRemoteMediaClient() na instância CastSession .

Todos os métodos de RemoteMediaClient que emitem solicitações para o receptor da Web retornam um objeto PendingResult que pode ser usado para rastrear essa solicitação.

Espera-se que a instância de RemoteMediaClient possa ser compartilhada por várias partes do seu app e, de fato, alguns componentes internos do framework, como os minicontroles persistentes e o serviço de notificação. Para isso, essa instância oferece suporte ao registro de várias instâncias de RemoteMediaClient.Listener.

Definir metadados de mídia

A classe MediaMetadata representa as informações sobre um item de mídia que você quer transmitir. O exemplo a seguir cria uma nova instância de MediaMetadata de um filme e define o título, a legenda e duas imagens.

Kotlin
val movieMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE)

movieMetadata.putString(MediaMetadata.KEY_TITLE, mSelectedMedia.getTitle())
movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, mSelectedMedia.getStudio())
movieMetadata.addImage(WebImage(Uri.parse(mSelectedMedia.getImage(0))))
movieMetadata.addImage(WebImage(Uri.parse(mSelectedMedia.getImage(1))))
Java
MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);

movieMetadata.putString(MediaMetadata.KEY_TITLE, mSelectedMedia.getTitle());
movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, mSelectedMedia.getStudio());
movieMetadata.addImage(new WebImage(Uri.parse(mSelectedMedia.getImage(0))));
movieMetadata.addImage(new WebImage(Uri.parse(mSelectedMedia.getImage(1))));

Consulte Seleção de imagens sobre o uso de imagens com metadados de mídia.

Carregar mídia

Seu app pode carregar um item de mídia, conforme mostrado no código a seguir. Primeiro, use MediaInfo.Builder com os metadados da mídia para criar uma instância MediaInfo. Receba o RemoteMediaClient da CastSession atual e carregue o MediaInfo nesse RemoteMediaClient. Use RemoteMediaClient para reproduzir, pausar e controlar um app de player de mídia em execução no receptor da Web.

Kotlin
val mediaInfo = MediaInfo.Builder(mSelectedMedia.getUrl())
    .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
    .setContentType("videos/mp4")
    .setMetadata(movieMetadata)
    .setStreamDuration(mSelectedMedia.getDuration() * 1000)
    .build()
val remoteMediaClient = mCastSession.getRemoteMediaClient()
remoteMediaClient.load(MediaLoadRequestData.Builder().setMediaInfo(mediaInfo).build())
Java
MediaInfo mediaInfo = new MediaInfo.Builder(mSelectedMedia.getUrl())
        .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
        .setContentType("videos/mp4")
        .setMetadata(movieMetadata)
        .setStreamDuration(mSelectedMedia.getDuration() * 1000)
        .build();
RemoteMediaClient remoteMediaClient = mCastSession.getRemoteMediaClient();
remoteMediaClient.load(new MediaLoadRequestData.Builder().setMediaInfo(mediaInfo).build());

Consulte também a seção sobre como usar faixas de mídia.

Formato de vídeo 4K

Para verificar o formato de vídeo da sua mídia, use getVideoInfo() em MediaStatus para receber a instância atual de VideoInfo. Essa instância contém o tipo de formato de TV HDR e a altura e largura da tela em pixels. As variantes do formato 4K são indicadas por constantes HDR_TYPE_*.

Notificações de controle remoto para vários dispositivos

Quando um usuário está transmitindo, outros dispositivos Android na mesma rede recebem uma notificação para também controlar a reprodução. Qualquer pessoa cujo dispositivo receba essas notificações pode desativá-las para esse dispositivo no app Configurações em Google > Google Cast > Mostrar notificações de controle remoto. As notificações incluem um atalho para o app Configurações. Para mais detalhes, consulte Notificações de controle remoto do Cast.

Adicionar minicontrole

De acordo com a lista de verificação de design do Cast, um app remetente precisa fornecer um controle persistente conhecido como minicontrole, que aparece quando o usuário sai da página de conteúdo atual para outra parte do app remetente. O minicontrole fornece um lembrete visível ao usuário da sessão de transmissão atual. Ao tocar no minicontrole, o usuário pode retornar à visualização de controle expandido em tela cheia do Cast.

O framework fornece uma visualização personalizada, MiniControllerFragment, que pode ser adicionada à parte de baixo do arquivo de layout de cada atividade em que você quer mostrar o minicontrole.

<fragment
    android:id="@+id/castMiniController"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:visibility="gone"
    class="com.google.android.gms.cast.framework.media.widget.MiniControllerFragment" />

Quando o app remetente está reproduzindo um stream ao vivo de vídeo ou áudio, o SDK mostra automaticamente um botão de reprodução/parada no lugar do botão de reprodução/pausa no minicontrole.

Para definir a aparência do texto do título e da legenda dessa visualização personalizada, e escolher os botões, consulte Personalizar o minicontrole.

Adicionar controle expandido

A lista de verificação de design do Google Cast exige que um app remetente forneça um controle expandido para a mídia transmitida. O controle expandido é uma versão em tela cheia do minicontrole.

O SDK do Cast fornece um widget para o controle expandido chamado ExpandedControllerActivity. Essa é uma classe abstrata que você precisa transformar em subclasse para adicionar um botão "Transmitir".

Primeiro, crie um novo arquivo de recursos do menu para o controle expandido fornecer o botão "Transmitir":

<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
            android:id="@+id/media_route_menu_item"
            android:title="@string/media_route_menu_title"
            app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
            app:showAsAction="always"/>

</menu>

Crie uma classe que estenda ExpandedControllerActivity.

Kotlin
class ExpandedControlsActivity : ExpandedControllerActivity() {
    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        super.onCreateOptionsMenu(menu)
        menuInflater.inflate(R.menu.expanded_controller, menu)
        CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item)
        return true
    }
}
Java
public class ExpandedControlsActivity extends ExpandedControllerActivity {
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        getMenuInflater().inflate(R.menu.expanded_controller, menu);
        CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item);
        return true;
    }
}

Agora, declare sua nova atividade no manifesto do app dentro da tag application:

<application>
...
<activity
        android:name=".expandedcontrols.ExpandedControlsActivity"
        android:label="@string/app_name"
        android:launchMode="singleTask"
        android:theme="@style/Theme.CastVideosDark"
        android:screenOrientation="portrait"
        android:parentActivityName="com.google.sample.cast.refplayer.VideoBrowserActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
    </intent-filter>
</activity>
...
</application>

Edite o CastOptionsProvider e mude NotificationOptions e CastMediaOptions para definir a atividade de destino para sua nova atividade:

Kotlin
override fun getCastOptions(context: Context): CastOptions? {
    val notificationOptions = NotificationOptions.Builder()
        .setTargetActivityClassName(ExpandedControlsActivity::class.java.name)
        .build()
    val mediaOptions = CastMediaOptions.Builder()
        .setNotificationOptions(notificationOptions)
        .setExpandedControllerActivityClassName(ExpandedControlsActivity::class.java.name)
        .build()

    return CastOptions.Builder()
        .setReceiverApplicationId(context.getString(R.string.app_id))
        .setCastMediaOptions(mediaOptions)
        .build()
}
Java
public CastOptions getCastOptions(Context context) {
    NotificationOptions notificationOptions = new NotificationOptions.Builder()
            .setTargetActivityClassName(ExpandedControlsActivity.class.getName())
            .build();
    CastMediaOptions mediaOptions = new CastMediaOptions.Builder()
            .setNotificationOptions(notificationOptions)
            .setExpandedControllerActivityClassName(ExpandedControlsActivity.class.getName())
            .build();

    return new CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .setCastMediaOptions(mediaOptions)
            .build();
}

Atualize o método loadRemoteMedia da LocalPlayerActivity para mostrar sua nova atividade quando a mídia remota for carregada:

Kotlin
private fun loadRemoteMedia(position: Int, autoPlay: Boolean) {
    val remoteMediaClient = mCastSession?.remoteMediaClient ?: return

    remoteMediaClient.registerCallback(object : RemoteMediaClient.Callback() {
        override fun onStatusUpdated() {
            val intent = Intent(this@LocalPlayerActivity, ExpandedControlsActivity::class.java)
            startActivity(intent)
            remoteMediaClient.unregisterCallback(this)
        }
    })

    remoteMediaClient.load(
        MediaLoadRequestData.Builder()
            .setMediaInfo(mSelectedMedia)
            .setAutoplay(autoPlay)
            .setCurrentTime(position.toLong()).build()
    )
}
Java
private void loadRemoteMedia(int position, boolean autoPlay) {
    if (mCastSession == null) {
        return;
    }
    final RemoteMediaClient remoteMediaClient = mCastSession.getRemoteMediaClient();
    if (remoteMediaClient == null) {
        return;
    }
    remoteMediaClient.registerCallback(new RemoteMediaClient.Callback() {
        @Override
        public void onStatusUpdated() {
            Intent intent = new Intent(LocalPlayerActivity.this, ExpandedControlsActivity.class);
            startActivity(intent);
            remoteMediaClient.unregisterCallback(this);
        }
    });
    remoteMediaClient.load(new MediaLoadRequestData.Builder()
            .setMediaInfo(mSelectedMedia)
            .setAutoplay(autoPlay)
            .setCurrentTime(position).build());
}

Quando o app remetente está reproduzindo um stream ao vivo de vídeo ou áudio, o SDK mostra automaticamente um botão de reprodução/parada no lugar do botão de reprodução/pausa no controle expandido.

Para definir a aparência usando temas, escolha os botões a serem mostrados, e adicione botões personalizados, consulte Personalizar o controle expandido.

Controle do volume

O framework gerencia automaticamente o volume do app remetente. O framework sincroniza automaticamente os apps remetente e receptor da Web para que a interface do remetente sempre informe o volume especificado pelo receptor da Web.

Controle de volume do botão físico

No Android, os botões físicos no dispositivo remetente podem ser usados para mudar o volume da sessão de transmissão no receptor da Web por padrão para qualquer dispositivo que use o Jelly Bean ou mais recente.

Controle de volume do botão físico anterior ao Jelly Bean

Para usar as teclas de volume físicas para controlar o volume do dispositivo receptor da Web em dispositivos Android mais antigos que o Jelly Bean, o app remetente precisa substituir dispatchKeyEvent nas atividades e chamar CastContext.onDispatchVolumeKeyEventBeforeJellyBean():

Kotlin
class MyActivity : FragmentActivity() {
    override fun dispatchKeyEvent(event: KeyEvent): Boolean {
        return (CastContext.getSharedInstance(this)
            .onDispatchVolumeKeyEventBeforeJellyBean(event)
                || super.dispatchKeyEvent(event))
    }
}
Java
class MyActivity extends FragmentActivity {
    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        return CastContext.getSharedInstance(this)
            .onDispatchVolumeKeyEventBeforeJellyBean(event)
            || super.dispatchKeyEvent(event);
    }
}

Adicionar controles de mídia à notificação e à tela de bloqueio

Somente no Android, a lista de verificação de design do Google Cast exige que um app remetente implemente controles de mídia em uma notificação e na tela de bloqueio, em que o remetente está transmitindo, mas o app remetente não tem foco. O framework fornece MediaNotificationService e MediaIntentReceiver para ajudar o app remetente a criar controles de mídia em uma notificação e na tela de bloqueio.

MediaNotificationService é executado quando o remetente está transmitindo e mostra uma notificação com miniatura da imagem e informações sobre o item de transmissão atual, um botão de reprodução/pausa e um botão de parada.

MediaIntentReceiver é um BroadcastReceiver que processa ações do usuário na notificação.

Seu app pode configurar a notificação e o controle de mídia na tela de bloqueio usando NotificationOptions. Seu app pode configurar quais botões de controle mostrar na notificação e qual Activity abrir quando a notificação for tocada pelo usuário. Se as ações não forem fornecidas explicitamente, os valores padrão, MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK e MediaIntentReceiver.ACTION_STOP_CASTING, serão usados.

Kotlin
// Example showing 4 buttons: "rewind", "play/pause", "forward" and "stop casting".
val buttonActions: MutableList<String> = ArrayList()
buttonActions.add(MediaIntentReceiver.ACTION_REWIND)
buttonActions.add(MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK)
buttonActions.add(MediaIntentReceiver.ACTION_FORWARD)
buttonActions.add(MediaIntentReceiver.ACTION_STOP_CASTING)

// Showing "play/pause" and "stop casting" in the compat view of the notification.
val compatButtonActionsIndices = intArrayOf(1, 3)

// Builds a notification with the above actions. Each tap on the "rewind" and "forward" buttons skips 30 seconds.
// Tapping on the notification opens an Activity with class VideoBrowserActivity.
val notificationOptions = NotificationOptions.Builder()
    .setActions(buttonActions, compatButtonActionsIndices)
    .setSkipStepMs(30 * DateUtils.SECOND_IN_MILLIS)
    .setTargetActivityClassName(VideoBrowserActivity::class.java.name)
    .build()
Java
// Example showing 4 buttons: "rewind", "play/pause", "forward" and "stop casting".
List<String> buttonActions = new ArrayList<>();
buttonActions.add(MediaIntentReceiver.ACTION_REWIND);
buttonActions.add(MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK);
buttonActions.add(MediaIntentReceiver.ACTION_FORWARD);
buttonActions.add(MediaIntentReceiver.ACTION_STOP_CASTING);

// Showing "play/pause" and "stop casting" in the compat view of the notification.
int[] compatButtonActionsIndices = new int[]{1, 3};

// Builds a notification with the above actions. Each tap on the "rewind" and "forward" buttons skips 30 seconds.
// Tapping on the notification opens an Activity with class VideoBrowserActivity.
NotificationOptions notificationOptions = new NotificationOptions.Builder()
    .setActions(buttonActions, compatButtonActionsIndices)
    .setSkipStepMs(30 * DateUtils.SECOND_IN_MILLIS)
    .setTargetActivityClassName(VideoBrowserActivity.class.getName())
    .build();

A exibição de controles de mídia na notificação e na tela de bloqueio é ativada por padrão e pode ser desativada chamando setNotificationOptions com nulo em CastMediaOptions.Builder. Atualmente, o recurso da tela de bloqueio fica ativado enquanto a notificação está ativada.

Kotlin
// ... continue with the NotificationOptions built above
val mediaOptions = CastMediaOptions.Builder()
    .setNotificationOptions(notificationOptions)
    .build()
val castOptions: CastOptions = Builder()
    .setReceiverApplicationId(context.getString(R.string.app_id))
    .setCastMediaOptions(mediaOptions)
    .build()
Java
// ... continue with the NotificationOptions built above
CastMediaOptions mediaOptions = new CastMediaOptions.Builder()
        .setNotificationOptions(notificationOptions)
        .build();
CastOptions castOptions = new CastOptions.Builder()
        .setReceiverApplicationId(context.getString(R.string.app_id))
        .setCastMediaOptions(mediaOptions)
        .build();

Quando o app remetente está reproduzindo um stream ao vivo de vídeo ou áudio, o SDK mostra automaticamente um botão de reprodução/parada no lugar do botão de reprodução/pausa no controle de notificação, mas não no controle da tela de bloqueio.

Observação: para mostrar os controles da tela de bloqueio em dispositivos anteriores ao Lollipop, RemoteMediaClient vai solicitar automaticamente a seleção de áudio em seu nome.

Solucionar erros

É muito importante que os apps remetentes processem todos os callbacks de erro e decidam a melhor resposta para cada estágio do ciclo de vida do Cast. O app pode mostrar caixas de diálogo de erro ao usuário ou decidir interromper a conexão com o receptor da Web.