Quản lý tương tác của người dùng TV

Trong trải nghiệm xem truyền hình trực tiếp, người dùng chuyển kênh và được hiển thị thông tin về kênh và chương trình trong giây lát trước khi thông tin biến mất. Các loại thông tin khác, chẳng hạn như thông báo ("KHÔNG THỬ TẠI NHÀ"), phụ đề hoặc quảng cáo có thể cần được duy trì. Cũng như bất kỳ ứng dụng truyền hình nào khác, thông tin này không được gây cản trở đến nội dung chương trình đang phát trên màn hình.

Hình 1. Một thông báo phủ lên trên ứng dụng truyền hình trực tiếp.

Ngoài ra, hãy cân nhắc xem liệu nội dung chương trình cụ thể nào đó có nên được hiển thị hay không, dựa trên xếp hạng nội dung và cài đặt kiểm soát của phụ huynh, cũng như cách ứng dụng của bạn hoạt động và thông báo cho người dùng khi nội dung bị chặn hoặc không khả dụng. Bài học này mô tả cách phát triển trải nghiệm người dùng cho đầu vào TV của bạn, có tính đến những yếu tố trên.

Hãy thử ứng dụng mẫu Dịch vụ đầu vào TV.

Tích hợp người chơi với bề mặt

Đầu vào TV phải kết xuất video vào một đối tượng Surface do phương thức TvInputService.Session.onSetSurface() truyền. Đây là một ví dụ về cách sử dụng một thể hiện MediaPlayer để phát nội dung trong đối tượng 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;
}

Tương tự, đây là cách thực hiện bằng cách sử dụng 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;
}

Sử dụng lớp phủ

Sử dụng lớp phủ để hiển thị phụ đề, thông báo, quảng cáo hoặc dữ liệu phát sóng MHEG-5. Theo mặc định, lớp phủ này bị vô hiệu hóa. Bạn có thể kích hoạt nó khi tạo phiên bằng cách gọi TvInputService.Session.setOverlayViewEnabled(true), như trong ví dụ sau:

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

Sử dụng đối tượng View cho lớp phủ, được trả về từ TvInputService.Session.onCreateOverlayView(), như được hiển thị ở đây:

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

Định nghĩa bố cục cho lớp phủ có thể trông giống như thế này:

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

Kiểm soát nội dung

Khi người dùng chọn một kênh, đầu vào TV của bạn sẽ xử lý lệnh gọi lại onTune() trong đối tượng TvInputService.Session. Chức năng kiểm soát của phụ huynh trên ứng dụng TV của hệ thống sẽ quyết định nội dung nào được hiển thị, dựa trên xếp hạng nội dung. Các phần sau đây mô tả cách quản lý việc lựa chọn kênh và chương trình bằng các phương thức TvInputService.Session notify giao tiếp với ứng dụng TV của hệ thống.

Tắt video đi

Khi người dùng chuyển kênh, bạn cần đảm bảo màn hình không hiển thị bất kỳ hiện tượng nhiễu hình ảnh nào trước khi tín hiệu đầu vào của TV xử lý nội dung. Khi bạn gọi TvInputService.Session.onTune(), bạn có thể ngăn video hiển thị bằng cách gọi TvInputService.Session.notifyVideoUnavailable() và truyền hằng số VIDEO_UNAVAILABLE_REASON_TUNING, như được hiển thị trong ví dụ sau.

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

Sau đó, khi nội dung được hiển thị trên Surface, bạn gọi TvInputService.Session.notifyVideoAvailable() để cho phép video hiển thị, như sau:

Kotlin

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

Java

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

Quá trình chuyển đổi này chỉ diễn ra trong tích tắc, nhưng việc hiển thị màn hình trống về mặt hình ảnh sẽ tốt hơn so với việc để hình ảnh nhấp nháy và giật cục một cách kỳ lạ.

Xem thêm Tích hợp trình phát với bề mặt để biết thêm thông tin về cách làm việc với Surface để hiển thị video.

Cung cấp chức năng kiểm soát của phụ huynh

Để xác định xem một nội dung cụ thể có bị chặn bởi tính năng kiểm soát của phụ huynh và xếp hạng nội dung hay không, bạn kiểm tra các phương thức của lớp TvInputManager, isParentalControlsEnabled()isRatingBlocked(android.media.tv.TvContentRating). Bạn cũng có thể muốn đảm bảo rằng nội dung TvContentRating nằm trong danh sách các xếp hạng nội dung được cho phép hiện tại. Những cân nhắc này được thể hiện trong ví dụ sau.

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

Sau khi bạn đã xác định xem nội dung đó có nên bị chặn hay không, hãy thông báo cho ứng dụng TV của hệ thống bằng cách gọi phương thức TvInputService.Session notifyContentAllowed() hoặc notifyContentBlocked() như trong ví dụ trước.

Sử dụng lớp TvContentRating để tạo chuỗi do hệ thống định nghĩa cho COLUMN_CONTENT_RATING bằng phương thức TvContentRating.createRating(), như được hiển thị ở đây:

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

Xử lý việc lựa chọn bài hát

Lớp TvTrackInfo chứa thông tin về các bản nhạc đa phương tiện như loại bản nhạc (video, âm thanh hoặc phụ đề), v.v.

Lần đầu tiên phiên nhập dữ liệu TV của bạn có thể lấy thông tin bài hát, nó sẽ gọi TvInputService.Session.notifyTracksChanged() với danh sách tất cả các bài hát để cập nhật ứng dụng TV hệ thống. Khi có thay đổi về thông tin bài hát, hãy gọi notifyTracksChanged() một lần nữa để cập nhật hệ thống.

Ứng dụng TV của hệ thống cung cấp giao diện cho người dùng chọn một bản nhạc cụ thể nếu có nhiều hơn một bản nhạc khả dụng cho cùng một loại bản nhạc; ví dụ: phụ đề bằng nhiều ngôn ngữ khác nhau. Đầu vào TV của bạn phản hồi lệnh gọi onSelectTrack() từ ứng dụng TV hệ thống bằng cách gọi notifyTrackSelected() , như được hiển thị trong ví dụ sau. Lưu ý rằng khi null được truyền làm ID bài hát, thao tác này bỏ chọn bài hát.

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