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

Dans l'expérience de la télévision en direct, l'utilisateur change de chaîne et des informations sur la chaîne et le programme s'affichent brièvement avant de disparaître. D'autres types d'informations, tels que des messages ("NE PAS TENTER CHEZ VOUS"), des sous-titres ou des annonces, peuvent devoir persister. Comme pour toute application TV, ces informations ne doivent pas interférer avec le contenu du programme diffusé à l'écran.

Figure 1 : Message de superposition dans une application TV en direct.

Déterminez également si certains contenus de programme doivent être présentés, en fonction de leur classification et des paramètres de contrôle parental, et comment votre application se comporte et informe 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 tenant compte de ces éléments.

Essayez l'application exemple de service d'entrée TV.

Intégrer le lecteur à la surface

Votre entrée TV doit afficher la vidéo sur un objet Surface, transmis par la TvInputService.Session.onSetSurface() méthode. 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 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é ici :

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 à afficher, 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 notify de TvInputService.Session qui communiquent avec l'application TV du 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 avant que votre entrée TV ne rende 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 rendu sur la Surface, vous appelez TvInputService.Session.notifyVideoAvailable() pour autoriser l'affichage de la vidéo, comme suit :

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 l'affichage d'un écran vide est visuellement préférable à celui d'une image qui clignote et tremble.

Pour en savoir plus sur l'utilisation de Surface pour travailler avec, consultez Intégrer le lecteur à la surface.

Fournir un contrôle parental

Pour déterminer si un contenu donné est bloqué par le contrôle parental et la classification du contenu, vous devez vérifier les méthodes de la classe TvInputManager, isParentalControlsEnabled() et isRatingBlocked(android.media.tv.TvContentRating). Vous pouvez également vous assurer que le TvContentRating du contenu est inclus dans un ensemble de classifications de contenu actuellement autorisées. Ces éléments sont présentés 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 être bloqué ou non, 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 pour générer la chaîne définie par le système pour le COLUMN_CONTENT_RATING avec la TvContentRating.createRating() méthode, 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 des pistes

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

La première fois que votre session d'entrée TV peut obtenir des informations sur les pistes, elle doit appeler TvInputService.Session.notifyTracksChanged() avec une liste de toutes les pistes pour mettre à jour l'application TV du système. En cas de modification des informations sur les pistes, appelez notifyTracksChanged() à nouveau 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 piste, cela désélectionne la piste.

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