TV-Nutzerinteraktion verwalten

Beim Live-TV wechselt der Nutzer den Kanal und erhält kurz Kanal- und Programminformationen, bevor diese ausgeblendet werden. Andere Arten von Informationen wie Nachrichten („DO NOT ATTEMPT AT HOME“), Untertitel oder Anzeigen müssen möglicherweise beibehalten werden. Wie bei jeder TV-App dürfen diese Informationen die auf dem Bildschirm wiedergegebenen Programminhalte nicht beeinträchtigen.

Abbildung 1: Eine Overlay-Nachricht in einer Live-TV-App.

Überlegen Sie auch, ob bestimmte Programminhalte präsentiert werden sollen. Berücksichtigen Sie dabei die Altersfreigabe und Jugendschutzeinstellungen der Inhalte, das Verhalten Ihrer App und eine Benachrichtigung, wenn Inhalte blockiert oder nicht verfügbar sind. In dieser Lektion wird beschrieben, wie Sie anhand dieser Überlegungen die User Experience Ihrer TV-Eingabe entwickeln können.

Probieren Sie die Beispiel-App TV Input Service aus.

Player in Oberfläche einbinden

Ihre TV-Eingabe muss das Video in einem Surface-Objekt rendern, das von der Methode TvInputService.Session.onSetSurface() übergeben wird. Hier ein Beispiel für die Verwendung einer MediaPlayer-Instanz zum Abspielen von Inhalten im Surface-Objekt:

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

So verwendest du 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;
}

Overlay verwenden

Mit Overlays können Sie Untertitel, Nachrichten, Werbung oder MHEG-5-Datensendungen einblenden. Standardmäßig ist das Overlay deaktiviert. Sie können sie beim Erstellen der Sitzung aktivieren, indem Sie TvInputService.Session.setOverlayViewEnabled(true) wie im folgenden Beispiel aufrufen:

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

Verwenden Sie wie hier gezeigt ein View-Objekt für das Overlay, das von TvInputService.Session.onCreateOverlayView() zurückgegeben wird:

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

Die Layoutdefinition für das Overlay könnte wie folgt aussehen:

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

Inhalte verwalten

Wenn der Nutzer einen Kanal auswählt, verarbeitet Ihre TV-Eingabe den onTune()-Callback im TvInputService.Session-Objekt. Die Jugendschutzeinstellungen des Systems bestimmen, welche Inhalte aufgrund der Altersfreigabe angezeigt werden. In den folgenden Abschnitten wird beschrieben, wie du die Kanal- und Programmauswahl mit den TvInputService.Session notify-Methoden verwaltest, die mit der System-TV-App kommunizieren.

Video nicht verfügbar machen

Wenn der Nutzer den Kanal wechselt, sollten Sie darauf achten, dass auf dem Bildschirm vor dem Rendern des Inhalts keine irgendeinen Videoartefakte auf dem Fernseher erscheinen. Wenn Sie TvInputService.Session.onTune() aufrufen, können Sie verhindern, dass das Video präsentiert wird. Dazu rufen Sie TvInputService.Session.notifyVideoUnavailable() auf und übergeben die Konstante VIDEO_UNAVAILABLE_REASON_TUNING, wie im folgenden Beispiel gezeigt.

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

Wenn der Inhalt dann im Surface gerendert wird, rufen Sie TvInputService.Session.notifyVideoAvailable() auf, damit das Video angezeigt werden kann. Beispiel:

Kotlin

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

Java

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

Dieser Übergang dauert nur Bruchteile einer Sekunde, aber die Darstellung eines leeren Bildschirms ist optisch besser, als im Bild seltsame Ausbrüche und Bildrisse zu erzeugen.

Weitere Informationen zum Rendern von Videos mit Surface findest du unter Player in Oberfläche integrieren.

Jugendschutzeinstellungen bereitstellen

Wenn Sie feststellen möchten, ob bestimmte Inhalte durch Jugendschutzeinstellungen und Altersfreigaben blockiert werden, sehen Sie sich die TvInputManager-Klassenmethoden isParentalControlsEnabled() und isRatingBlocked(android.media.tv.TvContentRating) an. Außerdem ist es sinnvoll, dafür zu sorgen, dass die TvContentRating des Inhalts in einer Reihe derzeit zulässiger Altersfreigaben enthalten ist. Diese Überlegungen werden im folgenden Beispiel gezeigt.

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

Sobald Sie festgestellt haben, ob der Inhalt blockiert werden soll, informieren Sie die System-TV-App durch Aufrufen der TvInputService.Session-Methode notifyContentAllowed() oder notifyContentBlocked(), wie im vorherigen Beispiel gezeigt.

Verwenden Sie die Klasse TvContentRating, um den systemdefinierten String für COLUMN_CONTENT_RATING mit der Methode TvContentRating.createRating() zu generieren, wie hier gezeigt:

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

Track-Auswahl behandeln

Die Klasse TvTrackInfo enthält Informationen zu Medienspuren, z. B. den Tracktyp (Video, Audio oder Untertitel).

Wenn Ihre TV-Eingabesitzung zum ersten Mal Titelinformationen abrufen kann, sollte sie TvInputService.Session.notifyTracksChanged() mit einer Liste aller Tracks aufrufen, um die System-TV-App zu aktualisieren. Wenn sich die Titelinformationen ändern, rufen Sie noch einmal notifyTracksChanged() auf, um das System zu aktualisieren.

Die System-TV-App bietet dem Nutzer eine Schnittstelle zur Auswahl eines bestimmten Titels, wenn für einen bestimmten Titeltyp mehr als ein Titel verfügbar ist, z. B. Untertitel in verschiedenen Sprachen. Die TV-Eingabe reagiert auf den onSelectTrack()-Aufruf der System-TV-App durch Aufrufen von notifyTrackSelected(), wie im folgenden Beispiel gezeigt. Wenn null als Track-ID übergeben wird, wird die Auswahl des Tracks aufgehoben.

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