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 reçoit brièvement des informations sur la chaîne et le programme avant qu'elles ne disparaissent. D'autres types d'informations, tels que les messages ("NE PAS TROUVER À LA MAISON"), les sous-titres ou les annonces peuvent avoir besoin de persister. Comme pour toute application TV, ces informations ne doivent pas interférer avec le contenu du programme diffusé à l'écran.

Figure 1 : Message en superposition dans une application de télévision en direct.

Déterminez également si certains contenus du programme doivent être présentés, compte tenu de leurs paramètres de classification et de contrôle parental, ainsi que du comportement de votre application et de l'information de l'utilisateur lorsque le contenu est bloqué ou indisponible. Cette leçon explique comment développer l'expérience utilisateur de votre entrée TV en fonction de ces considérations.

Essayez l'application exemple TV Input Service.

Intégrer le joueur à la surface

Votre entrée TV doit afficher la vidéo dans un objet Surface, qui est transmis par la méthode TvInputService.Session.onSetSurface(). Voici un exemple d'utilisation d'une instance MediaPlayer pour lire du contenu 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 lors de la création de 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;
}

La définition de la mise en page de la superposition peut se présenter comme suit:

<?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 contrôle parental de l'application TV du système 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 méthodes TvInputService.Session notify qui communiquent avec l'application TV système.

Rendre la vidéo indisponible

Lorsque l'utilisateur change de chaîne, vous devez vous assurer que l'écran n'affiche aucun artefact vidéo parasite avant que l'entrée TV n'affiche 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, appelez TvInputService.Session.notifyVideoAvailable() pour permettre l'affichage de la vidéo, comme ceci:

Kotlin

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

Java

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

Cette transition ne dure que quelques fractions de seconde, mais il est visuellement préférable de présenter un écran vide plutôt que de laisser l'image clignoter des gigues étranges.

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

Activer le contrôle parental

Pour déterminer si un contenu donné est bloqué par le contrôle parental et la classification du contenu, vérifiez les méthodes de classe TvInputManager, isParentalControlsEnabled() et isRatingBlocked(android.media.tv.TvContentRating). Vous pouvez également vous assurer que l'TvContentRating du contenu est inclus dans un ensemble de classifications de contenu actuellement autorisées. Ces considérations sont illustré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 l'application TV du système en appelant la méthode TvInputService.Session notifyContentAllowed() 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 COLUMN_CONTENT_RATING avec la méthode TvContentRating.createRating(), comme indiqué ci-dessous:

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 la piste

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

La première fois que votre session d'entrée TV parvient à obtenir des informations sur les titres, elle doit appeler TvInputService.Session.notifyTracksChanged() avec la liste de tous les titres pour mettre à jour l'application TV du système. En cas de modification des informations sur les titres, appelez à nouveau notifyTracksChanged() pour mettre à jour le système.

L'application TV du système fournit une interface permettant à l'utilisateur de sélectionner une piste spécifique si plusieurs pistes sont disponibles pour un type de piste donné (par exemple, des sous-titres dans différentes langues). Votre entrée TV répond à l'appel onSelectTrack() de l'application TV du système en appelant notifyTrackSelected(), comme illustré dans l'exemple suivant. Notez que lorsque null est transmis comme ID de canal, 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;
}