إدارة تفاعل مستخدمي التلفزيون

في تجربة البث التلفزيوني المباشر، يغيّر المستخدم القنوات ويظهر له معلومات حول القناة والبرنامج لفترة وجيزة قبل اختفاء المعلومات. أنواع أخرى من المعلومات، مثل الرسائل ("يُرجى عدم محاولة الوصول في الصفحة الرئيسية") أو الترجمة أو الإعلانات. كما هي الحال مع أي جهاز تلفزيون فلا يجب أن تتداخل مثل هذه المعلومات مع محتوى البرنامج الذي يتم تشغيله على الشاشة.

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