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

コレクションでコンテンツを整理 必要に応じて、コンテンツの保存と分類を行います。

1. 概要

Google Cast ロゴ

この Codelab では、Google Cast 対応デバイスでコンテンツをキャストするように既存の Android 動画アプリを変更する方法について説明します。

Google Cast とは

Google Cast では、ユーザーはモバイル デバイスからテレビにコンテンツをキャストできます。ユーザーは自分のモバイル デバイスをリモコンとして使い、テレビでのメディア再生を行うことが可能です。

Google Cast SDK を使うと、アプリを拡張してテレビやサウンド システムを制御できます。Cast SDK を使用すると、Google Cast デザイン チェックリストに基づいて、必要な UI コンポーネントを追加できます。

Google Cast デザイン チェックリストは、サポートされているすべてのプラットフォームにわたって、Cast ユーザー エクスペリエンスをシンプルで予測可能なものにするために使用します。

達成目標

この Codelab を修了すると、Google Cast 対応デバイスに動画をキャストできる Android 動画アプリが作成されます。

ラボの内容

  • サンプル動画アプリに Google Cast SDK を追加する方法
  • Google Cast デバイスを選択するキャスト アイコンを追加する方法
  • キャスト デバイスに接続してメディア レシーバーを起動する方法
  • 動画をキャストする方法
  • Cast ミニ コントローラをアプリに追加する方法
  • メディア通知とロック画面のコントロールをサポートする方法
  • 拡張コントローラを追加する方法
  • 案内用のオーバーレイを提供する方法
  • キャスト ウィジェットをカスタマイズする方法
  • Cast Connect と統合する方法

必要なもの

  • 最新の Android SDK
  • Android Studio バージョン 3.2 以降
  • Android 4.1、Jelly Bean(API レベル 16)以降を搭載したモバイル デバイス 1 台
  • モバイル デバイスを開発用コンピュータに接続するための USB データケーブル
  • インターネット アクセスが可能な ChromecastAndroid TV などの Google Cast デバイス。
  • HDMI 入力対応のテレビまたはモニター
  • Chromecast with Google TV は Cast Connect の統合をテストする際に必要ですが、この Codelab の残りの部分では省略できます。まだない場合は、このチュートリアルの最後にあるCast Connect サポートの追加の手順をスキップしてください。

テスト

  • Kotlin と Android の開発に関する予備知識が必要です。
  • 一般的なテレビの視聴経験も必要です。

このチュートリアルの利用方法をお選びください。

通読するのみ 通読し、演習を行う

Android アプリ作成のご経験についてお答えください。

初心者 中級者 上級者

テレビ視聴のご経験についてお答えください。

初級 中級 上級

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

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

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

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

コンパスのアイコン

まず、完成したサンプルアプリがどのようなものか見てみましょう。このアプリは基本的な動画プレーヤーです。ユーザーはリストから動画を選択し、デバイス上でローカルに再生するか、Google Cast デバイスにキャストできます。

コードをダウンロードしたら、以下の手順に沿って完成したサンプルアプリを Android Studio で開いて実行します。

ウェルカム画面で [Import Project] を選択するか、[File] > [New] > [Import Project...] メニュー オプションを選択します。

サンプルコード フォルダから フォルダ アイコンapp-done ディレクトリを選択し、[OK] をクリックします。

[File] > Android Studio の [Sync Project with Gradle] ボタン [Sync Project with Gradle Files] をクリックします。

Android デバイスで USB デバッグを有効にします。Android 4.2 以降の場合、[開発者向けオプション] 画面はデフォルトで非表示になっています。表示するには、[設定] > [デバイス情報] に移動して、[ビルド番号] を 7 回タップします。前の画面に戻り、[システム] > [詳細設定] に移動して、下部にある [開発者向けオプション] をタップし、[USB デバッグ] をタップしてオンにします。

Android デバイスを接続し、Android Studio で Android Studio の [Run] ボタン(右向きの緑色の三角形) [Run] ボタンをクリックします。数秒後に「Cast Videos」という動画アプリが表示されます。

動画アプリのキャスト アイコンをクリックし、Google Cast デバイスを選択します。

動画を選択して再生ボタンをクリックします。

Google Cast デバイスで動画の再生が開始されます。

拡張コントローラが表示されます。再生と一時停止ボタンを使用して再生をコントロールできます。

動画のリストに戻ります。

画面の下部にミニ コントローラが表示されるようになりました。「キャスト動画」アプリを実行している Android スマートフォンの画面の下にミニ コントローラが表示されているイラスト

ミニ コントローラの一時停止ボタンをクリックすると、レシーバーの動画が一時停止します。ミニ コントローラの再生ボタンをクリックして、動画の再生を再開します。

モバイル デバイスのホームボタンをクリックします。通知を下にスワイプすると、キャスト セッションの通知が表示されます。

スマートフォンをロックしてロック解除すると、ロック画面に通知が表示され、メディアの再生の操作や、キャストの停止が行えます。

動画アプリに戻り、キャスト アイコンをクリックして Google Cast デバイスのキャストを停止します。

よくある質問

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

「動画をキャスト」アプリを実行している Android スマートフォンのイラスト

ダウンロードした開始用アプリに Google Cast のサポートを追加する必要があります。この Codelab で使用する Google Cast の用語は以下のとおりです。

  • 送信側アプリはモバイル デバイスやノートパソコンで動作します。
  • レシーバー アプリは Google Cast デバイスで動作します。

これで、Android Studio を使ってスターター プロジェクト上に構築する準備が整いました。

  1. ダウンロードしたサンプルコードから フォルダ アイコンapp-start ディレクトリを選択します(ようこそ画面で [Import Project] を選択するか、[File] > [New] > [Import Project...] メニュー オプションを選択します)。
  2. Android Studio の [Sync Project with Gradle] ボタン [Sync Project with Gradle Files] ボタンをクリックします。
  3. Android Studio の [Run] ボタン(右向きの緑色の三角形) [Run] ボタンをクリックしてアプリを実行し、UI を確認します。

アプリの設計

アプリはリモートのウェブサーバーから動画のリストを取得し、ユーザーがブラウジングできるようにリストを提供します。動画を選択すると、その詳細情報を表示したり、モバイル デバイスで動画をローカルに再生したりできます。

このアプリは、VideoBrowserActivityLocalPlayerActivity という 2 つの主要なアクティビティで構成されています。Google Cast の機能を統合するには、ActivitiesAppCompatActivity またはその親 FragmentActivity から継承する必要があります。この制限が存在するのは、MediaRouter サポート ライブラリで提供される MediaRouteButtonMediaRouteActionProvider として追加する必要があり、これは、アクティビティが上記のクラスを継承している場合にのみ機能します。MediaRouter サポート ライブラリは、必要なクラスを提供する AppCompat サポート ライブラリに依存しています。

VideoBrowserActivity

このアクティビティには FragmentVideoBrowserFragment)が含まれます。このリストは ArrayAdapterVideoListAdapter)でサポートされています。動画とそれに関連するメタデータのリストは、リモート サーバー上で JSON ファイルとしてホストされます。AsyncTaskLoaderVideoItemLoader)はこの JSON を取得して処理し、MediaItem オブジェクトのリストを作成します。

MediaItem オブジェクトは、動画とその関連メタデータ(タイトル、説明、ストリームの URL、サポート画像の URL、および関連付けられているテキスト トラック(クローズド キャプション用)がある場合)をモデル化します。MediaItem オブジェクトはアクティビティ間で渡されるため、MediaItem には Bundle に変換するユーティリティ メソッドがあり、その逆も同様です。

ローダが MediaItems のリストを作成すると、そのリストは VideoListAdapter に渡され、これにより VideoBrowserFragmentMediaItems リストが表示されます。動画のサムネイルのリストと、各動画の簡単な説明が表示されます。アイテムを選択すると、対応する MediaItemBundle に変換されて LocalPlayerActivity に渡されます。

LocalPlayerActivity

このアクティビティは、特定の動画に関するメタデータを表示し、ユーザーはモバイル デバイスで動画をローカル再生できるようにします。

このアクティビティでは、VideoView、いくつかのメディア コントロール、テキスト領域に、選択した動画の説明が表示されます。画面の上部にはプレーヤーが表示され、その下には動画の詳細な説明を表示するスペースがあります。動画の再生と一時停止、動画のローカル再生をシークできます。

依存関係

AppCompatActivity を使用しているため、AppCompat サポート ライブラリが必要です。動画のリストを管理し、画像を非同期で取得するために、Volley ライブラリを使用します。

よくある質問

5. キャスト アイコンを追加する

動画キャスト アプリが実行されている Android スマートフォンの画面上部のイラスト

Cast 対応アプリでは、各アクティビティにキャスト アイコンが表示されます。キャスト アイコンをクリックすると、ユーザーが選択できるキャスト デバイスのリストが表示されます。送信側のデバイスでコンテンツをローカルで再生している場合は、キャスト デバイスを選択すると、そのキャスト デバイスで再生が開始または再開されます。キャスト セッション中、ユーザーはいつでもキャスト アイコンをクリックして、キャスト デバイスへのアプリのキャストを停止できます。Google Cast 設計チェックリストに記載されているとおり、アプリのアクティビティ中にユーザーがキャスト デバイスに接続または接続を解除できる必要があります。

依存関係

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

dependencies {
    implementation 'androidx.appcompat:appcompat:1.5.0'
    implementation 'androidx.mediarouter:mediarouter:1.3.1'
    implementation 'androidx.recyclerview:recyclerview:1.2.1'
    implementation 'com.google.android.gms:play-services-cast-framework:21.1.0'
    implementation 'com.android.volley:volley:1.2.1'
    implementation "androidx.core:core-ktx:1.8.0"
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
}

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

初期化

Cast フレームワークにはグローバル シングルトン オブジェクト CastContext があり、このオブジェクトがすべての Cast インタラクションを調整します。

OptionsProvider インターフェースを実装して、CastContext シングルトンの初期化に必要な CastOptions を指定する必要があります。最も重要なオプションは受信側のアプリ ID です。この ID は、キャスト デバイスの検出結果をフィルタリングして、キャスト セッションの開始時に受信側のアプリを起動するために使用されます。

独自の Cast 対応アプリを開発する場合は、Cast デベロッパーとして登録してからアプリのアプリ ID を取得する必要があります。この Codelab では、サンプルのアプリ ID を使用します。

次の新しい CastOptionsProvider.kt ファイルをプロジェクトの com.google.sample.cast.refplayer パッケージに追加します。

package com.google.sample.cast.refplayer

import android.content.Context
import com.google.android.gms.cast.framework.OptionsProvider
import com.google.android.gms.cast.framework.CastOptions
import com.google.android.gms.cast.framework.SessionProvider

class CastOptionsProvider : OptionsProvider {
    override fun getCastOptions(context: Context): CastOptions {
        return CastOptions.Builder()
                .setReceiverApplicationId(context.getString(R.string.app_id))
                .build()
    }

    override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? {
        return null
    }
}

次に、アプリの AndroidManifest.xml ファイルの「application」タグ内で OptionsProvider を宣言します。

<meta-data
    android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
    android:value="com.google.sample.cast.refplayer.CastOptionsProvider" />

VideoBrowserActivity onCreate メソッドで CastContext を必要に応じて初期化します。

import com.google.android.gms.cast.framework.CastContext

private var mCastContext: CastContext? = null

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.video_browser)
    setupActionBar()

    mCastContext = CastContext.getSharedInstance(this)
}

同じ初期化ロジックを LocalPlayerActivity に追加します。

キャスト アイコン

CastContext が初期化されたので、ユーザーがキャスト デバイスを選択できるようにキャスト アイコンを追加する必要があります。キャスト アイコンは、MediaRouter サポート ライブラリの MediaRouteButton によって実装されています。ActionBar または Toolbar を使用してアクティビティに追加できるアクション アイコンと同様に、まず、対応するメニュー項目をメニューに追加する必要があります。

res/menu/browse.xml ファイルを編集し、設定項目の前にメニューの MediaRouteActionProvider 項目を追加します。

<item
    android:id="@+id/media_route_menu_item"
    android:title="@string/media_route_menu_title"
    app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
    app:showAsAction="always"/>

VideoBrowserActivityonCreateOptionsMenu() メソッドをオーバーライドし、CastButtonFactory を使用して MediaRouteButton を Cast フレームワークに接続します。

import com.google.android.gms.cast.framework.CastButtonFactory

private var mediaRouteMenuItem: MenuItem? = null

override fun onCreateOptionsMenu(menu: Menu): Boolean {
     super.onCreateOptionsMenu(menu)
     menuInflater.inflate(R.menu.browse, menu)
     mediaRouteMenuItem = CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), menu,
                R.id.media_route_menu_item)
     return true
}

同様の方法で LocalPlayerActivityonCreateOptionsMenu をオーバーライドします。

[Android Studio の [Run] ボタン(右向きの緑色の三角形)Run] ボタンをクリックして、モバイル デバイスでアプリを実行します。アプリのアクションバーにキャスト アイコンが表示されます。このアイコンをクリックすると、ローカル ネットワーク上のキャスト デバイスが表示されます。デバイスの検出は CastContext によって自動的に管理されます。キャスト デバイスを選択すると、サンプルのレシーバー アプリがキャスト デバイスに読み込まれます。閲覧アクティビティとローカル プレーヤー アクティビティの間を移動できるようになり、キャスト アイコンの状態が同期されます。

メディア再生に関するサポートは設定していないため、まだキャスト デバイスで動画を再生することはできません。キャスト アイコンをクリックして接続を解除します。

6. 動画コンテンツのキャスト

「動画をキャスト」アプリを実行している Android スマートフォンのイラスト

キャスト デバイスでも動画をリモートで再生できるようにサンプルアプリを拡張します。そのためには、キャスト フレームワークによって生成された各種イベントをリッスンする必要があります。

メディアのキャスト

大まかに言えば、キャスト デバイスでメディアを再生する場合は、次の操作を行う必要があります。

  1. メディア アイテムをモデル化する MediaInfo オブジェクトを作成します。
  2. キャスト デバイスに接続してレシーバー アプリを起動します。
  3. MediaInfo オブジェクトをレシーバーに読み込み、コンテンツを再生します。
  4. メディアのステータスを追跡します。
  5. ユーザーの操作に基づいて再生コマンドをレシーバーに送信します。

前のセクションのステップ 2 はすでに完了しています。ステップ 3 はキャスト フレームワークで簡単に実行できます。ステップ 1 では、オブジェクトを別のオブジェクトにマッピングします。MediaInfo はキャスト フレームワークが認識するものであり、MediaItem はメディア アイテムに対するアプリのカプセル化です。MediaItemMediaInfo に簡単にマッピングできます。

サンプルアプリ LocalPlayerActivity では、すでに次の列挙型を使って、ローカル再生とリモート再生を区別しています。

private var mLocation: PlaybackLocation? = null

enum class PlaybackLocation {
    LOCAL, REMOTE
}

enum class PlaybackState {
    PLAYING, PAUSED, BUFFERING, IDLE
}

この Codelab では、すべてのサンプル プレーヤーにおけるロジックの仕組みを正確に理解する必要はありません。ここで重要なのは、2 つの再生場所を同じように認識するようにアプリのメディア プレーヤーを変更する必要があるということです。

現時点では、ローカル プレーヤーはキャスト状態について何も認識していないため、常にローカルの再生状態になっています。キャスト フレームワークで発生する状態遷移に基づいて UI を更新する必要があります。たとえば、キャストを開始するときは、ローカルの再生を停止して、いくつかのコントロールを無効にする必要があります。同様に、このアクティビティ中にキャストを停止した場合、ローカル再生に移行する必要があります。これに対応するには、キャスト フレームワークによって生成された各種イベントをリッスンする必要があります。

キャスト セッションの管理

キャスト フレームワークの場合、キャスト セッションは、デバイスへの接続、起動(または参加)、レシーバー アプリへの接続、メディア コントロール チャネルの初期化(該当する場合)の 2 つの手順で構成されます。メディア コントロール チャネルは、キャスト フレームワークがレシーバー メディア プレーヤーからメッセージを送受信する方法です。

キャスト アイコンからデバイスを選択すると自動的にキャスト セッションが開始され、ユーザーが切断すると自動的に停止します。ネットワークの問題によるレシーバー セッションへの再接続も、Cast SDK によって自動的に処理されます。

SessionManagerListenerLocalPlayerActivity に追加しましょう。

import com.google.android.gms.cast.framework.CastSession
import com.google.android.gms.cast.framework.SessionManagerListener
...

private var mSessionManagerListener: SessionManagerListener<CastSession>? = null
private var mCastSession: CastSession? = null
...

private fun setupCastListener() {
    mSessionManagerListener = object : SessionManagerListener<CastSession> {
        override fun onSessionEnded(session: CastSession, error: Int) {
            onApplicationDisconnected()
        }

        override fun onSessionResumed(session: CastSession, wasSuspended: Boolean) {
            onApplicationConnected(session)
        }

        override fun onSessionResumeFailed(session: CastSession, error: Int) {
            onApplicationDisconnected()
        }

        override fun onSessionStarted(session: CastSession, sessionId: String) {
            onApplicationConnected(session)
        }

        override fun onSessionStartFailed(session: CastSession, error: Int) {
            onApplicationDisconnected()
        }

        override fun onSessionStarting(session: CastSession) {}
        override fun onSessionEnding(session: CastSession) {}
        override fun onSessionResuming(session: CastSession, sessionId: String) {}
        override fun onSessionSuspended(session: CastSession, reason: Int) {}
        private fun onApplicationConnected(castSession: CastSession) {
            mCastSession = castSession
            if (null != mSelectedMedia) {
                if (mPlaybackState == PlaybackState.PLAYING) {
                    mVideoView!!.pause()
                    loadRemoteMedia(mSeekbar!!.progress, true)
                    return
                } else {
                    mPlaybackState = PlaybackState.IDLE
                    updatePlaybackLocation(PlaybackLocation.REMOTE)
                }
            }
            updatePlayButton(mPlaybackState)
            invalidateOptionsMenu()
        }

        private fun onApplicationDisconnected() {
            updatePlaybackLocation(PlaybackLocation.LOCAL)
            mPlaybackState = PlaybackState.IDLE
            mLocation = PlaybackLocation.LOCAL
            updatePlayButton(mPlaybackState)
            invalidateOptionsMenu()
       }
   }
}

LocalPlayerActivity アクティビティでは、キャスト デバイスとの接続や接続解除の際に通知を受け、ローカル プレーヤーとの切り替えができるようにします。モバイル デバイスで実行されているアプリのインスタンスだけでなく、別のモバイル デバイスで実行されているお使いの(または別の)アプリの別のインスタンスによっても、接続が中断されることがあります。

現在アクティブなセッションは、SessionManager.getCurrentSession() としてアクセスできます。セッションは、キャスト ダイアログでのユーザー操作に応じて自動的に作成、破棄されます。

セッション リスナーを登録して、アクティビティで使用する変数を初期化する必要があります。LocalPlayerActivity onCreate メソッドを以下のように変更します。

import com.google.android.gms.cast.framework.CastContext
...

private var mCastContext: CastContext? = null
...

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    mCastContext = CastContext.getSharedInstance(this)
    mCastSession = mCastContext!!.sessionManager.currentCastSession
    setupCastListener()
    ...
    loadViews()
    ...
    val bundle = intent.extras
    if (bundle != null) {
        ....
        if (shouldStartPlayback) {
              ....

        } else {
            if (mCastSession != null && mCastSession!!.isConnected()) {
                updatePlaybackLocation(PlaybackLocation.REMOTE)
            } else {
                updatePlaybackLocation(PlaybackLocation.LOCAL)
            }
            mPlaybackState = PlaybackState.IDLE
            updatePlayButton(mPlaybackState)
        }
    }
    ...
}

メディアの読み込み

Cast SDK には、レシーバーでのリモート メディア再生を管理するための一連の便利な API が RemoteMediaClient に用意されています。メディア再生をサポートする CastSession の場合、RemoteMediaClient のインスタンスは SDK によって自動的に作成されます。CastSession インスタンスで getRemoteMediaClient() メソッドを呼び出すことでアクセスできます。LocalPlayerActivity に次のメソッドを追加して、レシーバーで現在選択されている動画を読み込みます。

import com.google.android.gms.cast.framework.media.RemoteMediaClient
import com.google.android.gms.cast.MediaInfo
import com.google.android.gms.cast.MediaLoadOptions
import com.google.android.gms.cast.MediaMetadata
import com.google.android.gms.common.images.WebImage
import com.google.android.gms.cast.MediaLoadRequestData

private fun loadRemoteMedia(position: Int, autoPlay: Boolean) {
    if (mCastSession == null) {
        return
    }
    val remoteMediaClient = mCastSession!!.remoteMediaClient ?: return
    remoteMediaClient.load( MediaLoadRequestData.Builder()
                .setMediaInfo(buildMediaInfo())
                .setAutoplay(autoPlay)
                .setCurrentTime(position.toLong()).build())
}

private fun buildMediaInfo(): MediaInfo? {
    val movieMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE)
    mSelectedMedia?.studio?.let { movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, it) }
    mSelectedMedia?.title?.let { movieMetadata.putString(MediaMetadata.KEY_TITLE, it) }
    movieMetadata.addImage(WebImage(Uri.parse(mSelectedMedia!!.getImage(0))))
    movieMetadata.addImage(WebImage(Uri.parse(mSelectedMedia!!.getImage(1))))
    return mSelectedMedia!!.url?.let {
        MediaInfo.Builder(it)
            .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
            .setContentType("videos/mp4")
            .setMetadata(movieMetadata)
            .setStreamDuration((mSelectedMedia!!.duration * 1000).toLong())
            .build()
    }
}

次に、キャスト セッション ロジックを使用して、リモート再生をサポートするようにさまざまな既存のメソッドを更新します。

private fun play(position: Int) {
    startControllersTimer()
    when (mLocation) {
        PlaybackLocation.LOCAL -> {
            mVideoView!!.seekTo(position)
            mVideoView!!.start()
        }
        PlaybackLocation.REMOTE -> {
            mPlaybackState = PlaybackState.BUFFERING
            updatePlayButton(mPlaybackState)
            //seek to a new position within the current media item's new position 
            //which is in milliseconds from the beginning of the stream
            mCastSession!!.remoteMediaClient?.seek(position.toLong())
        }
        else -> {}
    }
    restartTrickplayTimer()
}
private fun togglePlayback() {
    ...
    PlaybackState.IDLE -> when (mLocation) {
        ...
        PlaybackLocation.REMOTE -> {
            if (mCastSession != null && mCastSession!!.isConnected) {
                loadRemoteMedia(mSeekbar!!.progress, true)
            }
        }
        else -> {}
    }
    ...
}
override fun onPause() {
    ...
    mCastContext!!.sessionManager.removeSessionManagerListener(
                mSessionManagerListener!!, CastSession::class.java)
}
override fun onResume() {
    Log.d(TAG, "onResume() was called")
    mCastContext!!.sessionManager.addSessionManagerListener(
            mSessionManagerListener!!, CastSession::class.java)
    if (mCastSession != null && mCastSession!!.isConnected) {
        updatePlaybackLocation(PlaybackLocation.REMOTE)
    } else {
        updatePlaybackLocation(PlaybackLocation.LOCAL)
    }
    super.onResume()
}

updatePlayButton メソッドの isConnected 変数の値を変更します。

private fun updatePlayButton(state: PlaybackState?) {
    ...
    val isConnected = (mCastSession != null
                && (mCastSession!!.isConnected || mCastSession!!.isConnecting))
    ...
}

次に、[Android Studio の [Run] ボタン(右向きの緑色の三角形)Run] ボタンをクリックして、モバイル デバイスでアプリを実行します。キャスト デバイスに接続して動画の再生を開始します。動画がレシーバーで再生されます。

7. ミニ コントローラ

キャスト デザイン チェックリストでは、ユーザーが現在のコンテンツ ページから移動したときに表示されるミニ コントローラをすべてのキャストアプリで指定する必要があります。ミニ コントローラを使用すると、すぐにアクセスでき、現在のキャスト セッションにリマインダーが表示されます。

Cast 動画アプリのミニプレーヤーを表示している Android スマートフォンの下部を示すイラスト

Cast SDK には、カスタム コントローラ MiniControllerFragment が用意されています。このビューを、ミニ コントローラを表示するアクティビティのアプリ レイアウト ファイルに追加できます。

res/layout/player_activity.xmlres/layout/video_browser.xml の両ファイルの一番下に、以下のフラグメント定義を追加します。

<fragment
    android:id="@+id/castMiniController"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:visibility="gone"
    class="com.google.android.gms.cast.framework.media.widget.MiniControllerFragment"/>

Android Studio の [Run] ボタン(右向きの緑色の三角形) [Run] ボタンをクリックしてアプリを実行し、動画をキャストします。レシーバーで再生が開始されると、各アクティビティの下部にミニ コントローラが表示されます。ミニ コントローラを使ってリモート再生を操作できます。閲覧アクティビティとローカル プレーヤー アクティビティ間を移動する場合は、ミニ コントローラの状態をレシーバーのメディア再生ステータスと同期させる必要があります。

8. 通知とロック画面

Google Cast の設計チェックリストでは、送信側アプリが通知ロック画面からメディア コントロールを実装する必要があります。

通知エリアにメディア コントロールが表示されている Android スマートフォンのイラスト

Cast SDK には、MediaNotificationService が用意されています。これは、送信側アプリが通知とロック画面用のメディア コントロールを構築するのに役立ちます。このサービスは Gradle によって自動的にアプリのマニフェストに統合されます。

MediaNotificationService は、送信側がキャストしているときにバックグラウンドで実行され、現在のキャスト アイテムに関する画像のサムネイルとメタデータ、再生 / 一時停止ボタン、および停止ボタンを含む通知を表示します。

CastContext を初期化するときに、CastOptions で通知とロック画面コントロールを有効にできます。通知とロック画面のメディア コントロールはデフォルトでオンになっています。ロック画面機能は、通知をオンにしている限りオンになります。

CastOptionsProvider を編集して、次のコードに合わせて getCastOptions の実装を変更します。

import com.google.android.gms.cast.framework.media.CastMediaOptions
import com.google.android.gms.cast.framework.media.NotificationOptions

override fun getCastOptions(context: Context): CastOptions {
   val notificationOptions = NotificationOptions.Builder()
            .setTargetActivityClassName(VideoBrowserActivity::class.java.name)
            .build()
    val mediaOptions = CastMediaOptions.Builder()
            .setNotificationOptions(notificationOptions)
            .build()
   return CastOptions.Builder()
                .setReceiverApplicationId(context.getString(R.string.app_id))
                .setCastMediaOptions(mediaOptions)
                .build()
}

[Android Studio の [Run] ボタン(右向きの緑色の三角形)Run] ボタンをクリックして、モバイル デバイスでアプリを実行します。動画をキャストし、サンプルアプリから離れます。レシーバーで現在再生中の動画に関する通知が表示されるはずです。これで、モバイル デバイスがロックされ、キャスト デバイスでメディア再生のコントロールがロック画面に表示されます。

ロック画面にメディア コントロールが表示されている Android スマートフォンのイラスト

9. 紹介オーバーレイ

Google Cast のデザイン チェックリストでは、送信側アプリが既存のユーザーにキャスト アイコンを導入して、送信側のアプリがキャストに対応していることと、Google Cast を初めて使用することをユーザーにお知らせいただく必要があります。

Android 版 Cast アプリのキャストボタンの周囲に、簡単な Cast オーバーレイが表示されているイラスト

Cast SDK には、カスタムビュー IntroductoryOverlay が用意されています。このビューを使用して、ユーザーが初めてキャスト アイコンを表示するときに、ハイライトできます。以下のコードを VideoBrowserActivity に追加します。

import com.google.android.gms.cast.framework.IntroductoryOverlay
import android.os.Looper

private var mIntroductoryOverlay: IntroductoryOverlay? = null

private fun showIntroductoryOverlay() {
    mIntroductoryOverlay?.remove()
    if (mediaRouteMenuItem?.isVisible == true) {
       Looper.myLooper().run {
           mIntroductoryOverlay = com.google.android.gms.cast.framework.IntroductoryOverlay.Builder(
                    this@VideoBrowserActivity, mediaRouteMenuItem!!)
                   .setTitleText("Introducing Cast")
                   .setSingleTime()
                   .setOnOverlayDismissedListener(
                           object : IntroductoryOverlay.OnOverlayDismissedListener {
                               override fun onOverlayDismissed() {
                                   mIntroductoryOverlay = null
                               }
                          })
                   .build()
          mIntroductoryOverlay!!.show()
        }
    }
}

次に、CastStateListener を追加し、キャスト デバイスが利用可能になったら onCreate メソッドを呼び出して showIntroductoryOverlay メソッドを呼び出し、onResume メソッドと onPause メソッドをオーバーライドして、以下の条件を満たすようにします。

import com.google.android.gms.cast.framework.CastState
import com.google.android.gms.cast.framework.CastStateListener

private var mCastStateListener: CastStateListener? = null

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.video_browser)
    setupActionBar()
    mCastStateListener = object : CastStateListener {
            override fun onCastStateChanged(newState: Int) {
                if (newState != CastState.NO_DEVICES_AVAILABLE) {
                    showIntroductoryOverlay()
                }
            }
        }
    mCastContext = CastContext.getSharedInstance(this)
}

override fun onResume() {
    super.onResume()
    mCastContext?.addCastStateListener(mCastStateListener!!)
}

override fun onPause() {
    super.onPause()
    mCastContext?.removeCastStateListener(mCastStateListener!!)
}

アプリデータを削除するか、デバイスからアプリを削除します。次に、Android Studio の [Run] ボタン(右向きの緑色の三角形) [Run] ボタンをクリックしてモバイル デバイスでアプリを実行すると、概要オーバーレイが表示されます(オーバーレイが表示されない場合はアプリデータが削除されます)。

10. コントローラを展開

Google Cast の設計チェックリストには、送信側アプリでキャスト対象のメディアの拡張コントローラを指定する必要があります。拡張コントローラは、ミニ コントローラの全画面バージョンです。

拡張コントローラが重なった Android スマートフォンで再生されている動画のイラスト

Cast SDK には、拡張コントローラ用のウィジェット ExpandedControllerActivity が用意されています。これは、キャスト アイコンを追加するためにサブクラス化する必要がある抽象クラスです。

まず、expanded_controller.xml という名前の新しいメニュー リソース ファイルを作成し、拡張コントローラにキャスト アイコンを用意します。

<?xml version="1.0" encoding="utf-8"?>

<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
            android:id="@+id/media_route_menu_item"
            android:title="@string/media_route_menu_title"
            app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
            app:showAsAction="always"/>

</menu>

com.google.sample.cast.refplayer パッケージに新しいパッケージ expandedcontrols を作成します。次に、com.google.sample.cast.refplayer.expandedcontrols パッケージに ExpandedControlsActivity.kt という新しいファイルを作成します。

package com.google.sample.cast.refplayer.expandedcontrols

import android.view.Menu
import com.google.android.gms.cast.framework.media.widget.ExpandedControllerActivity
import com.google.sample.cast.refplayer.R
import com.google.android.gms.cast.framework.CastButtonFactory

class ExpandedControlsActivity : ExpandedControllerActivity() {
    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        super.onCreateOptionsMenu(menu)
        menuInflater.inflate(R.menu.expanded_controller, menu)
        CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item)
        return true
    }
}

OPTIONS_PROVIDER_CLASS_NAME の上の application タグ内で、AndroidManifest.xmlExpandedControlsActivity を宣言します。

<application>
    ...
    <activity
        android:name="com.google.sample.cast.refplayer.expandedcontrols.ExpandedControlsActivity"
        android:label="@string/app_name"
        android:launchMode="singleTask"
        android:theme="@style/Theme.CastVideosDark"
        android:screenOrientation="portrait"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.MAIN"/>
        </intent-filter>
        <meta-data
            android:name="android.support.PARENT_ACTIVITY"
            android:value="com.google.sample.cast.refplayer.VideoBrowserActivity"/>
    </activity>
    ...
</application>

CastOptionsProvider を編集して NotificationOptionsCastMediaOptions を変更し、ターゲット アクティビティを ExpandedControlsActivity に設定します。

import com.google.sample.cast.refplayer.expandedcontrols.ExpandedControlsActivity

override fun getCastOptions(context: Context): CastOptions {
    val notificationOptions = NotificationOptions.Builder()
            .setTargetActivityClassName(ExpandedControlsActivity::class.java.name)
            .build()
    val mediaOptions = CastMediaOptions.Builder()
            .setNotificationOptions(notificationOptions)
            .setExpandedControllerActivityClassName(ExpandedControlsActivity::class.java.name)
            .build()
    return CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .setCastMediaOptions(mediaOptions)
            .build()
}

リモート メディアを読み込むときに ExpandedControlsActivity を表示するように LocalPlayerActivity loadRemoteMedia メソッドを更新します。

import com.google.sample.cast.refplayer.expandedcontrols.ExpandedControlsActivity

private fun loadRemoteMedia(position: Int, autoPlay: Boolean) {
    if (mCastSession == null) {
        return
    }
    val remoteMediaClient = mCastSession!!.remoteMediaClient ?: return
    remoteMediaClient.registerCallback(object : RemoteMediaClient.Callback() {
        override fun onStatusUpdated() {
            val intent = Intent(this@LocalPlayerActivity, ExpandedControlsActivity::class.java)
            startActivity(intent)
            remoteMediaClient.unregisterCallback(this)
        }
    })
    remoteMediaClient.load(MediaLoadRequestData.Builder()
                .setMediaInfo(buildMediaInfo())
                .setAutoplay(autoPlay)
                .setCurrentTime(position.toLong()).build())
}

Android Studio の [Run] ボタン(右向きの緑色の三角形) 実行ボタンをクリックして、モバイル デバイスでアプリを実行し、動画をキャストします。拡張コントローラが表示されます。動画のリストに戻り、ミニ コントローラをクリックすると、拡張コントローラが再度読み込まれます。アプリから移動して、通知を表示します。通知画像をクリックして、拡張コントローラを読み込みます。

11. Cast Connect のサポートを追加する

Cast Connect ライブラリを使用すると、既存の送信者アプリが Cast プロトコルを介して Android TV アプリと通信できます。Cast Connect は Cast インフラストラクチャ上に構築されており、Android TV アプリはレシーバーとして機能します。

依存関係

注: Cast Connect を実装するには、play-services-cast-framework19.0.0 以上である必要があります。

LaunchOptions

Android TV アプリ(Android レシーバー)を起動するには、LaunchOptions オブジェクトで setAndroidReceiverCompatible フラグを true に設定する必要があります。この LaunchOptions オブジェクトは、レシーバーがどのように起動するかを指定し、CastOptionsProvider クラスによって返される CastOptions に渡されます。上記のフラグを false に設定すると、定義されたアプリ ID のウェブ レシーバーが Cast デベロッパー コンソールで起動します。

CastOptionsProvider.kt ファイルで、getCastOptions メソッドに次の行を追加します。

import com.google.android.gms.cast.LaunchOptions
...
val launchOptions = LaunchOptions.Builder()
            .setAndroidReceiverCompatible(true)
            .build()
return new CastOptions.Builder()
        .setLaunchOptions(launchOptions)
        ...
        .build()

起動認証情報の設定

送信者側では、セッションに参加するユーザーを指定するために CredentialsData を指定できます。credentials は、ATV アプリが理解できる限り、ユーザー定義の文字列です。CredentialsData は、起動時または参加時にのみ Android TV アプリに渡されます。接続中にもう一度設定すると、Android TV アプリには渡されません。

起動認証情報を設定するには、CredentialsData を定義して LaunchOptions オブジェクトに渡す必要があります。CastOptionsProvider.kt ファイルの getCastOptions メソッドに次のコードを追加します。

import com.google.android.gms.cast.CredentialsData
...

val credentialsData = CredentialsData.Builder()
        .setCredentials("{\"userId\": \"abc\"}")
        .build()
val launchOptions = LaunchOptions.Builder()
       ...
       .setCredentialsData(credentialsData)
       .build()

LoadRequest に認証情報を設定する

ウェブ レシーバ アプリと Android TV アプリの credentials の処理方法がそれぞれ異なる場合は、それぞれで credentials を定義する必要があります。この処理を行うには、LocalPlayerActivity.kt ファイルの loadRemoteMedia 関数に次のコードを追加します。

remoteMediaClient.load(MediaLoadRequestData.Builder()
       ...
       .setCredentials("user-credentials")
       .setAtvCredentials("atv-user-credentials")
       .build())

送信者のキャスト先のレシーバアプリに応じて、現在のセッションで使用する認証情報が SDK で自動的に処理されるようになりました。

Cast Connect のテスト

Chromecast with Google TV に Android TV APK をインストールする手順

  1. Android TV デバイスの IP アドレスを確認します。通常、この設定は [設定] > [ネットワークとインターネット] >(デバイスが接続されているネットワーク名)で確認できます。右側に、ネットワーク上の詳細情報とデバイスの IP が表示されます。
  2. 端末の IP アドレスを使用して、ターミナルから ADB 経由でデバイスに接続します。
$ adb connect <device_ip_address>:5555
  1. ターミナル ウィンドウで、この Codelab の最初にダウンロードした Codelab サンプルの最上位フォルダに移動します。例:
$ cd Desktop/android_codelab_src
  1. 次のコマンドを実行して、このフォルダ内の .apk ファイルを Android TV にインストールします。
$ adb -s <device_ip_address>:5555 install android-tv-app.apk
  1. これで、Android TV デバイスの [アプリ] メニューに [動画をキャスト] というアプリが表示されます。
  2. Android Studio プロジェクトに戻り、[Run] ボタンをクリックし、実際のモバイル デバイスに送信者アプリをインストールして実行します。右上のキャスト アイコンをクリックし、表示されるオプションから Android TV デバイスを選択します。これで、Android TV デバイスで Android TV アプリが起動し、動画を再生できるようになります。動画を再生すると、Android TV リモコンを使って動画の再生を操作できるようになります。

12. キャスト ウィジェットをカスタマイズする

キャスト ウィジェットは、色の設定、ボタン、テキスト、サムネイルの外観、表示するボタンの種類を選択してカスタマイズできます。

res/values/styles_castvideo.xml の更新

<style name="Theme.CastVideosTheme" parent="Theme.AppCompat.Light.NoActionBar">
    ...
    <item name="mediaRouteTheme">@style/CustomMediaRouterTheme</item>
    <item name="castIntroOverlayStyle">@style/CustomCastIntroOverlay</item>
    <item name="castMiniControllerStyle">@style/CustomCastMiniController</item>
    <item name="castExpandedControllerStyle">@style/CustomCastExpandedController</item>
    <item name="castExpandedControllerToolbarStyle">
        @style/ThemeOverlay.AppCompat.ActionBar
    </item>
    ...
</style>

以下のカスタムテーマを宣言します。

<!-- Customize Cast Button -->
<style name="CustomMediaRouterTheme" parent="Theme.MediaRouter">
    <item name="mediaRouteButtonStyle">@style/CustomMediaRouteButtonStyle</item>
</style>
<style name="CustomMediaRouteButtonStyle" parent="Widget.MediaRouter.Light.MediaRouteButton">
    <item name="mediaRouteButtonTint">#EEFF41</item>
</style>

<!-- Customize Introductory Overlay -->
<style name="CustomCastIntroOverlay" parent="CastIntroOverlay">
    <item name="castButtonTextAppearance">@style/TextAppearance.CustomCastIntroOverlay.Button</item>
    <item name="castTitleTextAppearance">@style/TextAppearance.CustomCastIntroOverlay.Title</item>
</style>
<style name="TextAppearance.CustomCastIntroOverlay.Button" parent="android:style/TextAppearance">
    <item name="android:textColor">#FFFFFF</item>
</style>
<style name="TextAppearance.CustomCastIntroOverlay.Title" parent="android:style/TextAppearance.Large">
    <item name="android:textColor">#FFFFFF</item>
</style>

<!-- Customize Mini Controller -->
<style name="CustomCastMiniController" parent="CastMiniController">
    <item name="castShowImageThumbnail">true</item>
    <item name="castTitleTextAppearance">@style/TextAppearance.AppCompat.Subhead</item>
    <item name="castSubtitleTextAppearance">@style/TextAppearance.AppCompat.Caption</item>
    <item name="castBackground">@color/accent</item>
    <item name="castProgressBarColor">@color/orange</item>
</style>

<!-- Customize Expanded Controller -->
<style name="CustomCastExpandedController" parent="CastExpandedController">
    <item name="castButtonColor">#FFFFFF</item>
    <item name="castPlayButtonDrawable">@drawable/cast_ic_expanded_controller_play</item>
    <item name="castPauseButtonDrawable">@drawable/cast_ic_expanded_controller_pause</item>
    <item name="castStopButtonDrawable">@drawable/cast_ic_expanded_controller_stop</item>
</style>

13. 完了

Android の Cast SDK ウィジェットを使用して動画アプリを Cast 対応にする方法は以上です。

詳しくは、Android センダーのデベロッパー ガイドをご覧ください。