Esta página contém snippets de código e descrições dos recursos disponíveis para personalizar um app de receptor do Android TV.
Como configurar bibliotecas
Para disponibilizar as APIs Cast Connect ao app Android TV:
-
Abra o arquivo
build.gradle
no diretório do módulo do app. -
Verifique se
google()
está incluído narepositories
listada.repositories { google() }
-
Dependendo do tipo de dispositivo de destino do app, adicione as versões mais recentes
das bibliotecas às dependências:
-
Para o app Android Receiver:
dependencies { implementation 'com.google.android.gms:play-services-cast-tv:21.1.1' implementation 'com.google.android.gms:play-services-cast:22.0.0' }
-
Para o app Android Sender:
dependencies { implementation 'com.google.android.gms:play-services-cast:21.1.1' implementation 'com.google.android.gms:play-services-cast-framework:22.0.0' }
-
Para o app Android Receiver:
-
Salve as alterações e clique em
Sync Project with Gradle Files
na barra de ferramentas.
-
Verifique se o
Podfile
está segmentando ogoogle-cast-sdk
4.8.3 ou mais recente. -
Direcione o app para o iOS 14 ou versões mais recentes. Consulte as Notas da versão
para mais detalhes.
platform: ios, '14' def target_pods pod 'google-cast-sdk', '~>4.8.3' end
- É necessário usar a versão M87 ou mais recente do navegador Chromium.
-
Adicionar a biblioteca da API Web Sender ao seu projeto
<script src="//www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1"></script>
Requisito do AndroidX
Novas versões do Google Play Services exigem que um app tenha sido atualizado para usar
o namespace androidx
. Siga as instruções para
migrar para o AndroidX.
App Android TV: pré-requisitos
Para oferecer suporte ao Cast Connect no app Android TV, você precisa criar e oferecer suporte a eventos de uma sessão de mídia. Os dados fornecidos pela sessão de mídia contêm as informações básicas, como posição, estado de reprodução etc., para o status da mídia. A sessão de mídia também é usada pela biblioteca Cast Connect para sinalizar quando receber determinadas mensagens de um transmissor, como uma pausa.
Para mais informações sobre a sessão de mídia e como inicializá-la, consulte o guia de trabalho com uma sessão de mídia.
Ciclo de vida da sessão de mídia
O app precisa criar uma sessão de mídia quando a reprodução começar e liberar quando ela não puder ser controlada mais. Por exemplo, se o app for de vídeo, libere a sessão quando o usuário sair da atividade de reprodução, seja selecionando "Voltar" para procurar outro conteúdo ou colocando o app em segundo plano. Se o app for de música, libere-o quando ele não estiver mais reproduzindo mídia.
Como atualizar o status da sessão
Os dados na sessão de mídia precisam ser atualizados de acordo com o status do player. Por exemplo, quando a reprodução estiver pausada, atualize o estado de reprodução e as ações compatíveis. As tabelas a seguir listam os estados que você é responsável por manter atualizados.
MediaMetadataCompat
Campo de metadados | Descrição |
---|---|
METADATA_KEY_TITLE (obrigatório) | O título da mídia. |
METADATA_KEY_DISPLAY_SUBTITLE | A legenda. |
METADATA_KEY_DISPLAY_ICON_URI | O URL do ícone. |
METADATA_KEY_DURATION (obrigatório) | Duração da mídia. |
METADATA_KEY_MEDIA_URI | O Content ID. |
METADATA_KEY_ARTIST | O artista. |
METADATA_KEY_ALBUM | O álbum. |
PlaybackStateCompat
Método obrigatório | Descrição |
---|---|
setActions() | Define os comandos de mídia compatíveis. |
setState() | Defina o estado de reprodução e a posição atual. |
MediaSessionCompat
Método obrigatório | Descrição |
---|---|
setRepeatMode() | Define o modo de repetição. |
setShuffleMode() | Define o modo de ordem aleatória. |
setMetadata() | Define metadados de mídia. |
setPlaybackState() | Define o estado de reprodução. |
private fun updateMediaSession() { val metadata = MediaMetadataCompat.Builder() .putString(MediaMetadataCompat.METADATA_KEY_TITLE, "title") .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, "subtitle") .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, mMovie.getCardImageUrl()) .build() val playbackState = PlaybackStateCompat.Builder() .setState( PlaybackStateCompat.STATE_PLAYING, player.getPosition(), player.getPlaybackSpeed(), System.currentTimeMillis() ) .build() mediaSession.setMetadata(metadata) mediaSession.setPlaybackState(playbackState) }
private void updateMediaSession() { MediaMetadataCompat metadata = new MediaMetadataCompat.Builder() .putString(MediaMetadataCompat.METADATA_KEY_TITLE, "title") .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, "subtitle") .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI,mMovie.getCardImageUrl()) .build(); PlaybackStateCompat playbackState = new PlaybackStateCompat.Builder() .setState( PlaybackStateCompat.STATE_PLAYING, player.getPosition(), player.getPlaybackSpeed(), System.currentTimeMillis()) .build(); mediaSession.setMetadata(metadata); mediaSession.setPlaybackState(playbackState); }
Como processar o controle de transporte
O app precisa implementar o callback de controle de transporte da sessão de mídia. A tabela a seguir mostra quais ações de controle de transporte precisam ser processadas:
MediaSessionCompat.Callback
Ações | Descrição |
---|---|
onPlay() | Retomar |
onPause() | Pausar |
onSeekTo() | Procurar uma posição |
onStop() | Parar a mídia atual |
class MyMediaSessionCallback : MediaSessionCompat.Callback() { override fun onPause() { // Pause the player and update the play state. ... } override fun onPlay() { // Resume the player and update the play state. ... } override fun onSeekTo (long pos) { // Seek and update the play state. ... } ... } mediaSession.setCallback( MyMediaSessionCallback() );
public MyMediaSessionCallback extends MediaSessionCompat.Callback { public void onPause() { // Pause the player and update the play state. ... } public void onPlay() { // Resume the player and update the play state. ... } public void onSeekTo (long pos) { // Seek and update the play state. ... } ... } mediaSession.setCallback(new MyMediaSessionCallback());
Como configurar o suporte ao Google Cast
Quando uma solicitação de inicialização é enviada por um aplicativo de envio, uma intent é criada
com um namespace de aplicativo. Seu aplicativo é responsável por processá-lo
e criar uma instância do objeto
CastReceiverContext
quando o app de TV é iniciado. O objeto CastReceiverContext
é necessário
para interagir com o Google Cast enquanto o app de TV está em execução. Esse objeto permite que o app de TV
aceite mensagens de mídia do Google Cast de qualquer remetente conectado.
Configuração do Android TV
Como adicionar um filtro de intent de inicialização
Adicione um novo filtro de intent à atividade que você quer processar a intent de inicialização do app de envio:
<activity android:name="com.example.activity">
<intent-filter>
<action android:name="com.google.android.gms.cast.tv.action.LAUNCH" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
Especificar o provedor de opções do receptor
É necessário implementar um
ReceiverOptionsProvider
para fornecer
CastReceiverOptions
:
class MyReceiverOptionsProvider : ReceiverOptionsProvider { override fun getOptions(context: Context?): CastReceiverOptions { return CastReceiverOptions.Builder(context) .setStatusText("My App") .build() } }
public class MyReceiverOptionsProvider implements ReceiverOptionsProvider { @Override public CastReceiverOptions getOptions(Context context) { return new CastReceiverOptions.Builder(context) .setStatusText("My App") .build(); } }
Em seguida, especifique o provedor de opções no AndroidManifest
:
<meta-data
android:name="com.google.android.gms.cast.tv.RECEIVER_OPTIONS_PROVIDER_CLASS_NAME"
android:value="com.example.mysimpleatvapplication.MyReceiverOptionsProvider" />
O ReceiverOptionsProvider
é usado para fornecer o CastReceiverOptions
quando
o CastReceiverContext
é inicializado.
Contexto do receptor do Cast
Inicialize o
CastReceiverContext
quando o app for criado:
override fun onCreate() { CastReceiverContext.initInstance(this) ... }
@Override public void onCreate() { CastReceiverContext.initInstance(this); ... }
Inicie o CastReceiverContext
quando o app for para o primeiro plano:
CastReceiverContext.getInstance().start()
CastReceiverContext.getInstance().start();
Chame
stop()
no
CastReceiverContext
depois que o app for movido para segundo plano para apps de vídeo ou apps que não oferecem suporte
à reprodução em segundo plano:
// Player has stopped. CastReceiverContext.getInstance().stop()
// Player has stopped. CastReceiverContext.getInstance().stop();
Além disso, se o app tiver suporte para reprodução em segundo plano, chame stop()
no CastReceiverContext
quando ele parar de tocar em segundo plano.
Recomendamos o uso do LifecycleObserver da
biblioteca
androidx.lifecycle
para gerenciar chamadas de
CastReceiverContext.start()
e
CastReceiverContext.stop()
,
principalmente se o app nativo tiver várias atividades. Isso evita condições de
concorrência quando você chama start()
e stop()
de atividades diferentes.
// Create a LifecycleObserver class. class MyLifecycleObserver : DefaultLifecycleObserver { override fun onStart(owner: LifecycleOwner) { // App prepares to enter foreground. CastReceiverContext.getInstance().start() } override fun onStop(owner: LifecycleOwner) { // App has moved to the background or has terminated. CastReceiverContext.getInstance().stop() } } // Add the observer when your application is being created. class MyApplication : Application() { fun onCreate() { super.onCreate() // Initialize CastReceiverContext. CastReceiverContext.initInstance(this /* android.content.Context */) // Register LifecycleObserver ProcessLifecycleOwner.get().lifecycle.addObserver( MyLifecycleObserver()) } }
// Create a LifecycleObserver class. public class MyLifecycleObserver implements DefaultLifecycleObserver { @Override public void onStart(LifecycleOwner owner) { // App prepares to enter foreground. CastReceiverContext.getInstance().start(); } @Override public void onStop(LifecycleOwner owner) { // App has moved to the background or has terminated. CastReceiverContext.getInstance().stop(); } } // Add the observer when your application is being created. public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); // Initialize CastReceiverContext. CastReceiverContext.initInstance(this /* android.content.Context */); // Register LifecycleObserver ProcessLifecycleOwner.get().getLifecycle().addObserver( new MyLifecycleObserver()); } }
// In AndroidManifest.xml set MyApplication as the application class
<application
...
android:name=".MyApplication">
Como conectar a MediaSession ao MediaManager
Ao criar uma
MediaSession
,
também é necessário fornecer o token MediaSession
atual para
CastReceiverContext
para que ele saiba para onde enviar os comandos e recuperar o estado da reprodução de mídia:
val mediaManager: MediaManager = receiverContext.getMediaManager() mediaManager.setSessionCompatToken(currentMediaSession.getSessionToken())
MediaManager mediaManager = receiverContext.getMediaManager(); mediaManager.setSessionCompatToken(currentMediaSession.getSessionToken());
Quando você liberar a MediaSession
devido à reprodução inativa, defina um
token nulo em
MediaManager
:
myPlayer.stop() mediaSession.release() mediaManager.setSessionCompatToken(null)
myPlayer.stop(); mediaSession.release(); mediaManager.setSessionCompatToken(null);
Se o app oferecer suporte à reprodução de mídia enquanto estiver em segundo plano, em vez
de chamar
CastReceiverContext.stop()
quando o app for enviado para o segundo plano, chame-o apenas quando o app
estiver em segundo plano e não estiver mais reproduzindo mídia. Exemplo:
class MyLifecycleObserver : DefaultLifecycleObserver { ... // App has moved to the background. override fun onPause(owner: LifecycleOwner) { mIsBackground = true myStopCastReceiverContextIfNeeded() } } // Stop playback on the player. private fun myStopPlayback() { myPlayer.stop() myStopCastReceiverContextIfNeeded() } // Stop the CastReceiverContext when both the player has // stopped and the app has moved to the background. private fun myStopCastReceiverContextIfNeeded() { if (mIsBackground && myPlayer.isStopped()) { CastReceiverContext.getInstance().stop() } }
public class MyLifecycleObserver implements DefaultLifecycleObserver { ... // App has moved to the background. @Override public void onPause(LifecycleOwner owner) { mIsBackground = true; myStopCastReceiverContextIfNeeded(); } } // Stop playback on the player. private void myStopPlayback() { myPlayer.stop(); myStopCastReceiverContextIfNeeded(); } // Stop the CastReceiverContext when both the player has // stopped and the app has moved to the background. private void myStopCastReceiverContextIfNeeded() { if (mIsBackground && myPlayer.isStopped()) { CastReceiverContext.getInstance().stop(); } }
Como usar o Exoplayer com o Cast Connect
Se você estiver usando
Exoplayer
, use o
MediaSessionConnector
para manter automaticamente a sessão e todas as informações relacionadas, incluindo o
estado de reprodução, em vez de rastrear as mudanças manualmente.
MediaSessionConnector.MediaButtonEventHandler
pode ser usado para processar eventos de MediaButton chamando
setMediaButtonEventHandler(MediaButtonEventHandler)
,
que são processados por
MediaSessionCompat.Callback
por padrão.
Para integrar
MediaSessionConnector
ao app, adicione o seguinte à classe de atividade do player ou a qualquer lugar em que
você gerencie a sessão de mídia:
class PlayerActivity : Activity() { private var mMediaSession: MediaSessionCompat? = null private var mMediaSessionConnector: MediaSessionConnector? = null private var mMediaManager: MediaManager? = null override fun onCreate(savedInstanceState: Bundle?) { ... mMediaSession = MediaSessionCompat(this, LOG_TAG) mMediaSessionConnector = MediaSessionConnector(mMediaSession!!) ... } override fun onStart() { ... mMediaManager = receiverContext.getMediaManager() mMediaManager!!.setSessionCompatToken(currentMediaSession.getSessionToken()) mMediaSessionConnector!!.setPlayer(mExoPlayer) mMediaSessionConnector!!.setMediaMetadataProvider(mMediaMetadataProvider) mMediaSession!!.isActive = true ... } override fun onStop() { ... mMediaSessionConnector!!.setPlayer(null) mMediaSession!!.release() mMediaManager!!.setSessionCompatToken(null) ... } }
public class PlayerActivity extends Activity { private MediaSessionCompat mMediaSession; private MediaSessionConnector mMediaSessionConnector; private MediaManager mMediaManager; @Override protected void onCreate(Bundle savedInstanceState) { ... mMediaSession = new MediaSessionCompat(this, LOG_TAG); mMediaSessionConnector = new MediaSessionConnector(mMediaSession); ... } @Override protected void onStart() { ... mMediaManager = receiverContext.getMediaManager(); mMediaManager.setSessionCompatToken(currentMediaSession.getSessionToken()); mMediaSessionConnector.setPlayer(mExoPlayer); mMediaSessionConnector.setMediaMetadataProvider(mMediaMetadataProvider); mMediaSession.setActive(true); ... } @Override protected void onStop() { ... mMediaSessionConnector.setPlayer(null); mMediaSession.release(); mMediaManager.setSessionCompatToken(null); ... } }
Configuração do app de remetente
Ativar o suporte ao Cast Connect
Depois de atualizar o app de envio com suporte ao Cast Connect, declare
a prontidão definindo a flag
androidReceiverCompatible
em
LaunchOptions
como "true".
Exige o play-services-cast-framework
na versão
19.0.0
ou mais recente.
A flag androidReceiverCompatible
é definida em
LaunchOptions
, que faz parte de CastOptions
:
class CastOptionsProvider : OptionsProvider { override fun getCastOptions(context: Context?): CastOptions { val launchOptions: LaunchOptions = Builder() .setAndroidReceiverCompatible(true) .build() return CastOptions.Builder() .setLaunchOptions(launchOptions) ... .build() } }
public class CastOptionsProvider implements OptionsProvider { @Override public CastOptions getCastOptions(Context context) { LaunchOptions launchOptions = new LaunchOptions.Builder() .setAndroidReceiverCompatible(true) .build(); return new CastOptions.Builder() .setLaunchOptions(launchOptions) ... .build(); } }
Exige o google-cast-sdk
na versão v4.4.8
ou
mais recente.
A flag androidReceiverCompatible
é definida em
GCKLaunchOptions
, que faz parte de
GCKCastOptions
:
let options = GCKCastOptions(discoveryCriteria: GCKDiscoveryCriteria(applicationID: kReceiverAppID)) ... let launchOptions = GCKLaunchOptions() launchOptions.androidReceiverCompatible = true options.launchOptions = launchOptions GCKCastContext.setSharedInstanceWith(options)
Exige o navegador Chromium na versão
M87
ou mais recente.
const context = cast.framework.CastContext.getInstance(); const castOptions = new cast.framework.CastOptions(); castOptions.receiverApplicationId = kReceiverAppID; castOptions.androidReceiverCompatible = true; context.setOptions(castOptions);
Configuração do Play Console para desenvolvedores do Google Cast
Configurar o app Android TV
Adicione o nome do pacote do app Android TV no Developer Console do Cast para associá-lo ao ID do app Cast.
Registrar dispositivos de desenvolvedor
Registre o número de série do dispositivo Android TV que você vai usar para desenvolvimento no Play Console do Google Cast.
Sem o registro, o Cast Connect só vai funcionar para apps instalados da Google Play Store por motivos de segurança.
Para mais informações sobre como registrar um dispositivo Cast ou Android TV para desenvolvimento do Cast, consulte a página de registro.
Como carregar mídia
Se você já implementou o suporte a links diretos no app Android TV, deve ter uma definição semelhante configurada no manifesto do Android TV:
<activity android:name="com.example.activity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="https"/>
<data android:host="www.example.com"/>
<data android:pathPattern=".*"/>
</intent-filter>
</activity>
Carregar por entidade no remetente
Nos remetentes, é possível transmitir o link direto definindo entity
nas informações
de mídia para a solicitação de carregamento:
val mediaToLoad = MediaInfo.Builder("some-id") .setEntity("https://example.com/watch/some-id") ... .build() val loadRequest = MediaLoadRequestData.Builder() .setMediaInfo(mediaToLoad) .setCredentials("user-credentials") ... .build() remoteMediaClient.load(loadRequest)
MediaInfo mediaToLoad = new MediaInfo.Builder("some-id") .setEntity("https://example.com/watch/some-id") ... .build(); MediaLoadRequestData loadRequest = new MediaLoadRequestData.Builder() .setMediaInfo(mediaToLoad) .setCredentials("user-credentials") ... .build(); remoteMediaClient.load(loadRequest);
let mediaInfoBuilder = GCKMediaInformationBuilder(entity: "https://example.com/watch/some-id") ... mediaInformation = mediaInfoBuilder.build() let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder() mediaLoadRequestDataBuilder.mediaInformation = mediaInformation mediaLoadRequestDataBuilder.credentials = "user-credentials" ... let mediaLoadRequestData = mediaLoadRequestDataBuilder.build() remoteMediaClient?.loadMedia(with: mediaLoadRequestData)
Exige o navegador Chromium na versão
M87
ou mais recente.
let mediaInfo = new chrome.cast.media.MediaInfo('some-id"', 'video/mp4'); mediaInfo.entity = 'https://example.com/watch/some-id'; ... let request = new chrome.cast.media.LoadRequest(mediaInfo); request.credentials = 'user-credentials'; ... cast.framework.CastContext.getInstance().getCurrentSession().loadMedia(request);
O comando de carregamento é enviado por uma intent com o link direto e o nome do pacote definido no console do desenvolvedor.
Como definir credenciais de ATV no remetente
É possível que o app do receptor da Web e o app do Android TV ofereçam suporte a diferentes
links diretos e credentials
(por exemplo, se você estiver processando a autenticação
de forma diferente nas duas plataformas). Para resolver esse problema, forneça
entity
e credentials
alternativos para o Android TV:
val mediaToLoad = MediaInfo.Builder("some-id") .setEntity("https://example.com/watch/some-id") .setAtvEntity("myscheme://example.com/atv/some-id") ... .build() val loadRequest = MediaLoadRequestData.Builder() .setMediaInfo(mediaToLoad) .setCredentials("user-credentials") .setAtvCredentials("atv-user-credentials") ... .build() remoteMediaClient.load(loadRequest)
MediaInfo mediaToLoad = new MediaInfo.Builder("some-id") .setEntity("https://example.com/watch/some-id") .setAtvEntity("myscheme://example.com/atv/some-id") ... .build(); MediaLoadRequestData loadRequest = new MediaLoadRequestData.Builder() .setMediaInfo(mediaToLoad) .setCredentials("user-credentials") .setAtvCredentials("atv-user-credentials") ... .build(); remoteMediaClient.load(loadRequest);
let mediaInfoBuilder = GCKMediaInformationBuilder(entity: "https://example.com/watch/some-id") mediaInfoBuilder.atvEntity = "myscheme://example.com/atv/some-id" ... mediaInformation = mediaInfoBuilder.build() let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder() mediaLoadRequestDataBuilder.mediaInformation = mediaInformation mediaLoadRequestDataBuilder.credentials = "user-credentials" mediaLoadRequestDataBuilder.atvCredentials = "atv-user-credentials" ... let mediaLoadRequestData = mediaLoadRequestDataBuilder.build() remoteMediaClient?.loadMedia(with: mediaLoadRequestData)
Exige o navegador Chromium na versão
M87
ou mais recente.
let mediaInfo = new chrome.cast.media.MediaInfo('some-id"', 'video/mp4'); mediaInfo.entity = 'https://example.com/watch/some-id'; mediaInfo.atvEntity = 'myscheme://example.com/atv/some-id'; ... let request = new chrome.cast.media.LoadRequest(mediaInfo); request.credentials = 'user-credentials'; request.atvCredentials = 'atv-user-credentials'; ... cast.framework.CastContext.getInstance().getCurrentSession().loadMedia(request);
Se o app Web Receiver for iniciado, ele vai usar entity
e credentials
na
solicitação de carregamento. No entanto, se o app Android TV for iniciado, o SDK vai substituir
o entity
e o credentials
pelo atvEntity
e atvCredentials
(se especificado).
Como carregar por Content ID ou MediaQueueData
Se você não estiver usando entity
ou atvEntity
, e estiver usando o ID de conteúdo ou
o URL de conteúdo nas suas informações de mídia ou usar os dados de solicitação de carregamento de mídia
mais detalhados, adicione o seguinte filtro de intent predefinido no
seu app Android TV:
<activity android:name="com.example.activity">
<intent-filter>
<action android:name="com.google.android.gms.cast.tv.action.LOAD"/>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
No lado do remetente, semelhante ao carregamento por entidade, é possível
criar uma solicitação de carregamento com as informações do conteúdo e chamar load()
.
val mediaToLoad = MediaInfo.Builder("some-id").build() val loadRequest = MediaLoadRequestData.Builder() .setMediaInfo(mediaToLoad) .setCredentials("user-credentials") ... .build() remoteMediaClient.load(loadRequest)
MediaInfo mediaToLoad = new MediaInfo.Builder("some-id").build(); MediaLoadRequestData loadRequest = new MediaLoadRequestData.Builder() .setMediaInfo(mediaToLoad) .setCredentials("user-credentials") ... .build(); remoteMediaClient.load(loadRequest);
let mediaInfoBuilder = GCKMediaInformationBuilder(contentId: "some-id") ... mediaInformation = mediaInfoBuilder.build() let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder() mediaLoadRequestDataBuilder.mediaInformation = mediaInformation mediaLoadRequestDataBuilder.credentials = "user-credentials" ... let mediaLoadRequestData = mediaLoadRequestDataBuilder.build() remoteMediaClient?.loadMedia(with: mediaLoadRequestData)
Exige o navegador Chromium na versão
M87
ou mais recente.
let mediaInfo = new chrome.cast.media.MediaInfo('some-id"', 'video/mp4'); ... let request = new chrome.cast.media.LoadRequest(mediaInfo); ... cast.framework.CastContext.getInstance().getCurrentSession().loadMedia(request);
Como processar solicitações de carregamento
Na atividade, para processar essas solicitações de carregamento, é necessário processar as intents nos callbacks do ciclo de vida da atividade:
class MyActivity : Activity() { override fun onStart() { super.onStart() val mediaManager = CastReceiverContext.getInstance().getMediaManager() // Pass the intent to the SDK. You can also do this in onCreate(). if (mediaManager.onNewIntent(intent)) { // If the SDK recognizes the intent, you should early return. return } // If the SDK doesn't recognize the intent, you can handle the intent with // your own logic. ... } // For some cases, a new load intent triggers onNewIntent() instead of // onStart(). override fun onNewIntent(intent: Intent) { val mediaManager = CastReceiverContext.getInstance().getMediaManager() // Pass the intent to the SDK. You can also do this in onCreate(). if (mediaManager.onNewIntent(intent)) { // If the SDK recognizes the intent, you should early return. return } // If the SDK doesn't recognize the intent, you can handle the intent with // your own logic. ... } }
public class MyActivity extends Activity { @Override protected void onStart() { super.onStart(); MediaManager mediaManager = CastReceiverContext.getInstance().getMediaManager(); // Pass the intent to the SDK. You can also do this in onCreate(). if (mediaManager.onNewIntent(getIntent())) { // If the SDK recognizes the intent, you should early return. return; } // If the SDK doesn't recognize the intent, you can handle the intent with // your own logic. ... } // For some cases, a new load intent triggers onNewIntent() instead of // onStart(). @Override protected void onNewIntent(Intent intent) { MediaManager mediaManager = CastReceiverContext.getInstance().getMediaManager(); // Pass the intent to the SDK. You can also do this in onCreate(). if (mediaManager.onNewIntent(intent)) { // If the SDK recognizes the intent, you should early return. return; } // If the SDK doesn't recognize the intent, you can handle the intent with // your own logic. ... } }
Se MediaManager
detectar que a intent é uma intent de carregamento, ela extrai um
objeto MediaLoadRequestData
da intent e invoca
MediaLoadCommandCallback.onLoad()
.
É necessário substituir esse método para processar a solicitação de carregamento. O callback precisa
ser registrado antes de
MediaManager.onNewIntent()
ser chamado. É recomendável que ele esteja em um método onCreate()
de atividade ou aplicativo.
class MyActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val mediaManager = CastReceiverContext.getInstance().getMediaManager() mediaManager.setMediaLoadCommandCallback(MyMediaLoadCommandCallback()) } } class MyMediaLoadCommandCallback : MediaLoadCommandCallback() { override fun onLoad( senderId: String?, loadRequestData: MediaLoadRequestData ): Task{ return Tasks.call { // Resolve the entity into your data structure and load media. val mediaInfo = loadRequestData.getMediaInfo() if (!checkMediaInfoSupported(mediaInfo)) { // Throw MediaException to indicate load failure. throw MediaException( MediaError.Builder() .setDetailedErrorCode(DetailedErrorCode.LOAD_FAILED) .setReason(MediaError.ERROR_REASON_INVALID_REQUEST) .build() ) } myFillMediaInfo(MediaInfoWriter(mediaInfo)) myPlayerLoad(mediaInfo.getContentUrl()) // Update media metadata and state (this clears all previous status // overrides). castReceiverContext.getMediaManager() .setDataFromLoad(loadRequestData) ... castReceiverContext.getMediaManager().broadcastMediaStatus() // Return the resolved MediaLoadRequestData to indicate load success. return loadRequestData } } private fun myPlayerLoad(contentURL: String) { myPlayer.load(contentURL) // Update the MediaSession state. val playbackState: PlaybackStateCompat = Builder() .setState( player.getState(), player.getPosition(), System.currentTimeMillis() ) ... .build() mediaSession.setPlaybackState(playbackState) }
public class MyActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); MediaManager mediaManager = CastReceiverContext.getInstance().getMediaManager(); mediaManager.setMediaLoadCommandCallback(new MyMediaLoadCommandCallback()); } } public class MyMediaLoadCommandCallback extends MediaLoadCommandCallback { @Override public TaskonLoad(String senderId, MediaLoadRequestData loadRequestData) { return Tasks.call(() -> { // Resolve the entity into your data structure and load media. MediaInfo mediaInfo = loadRequestData.getMediaInfo(); if (!checkMediaInfoSupported(mediaInfo)) { // Throw MediaException to indicate load failure. throw new MediaException( new MediaError.Builder() .setDetailedErrorCode(DetailedErrorCode.LOAD_FAILED) .setReason(MediaError.ERROR_REASON_INVALID_REQUEST) .build()); } myFillMediaInfo(new MediaInfoWriter(mediaInfo)); myPlayerLoad(mediaInfo.getContentUrl()); // Update media metadata and state (this clears all previous status // overrides). castReceiverContext.getMediaManager() .setDataFromLoad(loadRequestData); ... castReceiverContext.getMediaManager().broadcastMediaStatus(); // Return the resolved MediaLoadRequestData to indicate load success. return loadRequestData; }); } private void myPlayerLoad(String contentURL) { myPlayer.load(contentURL); // Update the MediaSession state. PlaybackStateCompat playbackState = new PlaybackStateCompat.Builder() .setState( player.getState(), player.getPosition(), System.currentTimeMillis()) ... .build(); mediaSession.setPlaybackState(playbackState); }
Para processar a intent de carregamento, você pode analisar a intent nas estruturas de dados
definidas
(MediaLoadRequestData
para solicitações de carregamento).
Oferecer suporte a comandos de mídia
Suporte básico ao controle de reprodução
Os comandos de integração básicos incluem os comandos compatíveis com a sessão de mídia. Esses comandos são notificados por callbacks de sessão de mídia. É necessário registrar um callback na sessão de mídia para oferecer suporte a isso (talvez você já esteja fazendo isso).
private class MyMediaSessionCallback : MediaSessionCompat.Callback() { override fun onPause() { // Pause the player and update the play state. myPlayer.pause() } override fun onPlay() { // Resume the player and update the play state. myPlayer.play() } override fun onSeekTo(pos: Long) { // Seek and update the play state. myPlayer.seekTo(pos) } ... } mediaSession.setCallback(MyMediaSessionCallback())
private class MyMediaSessionCallback extends MediaSessionCompat.Callback { @Override public void onPause() { // Pause the player and update the play state. myPlayer.pause(); } @Override public void onPlay() { // Resume the player and update the play state. myPlayer.play(); } @Override public void onSeekTo(long pos) { // Seek and update the play state. myPlayer.seekTo(pos); } ... } mediaSession.setCallback(new MyMediaSessionCallback());
Oferecer suporte a comandos de controle do Google Cast
Alguns comandos do Google Cast não estão disponíveis no
MediaSession
,
como
skipAd()
ou
setActiveMediaTracks()
.
Além disso, alguns comandos de fila precisam ser implementados aqui porque a fila do Google Cast
não é totalmente compatível com a fila MediaSession
.
class MyMediaCommandCallback : MediaCommandCallback() { override fun onSkipAd(requestData: RequestData?): Task<Void?> { // Skip your ad ... return Tasks.forResult(null) } } val mediaManager = CastReceiverContext.getInstance().getMediaManager() mediaManager.setMediaCommandCallback(MyMediaCommandCallback())
public class MyMediaCommandCallback extends MediaCommandCallback { @Override public TaskonSkipAd(RequestData requestData) { // Skip your ad ... return Tasks.forResult(null); } } MediaManager mediaManager = CastReceiverContext.getInstance().getMediaManager(); mediaManager.setMediaCommandCallback(new MyMediaCommandCallback());
Especificar comandos de mídia compatíveis
Assim como o receptor do Google Cast, o app Android TV precisa especificar quais comandos
são aceitos para que os remetentes possam ativar ou desativar determinados controles de interface. Para
comandos que fazem parte de
MediaSession
,
especifique os comandos em
PlaybackStateCompat
.
Outros comandos precisam ser especificados no
MediaStatusModifier
.
// Set media session supported commands val playbackState: PlaybackStateCompat = PlaybackStateCompat.Builder() .setActions(PlaybackStateCompat.ACTION_PLAY or PlaybackStateCompat.ACTION_PAUSE) .setState(PlaybackStateCompat.STATE_PLAYING) .build() mediaSession.setPlaybackState(playbackState) // Set additional commands in MediaStatusModifier val mediaManager = CastReceiverContext.getInstance().getMediaManager() mediaManager.getMediaStatusModifier() .setMediaCommandSupported(MediaStatus.COMMAND_QUEUE_NEXT)
// Set media session supported commands PlaybackStateCompat playbackState = new PlaybackStateCompat.Builder() .setActions(PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE) .setState(PlaybackStateCompat.STATE_PLAYING) .build(); mediaSession.setPlaybackState(playbackState); // Set additional commands in MediaStatusModifier MediaManager mediaManager = CastReceiverContext.getInstance().getMediaManager(); mediaManager.getMediaStatusModifier() .setMediaCommandSupported(MediaStatus.COMMAND_QUEUE_NEXT);
Ocultar botões sem suporte
Se o app Android TV oferecer suporte apenas ao controle de mídia básico, mas o app do receptor da Web oferecer um controle mais avançado, verifique se o app de envio se comporta corretamente ao transmitir para o app Android TV. Por exemplo, se o app Android TV não oferecer suporte à alteração da taxa de reprodução, mas o app do receptor da Web oferecer, configure as ações com suporte corretamente em cada plataforma e verifique se o app de envio renderiza a interface corretamente.
Como modificar o MediaStatus
Para oferecer suporte a recursos avançados, como faixas, anúncios, transmissões ao vivo e filas, o app Android
TV precisa fornecer informações adicionais que não podem ser determinadas pelo
MediaSession
.
Fornecemos a classe
MediaStatusModifier
para que você consiga fazer isso. MediaStatusModifier
sempre vai operar na
MediaSession
que você definiu em
CastReceiverContext
.
Para criar e transmitir
MediaStatus
:
val mediaManager: MediaManager = castReceiverContext.getMediaManager() val statusModifier: MediaStatusModifier = mediaManager.getMediaStatusModifier() statusModifier .setLiveSeekableRange(seekableRange) .setAdBreakStatus(adBreakStatus) .setCustomData(customData) mediaManager.broadcastMediaStatus()
MediaManager mediaManager = castReceiverContext.getMediaManager(); MediaStatusModifier statusModifier = mediaManager.getMediaStatusModifier(); statusModifier .setLiveSeekableRange(seekableRange) .setAdBreakStatus(adBreakStatus) .setCustomData(customData); mediaManager.broadcastMediaStatus();
Nossa biblioteca de cliente vai receber o MediaStatus
base do MediaSession
. O
app Android TV pode especificar outros status e substituir o status usando um
modificador MediaStatus
.
Alguns estados e metadados podem ser definidos em MediaSession
e
MediaStatusModifier
. É altamente recomendável definir esses valores apenas em
MediaSession
. Você ainda pode usar o modificador para substituir os estados em
MediaSession
. Isso não é recomendado porque o status no modificador sempre
tem uma prioridade maior do que os valores fornecidos por MediaSession
.
Como interceptar o MediaStatus antes da transmissão
Assim como no SDK do receptor da Web, se você quiser fazer alguns ajustes finais antes
do envio, especifique um
MediaStatusInterceptor
para processar o
MediaStatus
a
ser enviado. Transmitimos um
MediaStatusWriter
para manipular o MediaStatus
antes do envio.
mediaManager.setMediaStatusInterceptor(object : MediaStatusInterceptor { override fun intercept(mediaStatusWriter: MediaStatusWriter) { // Perform customization. mediaStatusWriter.setCustomData(JSONObject("{data: \"my Hello\"}")) } })
mediaManager.setMediaStatusInterceptor(new MediaStatusInterceptor() { @Override public void intercept(MediaStatusWriter mediaStatusWriter) { // Perform customization. mediaStatusWriter.setCustomData(new JSONObject("{data: \"my Hello\"}")); } });
Como processar credenciais de usuário
Seu app para Android TV pode permitir que apenas alguns usuários iniciem ou participem da sessão do app. Por exemplo, permita que um remetente inicie ou participe apenas se:
- O app de envio está conectado à mesma conta e perfil que o app ATV.
- O app de envio está conectado à mesma conta, mas com um perfil diferente do app do Android TV.
Se o app puder processar vários usuários ou usuários anônimos, você poderá permitir que outros usuários participem da sessão de ATV. Se o usuário fornecer credenciais, o app do ATV precisa processá-las para que o progresso e outros dados do usuário possam ser rastreados corretamente.
Quando o app de envio é iniciado ou ingressa no app Android TV, ele precisa fornecer as credenciais que representam quem está participando da sessão.
Antes que um remetente inicie e participe do seu app Android TV, você pode especificar um verificador de inicialização para saber se as credenciais do remetente são permitidas. Caso contrário, o SDK Cast Connect volta a iniciar o receptor da Web.
Dados de credenciais de inicialização do app remetente
No lado do remetente, é possível especificar o CredentialsData
para representar quem está
participando da sessão.
O credentials
é uma string que pode ser definida pelo usuário, desde que o app
ATV consiga entendê-la. O credentialsType
define de qual plataforma o
CredentialsData
vem ou pode ser um valor personalizado. Por padrão, ele é definido
como a plataforma de onde está sendo enviado.
O CredentialsData
só é transmitido para o app Android TV durante a inicialização ou
a associação. Se você definir novamente enquanto estiver conectado, ele não será transmitido para
o app Android TV. Se o remetente mudar o perfil enquanto estiver conectado, você
poderá permanecer na sessão ou chamar
SessionManager.endCurrentCastSession(boolean stopCasting)
se achar que o novo perfil é incompatível com a sessão.
O
CredentialsData
de cada remetente pode ser recuperado usando
getSenders
no
CastReceiverContext
para receber o SenderInfo
,
getCastLaunchRequest()
para receber o
CastLaunchRequest
e, em seguida,
getCredentialsData()
.
Exige o play-services-cast-framework
na versão
19.0.0
ou mais recente.
CastContext.getSharedInstance().setLaunchCredentialsData( CredentialsData.Builder() .setCredentials("{\"userId\": \"abc\"}") .build() )
CastContext.getSharedInstance().setLaunchCredentialsData( new CredentialsData.Builder() .setCredentials("{\"userId\": \"abc\"}") .build());
Exige o google-cast-sdk
na versão v4.8.3
ou
mais recente.
Pode ser chamado a qualquer momento depois que as opções são definidas:
GCKCastContext.setSharedInstanceWith(options)
.
GCKCastContext.sharedInstance().setLaunch( GCKCredentialsData(credentials: "{\"userId\": \"abc\"}")
Exige o navegador Chromium na versão
M87
ou mais recente.
Pode ser chamado a qualquer momento depois que as opções são definidas:
cast.framework.CastContext.getInstance().setOptions(options);
.
let credentialsData = new chrome.cast.CredentialsData("{\"userId\": \"abc\"}"); cast.framework.CastContext.getInstance().setLaunchCredentialsData(credentialsData);
Como implementar o verificador de solicitações de lançamento do ATV
O
CredentialsData
é transmitido para o app Android TV quando um remetente tenta iniciar ou participar. Você pode
implementar um
LaunchRequestChecker
.
para permitir ou recusar essa solicitação.
Se uma solicitação for rejeitada, o Web Receiver será carregado em vez de ser iniciado de forma nativa no app ATV. É necessário rejeitar uma solicitação se o ATV não conseguir processar o usuário que solicita o lançamento ou a participação. Por exemplo, um usuário diferente está conectado ao app ATV do que está solicitando, e seu app não consegue processar a troca de credenciais ou não há um usuário conectado ao app ATV.
Se uma solicitação for permitida, o app ATV será iniciado. É possível personalizar esse
comportamento dependendo se o app oferece suporte ao envio de solicitações de carregamento quando um usuário
não está conectado no app ATV ou se há uma incompatibilidade do usuário. Esse comportamento é
totalmente personalizável no LaunchRequestChecker
.
Crie uma classe que implemente a
interface
CastReceiverOptions.LaunchRequestChecker
:
class MyLaunchRequestChecker : LaunchRequestChecker { override fun checkLaunchRequestSupported(launchRequest: CastLaunchRequest): Task{ return Tasks.call { myCheckLaunchRequest( launchRequest ) } } } private fun myCheckLaunchRequest(launchRequest: CastLaunchRequest): Boolean { val credentialsData = launchRequest.getCredentialsData() ?: return false // or true if you allow anonymous users to join. // The request comes from a mobile device, e.g. checking user match. return if (credentialsData.credentialsType == CredentialsData.CREDENTIALS_TYPE_ANDROID) { myCheckMobileCredentialsAllowed(credentialsData.getCredentials()) } else false // Unrecognized credentials type. }
public class MyLaunchRequestChecker implements CastReceiverOptions.LaunchRequestChecker { @Override public TaskcheckLaunchRequestSupported(CastLaunchRequest launchRequest) { return Tasks.call(() -> myCheckLaunchRequest(launchRequest)); } } private boolean myCheckLaunchRequest(CastLaunchRequest launchRequest) { CredentialsData credentialsData = launchRequest.getCredentialsData(); if (credentialsData == null) { return false; // or true if you allow anonymous users to join. } // The request comes from a mobile device, e.g. checking user match. if (credentialsData.getCredentialsType().equals(CredentialsData.CREDENTIALS_TYPE_ANDROID)) { return myCheckMobileCredentialsAllowed(credentialsData.getCredentials()); } // Unrecognized credentials type. return false; }
Em seguida, defina-o no
ReceiverOptionsProvider
:
class MyReceiverOptionsProvider : ReceiverOptionsProvider { override fun getOptions(context: Context?): CastReceiverOptions { return CastReceiverOptions.Builder(context) ... .setLaunchRequestChecker(MyLaunchRequestChecker()) .build() } }
public class MyReceiverOptionsProvider implements ReceiverOptionsProvider { @Override public CastReceiverOptions getOptions(Context context) { return new CastReceiverOptions.Builder(context) ... .setLaunchRequestChecker(new MyLaunchRequestChecker()) .build(); } }
A resolução de true
no
LaunchRequestChecker
iniciará o app ATV, e false
vai iniciar o app Web Receiver.
Como enviar e receber mensagens personalizadas
O protocolo Cast permite enviar mensagens de string personalizadas entre remetentes e
o aplicativo receptor. É necessário registrar um namespace (canal) para enviar
mensagens antes de inicializar o
CastReceiverContext
.
Android TV: especificar o namespace personalizado
É necessário especificar os namespaces com suporte no
CastReceiverOptions
durante a configuração:
class MyReceiverOptionsProvider : ReceiverOptionsProvider { override fun getOptions(context: Context?): CastReceiverOptions { return CastReceiverOptions.Builder(context) .setCustomNamespaces( Arrays.asList("urn:x-cast:com.example.cast.mynamespace") ) .build() } }
public class MyReceiverOptionsProvider implements ReceiverOptionsProvider { @Override public CastReceiverOptions getOptions(Context context) { return new CastReceiverOptions.Builder(context) .setCustomNamespaces( Arrays.asList("urn:x-cast:com.example.cast.mynamespace")) .build(); } }
Android TV: como enviar mensagens
// If senderId is null, then the message is broadcasted to all senders. CastReceiverContext.getInstance().sendMessage( "urn:x-cast:com.example.cast.mynamespace", senderId, customString)
// If senderId is null, then the message is broadcasted to all senders. CastReceiverContext.getInstance().sendMessage( "urn:x-cast:com.example.cast.mynamespace", senderId, customString);
Android TV: receber mensagens de namespace personalizado
class MyCustomMessageListener : MessageReceivedListener { override fun onMessageReceived( namespace: String, senderId: String?, message: String ) { ... } } CastReceiverContext.getInstance().setMessageReceivedListener( "urn:x-cast:com.example.cast.mynamespace", new MyCustomMessageListener());
class MyCustomMessageListener implements CastReceiverContext.MessageReceivedListener { @Override public void onMessageReceived( String namespace, String senderId, String message) { ... } } CastReceiverContext.getInstance().setMessageReceivedListener( "urn:x-cast:com.example.cast.mynamespace", new MyCustomMessageListener());