في تجربة البث التلفزيوني المباشر، يغيّر المستخدم القنوات ويتم تزويده بمعلومات القناة والبرنامج لفترة وجيزة قبل اختفاء المعلومات. قد تحتاج إلى مواصلة عرض أنواع أخرى من المعلومات، مثل الرسائل ("يُرجى عدم زيارة الصفحة الرئيسية") أو الترجمة أو الإعلانات. وكما هو الحال مع أي تطبيق تلفزيون، يجب ألا تتداخل هذه المعلومات مع محتوى البرنامج الذي يتم تشغيله على الشاشة.
![](https://developer.android.com/static/images/tv/do-not-attempt.png?authuser=5&hl=ar)
الشكل 1. رسالة تظهر على سطح الفيديو في تطبيق بث تلفزيوني مباشر
ويجب أيضًا مراعاة ما إذا كان يجب تقديم محتوى معيّن في البرنامج، استنادًا إلى تقييم المحتوى وإعدادات الرقابة الأبوية، وطريقة سلوك تطبيقك وإبلاغ المستخدم عندما يكون المحتوى محظورًا أو غير متوفّر. يصف هذا الدرس كيفية تطوير تجربة مستخدم إدخال التلفزيون بناءً على هذه الاعتبارات.
جرِّب نموذج تطبيق خدمة إدخال التلفزيون.
دمج المشغّل مع السطح
يجب أن يعرض إدخال التلفزيون الفيديو في العنصر 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
. تحدد أدوات الرقابة الأبوية في تطبيق YouTube TV المحتوى الذي يتم عرضه وفقًا لتقييم المحتوى.
توضّح الأقسام التالية طريقة إدارة اختيار القناة والبرامج باستخدام طرق 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); }
بعد تحديد ما إذا كان يجب حظر المحتوى أو عدم حظره، أبلغ تطبيق YouTube 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()
مع توفير قائمة بجميع المقاطع الصوتية لتحديث تطبيق YouTube TV. وعندما يحدث
تغيير في معلومات المقاطع الصوتية، اتصل بـ
notifyTracksChanged()
مرة أخرى لتحديث النظام.
يوفّر تطبيق System TV واجهة للمستخدم لاختيار مقطع صوتي معيّن إذا توفّر أكثر من مقطع صوتي واحد لنوع معيّن، مثل الترجمة بلغات مختلفة. يستجيب
التلفزيون للمكالمة
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; }