Android アプリにキャストを統合する

このデベロッパー ガイドでは、Android Sender SDK を使用して、Android 送信側アプリに Google Cast のサポートを追加する方法について説明します。

モバイル デバイスやノートパソコンは再生を制御する送信側、Google Cast デバイスはテレビにコンテンツを表示する受信側です。

送信側フレームワークとは、送信側のランタイムに存在する Cast クラス ライブラリ バイナリと関連リソースを指します。送信側アプリまたは Cast アプリとは、送信側でも実行されているアプリを指します。ウェブ レシーバー アプリとは、Cast 対応デバイスで動作する HTML アプリケーションのことです。

送信側フレームワークは、非同期コールバック設計を使用して、送信側アプリにイベントを通知し、Cast アプリのライフサイクルのさまざまな状態間を遷移します。

アプリケーションの流れ

次の手順は、送信側 Android アプリの一般的な実行フローの概要を示しています。

  • Cast フレームワークは、Activity のライフサイクルに基づいて、MediaRouter デバイスの検出を自動的に開始します。
  • ユーザーがキャスト アイコンをクリックすると、フレームワークは、検出されたキャスト デバイスのリストを含むキャスト ダイアログを表示します。
  • ユーザーがキャスト デバイスを選択すると、フレームワークはキャスト デバイスでウェブ レシーバー アプリの起動を試みます。
  • フレームワークは、送信側アプリでコールバックを呼び出して、ウェブ レシーバー アプリが起動されたことを確認します。
  • フレームワークは、送信側アプリとウェブ レシーバー アプリの間に通信チャネルを作成します。
  • フレームワークは、通信チャネルを使用して、ウェブ レシーバーでのメディア再生の読み込みと制御を行います。
  • フレームワークは、送信側とウェブ レシーバーの間でメディア再生状態を同期します。ユーザーが送信側 UI アクションを実行すると、フレームワークはそれらのメディア コントロール リクエストをウェブ レシーバーに渡します。ウェブ レシーバーがメディア ステータスの更新を送信すると、フレームワークは送信側 UI の状態を更新します。
  • ユーザーがキャスト アイコンをクリックしてキャスト デバイスから切断すると、フレームワークはウェブ レシーバーから送信側アプリを切断します。

Google Cast Android SDK のすべてのクラス、メソッド、イベントの一覧については、Android 向け Google Cast Sender API リファレンスをご覧ください。以下のセクションでは、Android アプリに Cast を追加する手順について説明します。

Android マニフェストを構成する

アプリの AndroidManifest.xml ファイルで、Cast SDK 用に次の要素を構成する必要があります。

uses-sdk

Cast SDK がサポートする最小 Android API レベルとターゲット Android API レベルを設定します。 現在、最小 API レベルは 24、ターゲット API レベルは 35 です。

<uses-sdk
        android:minSdkVersion="24"
        android:targetSdkVersion="35" />

android:theme

Android SDK の最小バージョンに基づいてアプリのテーマを設定します。たとえば、独自のテーマを実装していない場合、Lollipop より前の Android SDK の最小バージョンをターゲットにするときは、Theme.AppCompat のバリアントを使用する必要があります。

<application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/Theme.AppCompat" >
       ...
</application>

Cast コンテキストを初期化する

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

アプリは、OptionsProvider インターフェースを実装して、CastContext シングルトンの初期化に必要なオプションを提供する必要があります。OptionsProvider は、フレームワークの動作に影響するオプションを含む CastOptions のインスタンスを提供します。これらのうち最も重要なのは、ウェブ レシーバー アプリケーション ID です。これは、検出結果をフィルタリングし、キャスト セッションの開始時にウェブ レシーバー アプリを起動するために使用されます。

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

    override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? {
        return null
    }
}
Java
public class CastOptionsProvider implements OptionsProvider {
    @Override
    public CastOptions getCastOptions(Context context) {
        CastOptions castOptions = new CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .build();
        return castOptions;
    }
    @Override
    public List<SessionProvider> getAdditionalSessionProviders(Context context) {
        return null;
    }
}

実装された OptionsProvider の完全修飾名を、送信側アプリの AndroidManifest.xml ファイルでメタデータ フィールドとして宣言する必要があります。

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

CastContext は、CastContext.getSharedInstance() が呼び出されたときに遅延初期化されます。

Kotlin
class MyActivity : FragmentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        val castContext = CastContext.getSharedInstance(this)
    }
}
Java
public class MyActivity extends FragmentActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        CastContext castContext = CastContext.getSharedInstance(this);
    }
}

Cast UX ウィジェット

Cast フレームワークには、Cast デザイン チェックリストに準拠したウィジェットが用意されています。

  • 紹介オーバーレイ: フレームワークには、カスタムビュー IntroductoryOverlay が用意されています。このビューは、レシーバーが初めて利用可能になったときに、キャスト アイコンに注意を向けるためにユーザーに表示されます。送信側アプリは、タイトル テキストのテキストと位置をカスタマイズできます。

  • キャスト アイコン: キャスト アイコンは、キャスト デバイスが利用可能かどうかにかかわらず表示されます。 ユーザーがキャスト アイコンを初めてクリックすると、検出されたデバイスを一覧表示するキャスト ダイアログが表示されます。デバイスが接続されているときにユーザーがキャスト アイコンをクリックすると、現在のメディア メタデータ(タイトル、レコーディング スタジオの名前、サムネイル画像など)が表示されるか、キャスト デバイスから切断できるようになります。「キャスト アイコン」は「キャストボタン」と呼ばれることもあります。

  • ミニ コントローラ: ユーザーがコンテンツをキャストしているときに、現在のコンテンツ ページまたは拡張コントローラから送信側アプリの別の画面に移動すると、画面の下部にミニ コントローラが表示されます。これにより、ユーザーは現在キャストしているメディアのメタデータを確認し、再生をコントロールできます。

  • 拡張コントローラ: ユーザーがコンテンツをキャストしているときに、メディア通知またはミニ コントローラをクリックすると、拡張コントローラが起動します。拡張コントローラには、現在再生中のメディアのメタデータが表示され、メディア再生を制御するためのボタンがいくつか用意されています。

  • 通知: Android のみ。ユーザーがコンテンツをキャストしているときに、送信側アプリから移動すると、現在キャストしているメディアのメタデータと再生コントロールを示すメディア通知が表示されます。

  • ロック画面: Android のみ。ユーザーがコンテンツをキャストしているときにロック画面に移動する(またはデバイスがタイムアウトする)と、メディア ロック画面コントロールが表示され、現在キャストしているメディアのメタデータと再生コントロールが表示されます。

次のガイドでは、これらのウィジェットをアプリに追加する方法について説明します。

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

Android の MediaRouter API は、セカンダリ デバイスでのメディアの表示と再生を可能にするように設計されています。MediaRouter API を使用する Android アプリは、ユーザー インターフェースの一部としてキャスト アイコンを含める必要があります。これにより、ユーザーはメディア ルートを選択して、キャスト デバイスなどのセカンダリ デバイスでメディアを再生できます。

このフレームワークを使用すると、MediaRouteButton を Cast button として簡単に追加できます。まず、メニューを定義する XML ファイルにメニュー項目または MediaRouteButton を追加し、CastButtonFactory を使用してフレームワークに接続する必要があります。

// To add a Cast button, add the following snippet.
// menu.xml
<item
    android:id="@+id/media_route_menu_item"
    android:title="@string/media_route_menu_title"
    app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
    app:showAsAction="always" />
Kotlin
// Then override the onCreateOptionMenu() for each of your activities.
// MyActivity.kt
override fun onCreateOptionsMenu(menu: Menu): Boolean {
    super.onCreateOptionsMenu(menu)
    menuInflater.inflate(R.menu.main, menu)
    CastButtonFactory.setUpMediaRouteButton(
        applicationContext,
        menu,
        R.id.media_route_menu_item
    )
    return true
}
Java
// Then override the onCreateOptionMenu() for each of your activities.
// MyActivity.java
@Override public boolean onCreateOptionsMenu(Menu menu) {
    super.onCreateOptionsMenu(menu);
    getMenuInflater().inflate(R.menu.main, menu);
    CastButtonFactory.setUpMediaRouteButton(getApplicationContext(),
                                            menu,
                                            R.id.media_route_menu_item);
    return true;
}

次に、Activity が FragmentActivity を継承している場合は、MediaRouteButton をレイアウトに追加できます。

// activity_layout.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:gravity="center_vertical"
   android:orientation="horizontal" >

   <androidx.mediarouter.app.MediaRouteButton
       android:id="@+id/media_route_button"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_weight="1"
       android:mediaRouteTypes="user"
       android:visibility="gone" />

</LinearLayout>
Kotlin
// MyActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_layout)

    mMediaRouteButton = findViewById<View>(R.id.media_route_button) as MediaRouteButton
    CastButtonFactory.setUpMediaRouteButton(applicationContext, mMediaRouteButton)

    mCastContext = CastContext.getSharedInstance(this)
}
Java
// MyActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_layout);

   mMediaRouteButton = (MediaRouteButton) findViewById(R.id.media_route_button);
   CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), mMediaRouteButton);

   mCastContext = CastContext.getSharedInstance(this);
}

テーマを使用してキャスト アイコンの外観を設定するには、キャスト アイコンをカスタマイズするをご覧ください。

デバイス検出を構成する

デバイスの検出は、CastContext によって完全に管理されます。CastContext を初期化するときに、送信側アプリはウェブ レシーバー アプリケーション ID を指定します。また、必要に応じて、CastOptions で supportedNamespaces を設定して名前空間のフィルタリングをリクエストできます。CastContext は内部で MediaRouter への参照を保持し、次の条件で検出プロセスを開始します。

  • デバイス検出のレイテンシとバッテリー使用量のバランスを取るように設計されたアルゴリズムに基づいて、送信側アプリがフォアグラウンドに入ると、検出が自動的に開始されることがあります。
  • [キャスト] ダイアログが開いている。
  • Cast SDK がキャスト セッションの復元を試みています。

検出プロセスは、キャスト ダイアログが閉じられるか、送信側アプリがバックグラウンドに移行すると停止します。

Kotlin
class CastOptionsProvider : OptionsProvider {
    companion object {
        const val CUSTOM_NAMESPACE = "urn:x-cast:custom_namespace"
    }

    override fun getCastOptions(appContext: Context): CastOptions {
        val supportedNamespaces: MutableList<String> = ArrayList()
        supportedNamespaces.add(CUSTOM_NAMESPACE)

        return CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .setSupportedNamespaces(supportedNamespaces)
            .build()
    }

    override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? {
        return null
    }
}
Java
class CastOptionsProvider implements OptionsProvider {
    public static final String CUSTOM_NAMESPACE = "urn:x-cast:custom_namespace";

    @Override
    public CastOptions getCastOptions(Context appContext) {
        List<String> supportedNamespaces = new ArrayList<>();
        supportedNamespaces.add(CUSTOM_NAMESPACE);

        CastOptions castOptions = new CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .setSupportedNamespaces(supportedNamespaces)
            .build();
        return castOptions;
    }

    @Override
    public List<SessionProvider> getAdditionalSessionProviders(Context context) {
        return null;
    }
}

セッション管理の仕組み

Cast SDK には、Cast セッションというコンセプトが導入されています。Cast セッションを確立するには、デバイスへの接続、ウェブ レシーバー アプリの起動(または参加)、そのアプリへの接続、メディア コントロール チャンネルの初期化という手順を組み合わせます。キャスト セッションとウェブ レシーバーのライフサイクルの詳細については、ウェブ レシーバーのアプリケーション ライフサイクル ガイドをご覧ください。

セッションは SessionManager クラスによって管理されます。このクラスには、CastContext.getSessionManager() を介してアクセスできます。個々のセッションは、Session クラスのサブクラスで表されます。たとえば、CastSession は、キャスト デバイスでのセッションを表します。アプリは、SessionManager.getCurrentCastSession() を介して現在アクティブな Cast セッションにアクセスできます。

アプリは、SessionManagerListener クラスを使用して、作成、一時停止、再開、終了などのセッション イベントをモニタリングできます。セッションがアクティブな状態で異常終了または突然終了した場合、フレームワークは自動的に再開を試みます。

セッションは、MediaRouter ダイアログからのユーザー ジェスチャーに応じて自動的に作成、破棄されます。

Cast の起動エラーをより詳しく把握するために、アプリは CastContext#getCastReasonCodeForCastStatusCode(int) を使用して、セッション起動エラーを CastReasonCodes に変換できます。一部のセッション開始エラー(CastReasonCodes#CAST_CANCELLED など)は、意図された動作であり、エラーとしてログに記録しないでください。

セッションの状態の変化を把握する必要がある場合は、SessionManagerListener を実装します。この例では、Activity で CastSession の可用性をリッスンします。

Kotlin
class MyActivity : Activity() {
    private var mCastSession: CastSession? = null
    private lateinit var mCastContext: CastContext
    private lateinit var mSessionManager: SessionManager
    private val mSessionManagerListener: SessionManagerListener<CastSession> =
        SessionManagerListenerImpl()

    private inner class SessionManagerListenerImpl : SessionManagerListener<CastSession?> {
        override fun onSessionStarting(session: CastSession?) {}

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

        override fun onSessionStartFailed(session: CastSession?, error: Int) {
            val castReasonCode = mCastContext.getCastReasonCodeForCastStatusCode(error)
            // Handle error
        }

        override fun onSessionSuspended(session: CastSession?, reason Int) {}

        override fun onSessionResuming(session: CastSession?, sessionId: String) {}

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

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

        override fun onSessionEnding(session: CastSession?) {}

        override fun onSessionEnded(session: CastSession?, error: Int) {
            finish()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mCastContext = CastContext.getSharedInstance(this)
        mSessionManager = mCastContext.sessionManager
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession::class.java)
    }

    override fun onResume() {
        super.onResume()
        mCastSession = mSessionManager.currentCastSession
    }

    override fun onDestroy() {
        super.onDestroy()
        mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession::class.java)
    }
}
Java
public class MyActivity extends Activity {
    private CastContext mCastContext;
    private CastSession mCastSession;
    private SessionManager mSessionManager;
    private SessionManagerListener<CastSession> mSessionManagerListener =
            new SessionManagerListenerImpl();

    private class SessionManagerListenerImpl implements SessionManagerListener<CastSession> {
        @Override
        public void onSessionStarting(CastSession session) {}
        @Override
        public void onSessionStarted(CastSession session, String sessionId) {
            invalidateOptionsMenu();
        }
        @Override
        public void onSessionStartFailed(CastSession session, int error) {
            int castReasonCode = mCastContext.getCastReasonCodeForCastStatusCode(error);
            // Handle error
        }
        @Override
        public void onSessionSuspended(CastSession session, int reason) {}
        @Override
        public void onSessionResuming(CastSession session, String sessionId) {}
        @Override
        public void onSessionResumed(CastSession session, boolean wasSuspended) {
            invalidateOptionsMenu();
        }
        @Override
        public void onSessionResumeFailed(CastSession session, int error) {}
        @Override
        public void onSessionEnding(CastSession session) {}
        @Override
        public void onSessionEnded(CastSession session, int error) {
            finish();
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mCastContext = CastContext.getSharedInstance(this);
        mSessionManager = mCastContext.getSessionManager();
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession.class);
    }

    @Override
    protected void onResume() {
        super.onResume();
        mCastSession = mSessionManager.getCurrentCastSession();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession.class);
    }
}

ストリーミング転送

セッション状態の保持は、ストリーム転送の基本です。ユーザーは、音声コマンド、Google Home アプリ、スマートディスプレイを使用して、既存のオーディオ ストリームと動画ストリームをデバイス間で移動できます。メディアの再生が 1 つのデバイス(ソース)で停止し、別のデバイス(デスティネーション)で続行されます。最新のファームウェアを搭載した Cast デバイスは、ストリーム転送のソースまたは宛先として機能します。

ストリーミング転送中または拡張中に新しい宛先デバイスを取得するには、CastSession#addCastListener を使用して Cast.Listener を登録します。次に、onDeviceNameChanged コールバック中に CastSession#getCastDevice() を呼び出します。

詳細については、ウェブ レシーバーでのストリーム転送をご覧ください。

自動再接続

フレームワークには ReconnectionService が用意されており、送信側アプリで有効にすることで、次のような多くの微妙なコーナーケースで再接続を処理できます。

  • Wi-Fi が一時的に切断された場合の復旧
  • デバイスのスリープから復帰する
  • アプリをバックグラウンドから復帰させる
  • アプリがクラッシュした場合に復元する

このサービスはデフォルトでオンになっていますが、CastOptions.Builder でオフにできます。

このサービスは、Gradle ファイルで自動マージが有効になっている場合、アプリのマニフェストに自動的にマージできます。

フレームワークは、メディア セッションがあるときにサービスを開始し、メディア セッションが終了するとサービスを停止します。

メディア コントロールの仕組み

Cast フレームワークでは、Cast 2.x の RemoteMediaPlayer クラスが非推奨となり、代わりに新しいクラス RemoteMediaClient が導入されました。このクラスは、より便利な API セットで同じ機能を提供し、GoogleApiClient を渡す必要がなくなります。

アプリがメディア名前空間をサポートするウェブ レシーバー アプリとの CastSession を確立すると、RemoteMediaClient のインスタンスがフレームワークによって自動的に作成されます。アプリは、CastSession インスタンスで getRemoteMediaClient() メソッドを呼び出すことで、このインスタンスにアクセスできます。

ウェブ レシーバーにリクエストを発行する RemoteMediaClient のすべてのメソッドは、そのリクエストの追跡に使用できる PendingResult オブジェクトを返します。

RemoteMediaClient のインスタンスは、アプリの複数の部分で共有されることが想定されています。実際、永続的なミニコントローラや通知サービスなど、フレームワークの一部の内部コンポーネントでも共有されます。このため、このインスタンスは RemoteMediaClient.Listener の複数のインスタンスの登録をサポートしています。

メディア メタデータを設定する

MediaMetadata クラスは、キャストするメディア アイテムに関する情報を表します。次の例では、映画の新しい MediaMetadata インスタンスを作成し、タイトル、サブタイトル、2 つの画像を設定しています。

Kotlin
val movieMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE)

movieMetadata.putString(MediaMetadata.KEY_TITLE, mSelectedMedia.getTitle())
movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, mSelectedMedia.getStudio())
movieMetadata.addImage(WebImage(Uri.parse(mSelectedMedia.getImage(0))))
movieMetadata.addImage(WebImage(Uri.parse(mSelectedMedia.getImage(1))))
Java
MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);

movieMetadata.putString(MediaMetadata.KEY_TITLE, mSelectedMedia.getTitle());
movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, mSelectedMedia.getStudio());
movieMetadata.addImage(new WebImage(Uri.parse(mSelectedMedia.getImage(0))));
movieMetadata.addImage(new WebImage(Uri.parse(mSelectedMedia.getImage(1))));

メディア メタデータを含む画像の使用については、画像の選択をご覧ください。

メディアを読み込む

次のコードに示すように、アプリはメディア アイテムを読み込むことができます。まず、メディアのメタデータとともに MediaInfo.Builder を使用して、MediaInfo インスタンスをビルドします。現在の CastSession から RemoteMediaClient を取得し、その RemoteMediaClient に MediaInfo を読み込みます。RemoteMediaClient を使用して、ウェブ レシーバーで実行されているメディア プレーヤー アプリの再生、一時停止などの操作を行います。

Kotlin
val mediaInfo = MediaInfo.Builder(mSelectedMedia.getUrl())
    .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
    .setContentType("videos/mp4")
    .setMetadata(movieMetadata)
    .setStreamDuration(mSelectedMedia.getDuration() * 1000)
    .build()
val remoteMediaClient = mCastSession.getRemoteMediaClient()
remoteMediaClient.load(MediaLoadRequestData.Builder().setMediaInfo(mediaInfo).build())
Java
MediaInfo mediaInfo = new MediaInfo.Builder(mSelectedMedia.getUrl())
        .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
        .setContentType("videos/mp4")
        .setMetadata(movieMetadata)
        .setStreamDuration(mSelectedMedia.getDuration() * 1000)
        .build();
RemoteMediaClient remoteMediaClient = mCastSession.getRemoteMediaClient();
remoteMediaClient.load(new MediaLoadRequestData.Builder().setMediaInfo(mediaInfo).build());

メディア トラックの使用に関するセクションもご覧ください。

4K 動画形式

メディアの動画形式を確認するには、MediaStatus の getVideoInfo() を使用して、VideoInfo の現在のインスタンスを取得します。このインスタンスには、HDR TV 形式のタイプと、ディスプレイの高さと幅(ピクセル単位)が含まれます。4K フォーマットのバリエーションは、定数 HDR_TYPE_* で示されます。

複数のデバイスへのリモコン通知

ユーザーがキャストしているときに、同じネットワーク上の他の Android デバイスに通知が届き、再生を操作できるようになります。このような通知を受け取るデバイスのユーザーは、設定アプリの [Google] > [Google Cast] > [リモコンの通知を表示] で、そのデバイスの通知をオフにできます。(通知には設定アプリへのショートカットが含まれています)。詳しくは、キャストのリモコン通知をご覧ください。

ミニコントローラを追加する

Cast デザイン チェックリストによると、送信側アプリは、ミニ コントローラと呼ばれる永続的なコントロールを提供する必要があります。このコントロールは、ユーザーが現在のコンテンツ ページから送信側アプリの別の部分に移動したときに表示されます。ミニ コントローラは、現在のキャスト セッションをユーザーに視覚的に通知します。ミニ コントローラをタップすると、キャストの全画面表示の拡張コントローラ ビューに戻ることができます。

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

<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" />

送信側アプリが動画または音声のライブ ストリームを再生している場合、SDK はミニ コントローラの再生 / 一時停止ボタンの代わりに、再生 / 停止ボタンを自動的に表示します。

このカスタムビューのタイトルとサブタイトルのテキストの外観を設定し、ボタンを選択するには、ミニ コントローラをカスタマイズするをご覧ください。

拡張コントローラを追加する

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

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

まず、キャスト アイコンを提供する拡張コントローラ用の新しいメニュー リソース ファイルを作成します。

<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>

ExpandedControllerActivity を拡張する新しいクラスを作成します。

Kotlin
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
    }
}
Java
public class ExpandedControlsActivity extends ExpandedControllerActivity {
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        getMenuInflater().inflate(R.menu.expanded_controller, menu);
        CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item);
        return true;
    }
}

次に、アプリ マニフェストの application タグ内で新しいアクティビティを宣言します。

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

CastOptionsProvider を編集し、NotificationOptions と CastMediaOptions を変更して、ターゲット アクティビティを新しいアクティビティに設定します。

Kotlin
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()
}
Java
public CastOptions getCastOptions(Context context) {
    NotificationOptions notificationOptions = new NotificationOptions.Builder()
            .setTargetActivityClassName(ExpandedControlsActivity.class.getName())
            .build();
    CastMediaOptions mediaOptions = new CastMediaOptions.Builder()
            .setNotificationOptions(notificationOptions)
            .setExpandedControllerActivityClassName(ExpandedControlsActivity.class.getName())
            .build();

    return new CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .setCastMediaOptions(mediaOptions)
            .build();
}

リモート メディアが読み込まれたときに新しいアクティビティを表示するように、LocalPlayerActivity loadRemoteMedia メソッドを更新します。

Kotlin
private fun loadRemoteMedia(position: Int, autoPlay: Boolean) {
    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(mSelectedMedia)
            .setAutoplay(autoPlay)
            .setCurrentTime(position.toLong()).build()
    )
}
Java
private void loadRemoteMedia(int position, boolean autoPlay) {
    if (mCastSession == null) {
        return;
    }
    final RemoteMediaClient remoteMediaClient = mCastSession.getRemoteMediaClient();
    if (remoteMediaClient == null) {
        return;
    }
    remoteMediaClient.registerCallback(new RemoteMediaClient.Callback() {
        @Override
        public void onStatusUpdated() {
            Intent intent = new Intent(LocalPlayerActivity.this, ExpandedControlsActivity.class);
            startActivity(intent);
            remoteMediaClient.unregisterCallback(this);
        }
    });
    remoteMediaClient.load(new MediaLoadRequestData.Builder()
            .setMediaInfo(mSelectedMedia)
            .setAutoplay(autoPlay)
            .setCurrentTime(position).build());
}

送信側アプリが動画または音声のライブ ストリームを再生している場合、SDK は、展開コントローラの再生 / 一時停止ボタンの代わりに、再生 / 停止ボタンを自動的に表示します。

テーマを使用して外観を設定し、表示するボタンを選択して、カスタムボタンを追加するには、拡張コントローラをカスタマイズするをご覧ください。

音量の調整

フレームワークは、送信側アプリの音量を自動的に管理します。フレームワークは、送信側アプリとウェブ レシーバー アプリを自動的に同期するため、送信側 UI は常にウェブ レシーバーで指定された音量を報告します。

物理ボタンによる音量調節

Android では、Jelly Bean 以降を使用するデバイスの場合、デフォルトで、送信側デバイスの物理ボタンを使用して、ウェブ レシーバーの Cast セッションの音量を変更できます。

Jelly Bean より前の物理ボタンによる音量調節

Jelly Bean より前の Android デバイスで物理的な音量ボタンを使用してウェブ レシーバー デバイスの音量を制御するには、送信側アプリがアクティビティで dispatchKeyEvent をオーバーライドし、CastContext.onDispatchVolumeKeyEventBeforeJellyBean() を呼び出す必要があります。

Kotlin
class MyActivity : FragmentActivity() {
    override fun dispatchKeyEvent(event: KeyEvent): Boolean {
        return (CastContext.getSharedInstance(this)
            .onDispatchVolumeKeyEventBeforeJellyBean(event)
                || super.dispatchKeyEvent(event))
    }
}
Java
class MyActivity extends FragmentActivity {
    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        return CastContext.getSharedInstance(this)
            .onDispatchVolumeKeyEventBeforeJellyBean(event)
            || super.dispatchKeyEvent(event);
    }
}

通知とロック画面にメディア コントロールを追加する

Android のみ、Google Cast デザイン チェックリストでは、送信側がキャストしているが送信側アプリにフォーカスがない場合に、送信側アプリが通知とロック画面にメディア コントロールを実装する必要があります。フレームワークには、送信側アプリが通知とロック画面にメディア コントロールを構築できるように、MediaNotificationService と MediaIntentReceiver が用意されています。

MediaNotificationService は、送信者がキャストしているときに実行され、画像のサムネイルと現在のキャスト アイテムに関する情報、再生 / 一時停止ボタン、停止ボタンを含む通知を表示します。

MediaIntentReceiver は、通知からのユーザー アクションを処理する BroadcastReceiver です。

アプリは、NotificationOptions を使用して、ロック画面からの通知とメディア コントロールを構成できます。アプリは、通知に表示するコントロール ボタンと、ユーザーが通知をタップしたときに開く Activity を設定できます。アクションが明示的に指定されていない場合は、デフォルト値の MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK と MediaIntentReceiver.ACTION_STOP_CASTING が使用されます。

Kotlin
// Example showing 4 buttons: "rewind", "play/pause", "forward" and "stop casting".
val buttonActions: MutableList<String> = ArrayList()
buttonActions.add(MediaIntentReceiver.ACTION_REWIND)
buttonActions.add(MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK)
buttonActions.add(MediaIntentReceiver.ACTION_FORWARD)
buttonActions.add(MediaIntentReceiver.ACTION_STOP_CASTING)

// Showing "play/pause" and "stop casting" in the compat view of the notification.
val compatButtonActionsIndices = intArrayOf(1, 3)

// Builds a notification with the above actions. Each tap on the "rewind" and "forward" buttons skips 30 seconds.
// Tapping on the notification opens an Activity with class VideoBrowserActivity.
val notificationOptions = NotificationOptions.Builder()
    .setActions(buttonActions, compatButtonActionsIndices)
    .setSkipStepMs(30 * DateUtils.SECOND_IN_MILLIS)
    .setTargetActivityClassName(VideoBrowserActivity::class.java.name)
    .build()
Java
// Example showing 4 buttons: "rewind", "play/pause", "forward" and "stop casting".
List<String> buttonActions = new ArrayList<>();
buttonActions.add(MediaIntentReceiver.ACTION_REWIND);
buttonActions.add(MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK);
buttonActions.add(MediaIntentReceiver.ACTION_FORWARD);
buttonActions.add(MediaIntentReceiver.ACTION_STOP_CASTING);

// Showing "play/pause" and "stop casting" in the compat view of the notification.
int[] compatButtonActionsIndices = new int[]{1, 3};

// Builds a notification with the above actions. Each tap on the "rewind" and "forward" buttons skips 30 seconds.
// Tapping on the notification opens an Activity with class VideoBrowserActivity.
NotificationOptions notificationOptions = new NotificationOptions.Builder()
    .setActions(buttonActions, compatButtonActionsIndices)
    .setSkipStepMs(30 * DateUtils.SECOND_IN_MILLIS)
    .setTargetActivityClassName(VideoBrowserActivity.class.getName())
    .build();

通知とロック画面からのメディア コントロールの表示はデフォルトでオンになっています。CastMediaOptions.Builder で null を指定して setNotificationOptions を呼び出すことで、無効にできます。現在、ロック画面機能は、通知がオンになっている限りオンになります。

Kotlin
// ... continue with the NotificationOptions built above
val mediaOptions = CastMediaOptions.Builder()
    .setNotificationOptions(notificationOptions)
    .build()
val castOptions: CastOptions = Builder()
    .setReceiverApplicationId(context.getString(R.string.app_id))
    .setCastMediaOptions(mediaOptions)
    .build()
Java
// ... continue with the NotificationOptions built above
CastMediaOptions mediaOptions = new CastMediaOptions.Builder()
        .setNotificationOptions(notificationOptions)
        .build();
CastOptions castOptions = new CastOptions.Builder()
        .setReceiverApplicationId(context.getString(R.string.app_id))
        .setCastMediaOptions(mediaOptions)
        .build();

送信側アプリが動画または音声のライブ ストリームを再生している場合、SDK は、通知コントロールの再生 / 一時停止ボタンの代わりに、再生 / 停止ボタンを自動的に表示します。ただし、ロック画面コントロールには表示しません。

注: Lollipop より前のデバイスでロック画面のコントロールを表示するために、RemoteMediaClient は自動的にオーディオ フォーカスをリクエストします。

エラーを処理する

送信側アプリがすべてのエラー コールバックを処理し、キャスト ライフサイクルの各ステージに最適なレスポンスを決定することは非常に重要です。アプリは、エラー ダイアログをユーザーに表示することも、ウェブ レシーバーへの接続を破棄することもできます。