בחוויה של הטלוויזיה בשידור חי, המשתמש מחליף ערוצים ורואים אותו מידע קצר על הערוץ והתוכנית לפני שהמידע נעלם. סוגי מידע אחרים, כמו מסרים ('אל תפנו בבית'), ייתכן שכתוביות או מודעות לא יוצגו באופן קבוע. כמו בכל טלוויזיה מידע כזה לא יפריע לתוכן התוכנית לפעול במסך.
כמו כן, כדאי לשקול אם להציג תוכן מסוים של התוכנית, על הסיווג של התוכן ועל הגדרות בקרת ההורים, וגם על האופן שבו האפליקציה מתנהגת ומיידעת את המשתמש כאשר התוכן חסום או לא זמין. השיעור הזה מתאר כיצד לפתח את המשתמש של קלט הטלוויזיה של השיקולים האלה.
אפשר לנסות את אפליקציה לדוגמה של TV קלט Service.
שילוב הנגן עם משטח
קלט הטלוויזיה חייב לעבד את הסרטון לאובייקט 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
שיטות
מתקשרים עם אפליקציית המערכת לטלוויזיה.
הגדרת הסרטון כלא זמין
כשהמשתמש משנה את הערוץ, צריך לוודא שלא מוצגות במסך הודעות שגיאה.
ארטיפקטים לפני שקלט הטלוויזיה מעבד את התוכן. כשמתקשרים אל 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); }
אחרי שבחרת אם לחסום את התוכן או לא, עליך להודיע למערכת TV
באמצעות קריאה
אמצעי תשלום אחד (TvInputService.Session
) notifyContentAllowed()
או
notifyContentBlocked()
, כמו שאפשר לראות בדוגמה הקודמת.
משתמשים במחלקה TvContentRating
כדי ליצור את המחרוזת המוגדרת על ידי המערכת עבור
COLUMN_CONTENT_RATING
עם
TvContentRating.createRating()
method, כפי שמוצג כאן:
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; }