Android TV アプリをキャスト対応にする

1. 概要

Google Cast のロゴ

この Codelab では、既存の Android TV アプリを変更して、既存の Cast センダーアプリからのキャストと通信をサポートする方法について説明します。

Google Cast と Cast Connect とは

Google Cast では、ユーザーはモバイル デバイスからテレビにコンテンツをキャストできます。一般的な Google Cast セッションは、センダーアプリとレシーバー アプリという 2 つのコンポーネントで構成されます。モバイルアプリやウェブサイト(YouTube.com など)のようなセンダーアプリは、Cast レシーバー アプリの再生を開始し、制御します。Cast レシーバー アプリは、Chromecast デバイスと Android TV デバイスで動作する HTML 5 アプリです。

Cast セッションの状態の大部分はレシーバー アプリに保存されます。新しいメディア アイテムが読み込まれた場合など、状態が更新されると、メディア ステータスがすべてのセンダーにブロードキャストされます。こうしたブロードキャストには、Cast セッションの現在の状態が含まれます。センダーアプリは、このメディア ステータスを使用して再生情報を UI に表示します。

Cast Connect はこのインフラストラクチャ上に構築されており、Android TV アプリがレシーバーとして機能します。Cast Connect ライブラリを使用すると、Cast レシーバー アプリのように、Android TV アプリでメッセージを受信でき、メディア ステータスをブロードキャストできます。

達成目標

この Codelab を完了すると、Cast センダーアプリを使用して Android TV アプリに動画をキャストできるようになります。Android TV アプリは、Cast プロトコルを介してセンダーアプリと通信することもできます。

学習内容

  • サンプル ATV アプリに Cast Connect ライブラリを追加する方法。
  • Cast センダーに接続して ATV アプリを起動する方法。
  • Cast センダーアプリから ATV アプリでメディア再生を開始する方法。
  • ATV アプリから Cast センダーアプリにメディア ステータスを送信する方法。

必要なもの

2. サンプルコードを取得する

サンプルコードはすべてパソコンにダウンロードできます。

ダウンロードした ZIP ファイルを解凍します。

3. サンプルアプリを実行する

まず、完成したサンプルアプリがどのようなものか見てみましょう。Android TV アプリは、Leanback UI と基本的な動画プレーヤーを使用します。ユーザーはリストから動画を選択し、テレビで再生できます。付属のモバイル センダーアプリを使用して、Android TV アプリに動画をキャストすることもできます。

動画の全画面プレビューの上に重ねて表示されている一連の動画のサムネイル(1 つがハイライト表示されている)の画像。右上に「Cast Connect」という文字が表示されている

デベロッパー デバイスを登録する

アプリ開発で Cast Connect 機能を有効にするには、Cast Developer Console で使用する Android TV デバイスの内蔵 Chromecast のシリアル番号を登録する必要があります。シリアル番号は、Android TV で [設定] > [デバイス設定] > [Chromecast built-in] > [シリアル番号] に移動すると確認できます。物理デバイスのシリアル番号とは異なるため、この方法で取得する必要があります。

[Chromecast built-in] 画面、バージョン番号、シリアル番号が表示された Android TV 画面の画像

登録しないと、セキュリティ上の理由により、Cast Connect は Google Play ストアからインストールしたアプリでしか動作しません。登録プロセスを開始してから 15 分後に、デバイスを再起動します。

Android センダーアプリをインストールする

モバイル デバイスからのリクエストの送信をテストするために、Google では「動画キャスト」というシンプルな送信アプリをソースコードの zip ダウンロード内に mobile-sender-0629.apk ファイルとして提供しています。ADB を利用して APK をインストールします。すでに別のバージョンの Cast Videos をインストールしている場合は、続行する前に、デバイスにあるすべてのプロファイルからそのバージョンをアンインストールしてください。

  1. Android スマートフォンで開発者向けオプションと USB デバッグを有効にします
  2. USB データケーブルを差し込んで Android スマートフォンを開発用パソコンに接続します。
  3. Android スマートフォンに mobile-sender-0629.apk をインストールします。

adb install コマンドを実行して mobile-sender.apk をインストールするターミナル ウィンドウの画像

  1. Android スマートフォンで Cast Videos センダーアプリを確認できます。Cast Videos センダーアプリのアイコン

Android スマートフォンの画面で実行されている Cast Videos センダーアプリの画像

Android TV アプリをインストールする

完成したサンプルアプリを Android Studio で開いて実行する手順は次のとおりです。

  1. ウェルカム画面で [Import Project] を選択するか、[File] > [New] > [Import Project...] メニュー オプションを選択します。
  2. サンプルコード フォルダから フォルダ アイコンapp-done ディレクトリを選択し、[OK] をクリックします。
  3. [File] > Android App Studio の [Sync Project with Gradle] ボタン [Sync Project with Gradle Files] をクリックします。
  4. Android TV デバイスで開発者向けオプションと USB デバッグを有効にします
  5. ADB を Android TV デバイスに接続すると、そのデバイスが Android Studio に表示されます。Android Studio ツールバーに表示された Android TV デバイスを示す画像
  6. Android Studio の実行ボタン(右を向いた緑色の三角形)実行ボタンをクリックすると、数秒後に Cast Connect Codelab という ATV アプリが表示されます。

ATV アプリで Cast Connect を再生してみる

  1. Android TV のホーム画面に移動します。
  2. Android スマートフォンから Cast Videos センダーアプリを開きます。キャスト アイコン キャスト ボタンのアイコン をクリックし、ATV デバイスを選択します。
  3. ATV で Cast Connect Codelab ATV アプリが起動し、接続済みであることがセンダーのキャスト アイコンに示されます(色が反転したキャストボタン アイコン)。
  4. ATV アプリから動画を選択すると、その動画が ATV で再生されます。
  5. スマートフォンで、センダーアプリの下部にミニ コントローラが表示されます。再生と一時停止のボタンを使用して再生をコントロールできます。
  6. スマートフォンで動画を選択して再生します。ATV で動画が再生され、モバイル センダーに拡張コントローラが表示されます。
  7. スマートフォンをロックしてロック解除すると、ロック画面に通知が表示され、メディアの再生の操作や、キャストの停止が行えます。

動画を再生しているミニプレーヤーが表示された Android スマートフォンの画面の一部

4. 開始プロジェクトを準備する

完成したアプリの Cast Connect 統合を確認したので、ダウンロードした開始用アプリに Cast Connect のサポートを追加する必要があります。これで、Android Studio を使ってスターター プロジェクト上に構築する準備が整いました。

  1. ウェルカム画面で [Import Project] を選択するか、[File] > [New] > [Import Project...] メニュー オプションを選択します。
  2. サンプルコード フォルダから フォルダ アイコンapp-start ディレクトリを選択し、[OK] をクリックします。
  3. [File] > Android Studio の [Sync Project with Gradle] ボタン [Sync Project with Gradle Files] をクリックします。
  4. ATV デバイスを選択して Android Studio の実行ボタン(右向きの緑色の三角形)実行 ボタンをクリックし、アプリを実行して UI を確認します。選択した Android TV デバイスが表示されている Android Studio のツールバー

動画の全画面プレビューに重ねて表示された一連の動画サムネイル(1 つがハイライト表示されている)の画像。右上に「キャスト コネクト」という単語が表示されています。

アプリの設計

このアプリは、ユーザーが視聴できる動画のリストを提示します。ユーザーは Android TV で再生する動画を選択できます。アプリは、MainActivityPlaybackActivity という 2 つの主要なアクティビティで構成されます。

MainActivity

このアクティビティにはフラグメント(MainFragment)が含まれています。動画とその関連メタデータのリストは MovieList クラスで構成され、setupMovies() メソッドは Movie オブジェクトのリストを作成するために呼び出されます。

Movie オブジェクトは、タイトル、説明、画像のサムネイル、動画の URL で動画エンティティを表します。各 Movie オブジェクトは、タイトルとスタジオで動画サムネイルを表示するために CardPresenter にバインドされ、ArrayObjectAdapter に渡されます。

アイテムを選択すると、対応する Movie オブジェクトが PlaybackActivity に渡されます。

PlaybackActivity

このアクティビティには、ExoPlayer を使用して VideoView をホストするフラグメント(PlaybackVideoFragment)、メディア コントロール、選択した動画の説明を表示するテキスト領域が含まれ、ユーザーは Android TV で動画を再生できます。ユーザーはリモコンを使用して、動画の再生と一時停止や、再生位置の移動ができます。

Cast Connect の前提条件

Cast Connect は新しいバージョンの Google Play 開発者サービスを使用するため、ATV アプリが AndroidX 名前空間を使用するように更新されている必要があります。

Android TV アプリで Cast Connect をサポートするには、メディア セッションからのイベントを作成してサポートする必要があります。Cast Connect ライブラリは、メディア セッションのステータスに基づいてメディア ステータスを生成します。メディア セッションは Cast Connect ライブラリでも使用され、一時停止など、センダーから特定のメッセージを受信したことを通知します。

5. キャスト サポートの設定

依存関係

必要なライブラリ依存関係を含めるようにアプリの build.gradle ファイルを更新します。

dependencies {
    ....

    // Cast Connect libraries
    implementation 'com.google.android.gms:play-services-cast-tv:20.0.0'
    implementation 'com.google.android.gms:play-services-cast:21.1.0'
}

プロジェクトを同期して、エラーなくプロジェクトがビルドされていることを確認します。

初期化

CastReceiverContext は、すべての Cast インタラクションを調整するためのシングルトン オブジェクトです。CastReceiverContext が初期化される際に CastReceiverOptions を提供するには、ReceiverOptionsProvider インターフェースを実装する必要があります。

CastReceiverOptionsProvider.kt ファイルを作成し、次のクラスをプロジェクトに追加します。

package com.google.sample.cast.castconnect

import android.content.Context
import com.google.android.gms.cast.tv.ReceiverOptionsProvider
import com.google.android.gms.cast.tv.CastReceiverOptions

class CastReceiverOptionsProvider : ReceiverOptionsProvider {
    override fun getOptions(context: Context): CastReceiverOptions {
        return CastReceiverOptions.Builder(context)
                .setStatusText("Cast Connect Codelab")
                .build()
    }
}

次に、アプリの AndroidManifest.xml ファイルの <application> タグ内でレシーバー オプション プロバイダを指定します。

<application>
  ...
  <meta-data
    android:name="com.google.android.gms.cast.tv.RECEIVER_OPTIONS_PROVIDER_CLASS_NAME"
    android:value="com.google.sample.cast.castconnect.CastReceiverOptionsProvider" />
</application>

Cast センダーから ATV アプリに接続するには、起動するアクティビティを選択します。この Codelab では、Cast セッションの開始時にアプリの MainActivity を起動します。AndroidManifest.xml ファイルで、MainActivity に起動インテント フィルタを追加します。

<activity android:name=".MainActivity">
  ...
  <intent-filter>
    <action android:name="com.google.android.gms.cast.tv.action.LAUNCH" />
    <category android:name="android.intent.category.DEFAULT" />
  </intent-filter>
</activity>

Cast レシーバー コンテキストのライフサイクル

アプリを起動したときに CastReceiverContext を開始し、アプリをバックグラウンドに移行したときに CastReceiverContext を停止する必要があります。androidx.Lifecycle ライブラリLifecycleObserver を使用して、CastReceiverContext.start()CastReceiverContext.stop() の呼び出しを管理することをおすすめします。

MyApplication.kt ファイルを開き、アプリの onCreate メソッドで initInstance() を呼び出してキャスト コンテキストを初期化します。AppLifeCycleObserver クラスでは、アプリが再開するときに CastReceiverContextstart() し、アプリが一時停止するときに stop() します。

package com.google.sample.cast.castconnect

import com.google.android.gms.cast.tv.CastReceiverContext
...

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        CastReceiverContext.initInstance(this)
        ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleObserver())
    }

    class AppLifecycleObserver : DefaultLifecycleObserver {
        override fun onResume(owner: LifecycleOwner) {
            Log.d(LOG_TAG, "onResume")
            CastReceiverContext.getInstance().start()
        }

        override fun onPause(owner: LifecycleOwner) {
            Log.d(LOG_TAG, "onPause")
            CastReceiverContext.getInstance().stop()
        }
    }
}

MediaSession を MediaManager に接続する

MediaManagerCastReceiverContext シングルトンのプロパティであり、メディア ステータスを管理して読み込みインテントを処理します。また、センダーからのメディア名前空間メッセージをメディア コマンドに変換し、メディア ステータスをセンダーに送り返します。

MediaSession を作成するときは、現在の MediaSession トークンを MediaManager に提供する必要もあります。これにより、コマンドの送信先を把握し、メディアの再生状態を取得できます。PlaybackVideoFragment.kt ファイルで、トークンを MediaManager に設定する前に MediaSession が初期化されていることを確認します。

import com.google.android.gms.cast.tv.CastReceiverContext
import com.google.android.gms.cast.tv.media.MediaManager
...

class PlaybackVideoFragment : VideoSupportFragment() {
    private var castReceiverContext: CastReceiverContext? = null
    ...

    private fun initializePlayer() {
        if (mPlayer == null) {
            ...
            mMediaSession = MediaSessionCompat(getContext(), LOG_TAG)
            ...
            castReceiverContext = CastReceiverContext.getInstance()
            if (castReceiverContext != null) {
                val mediaManager: MediaManager = castReceiverContext!!.getMediaManager()
                mediaManager.setSessionCompatToken(mMediaSession!!.getSessionToken())
            }

        }
    }
}

再生がアクティブでないために MediaSession をリリースする場合は、MediaManager で null トークンを設定する必要があります。

private fun releasePlayer() {
    mMediaSession?.release()
    castReceiverContext?.mediaManager?.setSessionCompatToken(null)
    ...
}

サンプルアプリを実行してみる

Android Studio の [Run] ボタン(右を指す緑色の三角形)実行ボタンをクリックしてアプリを ATV デバイスにデプロイし、アプリを閉じて ATV のホーム画面に戻ります。センダーでキャスト アイコン キャスト ボタンのアイコン をクリックし、ATV デバイスを選択します。ATV デバイスで ATV アプリが起動し、キャスト アイコンが接続状態になります。

6. メディアの読み込み

読み込みコマンドは、Developer Console で定義したパッケージ名のインテントを介して送信されます。このインテントを受け取るターゲット アクティビティを指定するには、次の事前定義されたインテント フィルタを Android TV アプリに追加する必要があります。AndroidManifest.xml ファイルで、読み込みインテント フィルタを PlayerActivity に追加します。

<activity android:name="com.google.sample.cast.castconnect.PlaybackActivity"
          android:launchMode="singleTask"
          android:exported="true">
  <intent-filter>
     <action android:name="com.google.android.gms.cast.tv.action.LOAD"/>
     <category android:name="android.intent.category.DEFAULT" />
  </intent-filter>
</activity>

Android TV での読み込みリクエストの処理

読み込みリクエストを含むインテントを受け取るようにアクティビティが構成されたので、これを処理する必要があります。

アプリは、アクティビティの開始時に processIntent というプライベート メソッドを呼び出します。このメソッドには、受信インテントを処理するロジックが含まれます。読み込みリクエストを処理するには、このメソッドを変更し、MediaManager インスタンスの onNewIntent メソッドを呼び出して、さらに処理するインテントを送信します。MediaManager は、インテントが読み込みリクエストであることを検出すると、インテントから MediaLoadRequestData オブジェクトを抽出し、MediaLoadCommandCallback.onLoad() を呼び出します。読み込みリクエストを含むインテントを処理するように、PlaybackVideoFragment.kt ファイルの processIntent メソッドを変更します。

fun processIntent(intent: Intent?) {
    val mediaManager: MediaManager = CastReceiverContext.getInstance().getMediaManager()
    // Pass intent to Cast SDK
    if (mediaManager.onNewIntent(intent)) {
        return
    }

    // Clears all overrides in the modifier.
    mediaManager.getMediaStatusModifier().clear()

    // If the SDK doesn't recognize the intent, handle the intent with your own logic.
    ...
}

次に、MediaManager によって呼び出される onLoad() メソッドをオーバーライドする抽象クラス MediaLoadCommandCallback を拡張します。このメソッドは、読み込みリクエストのデータを受け取って、Movie オブジェクトに変換します。変換後、ローカル プレーヤーでムービーが再生されます。その後、MediaManagerMediaLoadRequest で更新され、接続されているセンダーに MediaStatus をブロードキャストします。PlaybackVideoFragment.kt ファイルに、MyMediaLoadCommandCallback というネストされたプライベート クラスを作成します。

import com.google.android.gms.cast.MediaLoadRequestData
import com.google.android.gms.cast.MediaInfo
import com.google.android.gms.cast.MediaMetadata
import com.google.android.gms.cast.MediaError
import com.google.android.gms.cast.tv.media.MediaException
import com.google.android.gms.cast.tv.media.MediaCommandCallback
import com.google.android.gms.cast.tv.media.QueueUpdateRequestData
import com.google.android.gms.cast.tv.media.MediaLoadCommandCallback
import com.google.android.gms.tasks.Task
import com.google.android.gms.tasks.Tasks
import android.widget.Toast
...

private inner class MyMediaLoadCommandCallback :  MediaLoadCommandCallback() {
    override fun onLoad(
        senderId: String?, mediaLoadRequestData: MediaLoadRequestData): Task<MediaLoadRequestData> {
        Toast.makeText(activity, "onLoad()", Toast.LENGTH_SHORT).show()
        return if (mediaLoadRequestData == null) {
            // Throw MediaException to indicate load failure.
            Tasks.forException(MediaException(
                MediaError.Builder()
                    .setDetailedErrorCode(MediaError.DetailedErrorCode.LOAD_FAILED)
                    .setReason(MediaError.ERROR_REASON_INVALID_REQUEST)
                    .build()))
        } else Tasks.call {
            play(convertLoadRequestToMovie(mediaLoadRequestData)!!)
            // Update media metadata and state
            val mediaManager = castReceiverContext!!.mediaManager
            mediaManager.setDataFromLoad(mediaLoadRequestData)
            mediaLoadRequestData
        }
    }
}

private fun convertLoadRequestToMovie(mediaLoadRequestData: MediaLoadRequestData?): Movie? {
    if (mediaLoadRequestData == null) {
        return null
    }
    val mediaInfo: MediaInfo = mediaLoadRequestData.getMediaInfo() ?: return null
    var videoUrl: String = mediaInfo.getContentId()
    if (mediaInfo.getContentUrl() != null) {
        videoUrl = mediaInfo.getContentUrl()
    }
    val metadata: MediaMetadata = mediaInfo.getMetadata()
    val movie = Movie()
    movie.videoUrl = videoUrl
    movie.title = metadata?.getString(MediaMetadata.KEY_TITLE)
    movie.description = metadata?.getString(MediaMetadata.KEY_SUBTITLE)
    if(metadata?.hasImages() == true) {
        movie.cardImageUrl = metadata.images[0].url.toString()
    }
    return movie
}

コールバックが定義されたので、MediaManager に登録する必要があります。コールバックは、MediaManager.onNewIntent() を呼び出す前に登録する必要があります。プレーヤーの初期化時に、setMediaLoadCommandCallback を追加します。

private fun initializePlayer() {
    if (mPlayer == null) {
        ...
        mMediaSession = MediaSessionCompat(getContext(), LOG_TAG)
        ...
        castReceiverContext = CastReceiverContext.getInstance()
        if (castReceiverContext != null) {
            val mediaManager: MediaManager = castReceiverContext.getMediaManager()
            mediaManager.setSessionCompatToken(mMediaSession.getSessionToken())
            mediaManager.setMediaLoadCommandCallback(MyMediaLoadCommandCallback())
        }
    }
}

サンプルアプリを実行してみる

Android Studio の [Run] ボタン(右を指す緑色の三角形) [Run] ボタンをクリックして、ATV デバイスにアプリをデプロイします。送信者からキャスト アイコン キャスト ボタンのアイコン をクリックし、ATV デバイスを選択します。ATV デバイスで ATV アプリが起動します。モバイルで動画を選択すると、その動画が ATV で再生されます。再生コントロールがあるスマートフォンで、通知を受信するかどうか確認します。コントロールを使用してみます(一時停止など。ATV デバイスの動画が一時停止するはずです)。

7. Cast コントロール コマンドのサポート

これで、アプリがメディア セッションと互換性のある基本的なコマンド(再生、一時停止、移動など)をサポートするようになりました。ただし、メディア セッションでは使用できない Cast コントロール コマンドがあります。Cast コントロール コマンドをサポートするには、MediaCommandCallback を登録する必要があります。

プレーヤーの初期化時に setMediaCommandCallback を使用して MediaManager インスタンスに MyMediaCommandCallback を追加します。

private fun initializePlayer() {
    ...
    castReceiverContext = CastReceiverContext.getInstance()
    if (castReceiverContext != null) {
        val mediaManager = castReceiverContext!!.mediaManager
        ...
        mediaManager.setMediaCommandCallback(MyMediaCommandCallback())
    }
}

MyMediaCommandCallback クラスを作成して、onQueueUpdate() などのメソッドをオーバーライドし、Cast コントロール コマンドをサポートします。

private inner class MyMediaCommandCallback : MediaCommandCallback() {
    override fun onQueueUpdate(
        senderId: String?,
        queueUpdateRequestData: QueueUpdateRequestData
    ): Task<Void> {
        Toast.makeText(getActivity(), "onQueueUpdate()", Toast.LENGTH_SHORT).show()
        // Queue Prev / Next
        if (queueUpdateRequestData.getJump() != null) {
            Toast.makeText(
                getActivity(),
                "onQueueUpdate(): Jump = " + queueUpdateRequestData.getJump(),
                Toast.LENGTH_SHORT
            ).show()
        }
        return super.onQueueUpdate(senderId, queueUpdateRequestData)
    }
}

8. メディア ステータスの使用

メディア ステータスの変更

Cast Connect はメディア セッションから基本メディア ステータスを取得します。高度な機能をサポートするために、Android TV アプリは MediaStatusModifier を介して追加のステータス プロパティを指定し、オーバーライドできます。MediaStatusModifier は、CastReceiverContext で設定した MediaSession で常に動作します。

たとえば、onLoad コールバックがトリガーされたときに setMediaCommandSupported を指定するには、次のようにします。

import com.google.android.gms.cast.MediaStatus
...
private class MyMediaLoadCommandCallback : MediaLoadCommandCallback() {
    fun onLoad(
        senderId: String?,
        mediaLoadRequestData: MediaLoadRequestData
    ): Task<MediaLoadRequestData> {
        Toast.makeText(getActivity(), "onLoad()", Toast.LENGTH_SHORT).show()
        ...
        return Tasks.call({
            play(convertLoadRequestToMovie(mediaLoadRequestData)!!)
            ...
            // Use MediaStatusModifier to provide additional information for Cast senders.
            mediaManager.getMediaStatusModifier()
                .setMediaCommandSupported(MediaStatus.COMMAND_QUEUE_NEXT, true)
                .setIsPlayingAd(false)
            mediaManager.broadcastMediaStatus()
            // Return the resolved MediaLoadRequestData to indicate load success.
            mediaLoadRequestData
        })
    }
}

送信前に MediaStatus をインターセプトする

Web Receiver SDK の MessageInterceptor と同様に、MediaManagerMediaStatusWriter を指定することで、接続されているセンダーにブロードキャストする前に、MediaStatus に追加の変更を行うことができます。

たとえば、モバイル センダーに送信する前に、MediaStatus でカスタムデータを設定できます。

import com.google.android.gms.cast.tv.media.MediaManager.MediaStatusInterceptor
import com.google.android.gms.cast.tv.media.MediaStatusWriter
import org.json.JSONObject
import org.json.JSONException
...

private fun initializePlayer() {
    if (mPlayer == null) {
        ...
        if (castReceiverContext != null) {
            ...
            val mediaManager: MediaManager = castReceiverContext.getMediaManager()
            ...
            // Use MediaStatusInterceptor to process the MediaStatus before sending out.
            mediaManager.setMediaStatusInterceptor(
                MediaStatusInterceptor { mediaStatusWriter: MediaStatusWriter ->
                    try {
                        mediaStatusWriter.setCustomData(JSONObject("{myData: 'CustomData'}"))
                    } catch (e: JSONException) {
                        Log.e(LOG_TAG,e.message,e);
                    }
            })
        }
    }
}        

9. 完了

ここでは、Cast Connect ライブラリを使用して Android TV アプリを Cast 対応にする方法について説明しました。

詳細については、デベロッパー ガイド(/cast/docs/android_tv_receiver)をご覧ください。