Dalam pengalaman menonton TV live, pengguna akan berganti-ganti saluran dan informasi saluran dan program akan ditampilkan secara singkat sebelum informasi tersebut hilang. Jenis informasi lainnya, seperti pesan ("JANGAN COBA DI RUMAH"), subtitel, atau iklan mungkin harus dipertahankan. Seperti aplikasi TV lainnya, informasi tersebut tidak boleh mengganggu konten program yang diputar di layar.
Pertimbangkan juga apakah konten program tertentu harus ditampilkan, berdasarkan setelan kontrol orang tua dan rating konten, serta bagaimana aplikasi Anda berperilaku dan memberi tahu pengguna saat konten diblokir atau tidak tersedia. Tutorial ini menjelaskan cara mengembangkan pengalaman pengguna input TV Anda atas pertimbangan tersebut.
Coba aplikasi contoh Layanan Input TV.
Mengintegrasikan pemutar dengan permukaan
Input TV Anda harus merender video ke objek Surface
, yang diteruskan oleh
metode
TvInputService.Session.onSetSurface()
. Berikut adalah contoh cara menggunakan instance MediaPlayer
untuk memutar
konten di objek 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; }
Demikian pula, berikut cara melakukannya menggunakan 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; }
Menggunakan overlay
Gunakan overlay untuk menampilkan subtitel, pesan, iklan, atau siaran data MHEG-5. Secara {i>default<i},
overlay dinonaktifkan. Anda dapat mengaktifkannya saat membuat sesi dengan memanggil
TvInputService.Session.setOverlayViewEnabled(true)
,
seperti dalam contoh berikut:
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; }
Gunakan objek View
untuk overlay, yang ditampilkan dari TvInputService.Session.onCreateOverlayView()
, seperti yang ditunjukkan di sini:
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; }
Definisi tata letak untuk overlay mungkin terlihat seperti ini:
<?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>
Mengontrol konten
Saat pengguna memilih saluran, input TV Anda akan menangani callback onTune()
di objek TvInputService.Session
. Kontrol orang tua
aplikasi TV sistem menentukan konten yang ditampilkan, berdasarkan rating konten.
Bagian berikut menjelaskan cara mengelola pemilihan saluran dan program menggunakan metode
TvInputService.Session
notify
yang
berkomunikasi dengan aplikasi TV sistem.
Menjadikan video tidak tersedia
Saat pengguna mengubah saluran, Anda ingin memastikan layar tidak menampilkan artefak
video terpisah sebelum input TV merender konten. Saat memanggil TvInputService.Session.onTune()
,
Anda dapat mencegah video ditampilkan dengan memanggil TvInputService.Session.notifyVideoUnavailable()
dan meneruskan konstanta VIDEO_UNAVAILABLE_REASON_TUNING
, seperti
yang ditunjukkan dalam contoh berikut.
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; }
Kemudian, saat konten dirender ke Surface
, panggil
TvInputService.Session.notifyVideoAvailable()
untuk mengizinkan video ditampilkan, seperti berikut:
Kotlin
fun onRenderedFirstFrame(surface:Surface) { firstFrameDrawn = true notifyVideoAvailable() }
Java
@Override public void onRenderedFirstFrame(Surface surface) { firstFrameDrawn = true; notifyVideoAvailable(); }
Transisi ini hanya berlangsung selama sepersekian detik, tetapi menampilkan layar kosong secara visual lebih baik daripada membiarkan gambar berkedip dan berkedip yang aneh.
Lihat juga, Mengintegrasikan pemutar dengan permukaan untuk mengetahui informasi selengkapnya tentang menggunakan
Surface
untuk merender video.
Menyediakan kontrol orang tua
Untuk menentukan apakah konten tertentu diblokir oleh kontrol orang tua dan rating konten, periksa
metode class TvInputManager
, isParentalControlsEnabled()
,
dan isRatingBlocked(android.media.tv.TvContentRating)
. Anda
mungkin juga ingin memastikan bahwa TvContentRating
konten disertakan dalam
kumpulan rating konten yang saat ini diizinkan. Pertimbangan ini ditampilkan pada contoh berikut.
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); }
Setelah menentukan apakah konten harus diblokir atau tidak boleh diblokir, beri tahu aplikasi TV
sistem dengan memanggil
metode TvInputService.Session
notifyContentAllowed()
atau
notifyContentBlocked()
,
seperti yang ditunjukkan pada contoh sebelumnya.
Gunakan class TvContentRating
untuk menghasilkan string yang ditentukan sistem untuk
COLUMN_CONTENT_RATING
dengan metode
TvContentRating.createRating()
, seperti yang ditampilkan di sini:
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");
Menangani pemilihan trek
Class TvTrackInfo
menyimpan informasi tentang trek media seperti
jenis trek (video, audio, atau subtitel) dan seterusnya.
Saat pertama kali sesi input TV Anda bisa mendapatkan informasi trek, sesi harus memanggil
TvInputService.Session.notifyTracksChanged()
dengan daftar semua trek untuk mengupdate aplikasi TV sistem. Jika ada
perubahan informasi trek, panggil
notifyTracksChanged()
lagi untuk mengupdate sistem.
Aplikasi TV sistem menyediakan antarmuka bagi pengguna untuk memilih trek tertentu jika lebih dari satu
trek tersedia untuk jenis trek tertentu; misalnya, subtitel dalam berbagai bahasa. Input TV
Anda merespons
panggilan
onSelectTrack()
dari aplikasi TV sistem dengan memanggil
notifyTrackSelected()
, seperti ditunjukkan dalam contoh berikut. Perhatikan bahwa jika null
diteruskan sebagai ID jalur, tindakan ini akan membatalkan pilihan jalur.
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; }