จัดการการโต้ตอบของผู้ใช้ทีวี

ในการรับชมรายการทีวีสด ผู้ใช้เปลี่ยนช่องและได้รับการนำเสนอด้วย ข้อมูลช่องและโปรแกรมเป็นเวลาสั้นๆ ก่อนที่ข้อมูลจะหายไป ข้อมูลประเภทอื่นๆ เช่น ข้อความ ("อย่าส่งมาที่บ้าน") คำบรรยาย หรือโฆษณาอาจต้องคงอยู่ตลอดไป เช่นเดียวกับทีวีทั่วไป ข้อมูลดังกล่าวไม่ควรรบกวนเนื้อหาโปรแกรมที่เล่นบนหน้าจอ

รูปที่ 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>

ควบคุมเนื้อหา

เมื่อผู้ใช้เลือกช่อง อินพุตทีวีจะจัดการ Callback onTune() ในออบเจ็กต์ TvInputService.Session ทีวีระบบ การควบคุมโดยผู้ปกครองของแอปจะเป็นตัวกำหนดเนื้อหาที่จะแสดงตามการจัดประเภทเนื้อหา ส่วนต่อไปนี้จะอธิบายวิธีจัดการการเลือกช่องและโปรแกรมโดยใช้ 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);
}

เมื่อคุณพิจารณาแล้วว่าควรบล็อกเนื้อหาดังกล่าวหรือไม่ควรบล็อก โปรดแจ้งทีวีของระบบ โดยการเรียกใช้ 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() พร้อมรายการแทร็กทั้งหมดเพื่ออัปเดตแอป System TV เมื่อมี คือการเปลี่ยนแปลงข้อมูลแทร็ก วันที่ notifyTracksChanged() อีกครั้งเพื่ออัปเดตระบบ

แอป System TV มีอินเทอร์เฟซให้ผู้ใช้เลือกแทร็กที่เฉพาะเจาะจงหากมีแทร็กมากกว่า 1 แทร็ก แทร็กพร้อมใช้งานสำหรับแทร็กประเภทที่กำหนด เช่น คำบรรยายในภาษาต่างๆ ทีวีของคุณ อินพุตจะตอบสนอง onSelectTrack() โทรจากแอป System TV โดยการโทร วันที่ 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;
}