
Na rua, ouço frequentemente audiolivros e podcasts de um smartphone. Quando chegar em casa, quero continuar ouvindo-os na Android TV ou na Página inicial do Google. Mas nem todos os aplicativos são compatíveis com o Chromecast. E seria conveniente.
De acordo com as estatísticas do Google nos últimos 3 anos, o número de dispositivos na Android TV aumentou 4 vezes e o número de parceiros de fabricação já ultrapassou uma centena: televisores "inteligentes", alto-falantes e decodificadores. Todos eles são compatíveis com o Chromecast. Mas ainda existem muitos aplicativos no mercado que obviamente não têm integração com ele.
Neste artigo, quero compartilhar minha experiência na integração do Chromecast a um aplicativo Android para reproduzir conteúdo de mídia.
Como isso funciona
Se for a primeira vez que você ouvir a palavra "Chromecast", tentarei informar brevemente. Em termos de uso, é algo parecido com isto:
- O usuário ouve música ou assiste a vídeos por meio de um aplicativo ou site.
- Um dispositivo Chromecast aparece na rede local.
- Um botão correspondente deve aparecer na interface do jogador.
- Ao clicar nele, o usuário seleciona o dispositivo desejado da lista. Pode ser um Nexus Player, Android TV ou um alto-falante inteligente.
- Mais reprodução continua com este dispositivo.

Tecnicamente, algo como o seguinte acontece:
- O Google Services monitora a presença de dispositivos Chromecast na rede local por meio da transmissão.
- Se o MediaRouter estiver conectado ao seu aplicativo, você receberá um evento sobre isso.
- Quando um usuário seleciona um dispositivo Cast e se conecta a ele, uma nova sessão de mídia (CastSession) é aberta.
- Já na sessão criada, transferiremos o conteúdo para reprodução.
Parece muito simples.
Integração
O Google possui seu próprio SDK do Chromecast, mas é pouco coberto pela documentação e seu código é ofuscado. Portanto, muitas coisas tiveram que ser verificadas digitando. Vamos colocar tudo em ordem.
Inicialização
Primeiro, precisamos conectar o Cast Application Framework e o MediaRouter:
implementation "com.google.android.gms:play-services-cast-framework:16.1.0" implementation "androidx.mediarouter:mediarouter:1.0.0"
Em seguida, o Cast Framework deve obter o identificador do aplicativo (mais sobre isso posteriormente) e os tipos de conteúdo de mídia compatível. Ou seja, se nosso aplicativo reproduzir apenas vídeo, a transmissão para a coluna Página inicial do Google será impossível e não estará na lista de dispositivos. Para fazer isso, crie uma implementação de OptionsProvider:
class CastOptionsProvider: OptionsProvider { override fun getCastOptions(context: Context): CastOptions { return CastOptions.Builder() .setReceiverApplicationId(BuildConfig.CHROMECAST_APP_ID) .build() } override fun getAdditionalSessionProviders(context: Context): MutableList<SessionProvider>? { return null } }
E declare isso em Manifest:
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME" android:value="your.app.package.CastOptionsProvider" />
Registrar o aplicativo
Para que o Chromecast funcione com nosso aplicativo, ele deve estar registrado no Google Cast SDK Developers Console . Isso requer uma conta de desenvolvedor do Chromecast (para não ser confundida com uma conta de desenvolvedor do Google Play). Ao se registrar, você terá que pagar uma taxa única de US $ 5. Depois de publicar o Aplicativo ChromeCast, você precisa esperar um pouco.
No console, você pode alterar a aparência do player do Cast para dispositivos com uma tela e ver as análises de transmissão no aplicativo.
O MediaRouteFramework é um mecanismo que permite encontrar todos os dispositivos de reprodução remota perto do usuário. Isso pode ser não apenas o Chromecast, mas também monitores e alto-falantes remotos usando protocolos de terceiros. Mas o que nos interessa é o Chromecast.

O MediaRouteFramework possui uma Visualização que reflete o estado da scooter de mídia. Existem duas maneiras de conectá-lo:
1) Através do menu:
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> ... <item android:id="@+id/menu_media_route" android:title="@string/cast" app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider" app:showAsAction="always"/> ... </menu>
2) Via layout:
<androidx.mediarouter.app.MediaRouteButton android:id="@+id/mediaRouteButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:mediaRouteTypes="user"/>
E a partir do código, você só precisa registrar o botão no CastButtonFactory. então, o estado atual da scooter de mídia será lançado nele:
CastButtonFactory.setUpMediaRouteButton(applicationContext, view.mediaRouteButton)
Agora que o aplicativo está registrado e o MediaRouter está configurado, você pode conectar-se aos dispositivos ChromeCast e abrir sessões a eles.
O ChromeCast suporta três tipos principais de conteúdo:
Dependendo das configurações do player, como conteúdo de mídia e dispositivo de transmissão, a interface do player pode variar.
Castsession
Assim, o usuário selecionou o dispositivo desejado, o CastFramework abriu uma nova sessão. Agora, nossa tarefa é responder a isso e passar informações ao dispositivo para reprodução.
Para descobrir o estado atual da sessão e se inscrever para atualizar esse estado, use o objeto SessionManager :
private val mediaSessionListener = object : SessionManagerListener<CastSession> { override fun onSessionStarted(session: CastSession, sessionId: String) { currentSession = session
E também podemos descobrir se há uma sessão aberta no momento:
val currentSession: CastSession? = sessionManager.currentCastSession
Temos duas condições principais sob as quais podemos começar a transmitir:
- A sessão já está aberta.
- Há conteúdo para transmissão.
Em cada um desses dois eventos, podemos verificar o status e, se tudo estiver em ordem, começaremos a transmitir.
Fundição
Agora que temos o que lançar e onde lançar, podemos avançar para a coisa mais importante. Entre outras coisas, o CastSession possui um objeto RemoteMediaClient responsável pelo estado de reprodução do conteúdo da mídia. Vamos trabalhar com ele.
Vamos criar o MediaMetadata , onde as informações sobre o autor, o álbum etc. serão armazenadas.É muito semelhante ao que transferimos para o MediaSession quando iniciamos a reprodução local.
val mediaMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MUSIC_TRACK ).apply { putString(MediaMetadata.KEY_TITLE, “In C”) putString(MediaMetadata.KEY_ARTIST, “Terry Riley”) mediaContent?.metadata?.posterUrl?.let { poster -> addImage(WebImage(Uri.parse(“https:
O MediaMetadata possui muitos parâmetros e é melhor vê-los na documentação. Fiquei agradavelmente surpreso que você pode adicionar uma imagem não via bitmap, mas simplesmente por um link dentro da WebImage.
O objeto MediaInfo carrega informações sobre os metadados do conteúdo e fala sobre de onde vem o conteúdo da mídia, que tipo é, como reproduzi-lo:
val mediaInfo = MediaInfo.Builder(“https:
Deixe-me lembrá-lo de que contentType é um tipo de conteúdo de acordo com a especificação MIME .
Você também pode transferir inserções de publicidade para o MediaInfo:
- setAdBreakClips - aceita uma lista de anúncios do AdBreakClipInfo com links para conteúdo, título, tempo e tempo em que o anúncio é ignorado.
- setAdBreaks - informações sobre o layout e o tempo das inserções de publicidade.
Em MediaLoadOptions, descrevemos como processaremos o fluxo de mídia (velocidade, posição inicial). A documentação também diz que através de setCredentials você pode passar o cabeçalho da solicitação de autorização, mas minhas solicitações do Chromecast não incluíram os campos de autorização solicitados.
val mediaLoadOptions = MediaLoadOptions.Builder() .setPlayPosition(position!!) .setAutoplay(true) .setPlaybackRate(playbackSpeed) .setCredentials(context.getString(R.string.bearer_token, authGateway.authState.accessToken!!)) .setCredentialsType(context.getString(R.string.authorization_header_key)) .build()
Quando tudo estiver pronto, podemos fornecer todos os dados ao RemoteMediaClient e o Chromecast começará a ser reproduzido. É importante pausar a reprodução local.
val remoteMediaClient = currentSession!!.remoteMediaClient remoteMediaClient.load(mediaInfo, mediaLoadOptions)
Manipulação de eventos
O vídeo começou a tocar, e então o que? E se o usuário pausar a TV? Para saber mais sobre os eventos do lado do Chromecast, o RemoteMediaClient possui retornos de chamada:
private val castStatusCallback = object : RemoteMediaClient.Callback() { override fun onStatusUpdated() {
Conhecer o progresso atual também é simples:
val periodMills = 1000L remoteMediaClient.addProgressListener( RemoteMediaClient.ProgressListener { progressMills, durationMills ->
Experiência na integração com um player existente
O aplicativo em que eu estava trabalhando já tinha um media player pronto. O objetivo era integrar o suporte ao Chromecast. O media player foi baseado na State Machine, e o primeiro pensamento foi adicionar um novo estado: "CastingState". Mas essa ideia foi imediatamente rejeitada, porque cada estado do player reflete o estado da reprodução e não importa o que serve como implementação do ExoPlayer ou ChromeCast.
Então surgiu a ideia de criar um certo sistema de delegados com priorização e processamento do "ciclo de vida" do jogador. Todos os delegados podem receber eventos de status do jogador: Reproduzir, Pausar etc. - mas apenas o delegado principal reproduzirá o conteúdo da mídia.

Temos algo parecido com esta interface do player:
interface Player { val isPlaying: Boolean val isReleased: Boolean val duration: Long var positionInMillis: Long var speed: Float var volume: Float var loop: Boolean fun addListener(listener: PlayerCallback) fun removeListener(listener: PlayerCallback): Boolean fun getListeners(): MutableSet<PlayerCallback> fun prepare(mediaContent: MediaContent) fun play() fun pause() fun release() interface PlayerCallback { fun onPlaying(currentPosition: Long) fun onPaused(currentPosition: Long) fun onPreparing() fun onPrepared() fun onLoadingChanged(isLoading: Boolean) fun onDurationChanged(duration: Long) fun onSetSpeed(speed: Float) fun onSeekTo(fromTimeInMillis: Long, toTimeInMillis: Long) fun onWaitingForNetwork() fun onError(error: String?) fun onReleased() fun onPlayerProgress(currentPosition: Long) } }
No interior, haverá uma Máquina de Estado com tantos estados:
- Vazio - estado inicial antes da inicialização.
- Preparando - o player inicia a reprodução do conteúdo da mídia.
- Preparado - A mídia está carregada e pronta para reprodução.
- Tocando
- Pausado
- Esperando pela rede
- Erro

Anteriormente, cada estado durante a inicialização emitia um comando no ExoPlayer. Agora ele emitirá o comando para a lista de delegados Tocando, e o delegado "Líder" poderá processá-lo. Como o delegado implementa todas as funções do jogador, ele também pode ser herdado da interface do jogador e usado separadamente, se necessário. Em seguida, o delegado abstrato ficará assim:
abstract class PlayingDelegate( protected val playerCallback: Player.PlayerCallback, var isLeading: Boolean = false ) : Player { fun setIsLeading(isLeading: Boolean, positionMills: Long, isPlaying: Boolean) { this.isLeading = isLeading if (isLeading) { onLeading(positionMills, isPlaying) } else { onDormant() } } final override fun addListener(listener: Player.PlayerCallback) {
Por exemplo, simplifiquei as interfaces. Na realidade, há um pouco mais de eventos.
Pode haver tantos delegados quanto fontes de reprodução. Um delegado do Chromecast pode ser algo assim:
ChromeCastDelegate.kt class ChromeCastDelegate( private val context: Context, private val castCallback: ChromeCastListener, playerCallback: Player.PlayerCallback ) : PlayingDelegate(playerCallback) { companion object { private const val CONTENT_TYPE_VIDEO = "videos/mp4" private const val CONTENT_TYPE_AUDIO = "audio/mp3" private const val PROGRESS_DELAY_MILLS = 500L } interface ChromeCastListener { fun onCastStarted() fun onCastStopped() } private var sessionManager: SessionManager? = null private var currentSession: CastSession? = null private var mediaContent: MediaContent? = null private var currentPosition: Long = 0 private val mediaSessionListener = object : SessionManagerListener<CastSession> { override fun onSessionStarted(session: CastSession, sessionId: String) { currentSession = session castCallback.onCastStarted() } override fun onSessionEnding(session: CastSession) { currentPosition = session.remoteMediaClient?.approximateStreamPosition ?: currentPosition stopCasting() } override fun onSessionResumed(session: CastSession, wasSuspended: Boolean) { currentSession = session castCallback.onCastStarted() } override fun onSessionStartFailed(session: CastSession, p1: Int) { stopCasting() } override fun onSessionEnded(session: CastSession, p1: Int) {
Antes de dar um comando sobre reprodução, precisamos decidir sobre o delegado principal. Para fazer isso, eles são adicionados em ordem de prioridade ao player e cada um deles pode dar seu estado de prontidão no método readyForLeading (). O código de exemplo completo pode ser visto no GitHub .
Existe vida após o ChromeCast

Depois de integrar o suporte ao Chromecast no aplicativo, fiquei mais satisfeito ao voltar para casa e desfrutar de livros de áudio não apenas pelos fones de ouvido, mas também pelo Google Home. Quanto à arquitetura, a implementação de players em diferentes aplicativos pode variar, portanto essa abordagem não será apropriada em todos os lugares. Mas, para a nossa arquitetura, surgiu. Espero que este artigo tenha sido útil e, em um futuro próximo, haverá mais aplicativos que podem se integrar ao ambiente digital!