ניהול האינטראקציה של משתמשים בטלוויזיה

בחוויית הטלוויזיה החיה, המשתמש מחליף ערוצים ומוצג לו מידע על הערוצים והתוכניות לזמן קצר לפני שהמידע נעלם. ייתכן שיהיה צורך להשאיר סוגים אחרים של מידע, כגון הודעות ("נא לא לנסות בבית"), כתוביות או פרסומות. כמו בכל אפליקציית טלוויזיה, מידע כזה לא אמור להפריע לתוכן התוכנית המופעל על המסך.

איור 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() כדי לעדכן את המערכת.

אפליקציית הטלוויזיה של המערכת מספקת ממשק למשתמש לבחירת רצועה ספציפית אם יותר מרצועה אחת זמינה עבור סוג רצועה נתון; לדוגמה, כתוביות בשפות שונות. קלט הטלוויזיה שלך מגיב לקריאה 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;
}