Mengelola interaksi pengguna TV

Dalam pengalaman menonton TV live, pengguna akan berganti-ganti saluran dan informasi tentang saluran dan program akan ditampilkan secara singkat sebelum informasi tersebut hilang. Jenis informasi lainnya, seperti pesan ("JANGAN COBA DI RUMAH"), subtitel, atau iklan mungkin terus ada. Seperti halnya semua aplikasi TV, informasi tersebut tidak boleh mengganggu konten program yang diputar di layar.

Gambar 1. Pesan overlay pada aplikasi TV live.

Pertimbangkan juga apakah konten program tertentu harus ditampilkan berdasarkan setelan kontrol orang tua dan rating konten, serta cara aplikasi Anda berperilaku dan menginformasikan kepada pengguna jika konten tersebut diblokir atau tidak tersedia. Atas pertimbangan di atas, tutorial ini menjelaskan cara mengembangakan pengalaman pengguna input TV Anda.

Coba aplikasi contoh Layanan Input TV.

Mengintegrasikan pemutar dengan permukaan

Input TV Anda harus merender video ke objek Surface, yang diteruskan oleh metode TvInputService.Session.onSetSurface(). Berikut adalah contoh cara menggunakan instance MediaPlayer untuk memutar konten pada objek 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;
    }
    

Sama seperti di atas, ini adalah cara menggunakannya dengan 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;
    }
    

Menggunakan overlay

Gunakan overlay untuk menampilkan subtitel, pesan, iklan, atau siaran data MHEG-5. Secara default, overlay dinonaktifkan. Anda dapat mengaktifkannya saat membuat sesi dengan memanggil TvInputService.Session.setOverlayViewEnabled(true), seperti pada contoh berikut:

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

Gunakan objek View untuk overlay, yang ditampilkan dari TvInputService.Session.onCreateOverlayView(), seperti yang ditunjukkan di bawah ini:

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

Definisi tata letak untuk overlay mungkin terlihat seperti ini:

    <?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>
    

Mengontrol konten

Saat pengguna memilih saluran, input TV Anda akan menangani callback onTune() pada objek TvInputService.Session. Kontrol orang tua pada aplikasi TV sistem menentukan konten yang ditampilkan, berdasarkan rating kontennya. Bagian berikut menjelaskan cara mengelola pemilihan saluran dan program menggunakan metode TvInputService.Session notify yang berkomunikasi dengan aplikasi TV sistem.

Menjadikan video tidak tersedia

Saat pengguna mengganti saluran, pastikan layar tidak menampilkan artefak video terpisah sebelum input TV Anda merender konten. Saat memanggil TvInputService.Session.onTune(), Anda dapat mencegah video ditampilkan dengan memanggil TvInputService.Session.notifyVideoUnavailable() dan meneruskan konstanta VIDEO_UNAVAILABLE_REASON_TUNING, seperti yang ditampilkan pada contoh berikut.

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

Kemudian, jika konten dirender ke Surface, panggil TvInputService.Session.notifyVideoAvailable() guna mengizinkan video ditampilkan, seperti:

Kotlin

    fun onRenderedFirstFrame(surface:Surface) {
        firstFrameDrawn = true
        notifyVideoAvailable()
    }
    

Java

    @Override
    public void onRenderedFirstFrame(Surface surface) {
        firstFrameDrawn = true;
        notifyVideoAvailable();
    }
    

Transisi ini hanya berlangsung selama sepersekian detik, tetapi menampilkan layar kosong yang secara visual lebih baik daripada membiarkan gambar menjalankan flash jitter dan blip aneh.

Lihat juga, Mengintegrasikan pemutar dengan permukaan untuk mengetahui informasi selengkapnya tentang menggunakan Surface untuk merender video.

Menyediakan kontrol orang tua

Untuk menentukan apakah konten yang diberikan diblokir oleh kontrol orang tua dan rating konten, periksa metode class TvInputManager, isParentalControlsEnabled() dan isRatingBlocked(android.media.tv.TvContentRating). Anda mungkin juga ingin pastikan TvContentRating konten disertakan pada serangkaian rating konten yang saat ini diizinkan. Pertimbangan ini ditampilkan pada contoh berikut.

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

Setelah menentukan konten tersebut harus diblokir atau tidak, beri tahu aplikasi TV sistem dengan memanggil metode TvInputService.Session, notifyContentAllowed() atau notifyContentBlocked(), seperti yang ditampilkan pada contoh sebelumnya.

Gunakan class TvContentRating agar menghasilkan string yang ditentukan sistem untuk COLUMN_CONTENT_RATING dengan metode TvContentRating.createRating(), seperti yang ditampilkan di sini:

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");
    

Menangani pemilihan trek

Class TvTrackInfo menyimpan informasi tentang trek media seperti jenis trek (video, audio, atau subtitel) dan seterusnya.

Pertama kali sesi input TV Anda bisa mendapatkan informasi trek, sesi ini akan memanggil TvInputService.Session.notifyTracksChanged() dengan daftar semua trek untuk memperbarui aplikasi TV sistem. Jika ada perubahan pada informasi trek, panggil notifyTracksChanged() untuk memperbarui sistem.

Aplikasi TV sistem menyediakan antarmuka bagi pengguna untuk memilih trek tertentu jika lebih dari satu trek tersedia untuk jenis trek tersebut; misalnya subtitel dalam berbagai bahasa. Input TV Anda merespons panggilan onSelectTrack() dari aplikasi TV sistem dengan memanggil notifyTrackSelected(), seperti yang ditampilkan pada contoh berikut. Ingat bahwa jika null diteruskan sebagai ID trek, maka ini akan membatalkan pilihan trek tersebut.

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