בחוויית הטלוויזיה החיה, המשתמש מחליף ערוצים ומוצג לו מידע על הערוצים והתוכניות לזמן קצר לפני שהמידע נעלם. ייתכן שיהיה צורך להשאיר סוגים אחרים של מידע, כגון הודעות ("נא לא לנסות בבית"), כתוביות או פרסומות. כמו בכל אפליקציית טלוויזיה, מידע כזה לא אמור להפריע לתוכן התוכנית המופעל על המסך.
כמו כן, שקלו האם יש להציג תוכן מסוים של התוכנית, בהתחשב בדירוג התוכן ובהגדרות בקרת ההורים, וכיצד האפליקציה שלכם מתנהגת ומודיעה למשתמש כאשר תוכן חסום או אינו זמין. בשיעור הזה נסביר איך לפתח את חוויית המשתמש של קלט הטלוויזיה בהתאם לשיקולים האלה.
נסה את אפליקציית הדוגמה של שירות קלט הטלוויזיה.
שלב נגן עם משטח
קלט הטלוויזיה שלך חייב לעבד וידאו על אובייקט 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>
תוכן שליטה
כאשר המשתמש בוחר ערוץ, קלט הטלוויזיה שלך מטפל בקריאה חוזרת של onTune() באובייקט TvInputService.Session. בקרות ההורים של אפליקציית הטלוויזיה של המערכת קובעות איזה תוכן מוצג, בהתחשב בדירוג התוכן.
הסעיפים הבאים מתארים כיצד לנהל בחירת ערוצים ותוכניות באמצעות שיטות TvInputService.Session notify שמתקשרות עם אפליקציית הטלוויזיה של המערכת.
הפיכת הסרטון ללא זמין
כאשר המשתמש משנה ערוץ, עליך לוודא שהמסך לא מציג תקרי וידאו תועים לפני שקלט הטלוויזיה שלך מעבד את התוכן. כאשר קוראים ל-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() עם רשימה של כל הרצועות כדי לעדכן את אפליקציית הטלוויזיה של המערכת. כאשר יש שינוי במידע על הרצועות, התקשר שוב ל-notifyTracksChanged() כדי לעדכן את המערכת.
אפליקציית הטלוויזיה של המערכת מספקת ממשק למשתמש לבחירת רצועה ספציפית אם יותר מרצועה אחת זמינה עבור סוג רצועה נתון; לדוגמה, כתוביות בשפות שונות. קלט הטלוויזיה שלך מגיב לקריאה onSelectTrack() מאפליקציית הטלוויזיה של המערכת על ידי קריאה ל-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; }