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 thấy 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, như tin nhắn ("ĐỪNG CHÚ Ý TẠI NHÀ"), phụ đề hoặc quảng cáo có thể cần phải duy trì. Giống như mọi TV Thông tin ứng dụng, thông tin như vậy không được cản trở nội dung 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 một ứ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 nội dung chương trình nhất định hay không, căn cứ vào mức phân loại nội dung và chế độ kiểm soát của cha mẹ 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 người dùng đầu vào TV trải nghiệm của người dùng về những điểm cần cân nhắc này.

Dùng thử Ứng dụng mẫu TV Input Service.

Tích hợp trình phát với giao diện

Đầu vào TV phải kết xuất video vào đối tượng Surface, đối tượng này được truyền qua TvInputService.Session.onSetSurface() . Sau đây là ví dụ về cách sử dụng thực thể MediaPlayer để chơi 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ự, dưới đây là cách thực hiện việc này 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ụ đề, thông điệp, quảng cáo hoặc truyền dữ liệu MHEG-5. Theo mặc định, lớp phủ bị vô hiệu hoá. Bạn có thể bật chế độ 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 bố cục cho lớp phủ có thể có dạ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. TV hệ thống chế độ kiểm soát của cha mẹ trong ứng dụng sẽ xác định nội dung hiển thị dựa trên mức phân loại 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 TvInputService.Session phương thức notify mà giao tiếp với ứng dụng TV của 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ỳ vị trí nào khác cấu phần phần mềm video trước khi đầu vào TV kết xuất nội dung. Khi bạn gọi TvInputService.Session.onTune(), bạn có thể ngăn việc hiện video bằng cách gọi TvInputService.Session.notifyVideoUnavailable() và truyền hằng số VIDEO_UNAVAILABLE_REASON_TUNING, như là như 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 cho Surface, bạn sẽ gọi TvInputService.Session.notifyVideoAvailable() để cho phép video hiển thị, chẳng hạn như:

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 hiển thị một màn hình trống trực quan hơn so với việc để hình ảnh nhấp nháy và biến động bất thường.

Xem thêm bài viết Tích hợp trình phát với nền tảng để biết thêm thông tin về cách xử lý với Surface để kết xuất video.

Cung cấp quyền kiểm soát của cha mẹ

Để xác định xem một nội dung cụ thể có bị chặn bởi chế độ kiểm soát của cha mẹ và mức phân loại nội dung hay không, bạn có thể chọn Phương thức của lớp TvInputManager, isParentalControlsEnabled()isRatingBlocked(android.media.tv.TvContentRating). Bạn bạn cũng nên đảm bảo TvContentRating của nội dung được đưa vào tập hợp 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.

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 đã xác định xem có nên chặn nội dung hay không, hãy thông báo cho TV hệ thống bằng cách gọi hàm TvInputService.Session phương thức notifyContentAllowed() hoặc notifyContentBlocked() , như được trình bày 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 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 chứa thông tin về các bản nhạc đa phương tiện, chẳng hạn như dưới dạng 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 đầu vào 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 ở đó là sự thay đổi trong thông tin theo dõi, cuộc gọi notifyTracksChanged() để cập nhật hệ thống một lần nữa.

Ứng dụng truyền hình hệ thống cung cấp giao diện để người dùng chọn một kênh cụ thể nếu có nhiều kênh bản nhạc có sẵn cho một loại bản nhạc nhất định; ví dụ: phụ đề bằng nhiều ngôn ngữ. TV của bạn dữ liệu đầu vào phản hồi với onSelectTrack() cuộc gọi từ ứng dụng TV hệ thống bằng cách gọi notifyTrackSelected() , như được trình bày trong ví dụ sau. Lưu ý rằng khi null được chuyển dưới dạng mã nhận dạng bản nhạc, thì thao tác này sẽ bỏ chọn kênh đó.

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