Gestire l'interazione degli utenti TV

Nell'esperienza di TV in diretta, l'utente cambia canale e le informazioni sul canale e sul programma vengono visualizzate brevemente prima di scomparire. Altri tipi di informazioni, come messaggi ("NON TENTARE A CASA"), sottotitoli o annunci, potrebbero dover rimanere visibili. Come per qualsiasi app TV, queste informazioni non devono interferire con i contenuti del programma riprodotti sullo schermo.

Figura 1. Un messaggio di overlay in un'app TV in diretta.

Valuta anche se determinati contenuti del programma devono essere presentati, in base alla classificazione dei contenuti e alle impostazioni del Controllo genitori, e come si comporta la tua app e informa l'utente quando i contenuti sono bloccati o non disponibili. Questa lezione descrive come sviluppare l'esperienza utente dell'input TV per questi aspetti.

Prova l'app di esempio del servizio di input TV .

Integrare il player con la superficie

L'input TV deve eseguire il rendering del video su un oggetto Surface, che viene passato da il TvInputService.Session.onSetSurface() metodo. Ecco un esempio di come utilizzare un'istanza MediaPlayer per riprodurre contenuti nell'oggetto 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;
}

Allo stesso modo, ecco come farlo utilizzando 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;
}

Utilizzare un overlay

Utilizza un overlay per visualizzare sottotitoli, messaggi, annunci o trasmissioni di dati MHEG-5. Per impostazione predefinita, l'overlay è disattivato. Puoi attivarlo quando crei la sessione chiamando TvInputService.Session.setOverlayViewEnabled(true), come nel seguente esempio:

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

Utilizza un oggetto View per l'overlay, restituito da TvInputService.Session.onCreateOverlayView(), come mostrato di seguito:

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 definizione del layout per l'overlay potrebbe essere simile alla seguente:

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

Controllare i contenuti

Quando l'utente seleziona un canale, l'input TV gestisce il callback onTune() nell'oggetto TvInputService.Session. I controlli genitori dell'app TV di sistema determinano quali contenuti vengono visualizzati, in base alla classificazione dei contenuti. Le sezioni seguenti descrivono come gestire la selezione di canali e programmi utilizzando i metodi notify di TvInputService.Session che comunicano con l'app TV di sistema.

Rendere il video non disponibile

Quando l'utente cambia canale, devi assicurarti che sullo schermo non vengano visualizzati artefatti video prima che l'input TV esegua il rendering dei contenuti. Quando chiami TvInputService.Session.onTune(), puoi impedire la presentazione del video chiamando TvInputService.Session.notifyVideoUnavailable() e passando la costante VIDEO_UNAVAILABLE_REASON_TUNING, come mostrato nel seguente esempio.

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

Poi, quando i contenuti vengono sottoposti a rendering su Surface, chiama TvInputService.Session.notifyVideoAvailable() per consentire la visualizzazione del video, come segue:

Kotlin

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

Java

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

Questa transizione dura solo frazioni di secondo, ma la presentazione di una schermata vuota è visivamente migliore rispetto alla visualizzazione di strani artefatti e tremolii.

Per ulteriori informazioni sull'utilizzo di Surface per il rendering dei video, consulta anche Integrare il player con la superficie.

Fornire il Controllo genitori

Per determinare se un determinato contenuto è bloccato dal Controllo genitori e dalla classificazione dei contenuti, controlla i metodi della classe TvInputManager, isParentalControlsEnabled() e isRatingBlocked(android.media.tv.TvContentRating). Potresti anche voler assicurarti che TvContentRating dei contenuti sia incluso in un insieme di classificazioni dei contenuti attualmente consentite. Questi aspetti sono mostrati nel seguente esempio.

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

Dopo aver stabilito se i contenuti devono essere bloccati o meno, notifica l'app TV di sistema chiamando il metodo TvInputService.Session notifyContentAllowed() o notifyContentBlocked() , come mostrato nell'esempio precedente.

Utilizza la classe TvContentRating per generare la stringa definita dal sistema per il COLUMN_CONTENT_RATING con il TvContentRating.createRating() metodo, come mostrato di seguito:

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

Gestire la selezione delle tracce

La classe TvTrackInfo contiene informazioni sulle tracce multimediali, come il tipo di traccia (video, audio o sottotitoli) e così via.

La prima volta che la sessione di input TV è in grado di ottenere informazioni sulle tracce, deve chiamare TvInputService.Session.notifyTracksChanged() con un elenco di tutte le tracce per aggiornare l'app TV di sistema. Quando le informazioni sulle tracce cambiano, chiama notifyTracksChanged() di nuovo per aggiornare il sistema.

L'app TV di sistema fornisce un'interfaccia per consentire all'utente di selezionare una traccia specifica se sono disponibili più tracce per un determinato tipo di traccia, ad esempio sottotitoli in lingue diverse. L'input TV risponde alla chiamata onSelectTrack() dall'app TV di sistema chiamando notifyTrackSelected() , come mostrato nel seguente esempio. Tieni presente che quando viene passato null come ID traccia, la traccia viene deselezionata.

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