在直播电视体验中,用户切换频道并看到 频道和节目信息暂时消失不见。其他类型的信息 如消息(“请勿在家模仿”)、字幕或广告,您可能需要一直保留该状态。与任何电视一样 应用,此类信息不应干扰屏幕上播放的节目内容。
![](https://developer.android.google.cn/static/images/tv/do-not-attempt.png?authuser=7&hl=sk)
图 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; }
对叠加层使用从 TvInputService.Session.onCreateOverlayView()
返回的 View
对象,如下所示:
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
应用的家长控制功能会根据内容分级来决定显示哪些内容。
以下各部分介绍了如何使用
TvInputService.Session
notify
方法
与系统 TV 应用通信。
让视频无法播放
当用户更改频道时,您需要确保屏幕不会显示任何散乱的画面。
在 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 集成。
并使用 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); }
确定是否应屏蔽相应内容后,请通知系统 TV
调用
TvInputService.Session
方法 notifyContentAllowed()
或
notifyContentBlocked()
,如上例所示。
使用 TvContentRating
类生成系统定义的字符串
包含COLUMN_CONTENT_RATING
TvContentRating.createRating()
方法,如下所示:
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 应用会提供一个界面,以便用户在有多个轨道时选择特定轨道
指定轨道类型可以使用哪些轨道;例如不同语言的字幕你的电视
会响应
onSelectTrack()
从系统 TV 应用调用
notifyTrackSelected()
,如以下示例所示。请注意,当 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; }