Dalam pengalaman menonton TV live, pengguna berganti saluran dan saluran dan program secara singkat sebelum informasi tersebut menghilang. Jenis informasi lainnya, seperti pesan ("JANGAN COBA DI RUMAH"), subtitel, atau iklan mungkin harus ada. Seperti pada TV lainnya informasi tersebut tidak boleh mengganggu konten program yang diputar di layar.
Pertimbangkan juga apakah konten program tertentu harus disajikan, mengingat setelan kontrol orang tua dan rating konten, serta perilaku aplikasi dan memberi tahu pengguna jika konten diblokir atau tidak tersedia. Pelajaran ini menjelaskan cara mengembangkan pengguna input TV pengalaman pengguna atas pertimbangan ini.
Cobalah Aplikasi contoh Layanan Input TV.
Mengintegrasikan pemutar dengan permukaan
Input TV Anda harus merender video ke objek Surface
, yang diteruskan oleh
TvInputService.Session.onSetSurface()
. Berikut adalah contoh cara menggunakan instance MediaPlayer
untuk memutar
konten dalam 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 ini 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 default,
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
. TV sistem
kontrol orang tua pada aplikasi menentukan konten yang ditampilkan, berdasarkan rating kontennya.
Bagian berikut menjelaskan cara mengelola pemilihan saluran dan program menggunakan
TvInputService.Session
metode notify
yang
berkomunikasi dengan
aplikasi TV yang ada di sistem.
Menjadikan video tidak tersedia
Saat pengguna mengganti saluran, Anda ingin memastikan layar tidak menampilkan jaringan yang
artefak video sebelum input TV Anda merender konten. Saat Anda memanggil TvInputService.Session.onTune()
,
Anda dapat mencegah video ditampilkan dengan memanggil TvInputService.Session.notifyVideoUnavailable()
dan meneruskan konstanta VIDEO_UNAVAILABLE_REASON_TUNING
, sebagai
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 ini:
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-kedip ganjil dan gelisah.
Lihat juga, Mengintegrasikan pemutar dengan permukaan untuk informasi selengkapnya tentang cara kerja
dengan 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
Anda mungkin juga ingin memastikan TvContentRating
konten disertakan dalam
serangkaian 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 Anda menentukan apakah konten harus diblokir atau tidak, beri tahu TV sistem
aplikasi Anda dengan memanggil
TvInputService.Session
metode notifyContentAllowed()
atau
notifyContentBlocked()
, seperti yang ditunjukkan dalam contoh sebelumnya.
Gunakan class TvContentRating
untuk membuat string yang ditentukan sistem untuk
COLUMN_CONTENT_RATING
dengan atribut
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
seperti jenis trek (video, audio, atau subtitel) dan sebagainya.
Pertama kali sesi input TV Anda bisa mendapatkan informasi trek, sesi input TV harus memanggil
TvInputService.Session.notifyTracksChanged()
dengan daftar semua jalur untuk mengupdate aplikasi TV sistem. Jika ada
adalah perubahan informasi jalur,
notifyTracksChanged()
lagi untuk memperbarui sistem.
Aplikasi TV yang ada pada sistem menyediakan antarmuka bagi pengguna untuk memilih jalur tertentu jika lebih dari satu
jalur tersedia untuk jenis jalur tertentu; misalnya, subtitel dalam berbagai bahasa. TV Anda
input merespons
onSelectTrack()
dari aplikasi TV sistem dengan memanggil
notifyTrackSelected()
, seperti yang ditunjukkan dalam contoh berikut. Perhatikan bahwa saat null
diteruskan sebagai ID trek, sehingga membatalkan pilihan trek tersebut.
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; }