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.
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()
và 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; }