TV のユーザー インタラクションを管理する

ライブ TV では、ユーザーがチャンネルを変えると、チャンネルとプログラムの情報が短時間表示されてから消えます。「ご自宅では試さないでください」といったメッセージ、字幕、広告などの他の種類の情報は、保持を必要とする場合があります。他の TV アプリと同様に、こういった情報は、画面で再生されているプログラム コンテンツに干渉しないようにする必要があります。

図 1. ライブ TV アプリのオーバーレイ メッセージ

また、コンテンツのレーティングと保護者による使用制限の設定を考慮して、特定のプログラム コンテンツを表示するかどうかを検討してください。コンテンツがブロックされている場合または利用できない場合、アプリがどのように動作し、どのようにユーザーに通知するかも検討してください。このレッスンでは、これらの点を考慮して 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;
    }