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