TV-Nutzerinteraktion verwalten

Beim Live-TV-Erlebnis wechselt der Nutzer den Sender und sieht kurz Informationen zum Sender und zur Sendung, bevor diese wieder verschwinden. Andere Arten von Informationen wie Nachrichten („NICHT ZU HAUSE NACHMACHEN“), Untertitel oder Anzeigen müssen möglicherweise länger angezeigt werden. Wie bei jeder TV-App sollten solche Informationen die Wiedergabe der Sendung auf dem Bildschirm nicht beeinträchtigen.

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

Überlegen Sie auch, ob bestimmte Sendungsinhalte aufgrund der Altersfreigabe und der Jugendschutzeinstellungen präsentiert werden sollen und wie sich Ihre App verhält und den Nutzer informiert, wenn Inhalte blockiert oder nicht verfügbar sind. In dieser Lektion wird beschrieben, wie Sie die Nutzererfahrung Ihres TV-Eingangs für diese Überlegungen entwickeln.

Probieren Sie die Beispiel-App für den TV-Eingabedienst aus.

Player mit Oberfläche verknüpfen

Ihr TV-Eingang muss Videos auf einem Surface Objekt rendern, das von der TvInputService.Session.onSetSurface() Methode übergeben wird. Hier ein Beispiel für die Verwendung einer MediaPlayer-Instanz zur Wiedergabe 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;
}

Hier ist eine ähnliche Vorgehensweise mit 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

Verwenden Sie ein Overlay, um Untertitel, Nachrichten, Anzeigen oder MHEG-5-Datenübertragungen anzuzeigen. Standardmäßig ist das Overlay deaktiviert. Sie können es beim Erstellen der Sitzung aktivieren, indem Sie TvInputService.Session.setOverlayViewEnabled(true) aufrufen, wie im folgenden Beispiel:

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 ein View Objekt für das Overlay, das von TvInputService.Session.onCreateOverlayView() zurückgegeben wird, wie hier gezeigt:

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 so 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 steuern

Wenn der Nutzer einen Sender auswählt, verarbeitet Ihr TV-Eingang den onTune()-Callback im TvInputService.Session-Objekt. Die Jugendschutzeinstellungen der TV-App des Systems bestimmen anhand der Altersfreigabe, welche Inhalte angezeigt werden. In den folgenden Abschnitten wird beschrieben, wie Sie die Auswahl von Sendern und Sendungen mit den notify-Methoden von TvInputService.Session verwalten, die mit der TV-App des Systems kommunizieren.

Video nicht verfügbar machen

Wenn der Nutzer den Sender wechselt, sollten auf dem Bildschirm keine zufälligen Videoartefakte angezeigt werden, bevor Ihr TV-Eingang die Inhalte rendert. Wenn Sie TvInputService.Session.onTune() aufrufen, können Sie verhindern, dass das Video präsentiert wird, indem Sie TvInputService.Session.notifyVideoUnavailable() aufrufen und die Konstante VIDEO_UNAVAILABLE_REASON_TUNING übergeben, 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 die Inhalte dann auf der Surface gerendert werden, rufen Sie TvInputService.Session.notifyVideoAvailable() auf, um die Anzeige des Videos zuzulassen, wie hier gezeigt:

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 ein leerer Bildschirm ist visuell besser, als wenn das Bild seltsame Blips und Ruckler zeigt.

Weitere Informationen zur Verwendung von Surface zum Rendern von Videos finden Sie unter Player mit Oberfläche verknüpfen.

Jugendschutzeinstellungen bereitstellen

Um zu ermitteln, ob bestimmte Inhalte durch Jugendschutzeinstellungen und Altersfreigaben blockiert werden, prüfen Sie die Methoden der Klasse TvInputManager, isParentalControlsEnabled() und isRatingBlocked(android.media.tv.TvContentRating). Außerdem sollten Sie prüfen, ob die TvContentRating der Inhalte in einer Gruppe von derzeit zulässigen 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 entschieden haben, ob die Inhalte blockiert werden sollen oder nicht, benachrichtigen Sie die TV-App des Systems, indem Sie die Methode TvInputService.Session notifyContentAllowed() oder notifyContentBlocked() aufrufen, wie im vorherigen Beispiel gezeigt.

Verwenden Sie die TvContentRating Klasse, um den vom System definierten String für die COLUMN_CONTENT_RATING mit der TvContentRating.createRating() Methode 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");

Titelauswahl verarbeiten

Die Klasse TvTrackInfo enthält Informationen zu Mediatiteln wie den Titeltyp (Video, Audio oder Untertitel).

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

Die TV-App des Systems bietet eine Oberfläche, über die der Nutzer einen bestimmten Titel auswählen kann, wenn für einen bestimmten Titeltyp mehrere Titel verfügbar sind, z. B. Untertitel in verschiedenen Sprachen. Ihr TV-Eingang reagiert auf den Aufruf onSelectTrack() von der TV-App des Systems, indem er notifyTrackSelected() aufruft, wie im folgenden Beispiel gezeigt. Wenn null als Titel-ID übergeben wird, wird der Titel abgewählt.

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