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

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

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

センダー フレームワークとは、実行時にセンダーに存在するキャスト クラス ライブラリ バイナリと関連リソースを指します。センダーアプリまたはキャストアプリは、センダーで実行されているアプリを指します。ウェブ レシーバー アプリは、Cast 対応デバイス上で実行される HTML アプリケーションを指します。

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

アプリケーションの流れ

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

  • キャスト フレームワークは、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 の最小レベルと対象 API レベルを設定します。現在、最小値は API レベル 21、ターゲットは API レベル 28 です。

<uses-sdk
        android:minSdkVersion="21"
        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 のインスタンスを提供します。最も重要なのはウェブ レシーバー アプリケーション ID です。この 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 ウィジェット

キャスト フレームワークは、キャスト デザイン チェックリストに準拠したウィジェットを提供します。

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

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

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

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

  • 通知: 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 への参照を内部で保持しており、次の条件で検出プロセスを開始します。

  • デバイス検出のレイテンシとバッテリー使用量のバランスを取るように設計されたアルゴリズムに基づき、センダーアプリがフォアグラウンドになると、検出が自動的に開始される場合があります。
  • キャスト ダイアログが開いています。
  • 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 ではキャスト セッションの概念が導入されています。キャスト セッションの確立は、デバイスへの接続、ウェブ レシーバー アプリの起動(または参加)、そのアプリへの接続、メディア コントロール チャンネルの初期化の各ステップを組み合わせて確立されます。キャスト セッションとウェブ レシーバーのライフサイクルについて詳しくは、ウェブ レシーバーのアプリケーション ライフサイクル ガイドをご覧ください。

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

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

セッションは、MediaRouter ダイアログからのユーザー操作に応じて自動的に作成および破棄されます。

キャスト開始エラーを詳細に把握するには、アプリで 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
    }

    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 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();
    }
    @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() を呼び出します。

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

自動再接続

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

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

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

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

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

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

Cast フレームワークでは、Cast 2.x での RemoteMediaPlayer クラスのサポートが終了し、より便利な API のセットで同じ機能を提供し、GoogleApiClient を渡す必要がない新しいクラス RemoteMediaClient に置き換えられました。

アプリがメディア名前空間をサポートするウェブ レシーバー アプリで 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" />

センダーアプリが動画や音声のライブ ストリームを再生しているときに、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 を編集し、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());
}

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

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

音量調節

フレームワークはセンダーアプリの音量を自動的に管理します。センダーアプリとウェブレシーバー アプリを自動的に同期して、センダーの UI は常にウェブ レシーバーが指定した音量を報告します。

物理ボタンの音量調節

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

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 デザイン チェックリストでは、センダーアプリは通知にメディア コントロールを実装する必要があります。また、センダーはロック画面(センダーはキャストされているがセンダー アプリにフォーカスがない状態)にメディア コントロールを実装する必要があります。フレームワークには 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 への接続を破棄したりできます。