ในการรับชมรายการทีวีสด ผู้ใช้เปลี่ยนช่องและได้รับการนำเสนอด้วย ข้อมูลช่องและโปรแกรมเป็นเวลาสั้นๆ ก่อนที่ข้อมูลจะหายไป ข้อมูลประเภทอื่นๆ เช่น ข้อความ ("อย่าส่งมาที่บ้าน") คำบรรยาย หรือโฆษณาอาจต้องคงอยู่ตลอดไป เช่นเดียวกับทีวีทั่วไป ข้อมูลดังกล่าวไม่ควรรบกวนเนื้อหาโปรแกรมที่เล่นบนหน้าจอ
พิจารณาด้วยว่าควรนำเสนอเนื้อหาของโปรแกรมหรือไม่ เนื่องจาก การตั้งค่าการจัดประเภทเนื้อหาและการควบคุมโดยผู้ปกครอง รวมถึงลักษณะการทำงานของแอปและแจ้งให้ผู้ใช้ทราบเมื่อ เนื้อหาถูกบล็อกหรือไม่พร้อมใช้งาน บทเรียนนี้จะอธิบายวิธีพัฒนาผู้ใช้อินพุตทีวีของคุณ ประสบการณ์ของคุณกับข้อควรพิจารณาเหล่านี้
ลองใช้ แอปตัวอย่างของบริการอินพุตทีวี
รวมเครื่องเล่นกับพื้นผิว
อินพุตทีวีของคุณต้องแสดงผลวิดีโอบนออบเจ็กต์ Surface
ซึ่งส่งผ่าน
TvInputService.Session.onSetSurface()
ต่อไปนี้คือตัวอย่างวิธีใช้อินสแตนซ์ MediaPlayer
เพื่อเล่น
เนื้อหาในออบเจ็กต์ 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; }
และนี่คือวิธีการทำโดยใช้ 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; }
ใช้การวางซ้อน
ใช้การวางซ้อนเพื่อแสดงคำบรรยาย ข้อความ โฆษณา หรือการออกอากาศข้อมูล MHEG-5 โดยค่าเริ่มต้น แอตทริบิวต์
ปิดใช้การวางซ้อนอยู่ คุณสามารถเปิดได้เมื่อสร้างเซสชันโดยการโทร
TvInputService.Session.setOverlayViewEnabled(true)
,
ดังตัวอย่างต่อไปนี้
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; }
ใช้ออบเจ็กต์ View
สำหรับการวางซ้อน ซึ่งส่งคืนจาก TvInputService.Session.onCreateOverlayView()
ตามที่แสดงที่นี่
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; }
การกำหนดเลย์เอาต์ของการวางซ้อนอาจมีลักษณะดังนี้
<?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>
ควบคุมเนื้อหา
เมื่อผู้ใช้เลือกช่อง อินพุตทีวีจะจัดการ Callback onTune()
ในออบเจ็กต์ TvInputService.Session
ทีวีระบบ
การควบคุมโดยผู้ปกครองของแอปจะเป็นตัวกำหนดเนื้อหาที่จะแสดงตามการจัดประเภทเนื้อหา
ส่วนต่อไปนี้จะอธิบายวิธีจัดการการเลือกช่องและโปรแกรมโดยใช้
TvInputService.Session
notify
เมธอดที่
สื่อสารกับแอป System TV
ทำให้วิดีโอไม่พร้อมใช้งาน
เมื่อผู้ใช้เปลี่ยนช่อง คุณควรตรวจสอบว่าหน้าจอไม่มีสิ่งรบกวน
อาร์ติแฟกต์วิดีโอก่อนที่อินพุตของทีวีจะแสดงผลเนื้อหา เมื่อคุณโทรหา TvInputService.Session.onTune()
คุณสามารถป้องกันไม่ให้ระบบนำเสนอวิดีโอโดยโทรไปที่ TvInputService.Session.notifyVideoUnavailable()
และส่งค่าคงที่ VIDEO_UNAVAILABLE_REASON_TUNING
ที่แสดงในตัวอย่างต่อไปนี้
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; }
จากนั้น เมื่อแสดงผลเนื้อหาเป็น Surface
คุณจะเรียกใช้
วันที่ TvInputService.Session.notifyVideoAvailable()
เพื่ออนุญาตให้วิดีโอแสดง เช่น
Kotlin
fun onRenderedFirstFrame(surface:Surface) { firstFrameDrawn = true notifyVideoAvailable() }
Java
@Override public void onRenderedFirstFrame(Surface surface) { firstFrameDrawn = true; notifyVideoAvailable(); }
การเปลี่ยนนี้จะเกิดขึ้นเพียงเสี้ยววินาที แต่การนำเสนอหน้าจอว่างเปล่า แต่จะดีกว่าการปล่อยให้ภาพกะพริบและกระตุกแปลกๆ
ดูข้อมูลเพิ่มเติมเกี่ยวกับการทำงานได้ที่รวมโปรแกรมเล่นกับแพลตฟอร์ม
ด้วย Surface
ในการแสดงผลวิดีโอ
ให้การควบคุมโดยผู้ปกครอง
หากต้องการดูว่าเนื้อหานั้นๆ ถูกบล็อกโดยการควบคุมโดยผู้ปกครองและการจัดประเภทเนื้อหาหรือไม่ ให้ตรวจสอบ
TvInputManager
วิธีการในชั้นเรียน isParentalControlsEnabled()
และ isRatingBlocked(android.media.tv.TvContentRating)
คุณ
คุณอาจต้องตรวจสอบด้วยว่า TvContentRating
ของเนื้อหารวมอยู่ใน
ชุดของการจัดประเภทเนื้อหาที่อนุญาตในปัจจุบัน ข้อควรพิจารณาเหล่านี้จะแสดงในตัวอย่างต่อไปนี้
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); }
เมื่อคุณพิจารณาแล้วว่าควรบล็อกเนื้อหาดังกล่าวหรือไม่ควรบล็อก โปรดแจ้งทีวีของระบบ
โดยการเรียกใช้
TvInputService.Session
วิธี notifyContentAllowed()
หรือ
notifyContentBlocked()
ตามที่แสดงในตัวอย่างก่อนหน้านี้
ใช้คลาส TvContentRating
เพื่อสร้างสตริงที่ระบบกำหนดสำหรับ
COLUMN_CONTENT_RATING
ที่มี
TvContentRating.createRating()
ดังที่แสดงไว้ที่นี่
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");
จัดการการเลือกแทร็ก
ชั้นเรียน TvTrackInfo
มีข้อมูลเกี่ยวกับแทร็กสื่อ เช่น
เป็นประเภทแทร็ก (วิดีโอ เสียง หรือคำบรรยาย) เป็นต้น
ครั้งแรกที่เซสชันอินพุตทีวีของคุณสามารถรับข้อมูลการติดตาม ระบบควรเรียกใช้
TvInputService.Session.notifyTracksChanged()
พร้อมรายการแทร็กทั้งหมดเพื่ออัปเดตแอป System TV เมื่อมี
คือการเปลี่ยนแปลงข้อมูลแทร็ก
วันที่ notifyTracksChanged()
อีกครั้งเพื่ออัปเดตระบบ
แอป System TV มีอินเทอร์เฟซให้ผู้ใช้เลือกแทร็กที่เฉพาะเจาะจงหากมีแทร็กมากกว่า 1 แทร็ก
แทร็กพร้อมใช้งานสำหรับแทร็กประเภทที่กำหนด เช่น คำบรรยายในภาษาต่างๆ ทีวีของคุณ
อินพุตจะตอบสนอง
onSelectTrack()
โทรจากแอป System TV โดยการโทร
วันที่ notifyTrackSelected()
ตามที่แสดงในตัวอย่างต่อไปนี้ โปรดทราบว่าเมื่อnull
จะส่งผ่านเป็นรหัสแทร็ก ซึ่งจะยกเลิกการเลือกแทร็กดังกล่าว
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; }