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

Trong trải nghiệm truyền hình trực tuyến, người dùng thay đổi kênh và nhận được thông tin về kênh và chương trình trong thời gian ngắn trước khi thông tin đó biến mất. Các loại thông tin khác, chẳng hạn như tin nhắn ("KHÔNG NÊN TÌM THẤY Ở NHÀ RIÊNG"), phụ đề hoặc quảng cáo có thể cần được giữ lại. Giống như với bất kỳ ứng dụng truyền hình nào, thông tin đó không được làm ảnh hưởng đến nội dung của chương trình đang phát trên màn hình.

Hình 1. Thông báo dạng lớp phủ trong ứng dụng truyền hình trực tuyến.

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

Dùng thử ứng dụng mẫu TV Input Service (Dịch vụ đầu vào TV).

Tích hợp trình phát với nền tảng

Đầu vào TV của bạn phải kết xuất video trên đối tượng Surface (được phương thức TvInputService.Session.onSetSurface() truyền). Dưới đây là ví dụ về cách sử dụng thực thể 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ự, sau đây là cách thực hiện bằ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ụ đề, tin nhắn, quảng cáo hoặc nội dung truyền phát dữ liệu MHEG-5. Theo mặc định, lớp phủ bị tắt. Bạn có thể bật thuộc tính này 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ư minh hoạ dưới đâ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 về bố cục cho lớp phủ có thể giống như sau:

<?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 sẽ xử lý lệnh gọi lại onTune() trong đối tượng TvInputService.Session. Chế độ kiểm soát của cha mẹ trên ứng dụng TV hệ thống xác định nội dung nào sẽ hiển thị, dựa trên mức phân loại nội dung. Các phần sau 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 hệ thống.

Đặt video ở chế độ không hoạt động

Khi người dùng thay đổi kênh, bạn cần đảm bảo màn hình không hiển thị bất kỳ cấu phần phần mềm video nào bị lạc trước khi đầu vào TV hiển thị nội dung. Khi 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ư minh hoạ 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 kết xuất vào 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ỉ kéo dài trong các phân số của giây, nhưng việc hiển thị một màn hình trống sẽ hiệu quả hơn so với việc cho phép hình ảnh nhấp nháy các viền và nhiễu.

Xem thêm bài viết 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 để kết xuất video.

Cung cấp chế độ kiểm soát của cha mẹ

Để xác định xem một nội dung nhất định có bị chế độ kiểm soát của cha mẹ và mức phân loại nội dung chặn hay không, bạn cần kiểm tra các phương thức của lớp TvInputManager, isParentalControlsEnabled()isRatingBlocked(android.media.tv.TvContentRating). Cũng có thể bạn muốn đảm bảo TvContentRating của nội dung có trong tập hợp các mức phân loại nội dung hiện được cho phép. Những điểm cần cân nhắc này được thể hiện trong mẫu sau đây.

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

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

Sử dụng lớp TvContentRating để tạo chuỗi do hệ thống xác định cho COLUMN_CONTENT_RATING bằng phương thức TvContentRating.createRating(), như minh hoạ dưới đâ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ý lựa chọn bản nhạc

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

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

Ứng dụng truyền hình hệ thống cung cấp giao diện để người dùng chọn một bản nhạc cụ thể nếu có nhiều bản nhạc cho một loại bản nhạc nhất định; ví dụ: phụ đề bằng nhiều ngôn ngữ. Đầ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ư minh hoạ trong ví dụ sau. Lưu ý rằng khi null được chuyển dưới dạng mã kênh, thao tác này sẽ bỏ chọn kênh này.

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