Cómo administrar la interacción del usuario con la TV

En la experiencia de TV en vivo, el usuario cambia de canal y se le presenta brevemente la información del canal y del programa antes de que desaparezca. Otros tipos de información, como mensajes ("NO INTENTAS EN CASA"), subtítulos o anuncios que deban permanecer. Como con cualquier TV Esta información no debe interferir con el contenido del programa que se reproduce en la pantalla.

Figura 1: Un mensaje de superposición en una app de TV en vivo

También considera si se debe presentar cierto contenido del programa, dada la la configuración de los controles parentales y de clasificación del contenido, y cómo se comporta tu app e informa al usuario cuando contenido bloqueado o no disponible. En esta lección, se describe cómo desarrollar el ancho de banda de entrada de TV experiencia para estas consideraciones.

Prueba la App de ejemplo de TV Input Service

Cómo integrar el jugador con la plataforma

La entrada de TV debe renderizar el video en un objeto Surface, que se pasa por TvInputService.Session.onSetSurface() . Este es un ejemplo de cómo usar una instancia de MediaPlayer para reproducir contenido 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í te mostramos cómo hacerlo utilizando 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, el la superposición está inhabilitada. Puedes habilitarlo cuando creas 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. La TV del sistema los controles parentales de la app determinan el contenido que se muestra según la clasificación del contenido. En las siguientes secciones, se describe cómo administrar la selección de canales y programas con el TvInputService.Session de métodos notify que comunicarse con la app de TV del sistema.

Cómo poner un video como no disponible

Cuando el usuario cambia el canal, debes asegurarte de que no se muestre ningún desvío en la pantalla. de video antes de que la entrada de TV renderice el contenido. Cuando llamas a TvInputService.Session.onTune(), Puedes evitar que el video se presente llamando a TvInputService.Session.notifyVideoUnavailable(). y pasa 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, debes llamar 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 parpadee intermitentemente.

Consulta también Cómo integrar el jugador con la plataforma para obtener más información sobre cómo trabajar con Surface para renderizar el video.

Cómo proporcionar controles parentales

Para determinar si un contenido determinado está bloqueado por los controles parentales y la clasificación del contenido, consulta la Métodos de clase TvInputManager, isParentalControlsEnabled() y isRatingBlocked(android.media.tv.TvContentRating). Tú asegúrate de que el TvContentRating del contenido se incluya en un conjunto de clasificaciones de contenido 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 TV del sistema de la app llamando al Método notifyContentAllowed() de TvInputService.Session o notifyContentBlocked() , como se muestra en el ejemplo anterior.

Usa la clase TvContentRating para generar la cadena definida por el sistema para el COLUMN_CONTENT_RATING con el TvContentRating.createRating() como se muestra aquí:

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 los segmentos multimedia, como como el tipo de pista (video, audio o subtítulo), etcétera.

La primera vez que tu sesión de entrada de TV pueda obtener información de la pista, debería llamar a TvInputService.Session.notifyTracksChanged() por una lista de todas las pistas para actualizar la app de TV del sistema. Cuándo es un cambio en la información de la pista, llamar notifyTracksChanged() de nuevo 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. está disponible para un tipo de segmento determinado; por ejemplo, subtítulos en diferentes idiomas. Tu TV de entrada responde a la onSelectTrack() desde la app de TV del sistema llamando notifyTrackSelected() , como se muestra en el siguiente ejemplo. Ten en cuenta que cuando null se pasa 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;
}