Gestire l'interazione degli utenti TV

Nell'esperienza TV in diretta, l'utente cambia canale e gli vengono presentate brevemente informazioni sul canale e sui programmi prima che queste scompaiano. Altri tipi di informazioni, come i messaggi ("NON TENTARE A CASA"), i sottotitoli o gli annunci potrebbero dover essere mantenuti. Come per qualsiasi app per TV, queste informazioni non devono interferire con la riproduzione dei contenuti del programma sullo schermo.

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

Considera inoltre se alcuni contenuti del programma devono essere presentati, in base alla classificazione dei contenuti e alle impostazioni del Controllo genitori, nonché al comportamento dell'app e al modo in cui informa l'utente quando i contenuti sono bloccati o non disponibili. In questa lezione viene descritto come sviluppare l'esperienza utente dell'input TV sulla base di queste considerazioni.

Prova l'app di esempio TV Input Service.

Integra player con Surface

L'ingresso della TV deve eseguire il rendering del video su un oggetto Surface, trasmesso dal metodo TvInputService.Session.onSetSurface(). 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;
}

Analogamente, ecco come fare 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 abilitarlo quando crei la sessione chiamando TvInputService.Session.setOverlayViewEnabled(true), come nell'esempio seguente:

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 qui:

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 avere il seguente aspetto:

<?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'ingresso TV gestisce il callback onTune() nell'oggetto TvInputService.Session. Il Controllo genitori dell'app per TV di sistema determina quali contenuti mostrare in base alla loro classificazione. Le seguenti sezioni descrivono come gestire la selezione di canali e programmi utilizzando i metodi TvInputService.Session notify che comunicano con l'app TV di sistema.

Rendi il video non disponibile

Quando l'utente cambia canale, vuoi assicurarti che lo schermo non mostri elementi video discontinui prima che l'input TV esegua il rendering dei contenuti. Quando chiami TvInputService.Session.onTune(), puoi impedire che il video venga visualizzato chiamando TvInputService.Session.notifyVideoUnavailable() e trasmettendo la costante VIDEO_UNAVAILABLE_REASON_TUNING, come mostrato nell'esempio seguente.

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

Quando i contenuti vengono visualizzati in Surface, chiami TvInputService.Session.notifyVideoAvailable() per consentire la visualizzazione del video, in questo modo:

Kotlin

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

Java

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

Questa transizione dura solo per frazioni di secondo, ma presentare uno schermo vuoto è visivamente meglio che consentire all'immagine di lampeggiare blip e tremolio dispari.

Consulta anche Integrazione del player con la piattaforma per ulteriori informazioni sull'utilizzo di Surface per il rendering del video.

Offri il Controllo genitori

Per stabilire se determinati contenuti sono bloccati dal Controllo genitori e dalla classificazione dei contenuti, controlla i TvInputManager metodi dei corsi, isParentalControlsEnabled() e isRatingBlocked(android.media.tv.TvContentRating). Ti consigliamo inoltre di assicurarti che TvContentRating dei contenuti sia incluso in un insieme di classificazioni dei contenuti attualmente consentite. Queste considerazioni sono mostrate 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 o non devono essere bloccati, invia una notifica all'app per 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 COLUMN_CONTENT_RATING con il metodo TvContentRating.createRating(), come mostrato qui:

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

Gestisci 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 con ingresso TV riesce a recuperare informazioni sulle tracce, dovrebbe chiamare TvInputService.Session.notifyTracksChanged() con un elenco di tutti i brani per aggiornare l'app TV di sistema. In caso di modifica delle informazioni sulle tracce, chiama di nuovo notifyTracksChanged() per aggiornare il sistema.

L'app di sistema per TV fornisce all'utente un'interfaccia per selezionare una traccia specifica se sono disponibili più tracce per un determinato tipo di traccia, ad esempio sottotitoli in diverse lingue. L'ingresso della TV risponde alla chiamata onSelectTrack() dall'app per TV di sistema chiamando notifyTrackSelected(), come mostrato nell'esempio che segue. Tieni presente che quando null viene trasmesso come ID canale, il canale deseleziona il canale.

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