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

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

الشكل 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;
}