En la experiencia de TV en vivo, el usuario cambia de canal y se le presenta brevemente la información del canal y el programa antes de que la información desaparezca. Es posible que deban persistir otros tipos de información, como mensajes ("NO INTENTES EN TU CASA"), subtítulos o anuncios. Al igual que con cualquier app para TV, esa información no debe interferir con el contenido del programa que se reproduce en la pantalla.
También considera si se debe presentar cierto contenido del programa, según la configuración de los controles parentales y la clasificación del contenido, y la forma en que tu app se comporta e informa al usuario cuando el contenido está bloqueado o no está disponible. En esta lección, se describe cómo desarrollar la experiencia del usuario de entrada de TV en función de estas consideraciones.
Prueba la app de ejemplo de TV Input Service.
Cómo integrar el jugador con la plataforma
Tu entrada de TV debe renderizar el video en un objeto Surface
, que se pasa por el método TvInputService.Session.onSetSurface()
. Este es un ejemplo de cómo usar una instancia de MediaPlayer
para reproducir contenido en el 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; }
De manera similar, aquí se muestra cómo hacerlo con 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; }
Cómo usar una superposición
Usa una superposición para mostrar subtítulos, mensajes, anuncios o emisiones de datos MHEG-5. De forma predeterminada, la superposición está inhabilitada. Puedes habilitarla cuando crees la sesión llamando a TvInputService.Session.setOverlayViewEnabled(true)
, como en el siguiente ejemplo:
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; }
Usa un objeto View
para la superposición que muestra TvInputService.Session.onCreateOverlayView()
, como se muestra a continuación:
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; }
La definición del diseño de la superposición será similar a la siguiente:
<?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>
Cómo controlar el contenido
Cuando el usuario selecciona un canal, tu entrada de TV controla la devolución de llamada de onTune()
en el objeto TvInputService.Session
. Los controles parentales de la app de TV del sistema determinan qué contenido se muestra, según su clasificación.
En las siguientes secciones, se describe cómo administrar la selección de canales y programas con los métodos notify
de TvInputService.Session
que se comunican con la app de TV del sistema.
Cómo poner un video como no disponible
Cuando el usuario cambie de canal, asegúrate de que la pantalla no muestre artefactos de video sueltos antes de que la entrada de TV procese el contenido. Cuando llamas a TvInputService.Session.onTune()
, puedes evitar que se presente el video llamando a TvInputService.Session.notifyVideoUnavailable()
y pasando la constante VIDEO_UNAVAILABLE_REASON_TUNING
, como se muestra en el siguiente ejemplo.
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; }
Luego, cuando el contenido se renderiza en Surface
, llama a TvInputService.Session.notifyVideoAvailable()
para permitir que se muestre el video, de la siguiente manera:
Kotlin
fun onRenderedFirstFrame(surface:Surface) { firstFrameDrawn = true notifyVideoAvailable() }
Java
@Override public void onRenderedFirstFrame(Surface surface) { firstFrameDrawn = true; notifyVideoAvailable(); }
Esta transición dura fracciones de segundo, pero presentar una pantalla en blanco es visualmente mejor que permitir que la imagen titile con inestabilidades y movimientos raros.
Consulta también Cómo integrar el reproductor con Surface para obtener más información sobre cómo trabajar con Surface
en el procesamiento de videos.
Cómo proporcionar controles parentales
Para determinar si un contenido determinado está bloqueado por controles parentales y clasificación del contenido, verifica los métodos de clase TvInputManager
, isParentalControlsEnabled()
y isRatingBlocked(android.media.tv.TvContentRating)
. Además, asegúrate de que el TvContentRating
del contenido esté incluido en un conjunto de clasificaciones permitidas actualmente. Estas consideraciones se muestran en el siguiente ejemplo.
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); }
Una vez que hayas determinado si el contenido se debe o no bloquear, notifica a la app de TV del sistema llamando al método TvInputService.Session
notifyContentAllowed()
o notifyContentBlocked()
, como se muestra en el ejemplo anterior.
Usa la clase TvContentRating
a fin de generar la string definida por el sistema para COLUMN_CONTENT_RATING
con el método TvContentRating.createRating()
, como se muestra a continuación:
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");
Cómo controlar la selección de pistas
La clase TvTrackInfo
contiene información sobre las pistas multimedia, como el tipo de pista (video, audio o subtítulo), etcétera.
La primera vez que la sesión de entrada de TV pueda obtener información de la pista, debería llamar a TvInputService.Session.notifyTracksChanged()
con una lista de todas las pistas para actualizar la app de TV del sistema. Cuando haya un cambio en la información de la pista, vuelve a llamar a notifyTracksChanged()
para actualizar el sistema.
La app de TV del sistema proporciona una interfaz para que el usuario seleccione una pista específica si hay más de una disponible disponible para un tipo de pista determinado (por ejemplo, subtítulos en diferentes idiomas). Tu entrada de TV responde a la llamada a onSelectTrack()
desde la app de TV del sistema llamando a notifyTrackSelected()
, como se muestra en el siguiente ejemplo. Ten en cuenta que, cuando se pasa null
como el ID de pista, se anula la selección de la pista.
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; }