Mengelola interaksi pengguna TV

Dalam pengalaman menonton TV live, pengguna berganti saluran dan saluran dan program secara singkat sebelum informasi tersebut menghilang. Jenis informasi lainnya, seperti pesan ("JANGAN COBA DI RUMAH"), subtitel, atau iklan mungkin harus ada. Seperti pada 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 disajikan, mengingat setelan kontrol orang tua dan rating konten, serta perilaku aplikasi dan memberi tahu pengguna jika konten diblokir atau tidak tersedia. Pelajaran ini menjelaskan cara mengembangkan pengguna input TV pengalaman pengguna atas pertimbangan ini.

Cobalah Aplikasi contoh Layanan Input TV.

Mengintegrasikan pemutar dengan permukaan

Input TV Anda harus merender video ke objek Surface, yang diteruskan oleh TvInputService.Session.onSetSurface() . Berikut adalah contoh cara menggunakan instance MediaPlayer untuk memutar konten dalam 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 ini 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 default, 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. TV sistem kontrol orang tua pada aplikasi menentukan konten yang ditampilkan, berdasarkan rating kontennya. Bagian berikut menjelaskan cara mengelola pemilihan saluran dan program menggunakan TvInputService.Session metode notify yang berkomunikasi dengan aplikasi TV yang ada di sistem.

Menjadikan video tidak tersedia

Saat pengguna mengganti saluran, Anda ingin memastikan layar tidak menampilkan jaringan yang artefak video sebelum input TV Anda merender konten. Saat Anda memanggil TvInputService.Session.onTune(), Anda dapat mencegah video ditampilkan dengan memanggil TvInputService.Session.notifyVideoUnavailable() dan meneruskan konstanta VIDEO_UNAVAILABLE_REASON_TUNING, sebagai 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 ini:

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-kedip ganjil dan gelisah.

Lihat juga, Mengintegrasikan pemutar dengan permukaan untuk informasi selengkapnya tentang cara kerja dengan 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 Anda mungkin juga ingin memastikan TvContentRating konten disertakan dalam 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 Anda menentukan apakah konten harus diblokir atau tidak, beri tahu TV sistem aplikasi Anda dengan memanggil TvInputService.Session metode notifyContentAllowed() atau notifyContentBlocked() , seperti yang ditunjukkan dalam contoh sebelumnya.

Gunakan class TvContentRating untuk membuat string yang ditentukan sistem untuk COLUMN_CONTENT_RATING dengan atribut 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 seperti jenis trek (video, audio, atau subtitel) dan sebagainya.

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

Aplikasi TV yang ada pada sistem menyediakan antarmuka bagi pengguna untuk memilih jalur tertentu jika lebih dari satu jalur tersedia untuk jenis jalur tertentu; misalnya, subtitel dalam berbagai bahasa. TV Anda input merespons onSelectTrack() dari aplikasi TV sistem dengan memanggil notifyTrackSelected() , seperti yang ditunjukkan dalam contoh berikut. Perhatikan bahwa saat null diteruskan sebagai ID trek, sehingga 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;
}