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