Gestire l'interazione degli utenti TV

Nell'esperienza con la TV in diretta, l'utente cambia canale e visualizza le informazioni sul canale e sul programma prima che le informazioni scompaiano. Altri tipi di informazioni, come messaggi ("NON TENTARE A CASA"), sottotitoli o annunci potrebbero dover persistere. Come con qualsiasi TV tali informazioni non devono interferire con il contenuto del programma riprodotto sullo schermo.

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

Valuta anche se devono essere presentati alcuni contenuti del programma, dato il la classificazione dei contenuti e le impostazioni del Controllo genitori, nonché il comportamento dell'app e informazioni all'utente quando se i contenuti sono bloccati o non disponibili. Questa lezione descrive come sviluppare l'input TV dell'utente per queste considerazioni.

Prova App di esempio del servizio di input TV.

Integra il player con la superficie

L'ingresso della TV deve eseguire il rendering del video su un oggetto Surface, che viene passato da 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 dell'overlay potrebbe avere un aspetto simile al 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>

Controlla i contenuti

Quando l'utente seleziona un canale, l'ingresso TV gestisce il callback onTune() nell'oggetto TvInputService.Session. La TV di sistema il Controllo genitori dell'app determina quali contenuti mostrare in base alla classificazione dei contenuti. Le seguenti sezioni descrivono come gestire la selezione del canale e del programma utilizzando il TvInputService.Session metodo notify che comunicare con l'app di sistema per la TV.

Rendi il video non disponibile

Quando l'utente cambia canale, devi assicurarti che lo schermo non mostri artefatti video prima che l'ingresso TV esegua il rendering dei contenuti. Quando chiami TvInputService.Session.onTune(), puoi impedire che il video venga presentato chiamando il numero TvInputService.Session.notifyVideoUnavailable() e passando la costante VIDEO_UNAVAILABLE_REASON_TUNING, come come mostrato nell'esempio che segue.

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 frazioni di secondo, ma la presentazione di una schermata vuota visivamente migliore rispetto a consentire all'immagine di lampeggiare strani blip e tremolio.

Vedi anche Integrare il player con la piattaforma per maggiori informazioni sul funzionamento. con Surface per eseguire il rendering del video.

Fornire il Controllo genitori

Per stabilire se determinati contenuti sono bloccati dal Controllo genitori e dalla classificazione dei contenuti, controlla le TvInputManager metodo del corso, isParentalControlsEnabled() e isRatingBlocked(android.media.tv.TvContentRating). Tu potresti anche voler assicurarti che l'elemento TvContentRating dei contenuti sia incluso in un 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, informa la TV di sistema richiamando l'app 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 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");

Gestire la selezione delle tracce

Il corso TvTrackInfo contiene informazioni sulle tracce multimediali, ad esempio come tipo di traccia (video, audio o sottotitoli) e così via.

La prima volta che la sessione di input TV riesce a ottenere informazioni sui brani, dovrebbe chiamare TvInputService.Session.notifyTracksChanged() con un elenco di tutti i canali per aggiornare l'app di sistema TV. Quando ci sono è un cambiamento nelle informazioni sulla traccia, notifyTracksChanged() di nuovo per aggiornare il sistema.

L'app TV di sistema fornisce all'utente un'interfaccia per selezionare un canale specifico se è presente più di un canale la traccia sia disponibile per un determinato tipo di canale; ad esempio sottotitoli in lingue diverse. La tua TV l'input risponde onSelectTrack() chiama dall'app di sistema per la TV chiamando notifyTrackSelected() , come mostrato nell'esempio seguente. Tieni presente che quando null viene trasmesso come ID traccia, la traccia deseleziona.

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