Zarządzanie interakcjami użytkowników telewizora

W przypadku telewizji na żywo użytkownik zmienia kanały i przez krótki czas widzi informacje o kanale i programie, zanim znikną. Inne rodzaje informacji, takie jak wiadomości („NIE PRÓBUJ W DOMU”), napisy lub reklamy, mogą wymagać dłuższego wyświetlania. Podobnie jak w przypadku każdej aplikacji telewizyjnej takie informacje nie powinny zakłócać odtwarzania treści programu na ekranie.

Rysunek 1. Nakładka z wiadomością w aplikacji telewizji na żywo.

Zastanów się też, czy określone treści programu powinny być wyświetlane, biorąc pod uwagę ocenę treści i ustawienia kontroli rodzicielskiej, oraz jak Twoja aplikacja zachowuje się i informuje użytkownika, gdy treści są zablokowane lub niedostępne. Z tej lekcji dowiesz się, jak wziąć pod uwagę te kwestie podczas projektowania interfejsu użytkownika dla wejścia telewizyjnego.

Wypróbuj przykładową aplikację usługi wejścia telewizyjnego .

Integracja odtwarzacza z powierzchnią

Wejście telewizyjne musi renderować obraz wideo na obiekcie Surface, który jest przekazywany przez metodę TvInputService.Session.onSetSurface(). Oto przykład użycia instancji 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;
}

Podobnie możesz to zrobić za pomocą ExoPlayera:

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żywanie nakładki

Użyj nakładki, aby wyświetlać napisy, wiadomości, reklamy lub transmisje danych MHEG-5. Domyślnie nakładka jest wyłączona. Możesz ją włączyć podczas tworzenia sesji, wywołując metodę 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, który jest zwracany przez metodę 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ć 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>

Kontrolowanie treści

Gdy użytkownik wybierze kanał, wejście telewizyjne obsługuje wywołanie zwrotne onTune() w obiekcie TvInputService.Session. Ustawienia kontroli rodzicielskiej w systemowej aplikacji telewizyjnej określają, jakie treści mają być wyświetlane, biorąc pod uwagę ocenę treści. W kolejnych sekcjach opisano, jak zarządzać wyborem kanałów i programów za pomocą metod notify w TvInputService.Session, które komunikują się z systemową aplikacją TV.

Udostępnianie filmu

Gdy użytkownik zmieni kanał, musisz się upewnić, że zanim wejście telewizyjne wyrenderuje treści, na ekranie nie będą wyświetlane żadne niepożądane artefakty wideo. Gdy 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 tym 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;
}

Gdy treści zostaną wyrenderowane na powierzchni Surface, wywołaj metodę TvInputService.Session.notifyVideoAvailable(), aby umożliwić wyświetlanie filmu, jak pokazano poniżej:

Kotlin

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

Java

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

Ta zmiana trwa tylko ułamki sekundy, ale wyświetlanie pustego ekranu jest wizualnie lepsze niż miganie obrazu.

Więcej informacji o pracy z Surface w celu renderowania obrazu wideo znajdziesz też w sekcji Integracja odtwarzacza z powierzchnią.

Kontrola rodzicielska

Aby sprawdzić, czy dane treści są zablokowane przez kontrolę rodzicielską i ocenę treści, użyj metod klasy TvInputManager: isParentalControlsEnabled() i isRatingBlocked(android.media.tv.TvContentRating). Możesz też sprawdzić, czy TvContentRating treści jest uwzględniona w zbiorze aktualnie dozwolonych ocen treści. Te kwestie są pokazane w tym przykładzie.

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ści powinny być zablokowane, czy nie, powiadom systemową aplikację telewizyjną, wywołując metodę TvInputService.Session notifyContentAllowed() lub notifyContentBlocked() , jak pokazano w poprzednim przykładzie.

Użyj klasy TvContentRating, aby wygenerować zdefiniowany przez system ciąg znaków dla COLUMN_CONTENT_RATING za pomocą TvContentRating.createRating() metody, jak pokazano tutaj:

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 multimediów, takie jak typ ścieżki (wideo, audio lub napisy) itp.

Gdy sesja wejścia telewizyjnego po raz pierwszy uzyska informacje o ścieżce, powinna wywołać TvInputService.Session.notifyTracksChanged() z listą wszystkich ścieżek, aby zaktualizować systemową aplikację TV. Gdy informacje o ścieżce się zmienią, ponownie wywołaj notifyTracksChanged() , aby zaktualizować system.

Systemowa aplikacja TV udostępnia interfejs, który umożliwia użytkownikowi wybranie konkretnej ścieżki, jeśli dla danego typu ścieżki dostępna jest więcej niż 1 ścieżka, np. napisy w różnych językach. Wejście telewizyjne odpowiada na wywołanie onSelectTrack() z systemowej aplikacji TV, wywołując metodę notifyTrackSelected() , jak pokazano w tym przykładzie. Pamiętaj, że gdy jako identyfikator ścieżki zostanie przekazana wartość null, ścieżka zostanie odznaczona.

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