Zarządzanie interakcjami użytkowników telewizora

W telewizji na żywo użytkownik zmienia kanał i na chwilę widzi informacje o kanale i programie, zanim znikną. Inne typy informacji, np. wiadomości („NIE PRÓBUJ W DOMU”), napisy lub reklamy, mogą być widoczne na stałe. Podobnie jak w przypadku każdej aplikacji telewizyjnej, informacje takie nie powinny zakłócać wyświetlania treści programu na ekranie.

Rysunek 1. Nakładka w aplikacji do telewizji na żywo

Zastanów się też, czy ze względu na ocenę treści i ustawienia kontroli rodzicielskiej powinny być prezentowane określone treści w programie, a także jak zachowuje się aplikacja i informuje użytkownika, gdy treści są zablokowane lub niedostępne. Z tej lekcji dowiesz się, jak dostosować wejście TV do tych wymagań

Wypróbuj przykładową aplikację Usługa wprowadzania danych TV.

Zintegruj odtwarzacz z powierzchnią

Wejście telewizyjne musi renderować obraz w obiekcie Surface, który jest przekazywany przez metodę TvInputService.Session.onSetSurface(). Oto przykład użycia wystąpienia MediaPlayer do odtwarzania treści w obiekcie 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;
}

Aby to zrobić, użyj narzędzia 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;
}

Użyj nakładki

Użyj nakładki do wyświetlania napisów, komunikatów, reklam lub transmisji danych MHEG-5. Domyślnie nakładka jest wyłączona. Możesz go włączyć podczas tworzenia sesji, wywołując TvInputService.Session.setOverlayViewEnabled(true), jak w tym przykładzie:

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

Użyj obiektu View dla nakładki zwróconego z TvInputService.Session.onCreateOverlayView(), jak pokazano poniżej:

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

Definicja układu nakładki może wyglądać mniej więcej tak:

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

Kontroluj treści

Gdy użytkownik wybierze kanał, wejście TV obsługuje wywołanie zwrotne onTune() w obiekcie TvInputService.Session. Kontrola rodzicielska w aplikacji TV określa, jakie treści są wyświetlane w zależności od oceny treści. W kolejnych sekcjach opisujemy, jak zarządzać wyborem kanałów i programów za pomocą metod TvInputService.Session notify, które komunikują się z systemową aplikacją TV.

Ustaw film jako niedostępny

Gdy użytkownik zmieni kanał, upewnij się, że na ekranie nie wyświetlają się żadne zbędne zakłócenia wideo, zanim sygnał wejściowego telewizora wyrenderuje treści. Jeśli wywołasz metodę TvInputService.Session.onTune(), możesz zapobiec wyświetlaniu filmu, wywołując metodę TvInputService.Session.notifyVideoUnavailable() i przekazując stałą VIDEO_UNAVAILABLE_REASON_TUNING, jak pokazano w następnym przykładzie.

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

Następnie podczas renderowania treści w interfejsie Surface wywołujesz TvInputService.Session.notifyVideoAvailable(), by umożliwić wyświetlenie filmu. Na przykład:

Kotlin

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

Java

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

Trwa to tylko przez ułamki sekundy, ale podawanie pustego ekranu jest znacznie lepsze niż pokazywanie na obrazie osobliwych nagłych wzrostów lub zakłóceń.

Więcej informacji o renderowaniu wideo przy użyciu technologii Surface znajdziesz w artykule Integrowanie odtwarzacza z platformą.

Kontrola rodzicielska

Aby określić, czy dane treści są zablokowane przez kontrolę rodzicielską i ocenę treści, sprawdź metody klas TvInputManager (isParentalControlsEnabled() i isRatingBlocked(android.media.tv.TvContentRating)). Możesz też się upewnić, że pole TvContentRating treści jest uwzględnione w zestawie aktualnie dozwolonych ocen treści. Te kwestie zostały przedstawione w przykładzie poniżej.

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

Po określeniu, czy treści powinny być blokowane, powiadom systemową aplikację TV, wywołując metodę TvInputService.Session notifyContentAllowed() lub notifyContentBlocked(), jak pokazano w poprzednim przykładzie.

Użyj klasy TvContentRating do wygenerowania ciągu znaków zdefiniowanego przez system dla elementu COLUMN_CONTENT_RATING za pomocą metody TvContentRating.createRating(), jak w tym przykładzie:

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

Wybierz utwór

Klasa TvTrackInfo zawiera informacje o ścieżkach multimedialnych, takie jak typ ścieżki (wideo, dźwięk lub napisy).

Gdy sesja wejścia TV po raz pierwszy może pobrać informacje o utworze, powinna wywołać TvInputService.Session.notifyTracksChanged() z listą wszystkich ścieżek, aby zaktualizować systemową aplikację telewizora. Gdy informacje o utworze zostaną zmienione, wywołaj jeszcze raz polecenie notifyTracksChanged(), aby zaktualizować system.

Systemowa aplikacja TV ma interfejs, który pozwala użytkownikowi wybrać konkretną ścieżkę, jeśli dla danego typu dostępnych jest więcej ścieżek, na przykład napisy w różnych językach. Wejście telewizora odpowiada na wywołanie onSelectTrack() z systemowej aplikacji TV, wywołując metodę notifyTrackSelected(), jak w tym przykładzie. Pamiętaj, że przekazanie null jako identyfikatora ścieżki usuwa jej zaznaczenie.

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