Integrar o Cast ao seu app Android

Mantenha tudo organizado com as coleções Salve e categorize o conteúdo com base nas suas preferências.

Este guia para desenvolvedores descreve como adicionar compatibilidade com o Google Cast ao seu app de envio Android usando o SDK do remetente do Android.

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

O framework do remetente refere-se ao binário da biblioteca de classe do Google Cast e aos recursos associados no momento da execução no remetente. O app de transmissão ou o app de transmissão se refere a um app que também é executado no remetente. O app receptor da Web se refere ao app HTML em execução no dispositivo compatível com Cast.

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

Fluxo de aplicativos

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

  • O framework do Google Cast inicia a descoberta de dispositivos MediaRouter com base no ciclo de vida do Activity.
  • Quando o usuário clica no botão Transmitir, o framework apresenta a caixa de diálogo de transmissão 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 Web Receiver no dispositivo de transmissão.
  • O framework invoca callbacks no app remetente para confirmar que o app Web Receiver foi iniciado.
  • O framework cria um canal de comunicação entre o remetente e os apps do 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 realiza ações da IU do remetente, o framework transmite essas solicitações de controle de mídia para o receptor da Web e, quando o receptor da Web envia atualizações do status de mídia, o framework atualiza o estado da IU 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 ver uma lista abrangente de todas as classes, métodos e eventos no SDK do Google Cast para Android, consulte a Referência da API Google Cast Sender para Android. As seções abaixo abordam as etapas para adicionar o Google 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ínimos e desejados da API Android compatíveis com o SDK do Cast. Atualmente, o mínimo é a API de nível 19, e o destino é a API de nível 28.

<uses-sdk
        android:minSdkVersion="19"
        android:targetSdkVersion="28" />

android:theme (link em inglês)

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 de transmissão

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

Seu app precisa implementar a interface OptionsProvider para fornecer opções necessárias para inicializar o singleton CastContext. OptionsProvider fornece uma instância de CastOptions que contém opções que afetam o comportamento do framework. O mais importante é o ID do app Web Receiver, que é usado para filtrar os resultados de descoberta e iniciar o app Web Receiver 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;
    }
}

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

O 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 Google Cast

O framework do Google Cast fornece os widgets compatíveis com a lista de verificação de design de transmissão:

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

  • Botão Transmitir: o botão Transmitir fica visível quando um receptor é compatível com seu app. Quando o usuário clica no botão Transmitir pela primeira vez, uma caixa de diálogo é exibida mostrando os 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.

  • MiniControl: quando o usuário está transmitindo conteúdo e sai 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 em transmissão no momento e controle a reprodução.

  • Controlador expandido: quando o usuário está transmitindo conteúdo, se clicar na notificação de mídia ou no minicontrole, o controle expandido é iniciado, mostrando os metadados de mídia em reprodução e fornece 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 é exibida mostrando os metadados de mídia 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, é exibido um controle de tela de bloqueio de mídia que mostra os metadados de mídia atuais 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 e a reprodução de mídia 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 uma rota de mídia para reproduzir mídia em um dispositivo secundário, como um dispositivo de transmissão.

O framework facilita a adição de uma MediaRouteButton como uma Cast button. Primeiro, adicione um item de menu ou um MediaRouteButton ao 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 da FragmentActivity, você poderá adicionar uma 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 botão Transmitir.

Configurar a descoberta do dispositivo

A descoberta de dispositivos é totalmente gerenciada pelo CastContext. Ao inicializar o CastContext, o app remetente especifica o ID do aplicativo Web Receiver e pode solicitar a filtragem de namespace definindo supportedNamespaces em CastOptions. CastContext mantém uma referência para o MediaRouter internamente e inicia o processo de descoberta quando o app remetente entra no primeiro plano e termina quando o app remetente entra 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 introduz o conceito de uma sessão do Cast, que estabelece as etapas para se conectar a um dispositivo, iniciar ou participar de um app do receptor da Web, se conectar a esse app e inicializar um canal de controle de mídia. Consulte o Guia do ciclo de vida do aplicativo do receptor da Web para mais informações sobre as sessões de transmissão e o ciclo de vida do receptor.

As sessões são gerenciadas pela classe SessionManager, que o app pode acessar 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. O app pode acessar a sessão de transmissão atualmente ativa usando SessionManager.getCurrentCastSession().

Seu app pode usar a classe SessionManagerListener 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 a gestos do usuário nas caixas de diálogo MediaRouter.

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

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

    private inner class SessionManagerListenerImpl : SessionManagerListener<CastSession?> {
        override fun onSessionStarted(session: CastSession?, sessionId: String) {
            invalidateOptionsMenu()
        }

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

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

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mSessionManager = CastContext.getSharedInstance(this).sessionManager
    }

    override fun onResume() {
        super.onResume()
        mCastSession = mSessionManager.currentCastSession
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession::class.java)
    }

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

    private class SessionManagerListenerImpl implements SessionManagerListener<CastSession> {
        @Override
        public void onSessionStarted(CastSession session, String sessionId) {
            invalidateOptionsMenu();
        }
        @Override
        public void onSessionResumed(CastSession session, boolean wasSuspended) {
            invalidateOptionsMenu();
        }
        @Override
        public void onSessionEnded(CastSession session, int error) {
            finish();
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mSessionManager = CastContext.getSharedInstance(this).getSessionManager();
    }
    @Override
    protected void onResume() {
        super.onResume();
        mCastSession = mSessionManager.getCurrentCastSession();
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession.class);
    }
    @Override
    protected void onPause() {
        super.onPause();
        mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession.class);
        mCastSession = null;
    }
}

Transferência de stream

Preservar o estado da sessão é a base da transferência de stream, em que os usuários podem mover streams de áudio e vídeo entre dispositivos usando comandos de voz, app Google Home ou smart displays. A mídia é interrompida em um dispositivo (a fonte) e continua em outro (o destino). Qualquer dispositivo de transmissão com o firmware mais recente pode servir como fontes ou destinos em uma transferência de stream.

Para ter o novo dispositivo de destino durante uma transferência ou expansão de stream, 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 processar a reconexão em muitos casos sutis, como:

  • Recuperar-se de uma perda temporária de Wi-Fi
  • Recuperar da suspensão do dispositivo
  • Recuperar o app em segundo plano
  • Recuperar se o app falhar

Esse serviço está ativado por padrão e pode ser desativado no CastOptions.Builder.

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

O framework iniciará o serviço quando houver uma sessão de mídia e o interromperá quando a sessão de mídia terminar.

Como o controle de mídia funciona

O framework do Google Cast descontinuou o uso da classe RemoteMediaPlayer do Cast 2.x em favor de uma nova classe RemoteMediaClient, que oferece a mesma funcionalidade em um conjunto de APIs mais convenientes e evita transmitir um GoogleApiClient.

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

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

Espera-se que a instância de RemoteMediaClient seja compartilhada por várias partes do seu app e, de fato, alguns componentes internos do framework, como os minicontroladores permanentes e o serviço de notificação. Para isso, ela é compatível com o 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 abaixo cria uma nova instância MediaMetadata de um filme e define o título, o subtítulo 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

O app pode carregar um item de mídia, conforme mostrado no código a seguir. Primeiro, use MediaInfo.Builder com os metadados de mídia para criar uma instância MediaInfo. Acesse o RemoteMediaClient do 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 Web Receiver.

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 em 4K

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

Controle as notificações remotamente para vários dispositivos

Quando um usuário estiver transmitindo, outros dispositivos Android na mesma rede receberão uma notificação para permitir que ele controle a reprodução. Qualquer pessoa com dispositivo que receba essas notificações pode desativá-las 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 Transmitir notificações de controle remoto.

Adicionar minicontrole

De acordo com a Lista de verificação de design de transmissão, um app remetente precisa fornecer um controle persistente conhecido como mini controlador que aparece quando o usuário sai da página de conteúdo atual para outra parte do app remetente. O minicontrole oferece 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 do controle expandido do Google Cast em tela cheia.

O framework fornece uma visualização personalizada, MiniControllerFragment, que você pode adicionar à parte inferior do arquivo de layout de cada atividade em que você quer mostrar o minicontrolador.

<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 uma transmissão de vídeo ou áudio ao vivo, o SDK exibe automaticamente um botão "Assistir/Parar" no lugar do botão "Reproduzir"/"Pausar" no minicontrole.

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

Adicionar controlador expandido

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

O SDK do Cast oferece 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 de menu para que o controlador expandido forneça 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 nova 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 na 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 como a 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 exibir 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 uma transmissão de vídeo ou áudio ao vivo, o SDK exibe automaticamente um botão "Assistir/Parar" no lugar do botão "Reproduzir"/"Pausar" no controle expandido.

Para definir a aparência usando temas, escolha quais botões exibir e adicione botões personalizados em Personalizar controle expandido.

Controle do volume

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

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 usando o Jelly Bean ou uma versão mais recente.

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

Para usar as teclas de volume físicas para controlar o volume do receptor da Web em dispositivos Android anteriores ao 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

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 está em 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 mostrará uma notificação com miniatura de imagem e informações sobre o item de transmissão atual, um botão de reprodução/pausar e um botão de parada.

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

O app pode configurar o controle de mídia e notificação na tela de bloqueio usando NotificationOptions. Seu app pode configurar quais botões de controle serão exibidos na notificação e quais 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();

Os controles de mídia da notificação e da tela de bloqueio são ativados por padrão e podem ser desativados 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 uma transmissão de vídeo ou áudio ao vivo, o SDK exibe automaticamente um botão "Reproduzir"/"Parar" no lugar desse botão no controle de notificações, mas não no de tela de bloqueio.

Observação: para exibir controles de tela de bloqueio em dispositivos anteriores ao Lollipop, o RemoteMediaClient vai solicitar a seleção de áudio automaticamente para você.

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 exibir caixas de diálogo de erro para o usuário ou decidir excluir a conexão com o Web Receiver.