Na experiência de TV ao vivo, o usuário muda de canal e recebe informações sobre o canal e o programa brevemente 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 mantidos. Como acontece com qualquer app de TV, essas informações não podem interferir no conteúdo do programa exibido na tela.
Considere também se determinado conteúdo do programa precisa ser apresentado, de acordo com as configurações de classificação e controle da família do conteúdo, além de como o app se comporta e informa ao usuário quando o conteúdo está bloqueado ou indisponível. Esta lição descreve como desenvolver a experiência do usuário da sua entrada de TV com base nessas considerações.
Teste o app de exemplo Serviço de entrada de TV (link em inglês).
Integrar o player com a plataforma
Sua entrada de TV precisa renderizar o vídeo em um objeto Surface
, que é transmitido pelo
método
TvInputService.Session.onSetSurface()
. Confira um exemplo de como usar uma instância do 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. Por padrão, a sobreposição está desativada. É possível ativá-lo 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 da família do app de TV
do sistema determina qual conteúdo é exibido, de acordo com a classificação do conteúdo.
As seções abaixo descrevem como gerenciar a seleção de canais e programas usando os
métodos TvInputService.Session
notify
que
se comunicam com o app de TV do sistema.
Tornar o vídeo indisponível
Quando o usuário muda de canal, é importante garantir que a tela não mostre artefatos de vídeo
desaparecidos antes que a 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
, como
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 apenas frações de segundo, mas apresentar uma tela em branco é visualmente melhor do que permitir que a imagem pisque em mensagens estranhas e instabilidades.
Consulte também Integrar o player com a superfície para ver 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 da família 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()
,
como 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 a seguir:
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 as faixas, ela precisará 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 o usuário selecionar uma faixa específica se mais de uma
estiver disponível para um determinado tipo. Por exemplo, legendas em diferentes idiomas. A entrada
de TV responde à chamada
onSelectTrack()
do app de TV do sistema chamando
notifyTrackSelected()
,
conforme mostrado no exemplo abaixo. Quando null
é transmitido como o 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; }