在電視直播中,使用者變更頻道,並在資訊消失前短暫顯示頻道和節目資訊。其他類型的資訊,例如訊息 (「請勿打擾」住家)、字幕或廣告等。與任何電視應用程式一樣,這些資訊不得乾擾螢幕上播放的節目內容。
此外,也請考量內容評分和家長監護設定是否應顯示特定計畫內容,以及應用程式會在內容封鎖或無法使用時的行為和通知使用者。本課程將說明如何針對這些注意事項開發電視輸入的使用者體驗。
歡迎試用 TV Input Service 範例應用程式。
將玩家與表面整合
電視輸入必須將影片算繪到 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>
控制內容
使用者選取頻道時,電視輸入會處理 TvInputService.Session
物件中的 onTune()
回呼。系統電視應用程式的家長監護功能會根據內容分級,決定要顯示哪些內容。以下各節說明如何使用 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
算繪影片,另請參閱「將播放器與介面整合」。
提供家長監護
如要判斷特定內容是否遭到家長監護功能和內容分級封鎖,請查看 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()
,通知系統電視應用程式,如上一個範例所示。
使用 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
類別會保留媒體曲目的相關資訊,例如音軌類型 (影片、音訊或字幕) 等。
當電視輸入工作階段首次能夠取得曲目資訊時,應呼叫 TvInputService.Session.notifyTracksChanged()
內含所有測試群組的清單,以更新系統 TV 應用程式。當測試群組資訊有所變更時,請再次呼叫 notifyTracksChanged()
以更新系統。
如果特定曲目類型有多首曲目可用 (例如不同語言的字幕),系統電視應用程式會提供介面,讓使用者選取特定音軌。電視輸入藉由呼叫 notifyTrackSelected()
回應來自系統電視應用程式的 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; }