Mengelola interaksi pengguna TV

Dalam pengalaman menonton TV live, pengguna akan berganti-ganti saluran dan informasi saluran dan program akan ditampilkan secara singkat sebelum informasi tersebut hilang. Jenis informasi lainnya, seperti pesan ("JANGAN COBA DI RUMAH"), subtitel, atau iklan mungkin harus dipertahankan. Seperti aplikasi TV lainnya, 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 bagaimana aplikasi Anda berperilaku dan memberi tahu pengguna saat konten diblokir atau tidak tersedia. Tutorial ini menjelaskan cara mengembangkan pengalaman pengguna input TV Anda atas pertimbangan tersebut.

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

Demikian pula, berikut cara melakukannya menggunakan 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 {i>default<i}, overlay dinonaktifkan. Anda dapat mengaktifkannya saat membuat sesi dengan memanggil TvInputService.Session.setOverlayViewEnabled(true), seperti dalam 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 sini:

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() di objek TvInputService.Session. Kontrol orang tua aplikasi TV sistem menentukan konten yang ditampilkan, berdasarkan rating konten. 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 mengubah saluran, Anda ingin memastikan layar tidak menampilkan artefak video terpisah sebelum input TV 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 ditunjukkan dalam 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, saat konten dirender ke Surface, panggil TvInputService.Session.notifyVideoAvailable() untuk mengizinkan video ditampilkan, seperti berikut:

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 secara visual lebih baik daripada membiarkan gambar berkedip dan berkedip yang 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 tertentu diblokir oleh kontrol orang tua dan rating konten, periksa metode class TvInputManager, isParentalControlsEnabled(), dan isRatingBlocked(android.media.tv.TvContentRating). Anda mungkin juga ingin memastikan bahwa TvContentRating konten disertakan dalam kumpulan 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 apakah konten harus diblokir atau tidak boleh diblokir, beri tahu aplikasi TV sistem dengan memanggil metode TvInputService.Session notifyContentAllowed() atau notifyContentBlocked(), seperti yang ditunjukkan pada contoh sebelumnya.

Gunakan class TvContentRating untuk 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.

Saat pertama kali sesi input TV Anda bisa mendapatkan informasi trek, sesi harus memanggil TvInputService.Session.notifyTracksChanged() dengan daftar semua trek untuk mengupdate aplikasi TV sistem. Jika ada perubahan informasi trek, panggil notifyTracksChanged() lagi untuk mengupdate sistem.

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

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