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

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

איור 1. הודעה בשכבת-על באפליקציית טלוויזיה בשידור חי.

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

אפשר לנסות את אפליקציה לדוגמה של TV קלט Service.

שילוב הנגן עם משטח

קלט הטלוויזיה חייב לעבד את הסרטון לאובייקט 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>

שליטה בתוכן

כשהמשתמש בוחר ערוץ, קלט הטלוויזיה מטפל בקריאה החוזרת (callback) של 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);
}

אחרי שבחרת אם לחסום את התוכן או לא, עליך להודיע למערכת TV באמצעות קריאה אמצעי תשלום אחד (TvInputService.Session) notifyContentAllowed() או notifyContentBlocked() , כמו שאפשר לראות בדוגמה הקודמת.

משתמשים במחלקה TvContentRating כדי ליצור את המחרוזת המוגדרת על ידי המערכת עבור COLUMN_CONTENT_RATING עם TvContentRating.createRating() method, כפי שמוצג כאן:

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;
}