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

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

モバイル デバイスまたはノートパソコンが再生を制御する送信側となり、Google Cast デバイスが受信側としてテレビにコンテンツを表示します。

送信側フレームワークとは、送信側で実行される、キャスト クラス ライブラリ バイナリと関連リソースを指します。「送信側アプリ」または「キャストアプリ」とは、送信側アプリでも実行されているアプリのことです。ウェブ レシーバー アプリとは、Cast 対応デバイス上で実行される HTML アプリを指します。

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

アプリケーションの流れ

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

  • キャスト フレームワークは、Activity のライフサイクルに基づいて、MediaRouter デバイスの検出を自動的に開始します。
  • ユーザーがキャスト ボタンをクリックすると、検出されたキャスト デバイスのリストを含むキャスト ダイアログが表示されます。
  • ユーザーがキャスト デバイスを選択すると、フレームワークはキャスト デバイスでウェブ レシーバー アプリを起動しようとします。
  • フレームワークは送信側のアプリでコールバックを呼び出し、Web Receiver アプリが起動されたことを確認します。
  • フレームワークは、送信者アプリとウェブ受信者アプリの間に通信チャネルを作成します。
  • フレームワークは通信チャネルを使用して、Web Receiver でのメディア再生の読み込みと制御を行います。
  • フレームワークは、送信者とウェブ レシーバーの間でメディアの再生状態を同期します。ユーザーが送信者 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 レベル 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 のインスタンスを提供します。この中で最も重要なのは Web Receiver アプリケーション 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;
    }
}

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

<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 Design チェックリストを遵守したウィジェットが用意されています。

  • 導入オーバーレイ: フレームワークはカスタムビュー 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 を初期化するときに、送信側アプリは Web Receiver アプリケーション ID を指定し、必要に応じて CastOptionssupportedNamespaces を設定して名前空間フィルタリングをリクエストできます。CastContextMediaRouter への参照を内部で保持し、次の条件下で検出プロセスを開始します。

  • デバイス検出のレイテンシとバッテリー使用量のバランスを取るように設計されたアルゴリズムに基づいて、送信側アプリがフォアグラウンドになると、検出が自動的に開始されることがあります。
  • キャスト ダイアログが開きます。
  • 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 セッションと Web Receiver のライフサイクルについて詳しくは、Web Receiver のアプリケーション ライフサイクル ガイドをご覧ください。

セッションは、アプリが CastContext.getSessionManager() を介してアクセスできるクラス SessionManager によって管理されます。個々のセッションは、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 クラスのサポートが終了し、新しいクラス RemoteMediaClient に置き換えられました。RemoteMediaClient は、同じ機能を便利な API セットで提供し、GoogleApiClient を渡す必要がありません。

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

Web Receiver にリクエストを発行する 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 を使用して、Web Receiver で実行されているメディア プレーヤー アプリの再生、一時停止などの操作を行います。

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

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

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

音量の調整

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

物理ボタンの音量調節

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

Jelly Bean 以前の物理ボタンの音量調節

Jelly Bean よりも古い Android デバイスで、物理的な音量キーを使用して Web Receiver デバイスの音量を制御するには、送信側アプリがアクティビティの 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);
    }
}

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

Google Cast デザイン チェックリスト(Android のみ)では、送信側アプリで通知内ロック画面(送信側アプリはフォーカスがない状態)にメディア コントロールを実装する必要があります。このフレームワークには 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 への接続を破棄することもできます。