Gérer les interactions des utilisateurs avec le téléviseur

Dans l'expérience de télévision en direct, l'utilisateur change de chaîne et voit s'afficher brièvement les informations sur la chaîne et le programme avant qu'elles ne disparaissent. D'autres types d'informations, par exemple des messages ("NE PAS ATTENDRE À LA MAISON"), des sous-titres ou des publicités doivent rester affichés. Comme pour tous les téléviseurs application, ces informations ne doivent pas interférer avec le contenu du programme qui s'affiche à l'écran.

Figure 1 : Un message en superposition dans une application de télévision en direct

Demandez également si certains contenus de programme doivent être présentés, compte tenu de la la classification du contenu et les paramètres de contrôle parental, le comportement de votre application et le contenu est bloqué ou indisponible. Cette leçon explique comment définir les paramètres utilisateur de l'entrée TV pour ces considérations.

Essayez le Exemple d'application TV Input Service.

Intégrer le lecteur à la surface

Votre entrée TV doit effectuer le rendu de la vidéo dans un objet Surface, qui est transmis par le TvInputService.Session.onSetSurface() . Voici un exemple d'utilisation d'une instance MediaPlayer pour la lecture dans l'objet 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 même, voici comment procéder avec 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;
}

Utiliser une superposition

Utilisez une superposition pour afficher des sous-titres, des messages, des annonces ou des diffusions de données MHEG-5. Par défaut, la superposition est désactivée. Vous pouvez l'activer lorsque vous créez la session en appelant TvInputService.Session.setOverlayViewEnabled(true), comme dans l'exemple suivant:

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;
}

Utilisez un objet View pour la superposition, renvoyé par TvInputService.Session.onCreateOverlayView(), comme indiqué ci-dessous:

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;
}

Voici à quoi peut ressembler la mise en page de la superposition:

<?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>

Contrôler le contenu

Lorsque l'utilisateur sélectionne une chaîne, votre entrée TV gère le rappel onTune() dans l'objet TvInputService.Session. Le téléviseur système le contrôle parental de l'application détermine le contenu qui s'affiche, en fonction de sa classification. Les sections suivantes décrivent comment gérer la sélection des chaînes et des programmes à l'aide des TvInputService.Session méthode notify qui communiquer avec l'application System TV.

Rendre la vidéo indisponible

Lorsque l'utilisateur change de chaîne, vous devez vous assurer que l'écran n'affiche aucun des artefacts vidéo avant que l'entrée TV ne restitue le contenu. Lorsque vous appelez TvInputService.Session.onTune(), vous pouvez empêcher la présentation de la vidéo en appelant TvInputService.Session.notifyVideoUnavailable() et en transmettant la constante VIDEO_UNAVAILABLE_REASON_TUNING, comme illustré dans l'exemple suivant.

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;
}

Ensuite, lorsque le contenu est affiché dans Surface, vous appelez TvInputService.Session.notifyVideoAvailable() pour autoriser l'affichage de la vidéo:

Kotlin

fun onRenderedFirstFrame(surface:Surface) {
    firstFrameDrawn = true
    notifyVideoAvailable()
}

Java

@Override
public void onRenderedFirstFrame(Surface surface) {
    firstFrameDrawn = true;
    notifyVideoAvailable();
}

Cette transition ne dure que des fractions de seconde, mais présenter un écran vide est visuellement plus efficace que de laisser la photo clignoter avec des saccades étranges.

Pour en savoir plus sur l'utilisation, consultez également Intégrer le lecteur avec la surface. avec Surface pour effectuer le rendu de la vidéo.

Fournir un contrôle parental

Pour déterminer si un contenu donné est bloqué par le contrôle parental et la classification du contenu, consultez le Méthodes de classe TvInputManager, isParentalControlsEnabled() et isRatingBlocked(android.media.tv.TvContentRating). Toi vous pouvez également vous assurer que le TvContentRating du contenu est inclus dans ensemble de classifications de contenu actuellement autorisées. Ces considérations sont présentées dans l'exemple suivant.

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);
}

Une fois que vous avez déterminé si le contenu doit ou non être bloqué, informez-en le téléviseur système en appelant la méthode Méthode notifyContentAllowed() de TvInputService.Session ou notifyContentBlocked() , comme illustré dans l'exemple précédent.

Utilisez la classe TvContentRating afin de générer la chaîne définie par le système pour le COLUMN_CONTENT_RATING par TvContentRating.createRating() , comme indiqué ici:

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");

Gérer la sélection de titres

La classe TvTrackInfo contient des informations sur les pistes multimédias telles que comme type de piste (vidéo, audio ou sous-titres), etc.

La première fois que votre session d'entrée TV est en mesure d'obtenir des informations sur un titre, elle doit appeler TvInputService.Session.notifyTracksChanged() avec la liste de tous les canaux pour mettre à jour l'appli TV système. Lorsqu'il est un changement dans les informations sur le titre, notifyTracksChanged() pour mettre à jour le système.

L'application System TV fournit une interface permettant à l'utilisateur de sélectionner une piste spécifique est disponible pour un type de piste donné ; comme des sous-titres dans différentes langues. Votre téléviseur répond à l'événement onSelectTrack() à partir de l'application system TV en appelant notifyTrackSelected() , comme illustré dans l'exemple suivant. Notez que lorsque null est transmis en tant qu'ID de piste, le titre est désélectionné.

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;
}