Na experiência de TV ao vivo, o usuário muda de canal e recebe informações sobre canais e programas pouco antes que elas desapareçam. Outros tipos de informações, como mensagens ("NÃO TENTE ISSO EM CASA"), legendas ou anúncios, podem precisar ser persistidas. Como para qualquer app de TV, essas informações não podem interferir no conteúdo da programação na tela.

Figura 1. Mensagem sobreposta em um app de TV ao vivo.
Analise também se determinados conteúdos de programação devem ser apresentados de acordo com a classificação do conteúdo e com as configurações de controle dos pais. Essa decisão também depende de como seu app se comporta e informa o usuário quando um conteúdo está bloqueado ou indisponível. Esta lição descreve como desenvolver a experiência do usuário da entrada de TV com essas considerações em mente.
Teste o app de amostra Serviço de entrada de TV (link em inglês).
Integrar o player com a superfície
Sua entrada de TV precisa renderizar o vídeo em um objeto Surface
, que é transmitido pelo método TvInputService.Session.onSetSurface()
. Veja um exemplo de como usar uma instância MediaPlayer
para reproduzir conteúdo no objeto Surface
:
Kotlin
override fun onSetSurface(surface: Surface?): Boolean { player?.setSurface(surface) mSurface = surface return true } override fun onSetStreamVolume(volume: Float) { player?.setVolume(volume, volume) mVolume = volume }
Java
@Override public boolean onSetSurface(Surface surface) { if (player != null) { player.setSurface(surface); } mSurface = surface; return true; } @Override public void onSetStreamVolume(float volume) { if (player != null) { player.setVolume(volume, volume); } mVolume = volume; }
Da mesma forma, veja como fazer isso usando o ExoPlayer:
Kotlin
override fun onSetSurface(surface: Surface?): Boolean { player?.createMessage(videoRenderer)?.apply { type = MSG_SET_SURFACE payload = surface send() } mSurface = surface return true } override fun onSetStreamVolume(volume: Float) { player?.createMessage(audioRenderer)?.apply { type = MSG_SET_VOLUME payload = volume send() } mVolume = volume }
Java
@Override public boolean onSetSurface(@Nullable Surface surface) { if (player != null) { player.createMessage(videoRenderer) .setType(MSG_SET_SURFACE) .setPayload(surface) .send(); } mSurface = surface; return true; } @Override public void onSetStreamVolume(float volume) { if (player != null) { player.createMessage(videoRenderer) .setType(MSG_SET_VOLUME) .setPayload(volume) .send(); } mVolume = volume; }
Usar uma sobreposição
Use uma sobreposição para exibir legendas, mensagens, anúncios ou transmissões de dados MHEG-5. A sobreposição é desativada por padrão. Você pode ativá-la ao criar a sessão chamando TvInputService.Session.setOverlayViewEnabled(true)
, como no exemplo a seguir:
Kotlin
override fun onCreateSession(inputId: String): Session = onCreateSessionInternal(inputId).apply { setOverlayViewEnabled(true) sessions.add(this) }
Java
@Override public final Session onCreateSession(String inputId) { BaseTvInputSessionImpl session = onCreateSessionInternal(inputId); session.setOverlayViewEnabled(true); sessions.add(session); return session; }
Use um objeto View
para a sobreposição, retornado de TvInputService.Session.onCreateOverlayView()
, como mostrado aqui:
Kotlin
override fun onCreateOverlayView(): View = (context.getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater).run { inflate(R.layout.overlayview, null).apply { subtitleView = findViewById<SubtitleView>(R.id.subtitles).apply { // Configure the subtitle view. val captionStyle: CaptionStyleCompat = CaptionStyleCompat.createFromCaptionStyle(captioningManager.userStyle) setStyle(captionStyle) setFractionalTextSize(captioningManager.fontScale) } } }
Java
@Override public View onCreateOverlayView() { LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); View view = inflater.inflate(R.layout.overlayview, null); subtitleView = (SubtitleView) view.findViewById(R.id.subtitles); // Configure the subtitle view. CaptionStyleCompat captionStyle; captionStyle = CaptionStyleCompat.createFromCaptionStyle( captioningManager.getUserStyle()); subtitleView.setStyle(captionStyle); subtitleView.setFractionalTextSize(captioningManager.fontScale); return view; }
A definição de layout para a sobreposição pode ser semelhante a esta:
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> <com.google.android.exoplayer.text.SubtitleView android:id="@+id/subtitles" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|center_horizontal" android:layout_marginLeft="16dp" android:layout_marginRight="16dp" android:layout_marginBottom="32dp" android:visibility="invisible"/> </FrameLayout>
Controlar o conteúdo
Quando o usuário seleciona um canal, a entrada da TV manipula o callback onTune()
no objeto TvInputService.Session
. O controle dos pais do app de TV do sistema determina o conteúdo a ser exibido de acordo com a classificação.
As seções a seguir descrevem como processar a seleção de canais e programas usando os métodos TvInputService.Session
notify
que se comunicam com o app da TV do sistema.
Tornar o vídeo indisponível
Quando o usuário muda de canal, é importante garantir que a tela não exiba nenhum artefato de vídeo perdido antes que sua entrada de TV renderize o conteúdo. Ao chamar TvInputService.Session.onTune()
, você pode impedir que o vídeo seja apresentado chamando TvInputService.Session.notifyVideoUnavailable()
e transmitindo a constante VIDEO_UNAVAILABLE_REASON_TUNING
, conforme mostrado no exemplo a seguir.
Kotlin
override fun onTune(channelUri: Uri): Boolean { subtitleView?.visibility = View.INVISIBLE notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING) unblockedRatingSet.clear() dbHandler.apply { removeCallbacks(playCurrentProgramRunnable) playCurrentProgramRunnable = PlayCurrentProgramRunnable(channelUri) post(playCurrentProgramRunnable) } return true }
Java
@Override public boolean onTune(Uri channelUri) { if (subtitleView != null) { subtitleView.setVisibility(View.INVISIBLE); } notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING); unblockedRatingSet.clear(); dbHandler.removeCallbacks(playCurrentProgramRunnable); playCurrentProgramRunnable = new PlayCurrentProgramRunnable(channelUri); dbHandler.post(playCurrentProgramRunnable); return true; }
Em seguida, quando o conteúdo for renderizado para o Surface
, chame TvInputService.Session.notifyVideoAvailable()
para permitir que o vídeo seja exibido, desta forma:
Kotlin
fun onRenderedFirstFrame(surface:Surface) { firstFrameDrawn = true notifyVideoAvailable() }
Java
@Override public void onRenderedFirstFrame(Surface surface) { firstFrameDrawn = true; notifyVideoAvailable(); }
Essa transição dura frações de segundo, mas a apresentação de uma tela vazia é visualmente melhor do que permitir imagens com linhas estranhas e instáveis.
Consulte também Integrar o player à superfície para mais informações sobre como trabalhar com Surface
para renderizar vídeos.
Disponibilizar "controle dos pais"
Para determinar se um determinado conteúdo está bloqueado pelo controle dos pais e pela classificação do conteúdo, verifique os métodos da classe TvInputManager
, isParentalControlsEnabled()
e isRatingBlocked(android.media.tv.TvContentRating)
. Verifique também se o TvContentRating
do conteúdo está incluído em um conjunto de classificações de conteúdo permitidas no momento. Essas considerações são mostradas na amostra a seguir:
Kotlin
private fun checkContentBlockNeeded() { currentContentRating?.also { rating -> if (!tvInputManager.isParentalControlsEnabled || !tvInputManager.isRatingBlocked(rating) || unblockedRatingSet.contains(rating)) { // Content rating is changed so we don't need to block anymore. // Unblock content here explicitly to resume playback. unblockContent(null) return } } lastBlockedRating = currentContentRating player?.run { // Children restricted content might be blocked by TV app as well, // but TIF should do its best not to show any single frame of blocked content. releasePlayer() } notifyContentBlocked(currentContentRating) }
Java
private void checkContentBlockNeeded() { if (currentContentRating == null || !tvInputManager.isParentalControlsEnabled() || !tvInputManager.isRatingBlocked(currentContentRating) || unblockedRatingSet.contains(currentContentRating)) { // Content rating is changed so we don't need to block anymore. // Unblock content here explicitly to resume playback. unblockContent(null); return; } lastBlockedRating = currentContentRating; if (player != null) { // Children restricted content might be blocked by TV app as well, // but TIF should do its best not to show any single frame of blocked content. releasePlayer(); } notifyContentBlocked(currentContentRating); }
Depois de determinar se o conteúdo precisa ou não ser bloqueado, notifique o app de TV do sistema chamando o método TvInputService.Session
notifyContentAllowed()
ou notifyContentBlocked()
, conforme mostrado no exemplo anterior.
Use a classe TvContentRating
para gerar a string definida pelo sistema para o COLUMN_CONTENT_RATING
com o método TvContentRating.createRating()
, conforme mostrado aqui:
Kotlin
val rating = TvContentRating.createRating( "com.android.tv", "US_TV", "US_TV_PG", "US_TV_D", "US_TV_L" )
Java
TvContentRating rating = TvContentRating.createRating( "com.android.tv", "US_TV", "US_TV_PG", "US_TV_D", "US_TV_L");
Processar a seleção de faixas
A classe TvTrackInfo
contém informações sobre faixas de mídia, como o tipo de faixa (vídeo, áudio ou legenda) e assim por diante.
Na primeira vez que a sessão de entrada de TV conseguir informações sobre a faixa, chamará TvInputService.Session.notifyTracksChanged()
com uma lista de todas as faixas para atualizar o app de TV do sistema. Quando houver uma mudança nas informações da faixa, chame notifyTracksChanged()
novamente para atualizar o sistema.
O app de TV do sistema oferece uma interface para que o usuário selecione uma faixa específica quando houver mais de uma disponível para um determinado tipo de faixa. Por exemplo, legendas em vários idiomas. Sua entrada de TV responde à chamada onSelectTrack()
do app de TV do sistema chamando notifyTrackSelected()
, conforme mostrado no exemplo a seguir. Quando null
é transmitido como ID de faixa, essa ação desmarca a faixa.
Kotlin
override fun onSelectTrack(type: Int, trackId: String?): Boolean = mPlayer?.let { player -> if (type == TvTrackInfo.TYPE_SUBTITLE) { if (!captionEnabled && trackId != null) return false selectedSubtitleTrackId = trackId subtitleView.visibility = if (trackId == null) View.INVISIBLE else View.VISIBLE } player.trackInfo.indexOfFirst { it.trackType == type }.let { trackIndex -> if( trackIndex >= 0) { player.selectTrack(trackIndex) notifyTrackSelected(type, trackId) true } else false } } ?: false
Java
@Override public boolean onSelectTrack(int type, String trackId) { if (player != null) { if (type == TvTrackInfo.TYPE_SUBTITLE) { if (!captionEnabled && trackId != null) { return false; } selectedSubtitleTrackId = trackId; if (trackId == null) { subtitleView.setVisibility(View.INVISIBLE); } } int trackIndex = -1; MediaPlayer.TrackInfo[] trackInfos = player.getTrackInfo(); for (int index = 0; index < trackInfos.length; index++) { MediaPlayer.TrackInfo trackInfo = trackInfos[index]; if (trackInfo.getTrackType() == type) { trackIndex = index; break; } } if (trackIndex >= 0) { player.selectTrack(trackIndex); notifyTrackSelected(type, trackId); return true; } } return false; }