1. 概要
この 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 センダーアプリにメディア ステータスを送信する方法。
必要なもの
- 最新の Android SDK。
- 最新の Android Studio。具体的には、
Chipmunk | 2021.2.1
以降のバージョンです。 - 開発者向けオプションと USB デバッグが有効になっている Android TV デバイス。
- 開発者向けオプションと USB デバッグが有効になっている Android スマートフォン。
- Android スマートフォンと Android TV デバイスを開発用パソコンに接続するための USB データケーブル。
- Kotlin を使用した Android アプリ開発に関する基本的な知識。
2. サンプルコードを取得する
サンプルコードはすべてパソコンにダウンロードできます。
ダウンロードした ZIP ファイルを解凍します。
3. サンプルアプリを実行する
まず、完成したサンプルアプリがどのようなものか見てみましょう。Android TV アプリは、Leanback UI と基本的な動画プレーヤーを使用します。ユーザーはリストから動画を選択し、テレビで再生できます。付属のモバイル センダーアプリを使用して、Android TV アプリに動画をキャストすることもできます。
デベロッパー デバイスを登録する
アプリ開発で Cast Connect 機能を有効にするには、Cast Developer Console で使用する Android TV デバイスの内蔵 Chromecast のシリアル番号を登録する必要があります。シリアル番号は、Android TV で [設定] > [デバイス設定] > [Chromecast built-in] > [シリアル番号] に移動すると確認できます。物理デバイスのシリアル番号とは異なるため、この方法で取得する必要があります。
登録しないと、セキュリティ上の理由により、Cast Connect は Google Play ストアからインストールしたアプリでしか動作しません。登録プロセスを開始してから 15 分後に、デバイスを再起動します。
Android センダーアプリをインストールする
モバイル デバイスからのリクエストの送信をテストするために、Google では「動画キャスト」というシンプルな送信アプリをソースコードの zip ダウンロード内に mobile-sender-0629.apk
ファイルとして提供しています。ADB を利用して APK をインストールします。すでに別のバージョンの Cast Videos をインストールしている場合は、続行する前に、デバイスにあるすべてのプロファイルからそのバージョンをアンインストールしてください。
- Android スマートフォンで開発者向けオプションと USB デバッグを有効にします。
- USB データケーブルを差し込んで Android スマートフォンを開発用パソコンに接続します。
- Android スマートフォンに
mobile-sender-0629.apk
をインストールします。
- Android スマートフォンで Cast Videos センダーアプリを確認できます。
Android TV アプリをインストールする
完成したサンプルアプリを Android Studio で開いて実行する手順は次のとおりです。
- ウェルカム画面で [Import Project] を選択するか、[File] > [New] > [Import Project...] メニュー オプションを選択します。
- サンプルコード フォルダから
app-done
ディレクトリを選択し、[OK] をクリックします。 - [File] > [Sync Project with Gradle Files] をクリックします。
- Android TV デバイスで開発者向けオプションと USB デバッグを有効にします。
- ADB を Android TV デバイスに接続すると、そのデバイスが Android Studio に表示されます。
- 実行ボタンをクリックすると、数秒後に Cast Connect Codelab という ATV アプリが表示されます。
ATV アプリで Cast Connect を再生してみる
- Android TV のホーム画面に移動します。
- Android スマートフォンから Cast Videos センダーアプリを開きます。キャスト アイコン をクリックし、ATV デバイスを選択します。
- ATV で Cast Connect Codelab ATV アプリが起動し、接続済みであることがセンダーのキャスト アイコンに示されます()。
- ATV アプリから動画を選択すると、その動画が ATV で再生されます。
- スマートフォンで、センダーアプリの下部にミニ コントローラが表示されます。再生と一時停止のボタンを使用して再生をコントロールできます。
- スマートフォンで動画を選択して再生します。ATV で動画が再生され、モバイル センダーに拡張コントローラが表示されます。
- スマートフォンをロックしてロック解除すると、ロック画面に通知が表示され、メディアの再生の操作や、キャストの停止が行えます。
4. 開始プロジェクトを準備する
完成したアプリの Cast Connect 統合を確認したので、ダウンロードした開始用アプリに Cast Connect のサポートを追加する必要があります。これで、Android Studio を使ってスターター プロジェクト上に構築する準備が整いました。
- ウェルカム画面で [Import Project] を選択するか、[File] > [New] > [Import Project...] メニュー オプションを選択します。
- サンプルコード フォルダから
app-start
ディレクトリを選択し、[OK] をクリックします。 - [File] > [Sync Project with Gradle Files] をクリックします。
- ATV デバイスを選択して 実行 ボタンをクリックし、アプリを実行して UI を確認します。
アプリの設計
このアプリは、ユーザーが視聴できる動画のリストを提示します。ユーザーは Android TV で再生する動画を選択できます。アプリは、MainActivity
と PlaybackActivity
という 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
クラスでは、アプリが再開するときに CastReceiverContext
を start()
し、アプリが一時停止するときに 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 に接続する
MediaManager
は CastReceiverContext
シングルトンのプロパティであり、メディア ステータスを管理して読み込みインテントを処理します。また、センダーからのメディア名前空間メッセージをメディア コマンドに変換し、メディア ステータスをセンダーに送り返します。
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)
...
}
サンプルアプリを実行してみる
実行ボタンをクリックしてアプリを 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
オブジェクトに変換します。変換後、ローカル プレーヤーでムービーが再生されます。その後、MediaManager
は MediaLoadRequest
で更新され、接続されているセンダーに 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())
}
}
}
サンプルアプリを実行してみる
[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
と同様に、MediaManager
で MediaStatusWriter
を指定することで、接続されているセンダーにブロードキャストする前に、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)をご覧ください。