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.
![](https://developer.android.com/static/images/tv/do-not-attempt.png?authuser=5&hl=vi)
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()
và 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; }