Este guia para desenvolvedores descreve como adicionar suporte ao Google Cast ao seu app remetente Android usando o SDK do remetente Android.
O dispositivo móvel ou laptop é o remetente, que controla a reprodução, e o dispositivo Google Cast é o receptor, que exibe 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 Cast se refere a um app que também é executado no remetente. O app receptor da Web se refere ao aplicativo HTML executado 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 Google Cast.
Fluxo de aplicativos
As etapas a seguir descrevem o fluxo de execução típico de alto nível para um app Android de envio:
- O framework do Google Cast inicia automaticamente a
descoberta de dispositivos
MediaRouter
com base no ciclo de vida doActivity
. - Quando o usuário clica no botão "Transmitir", o framework apresenta a caixa de diálogo 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 de envio para confirmar se o app Web Receiver foi iniciado.
- O framework cria um canal de comunicação entre o remetente e os apps do Web Receiver.
- O framework usa o canal de comunicação para carregar e controlar a reprodução de mídia no Web Receiver.
- 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 na IU 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 IU do remetente.
- Quando o usuário clica no botão "Transmitir" para se desconectar do dispositivo, o framework desconecta o app de envio do Web Receiver.
Para conferir uma lista completa 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 a seguir abrangem as etapas para adicionar o recurso Transmitir 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 preferencial da API do Android com suporte do SDK do Cast. Atualmente, o mínimo é o nível 23 da API, e o desejado é o nível 34 da API.
<uses-sdk
android:minSdkVersion="23"
android:targetSdkVersion="34" />
android:theme
Defina o tema do 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
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.
O app precisa implementar a interface
OptionsProvider
para fornecer as 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. A mais
importante delas é o ID do aplicativo do receptor da Web, que é usado para filtrar
os resultados da descoberta e iniciar o app do receptor da Web quando uma sessão de transmissão é
iniciada.
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 } }
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; } }
É necessário declarar o nome totalmente qualificado do OptionsProvider
implementado
como um campo de metadados no arquivo AndroidManifest.xml do app de envio:
<application>
...
<meta-data
android:name=
"com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="com.foo.CastOptionsProvider" />
</application>
O CastContext
é inicializado de forma lenta quando o CastContext.getSharedInstance()
é chamado.
class MyActivity : FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { val castContext = CastContext.getSharedInstance(this) } }
public class MyActivity extends FragmentActivity { @Override public void onCreate(Bundle savedInstanceState) { CastContext castContext = CastContext.getSharedInstance(this); } }
Os widgets de UX do Google Cast
O framework do Cast fornece os widgets que obedecem à lista de verificação de design do Cast:
Sobreposição introdutória: o framework oferece 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 do remetente pode personalizar o texto e a posição do texto do título.Botão "Transmitir": o botão "Transmitir" fica visível, independente da disponibilidade de dispositivos de transmissão. Quando o usuário clica no botão "Transmitir" pela primeira vez, uma caixa de diálogo é mostrada, listando os dispositivos encontrados. 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 de transmissão" às vezes é chamado de "ícone de transmissão".
Minicontrole: quando o usuário está transmitindo conteúdo e saiu da página de conteúdo atual ou expandiu o controle para outra tela no app de envio, o minicontrole é mostrado na parte de baixo da tela para permitir que o usuário veja os metadados de mídia transmitidos 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 da mídia que está sendo reproduzida e oferecendo vários botões para controlar a reprodução.
Notificação: somente Android. Quando o usuário está transmitindo conteúdo e sai do app de envio, uma notificação de mídia é exibida mostrando 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 termina o tempo) para a tela de bloqueio, um controle de tela de bloqueio de mídia é exibido, mostrando os metadados de mídia e os controles de reprodução que estão sendo transmitidos.
O guia a seguir inclui descrições de como adicionar esses widgets ao seu app.
Adicionar um botão de transmissão
As APIs
MediaRouter
do Android foram desenvolvidas 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 de transmissão como parte
da interface do usuário para permitir que os usuários selecionem uma rota de mídia para abrir mídia em
um dispositivo secundário, como um dispositivo de transmissão.
O framework facilita a adição de um
MediaRouteButton
como um
Cast button
. Primeiro, adicione um item de menu ou um MediaRouteButton
no arquivo
XML que define o 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" />
// 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 }
// 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 a Activity
herdar de
FragmentActivity
,
é possível 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>
// 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) }
// 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 de envio especifica o ID do aplicativo
do receptor da Web e pode solicitar a filtragem de namespace definindo
supportedNamespaces
em
CastOptions
.
O 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 da descoberta do dispositivo e o uso da bateria, a descoberta será iniciada automaticamente quando o app do remetente entrar em primeiro plano.
- A caixa de diálogo "Transmitir" está aberta.
- O SDK do Google Cast está tentando recuperar uma sessão do Google Cast.
O processo de descoberta será interrompido quando a caixa de diálogo do Google Cast for fechada ou quando o app de envio for colocado em segundo plano.
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 } }
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 Google Cast apresenta o conceito de uma sessão do Google Cast, 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 do ciclo de vida do aplicativo do receptor da Web para mais informações sobre as sessões do Google Cast e o ciclo de vida do receptor da Web.
As sessões são gerenciadas pela classe
SessionManager
,
que o app pode acessar por
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
do Google Cast ativa no momento via
SessionManager.getCurrentCastSession()
.
O 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 uma
finalização anormal/abrupta 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 Google 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 esperados e não devem ser registrados como um erro.
Se você precisar saber sobre as mudanças de estado da sessão, implemente
um SessionManagerListener
. Este exemplo detecta a disponibilidade de um
CastSession
em um Activity
.
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) } }
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 streaming, em que os usuários podem mover streams de áudio e vídeo entre dispositivos usando comandos de voz, o app Google Home ou telas inteligentes. A mídia para de ser reproduzida em um dispositivo (a origem) e continua em outro (o destino). Qualquer dispositivo Cast com o firmware mais recente pode servir como origem ou destino em uma transferência de streaming.
Para receber 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 Web Receiver para mais informações.
Reconexão automática
O framework oferece um
ReconnectionService
que pode ser ativado pelo app do remetente para processar a reconexão em muitos casos
excepcionais, como:
- Recuperar-se de uma perda temporária de Wi-Fi
- Recuperar do modo de suspensão do dispositivo
- Recuperar o app em segundo plano
- Recuperar se o app falhar
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 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 interromper quando ela terminar.
Como o controle de mídia funciona
O framework Cast descontinua a 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 a necessidade de transmitir um GoogleApiClient.
Quando o app estabelece uma
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. O app pode
acessar essa instância chamando o método getRemoteMediaClient()
na instância
CastSession
.
Todos os métodos de RemoteMediaClient
que emitem solicitações para o receptor da Web vão
retornar um objeto PendingResult que pode ser usado para rastrear essa solicitação.
É esperado que a instância de RemoteMediaClient
seja compartilhada por
várias partes do app e, de fato, por alguns componentes internos do
framework, como os minicontroladores 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 MediaMetadata de um filme e define o
título, a legenda e duas imagens.
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))))
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 para saber como usar imagens com metadados de mídia.
Carregar mídia
O app pode carregar um item de mídia, conforme mostrado no código abaixo. Primeiro, use
MediaInfo.Builder
com os metadados da mídia para criar uma
instância
MediaInfo
. Conseguir o
RemoteMediaClient
do CastSession
atual e carregar o MediaInfo
nesse
RemoteMediaClient
. Use RemoteMediaClient
para tocar, pausar e
controlar um app de player de mídia em execução no receptor da Web.
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())
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 do vídeo da mídia, use
getVideoInfo()
em MediaStatus para acessar a instância atual de
VideoInfo
.
Essa instância contém o tipo de formato de TV HDR e a altura
e a 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 controlar a reprodução. Qualquer pessoa que receba essas notificações pode desativá-las no 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 Google Cast.
Adicionar minicontrole
De acordo com a Lista de verificação de design do Google Cast, um app remetente precisa oferecer 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 oferece um lembrete visível ao usuário da sessão atual do Google Cast. Ao tocar no minicontrole, o usuário pode retornar à visualização do controle expandido em tela cheia do Google Cast.
O framework oferece 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 de envio está reproduzindo uma transmissão ao vivo de vídeo ou áudio, o SDK mostra automaticamente um botão de reprodução/parada no lugar do botão de assistir/pausar no minicontrolador.
Para definir a aparência do texto do título e do subtítulo dessa visualização personalizada e escolher os botões, consulte Personalizar o Mini Controlador.
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 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 recurso do menu para que o controle 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
.
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 } }
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 como a nova atividade:
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() }
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 a
nova atividade quando a mídia remota for carregada:
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() ) }
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 de envio está reproduzindo uma transmissão ao vivo de vídeo ou áudio, o SDK mostra automaticamente um botão de reprodução/parada no lugar do botão de assistir/pausar no controle expandido.
Para definir a aparência usando temas, escolher quais botões exibir e adicionar botões personalizados, consulte Personalizar o controle aberto.
Controle do volume
O framework gerencia automaticamente o volume do app de envio. Ele sincroniza automaticamente os apps de envio e de recebimento da Web para que a IU do remetente sempre informe o volume especificado pelo receptor da Web.
Controle de volume com botão físico
No Android, os botões físicos do dispositivo de envio podem ser usados para mudar o volume da sessão do Google Cast no Web Receiver por padrão em qualquer dispositivo que use 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 dispositivo do Web Receiver em
dispositivos Android mais antigos que o Jelly Bean, o app de envio precisa substituir
dispatchKeyEvent
nas atividades e chamar
CastContext.onDispatchVolumeKeyEventBeforeJellyBean()
:
class MyActivity : FragmentActivity() { override fun dispatchKeyEvent(event: KeyEvent): Boolean { return (CastContext.getSharedInstance(this) .onDispatchVolumeKeyEventBeforeJellyBean(event) || super.dispatchKeyEvent(event)) } }
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 tem foco. O
framework oferece
MediaNotificationService
e
MediaIntentReceiver
para ajudar o app remetente a criar controles de mídia em uma notificação e na tela
de bloqueio.
O MediaNotificationService
é executado quando o remetente está transmitindo e mostra uma
notificação com uma miniatura de imagem e informações sobre o item de transmissão
atual, um botão "Assistir/Pausar" e um botão "Parar".
MediaIntentReceiver
é um BroadcastReceiver
que processa as ações do usuário na
notificação.
O app pode configurar o controle de notificações e mídia na tela de bloqueio usando
NotificationOptions
.
O 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.
// 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()
// 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 da notificação e da tela de bloqueio fica ativada por
padrão e pode ser desativada chamando
setNotificationOptions
com null em
CastMediaOptions.Builder
.
No momento, o recurso da tela de bloqueio fica ativado enquanto a notificação está
ativada.
// ... 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()
// ... 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 do remetente está reproduzindo uma transmissão 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,
o RemoteMediaClient
vai solicitar automaticamente o foco de áudio em seu nome.
Solucionar erros
É muito importante que os apps de envio 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 pode decidir encerrar a conexão com o Web Receiver.