در تجربه تلویزیون زنده، کاربر کانالها را تغییر میدهد و اطلاعات کانال و برنامه بهطور خلاصه قبلاز ناپدید شدن اطلاعات به او ارائه میشود. انواع دیگر اطلاعات، مانند پیامها («در خانه امتحان نکنید»)، زیرنویسها، یا آگهیها ممکن است نیاز به ماندگاری داشته باشند. مانند هر برنامه تلویزیونی، چنین اطلاعاتی نباید با محتوای برنامه درحال پخش روی صفحه تداخل داشته باشد.
همچنین با درنظر گرفتن ردهبندی محتوا و تنظیمات کنترلهای والدین، و نحوه رفتار برنامه شما و اطلاعرسانی به کاربر درصورت مسدود شدن یا دردسترس نبودن محتوا، بررسی کنید که آیا محتوای برنامه خاصی باید ارائه شود یا نه. این درس نحوه توسعه تجربه کاربری ورودی تلویزیون را برای این ملاحظات شرح میدهد.
برنامه نمونه سرویس ورودی تلویزیون را امتحان کنید.
ادغام پخشکننده با سطح
ورودی تلویزیون شما باید ویدیو را روی شیء Surface رندر کند که توسط روش TvInputService.Session.onSetSurface() ارسال میشود. در اینجا مثالی از نحوه استفاده از نمونه MediaPlayer برای پخش
محتوا در شیء Surface آورده شده است:
کاتلین
override fun onSetSurface(surface: Surface?): Boolean { player?.setSurface(surface) mSurface = surface return true } override fun onSetStreamVolume(volume: Float) { player?.setVolume(volume, volume) mVolume = volume }
جاوا
@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 آورده شده است:
کاتلین
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 }
جاوا
@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)
فعال کنید،
همانطور که در مثال زیر نشان داده شده است:
کاتلین
override fun onCreateSession(inputId: String): Session = onCreateSessionInternal(inputId).apply { setOverlayViewEnabled(true) sessions.add(this) }
جاوا
@Override public final Session onCreateSession(String inputId) { BaseTvInputSessionImpl session = onCreateSessionInternal(inputId); session.setOverlayViewEnabled(true); sessions.add(session); return session; }
از شیء View برای همپوشانی استفاده کنید که از TvInputService.Session.onCreateOverlayView() برگردانده شده است، همانطور که در اینجا نشان داده شده است:
کاتلین
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) } } }
جاوا
@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() callback را در شیء TvInputService.Session مدیریت میکند. کنترلهای والدین برنامه تلویزیون سیستم تعیین میکند که چه محتوایی، باتوجهبه ردهبندی محتوا، نمایش داده شود.
بخشهای زیر نحوه مدیریت انتخاب کانال و برنامه را بااستفاده از روشهای
TvInputService.Session notify که
با برنامه تلویزیون سیستم ارتباط برقرار میکنند شرح میدهد.
ویدیو دردسترس نباشد
وقتی کاربر کانال را تغییر میدهد، باید مطمئن شوید که صفحهنمایش هیچگونه مصنوعات ویدیویی سرگردانی را قبلاز اینکه ورودی تلویزیون محتوا را رندر کند، نمایش ندهد. وقتی TvInputService.Session.onTune() را فراخوانی میکنید،
میتوانید با فراخوانی TvInputService.Session.notifyVideoUnavailable()
و ارسال ثابت VIDEO_UNAVAILABLE_REASON_TUNING، از ارائه ویدیو جلوگیری کنید،
همانطور که در مثال زیر نشان داده شده است.
کاتلین
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 }
جاوا
@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()
را فراخوانی میکنید تا ویدیو نمایش داده شود، مانند این:
کاتلین
fun onRenderedFirstFrame(surface:Surface) { firstFrameDrawn = true notifyVideoAvailable() }
جاوا
@Override public void onRenderedFirstFrame(Surface surface) { firstFrameDrawn = true; notifyVideoAvailable(); }
این انتقال فقط کسری از ثانیه طول میکشد، اما ارائه صفحهنمایش خالی ازنظر بصری بهتر از این است که اجازه دهید تصویر چشمکهای عجیب و لرزش داشته باشد.
همچنین، برای اطلاعات بیشتر درباره کار با Surface برای رندر کردن ویدیو، ادغام پخشکننده با سطح را ببینید.
ارائه کنترل والدین
برای تعیین اینکه آیا محتوای خاصی توسط کنترلهای والدین و ردهبندی محتوا مسدود شده است یا نه، روشهای کلاس
TvInputManager، isParentalControlsEnabled()
و isRatingBlocked(android.media.tv.TvContentRating) را بررسی میکنید. همچنین
بهتر است مطمئن شوید TvContentRating محتوا در
مجموعهای از ردهبندیهای محتوای مجاز فعلی گنجانده شده باشد. این ملاحظات در نمونه زیر نشان داده شده است.
کاتلین
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) }
جاوا
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()
استفاده کنید، همانطور که در اینجا نشان داده شده است:
کاتلین
val rating = TvContentRating.createRating( "com.android.tv", "US_TV", "US_TV_PG", "US_TV_D", "US_TV_L" )
جاوا
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
بهعنوان شناسه قطعه ارسال میشود، این کار قطعه را لغو انتخاب میکند.
کاتلین
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
جاوا
@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; }