ライブテレビ エクスペリエンスでは、ユーザーがチャンネルを変更し、情報が消える前にチャンネルと番組の情報が短時間表示されます。メッセージ(「自宅で試みないでください」)、字幕、広告など、他の種類の情報は保持しなければならない場合があります。他の TV アプリと同様に、こうした情報が画面上の番組コンテンツの再生を妨げないようにする必要があります。
また、コンテンツのレーティングと保護者による使用制限の設定に基づいて特定のプログラム コンテンツを表示すべきかどうかや、コンテンツがブロックされている場合やコンテンツを利用できない場合にアプリがどのように動作し、ユーザーに通知するかも検討してください。このレッスンでは、これらの点を考慮してテレビ入力のユーザー エクスペリエンスを開発する方法について説明します。
TV 入力サービスのサンプルアプリを試す。
プレーヤーを Surface と統合する
TV 入力は、TvInputService.Session.onSetSurface()
メソッドによって渡される Surface
オブジェクトに動画をレンダリングする必要があります。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(); }
この遷移はわずか 1 秒しか持続しませんが、空白の画面を表示する方が、不自然なメッセージやジッターを点滅させるよりも視覚的に優れています。
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; }