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

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

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

送信者フレームワークとは、送信時にランタイムに存在する、キャスト クラス ライブラリ バイナリと関連リソースを指します。送信者アプリまたはキャストアプリとは、同じく送信側で実行されているアプリを指します。Web Receiver アプリは、Cast 対応デバイスで実行されている HTML アプリケーションです。

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

アプリケーションの流れ

送信者の Android アプリの一般的な実行フローは次のとおりです。

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

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

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

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

uses-sdk

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

<uses-sdk
        android:minSdkVersion="19"
        android:targetSdkVersion="28" />

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>

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

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

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

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);
    }
}

キャスト UX ウィジェット

キャスト フレームワークには、キャスト デザイン チェックリストを遵守するウィジェットが用意されています。

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

  • キャスト アイコン: キャスト アイコンは、アプリをサポートするレシーバーが検出されたときに表示されます。ユーザーが最初にキャスト ボタンをクリックすると、検出されたデバイスのリストがキャスト ダイアログに表示されます。ユーザーがデバイスを接続した状態でキャスト ボタンをクリックすると、現在のメディア メタデータ(タイトル、録音スタジオの名前、サムネイル画像など)が表示されます。また、ユーザーがキャスト デバイスとの接続を解除することもできます。

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

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

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

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

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

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

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

このフレームワークを使用すると、MediaRouteButtonCast 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;
}

ActivityFragmentActivity を継承する場合は、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 を指定し、必要に応じて CastOptionssupportedNamespaces を設定して名前空間フィルタリングをリクエストできます。CastContext は、内部で MediaRouter への参照を保持し、送信側アプリがフォアグラウンドに入ると検出プロセスを開始し、送信側アプリがバックグラウンドに入ると停止します。

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 には、キャスト セッションというコンセプトが導入されています。これには、デバイスへの接続、Web Receiver アプリの起動(または参加)、アプリの接続、メディア コントロール チャネルの初期化のステップが確立されます。キャスト セッションと Web Receiver のライフサイクルについて詳しくは、Web Receiver のアプリケーション ライフサイクル ガイドをご覧ください。

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

アプリでは、SessionManagerListener クラスを使用して、作成、停止、再開、終了などのセッション イベントをモニタリングできます。フレームワークは、セッション中にアクティブな異常や突然の終了から自動的に再開しようとします。

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

セッションの状態変化を認識する必要がある場合は、SessionManagerListener を実装できます。この例では、ActivityCastSession の可用性をリッスンしています。

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

    private inner class SessionManagerListenerImpl : SessionManagerListener<CastSession?> {
        override fun onSessionStarted(session: CastSession?, sessionId: String) {
            invalidateOptionsMenu()
        }

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

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

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mSessionManager = CastContext.getSharedInstance(this).sessionManager
    }

    override fun onResume() {
        super.onResume()
        mCastSession = mSessionManager.currentCastSession
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession::class.java)
    }

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

    private class SessionManagerListenerImpl implements SessionManagerListener<CastSession> {
        @Override
        public void onSessionStarted(CastSession session, String sessionId) {
            invalidateOptionsMenu();
        }
        @Override
        public void onSessionResumed(CastSession session, boolean wasSuspended) {
            invalidateOptionsMenu();
        }
        @Override
        public void onSessionEnded(CastSession session, int error) {
            finish();
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mSessionManager = CastContext.getSharedInstance(this).getSessionManager();
    }
    @Override
    protected void onResume() {
        super.onResume();
        mCastSession = mSessionManager.getCurrentCastSession();
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession.class);
    }
    @Override
    protected void onPause() {
        super.onPause();
        mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession.class);
        mCastSession = null;
    }
}

ストリーミング転送

セッション状態を維持することはストリーム転送の基礎であり、ユーザーは音声コマンド、Google Home アプリ、スマートディスプレイを使用して、既存の音声および動画ストリームをデバイス間で移動できます。メディアは、あるデバイス(ソース)で再生を停止し、別のデバイス(宛先)で再生を続けます。最新のファームウェアを備えたキャスト デバイスは、ストリーミング転送のソースまたは宛先として機能できます。

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

詳細については、Web Receiver でのストリーム転送をご覧ください。

自動再接続

フレームワークには 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 を取得し、その RemoteMediaClientMediaInfo を読み込みます。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 からリモコンの通知を表示することもできます。(通知には設定アプリへのショートカットが含まれています)。詳しくは、リモコンの通知をキャストするをご覧ください。

ミニ コントローラを追加

キャスト デザインのチェックリストに従って、送信側アプリは「ミニ コントローラ」という永続的なコントロールを提供する必要があります。このコントロールは、ユーザーが現在のコンテンツ ページから離れて送信側アプリの別の部分に移動したときに表示されるものです。ミニ コントローラは、現在のキャスト セッションのあらためてユーザーにリマインダーを表示します。ミニ コントローラをタップすると、全画面表示のコントローラのキャスト表示に戻ることができます。

このフレームワークには、ミニビューの 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" />

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

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

拡張コントローラを追加

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 を編集し、NotificationOptionsCastMediaOptions を変更して、ターゲット アクティビティを新しいアクティビティに設定します。

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 は展開されたコントローラの再生/一時停止ボタンの代わりに再生/停止ボタンを自動的に表示します。

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

音量調節

フレームワークは、送信側アプリの音量を自動的に管理します。このフレームワークは、送信側アプリと Web 受信側アプリを自動的に同期し、送信側の UI で Web Receiver で指定されたボリュームが常に報告されるようにします。

物理ボタンの音量調節

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

Jelly Bean 導入前の物理ボタンの音量調整

物理ボリューム キーを使用して、Jelly Bean より前の Android デバイスで Web レシーバー デバイスのボリュームを制御するには、送信者アプリで、アクティビティの 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 の設計チェックリストでは、送信側アプリは通知内にメディア コントロールを実装し、ロック画面に実装する必要があります。送信側はキャストしますが、送信側アプリはフォーカスがありません。フレームワークは MediaNotificationServiceMediaIntentReceiver を提供します。これにより、送信側アプリは通知内とロック画面内にメディア コントロールを作成できます。

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

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

アプリは、NotificationOptions を使用して、ロック画面から通知とメディア コントロールを構成できます。アプリでは、通知に表示するコントロール ボタンと、ユーザーが通知をタップしたときに開く Activity を設定できます。アクションが明示的に指定されていない場合は、デフォルト値の MediaIntentReceiver.ACTION_TOGGLE_PLAYBACKMediaIntentReceiver.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 が音声フォーカスを自動的にリクエストします。

エラーを処理する

送信側アプリは、すべてのエラー コールバックを処理し、キャスト ライフサイクルの各ステージで最適な応答を決定することが重要です。アプリでは、ユーザーにエラー ダイアログを表示するか、Web Receiver への接続を破棄するかを選択できます。