في تجربة البث التلفزيوني المباشر، يغيّر المستخدم القنوات وتظهر له معلومات القناة والبرنامج لفترة وجيزة قبل أن تختفي. قد تحتاج إلى الاحتفاظ بأنواع أخرى من المعلومات، مثل الرسائل ("لا تحاول تنفيذ ذلك في المنزل") أو الترجمة أو الإعلانات. وكما هو الحال مع أي تطبيق تلفزيون، يجب ألا تتداخل هذه المعلومات مع محتوى البرنامج المعروض على الشاشة.
يجب أيضًا مراعاة ما إذا كان يجب عرض محتوى معيّن من البرنامج، وذلك وفقًا لتقييم المحتوى وإعدادات رقابة الأهل، وكيفية تصرّف تطبيقك وإعلام المستخدم عند حظر المحتوى أو عدم توفّره. توضّح هذه الدرس كيفية تطوير تجربة المستخدم الخاصة بإدخال التلفزيون مع مراعاة هذه الاعتبارات.
جرِّب تطبيق TV Input 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>
التحكّم في المحتوى
عندما يختار المستخدم قناة، يتعامل إدخال التلفزيون مع معاودة الاتصال 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()، كما هو موضّح هنا:
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; }