Zarządzanie interakcjami użytkowników telewizora

W telewizji na żywo użytkownik zmienia kanał, na którym wyświetlane są informacje o kanale i programie. Inne typy informacji np. komunikaty („NIE ATTEMPT AT HOME”), napisy lub reklamy. Jak w każdym telewizorze aplikacji, takie informacje nie powinny zakłócać odtwarzania treści programu na ekranie.

Rysunek 1. Wiadomość w formie nakładki w aplikacji na żywo.

Zastanów się też, czy określone treści z programu powinny być prezentowane, oceny treści i ustawień kontroli rodzicielskiej oraz tego, jak aplikacja zachowuje się i informuje użytkownika, treść jest zablokowana lub niedostępna. Ta lekcja opisuje, jak skonfigurować użytkownika wejścia TV tych kwestii.

Wypróbuj Przykładowa aplikacja do obsługi wejścia TV.

Zintegruj odtwarzacz z powierzchnią

Wejście TV musi renderować film na obiekcie Surface, który jest przekazywany przez TvInputService.Session.onSetSurface() . Oto przykład użycia instancji MediaPlayer do odtwarzania zawartość obiektu 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;
}

Oto jak to zrobić przy użyciu 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, wiadomości, reklam lub transmisji danych MHEG-5. Domyślnie atrybut nakładka jest wyłączona. Możesz ją 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 zwracanej z narzędzia TvInputService.Session.onCreateOverlayView(), jak pokazano tutaj:

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ć np. 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>

Kontrola treści

Gdy użytkownik wybierze kanał, wejście TV obsługuje wywołanie zwrotne onTune() w obiekcie TvInputService.Session. System TV funkcje kontroli rodzicielskiej w aplikacji określają, jakie treści są wyświetlane w zależności od oceny treści. W poniższych sekcjach opisano, jak zarządzać wyborem kanałów i programów za pomocą TvInputService.Session notify metody, które komunikują się z systemową aplikacją TV.

Ustaw film jako niedostępny

Gdy użytkownik zmienia kanał, chcesz mieć pewność, że na ekranie nie pojawią się żadne przypadkowe ruchy. przed wyrenderowaniem treści przez wejście TV. Gdy dzwonisz pod numer TvInputService.Session.onTune(), możesz zapobiec wyświetlaniu filmu wideo, dzwoniąc pod numer TvInputService.Session.notifyVideoUnavailable() i przekazując stałą VIDEO_UNAVAILABLE_REASON_TUNING, jak w poniższym 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, gdy treść jest renderowana w narzędziu Surface, wywołujesz funkcję TvInputService.Session.notifyVideoAvailable() aby film mógł się wyświetlić, na przykład:

Kotlin

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

Java

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

To przejście trwa tylko ułamki sekundy, ale prezentowanie pustego ekranu jest jest lepszy wizualnie niż zezwalanie na wyświetlanie na zdjęciu dziwnych skoków i zakłóceń.

Więcej informacji o pracy znajdziesz w artykule Integrowanie odtwarzacza z platformą. używając pola Surface, by wygenerować film.

Włącz kontrolę rodzicielską

Aby określić, czy dane treści są blokowane przez kontrolę rodzicielską i ocenę treści, sprawdź TvInputManager metody klasy, isParentalControlsEnabled() i isRatingBlocked(android.media.tv.TvContentRating). Ty sprawdź też, czy atrybut TvContentRating treści jest uwzględniony w zestaw aktualnie dozwolonych ocen treści. Wzięte pod uwagę te kwestie znajdziesz 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);
}

Gdy ustalisz, czy treść powinna zostać zablokowana, powiadom system TV przez wywołanie TvInputService.Session metoda notifyContentAllowed() lub notifyContentBlocked() tak jak w poprzednim przykładzie.

Użyj klasy TvContentRating, aby wygenerować zdefiniowany przez system ciąg znaków dla COLUMN_CONTENT_RATING z TvContentRating.createRating() Jak poniżej:

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

Obsługa wyboru ścieżki

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

Za pierwszym razem, gdy sesja wejścia TV może uzyskać informacje o utworze, powinna wywołać metodę TvInputService.Session.notifyTracksChanged() z listą wszystkich utworów do zaktualizowania systemowej aplikacji TV. Kiedy tam dotyczy zmiany informacji o ścieżce, notifyTracksChanged() aby zaktualizować system.

Systemowa aplikacja TV udostępnia interfejs, w którym użytkownik może wybrać konkretną ścieżkę ścieżka audio jest dostępna dla danego typu ścieżki; np. napisów w różnych językach. Twój telewizor dane wejściowe reagują na onSelectTrack() z systemowej aplikacji TV, wybierając notifyTrackSelected() Jak widać w przykładzie poniżej. Pamiętaj, że gdy null jest przekazywany jako identyfikator ścieżki, co powoduje usunięcie ścieżki.

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