Na experiência de TV ao vivo, o usuário muda de canal e recebe informações do canal e do programa rapidamente antes que elas desapareçam. Outros tipos de informações, como mensagens ("NÃO TENTE EM CASA"), legendas ou anúncios, por exemplo. Como em qualquer TV aplicativo, essas informações não devem interferir com o conteúdo do programa exibido na tela.
Considere também se determinado conteúdo do programa deve ser apresentado, dadas as classificação do conteúdo e configurações de controle da família, além de como seu app se comporta e informa o usuário quando está bloqueado ou indisponível. Esta lição descreve como desenvolver um conjunto de dados de usuário experiência do usuário para essas considerações.
Experimente o App de exemplo do 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
TvInputService.Session.onSetSurface()
. Confira um exemplo de como usar uma instância do MediaPlayer
para abrir
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 ExoPlayer (em inglês):
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, o
está desativada. É possível 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
. A TV do sistema
o controle da família do app determina qual conteúdo é exibido de acordo com a classificação dele.
As seções a seguir descrevem como gerenciar a seleção de canais e programas usando o
TvInputService.Session
métodos notify
que
se comunicar com o app de TV do sistema.
Tornar o vídeo indisponível
Quando o usuário muda de canal, você precisa ter certeza de que a tela não exibe nada fora do comum.
artefatos de vídeo antes que a entrada de TV renderize o conteúdo. Quando você chama TvInputService.Session.onTune()
,
você pode impedir que o vídeo seja apresentado chamando TvInputService.Session.notifyVideoUnavailable()
e passando a constante VIDEO_UNAVAILABLE_REASON_TUNING
, conforme
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 a exibição do vídeo, da seguinte 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 mostre falhas e tremores estranhos.
Consulte também Integrar o player com a superfície para mais informações sobre como trabalhar
com Surface
para renderizar vídeos.
Disponibilizar "controle dos pais"
Para determinar se um conteúdo específico está bloqueado pelo controle dos pais e pela classificação do conteúdo, verifique o
Métodos da classe TvInputManager
, isParentalControlsEnabled()
e isRatingBlocked(android.media.tv.TvContentRating)
. Você
verifique se o TvContentRating
do conteúdo está incluído em um
o 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 deve ou não ser bloqueado, notifique a TV do sistema.
aplicativo chamando
Método notifyContentAllowed()
do TvInputService.Session
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
TvContentRating.createRating()
, como 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
como o tipo de faixa (vídeo, áudio ou legenda) e assim por diante.
Na primeira vez que sua sessão de entrada de TV conseguir informações de faixa, ela deverá chamar
TvInputService.Session.notifyTracksChanged()
com uma lista de todas as faixas para atualizar o app de TV do sistema. Quando
é 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 caso haja mais de uma
está disponível para um determinado tipo de faixa. como legendas em idiomas diferentes. Sua TV
entrada responde à solicitação
onSelectTrack()
do app de TV do sistema chamando
notifyTrackSelected()
, como mostrado no exemplo a seguir. Quando null
é transmitido como o ID da faixa, esta 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; }