在直播电视体验中,用户切换频道后会短暂显示频道和节目信息,然后信息会消失。其他类型的信息,如消息(“请勿在家尝试”)、字幕或广告可能需要持续显示。与任何 TV 应用一样,此类信息不应干扰屏幕上播放的节目内容。

图 1. 直播电视应用中的叠加消息。
此外,还要考虑根据内容的评级和家长控制设置,是否应显示某些节目内容,以及当内容被阻止或不可用时,您的应用如何表现并通知用户。本课将介绍如何为这些考量因素开发您的 TV 输入的用户体验。
尝试 TV 输入服务 示例应用。
将播放器与 Surface 集成
您的 TV 输入必须将视频渲染到 Surface
对象上,该对象由 TvInputService.Session.onSetSurface()
方法传递。以下是使用 MediaPlayer
实例在 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; }
同样,以下是使用 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; }
使用叠加层
使用叠加层来显示字幕、消息、广告或 MHEG-5 数据广播。默认情况下,叠加层处于停用状态。您可以在创建会话时通过调用 TvInputService.Session.setOverlayViewEnabled(true)
来启用它,示例如下
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; }
使用一个 View
对象作为叠加层,该对象从 TvInputService.Session.onCreateOverlayView()
返回,如下所示
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; }
叠加层的布局定义可能如下所示
<?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>
控制内容
当用户选择频道时,您的 TV 输入会在 TvInputService.Session
对象中处理 onTune()
回调。系统 TV 应用的家长控制会根据内容评级确定显示哪些内容。以下部分介绍了如何使用与系统 TV 应用通信的 TvInputService.Session
notify
方法来管理频道和节目选择。
使视频不可用
当用户切换频道时,您需要确保在您的 TV 输入渲染内容之前,屏幕不会显示任何杂散的视频伪影。当您调用 TvInputService.Session.onTune()
时,您可以通过调用 TvInputService.Session.notifyVideoUnavailable()
并传递 VIDEO_UNAVAILABLE_REASON_TUNING
常量来阻止视频显示,如以下示例所示。
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; }
然后,当内容渲染到 Surface
时,您调用 TvInputService.Session.notifyVideoAvailable()
允许视频显示,如下所示
Kotlin
fun onRenderedFirstFrame(surface:Surface) { firstFrameDrawn = true notifyVideoAvailable() }
Java
@Override public void onRenderedFirstFrame(Surface surface) { firstFrameDrawn = true; notifyVideoAvailable(); }
这种过渡只持续几分之一秒,但显示空白屏幕在视觉上比允许画面闪烁奇怪的斑点和抖动要好。
另请参阅 将播放器与 Surface 集成,了解有关使用 Surface
渲染视频的更多信息。
提供家长控制
要确定给定内容是否被家长控制和内容评级阻止,请检查 TvInputManager
类方法 isParentalControlsEnabled()
和 isRatingBlocked(android.media.tv.TvContentRating)
。您可能还需要确保内容的 TvContentRating
包含在当前允许的内容评级集中。这些考量因素在以下示例中显示。
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); }
一旦您确定内容是否应被阻止,通过调用 TvInputService.Session
方法 notifyContentAllowed()
或 notifyContentBlocked()
来通知系统 TV 应用,如上一个示例所示。
使用 TvContentRating
类通过 TvContentRating.createRating()
方法生成 COLUMN_CONTENT_RATING
的系统定义字符串,如下所示
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");
处理音轨选择
TvTrackInfo
类包含媒体音轨的信息,例如音轨类型(视频、音频或字幕)等。
您的 TV 输入会话首次能够获取音轨信息时,应调用 TvInputService.Session.notifyTracksChanged()
并传入所有音轨的列表,以更新系统 TV 应用。当音轨信息发生变化时,再次调用 notifyTracksChanged()
来更新系统。
如果给定音轨类型有多个音轨可用(例如,不同语言的字幕),系统 TV 应用会提供一个界面供用户选择特定的音轨。您的 TV 输入通过调用 notifyTrackSelected()
来响应系统 TV 应用的 onSelectTrack()
调用,如以下示例所示。请注意,当传入 null
作为音轨 ID 时,这会取消选择音轨。
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; }