このデベロッパー ガイドでは、Android Sender SDK を使用して Android 送信側アプリに Google Cast のサポートを追加する方法について説明します。
モバイル デバイスまたはノートパソコンは再生を制御する送信側、Google Cast デバイスはテレビにコンテンツを表示する受信側です。
送信側フレームワークは、送信側で実行時に存在する Cast クラス ライブラリ バイナリと関連リソースを指します。センダーアプリまたはキャストアプリは、センダーでも実行されているアプリを指します。ウェブ レシーバー アプリとは、キャスト対応デバイスで動作する HTML アプリケーションのことです。
送信側フレームワークは、非同期コールバック設計を使用して、送信側アプリにイベントを通知し、キャスト アプリのライフサイクルのさまざまな状態間を遷移します。
アプリケーションの流れ
送信側 Android アプリの一般的な実行フローの概要は次のとおりです。
- Cast フレームワークは、
Activity
ライフサイクルに基づいてMediaRouter
デバイスの検出を自動的に開始します。 - ユーザーがキャスト アイコンをクリックすると、フレームワークは検出されたキャスト デバイスのリストを含むキャスト ダイアログを表示します。
- ユーザーがキャスト デバイスを選択すると、フレームワークはキャスト デバイスで Web レシーバー アプリを起動しようとします。
- フレームワークは、センダーアプリでコールバックを呼び出して、ウェブ レシーバー アプリが起動したことを確認します。
- フレームワークは、送信側アプリと Web Receiver アプリの間に通信チャネルを作成します。
- フレームワークは、通信チャネルを使用して Web レシーバーでメディア再生を読み込んで制御します。
- フレームワークは、送信側とウェブ レシーバーの間でメディア再生状態を同期します。ユーザーが送信側の UI アクションを行うと、フレームワークはメディア コントロール リクエストをウェブ レシーバーに渡し、ウェブ レシーバーがメディア ステータスの更新を送信すると、フレームワークは送信側の UI の状態を更新します。
- ユーザーがキャスト アイコンをクリックしてキャスト デバイスから切断すると、フレームワークはセンダーアプリをウェブ レシーバーから切断します。
Google Cast Android SDK のすべてのクラス、メソッド、イベントの一覧については、Android 向け Google Cast 送信側 API リファレンスをご覧ください。以下のセクションでは、Android アプリに Cast を追加する手順について説明します。
Android マニフェストを構成する
アプリの AndroidManifest.xml ファイルでは、Cast SDK 用に次の要素を構成する必要があります。
uses-sdk
Cast SDK がサポートする最小 Android API レベルと対象 Android API レベルを設定します。現在の最小 API レベルは 23、対象 API レベルは 34 です。
<uses-sdk
android:minSdkVersion="23"
android:targetSdkVersion="34" />
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
があります。
CastContext
シングルトンを初期化するために必要なオプションを提供するには、アプリで OptionsProvider
インターフェースを実装する必要があります。OptionsProvider
は、フレームワークの動作に影響するオプションを含む CastOptions
のインスタンスを提供します。最も重要なオプションはウェブ レシーバー アプリケーション ID で、検出結果をフィルタし、キャスト セッションの開始時にウェブ レシーバー アプリを起動するために使用されます。
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 } }
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()
が呼び出されたときに遅延初期化されます。
class MyActivity : FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { val castContext = CastContext.getSharedInstance(this) } }
public class MyActivity extends FragmentActivity { @Override public void onCreate(Bundle savedInstanceState) { CastContext castContext = CastContext.getSharedInstance(this); } }
キャスト UX ウィジェット
Cast フレームワークには、Cast デザイン チェックリストに準拠したウィジェットが用意されています。
導入オーバーレイ: フレームワークには、カスタム View
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" />
// 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 }
// 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>
// 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) }
// 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 が Cast セッションの復元を試みています。
キャスト ダイアログが閉じられるか、送信側アプリがバックグラウンドに移行すると、検出プロセスは停止します。
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 } }
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 レシーバのライフサイクルについて詳しくは、Web レシーバのアプリケーション ライフサイクル ガイドをご覧ください。
セッションは SessionManager
クラスによって管理され、アプリは CastContext.getSessionManager()
を介してアクセスできます。個々のセッションは、Session
クラスのサブクラスで表されます。たとえば、CastSession
はキャスト デバイスでのセッションを表します。アプリは、SessionManager.getCurrentCastSession()
を介して現在アクティブな Cast セッションにアクセスできます。
アプリは SessionManagerListener
クラスを使用して、作成、停止、再開、終了などのセッション イベントをモニタリングできます。フレームワークは、セッションがアクティブな間に異常な終了が発生した場合、自動的に再開を試みます。
セッションは、MediaRouter
ダイアログでのユーザー操作に応じて自動的に作成、終了されます。
キャストの開始エラーをより深く理解するために、アプリは CastContext#getCastReasonCodeForCastStatusCode(int)
を使用してセッションの開始エラーを CastReasonCodes
に変換できます。セッションの開始エラー(CastReasonCodes#CAST_CANCELLED
など)は意図的な動作であり、エラーとして記録しないでください。
セッションの状態の変化を把握する必要がある場合は、SessionManagerListener
を実装できます。この例では、Activity
内の CastSession
の可用性をリッスンします。
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) } }
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 を渡す必要がありません。
メディア名前空間をサポートする Web レシーバー アプリでアプリが CastSession
を確立すると、フレームワークによって RemoteMediaClient
のインスタンスが自動的に作成されます。アプリは、CastSession
インスタンスで getRemoteMediaClient()
メソッドを呼び出すことで、このインスタンスにアクセスできます。
Web Receiver にリクエストを発行する RemoteMediaClient
のすべてのメソッドは、そのリクエストの追跡に使用できる PendingResult オブジェクトを返します。
RemoteMediaClient
のインスタンスはアプリの複数の部分で共有されることが想定されており、実際、永続的なミニ コントローラや通知サービスなど、フレームワークの内部コンポーネントの一部で共有されています。そのため、このインスタンスは RemoteMediaClient.Listener
の複数のインスタンスの登録をサポートします。
メディアのメタデータを設定する
MediaMetadata
クラスは、キャストするメディア アイテムに関する情報を表します。次の例では、映画の新しい MediaMetadata インスタンスを作成し、タイトル、サブタイトル、2 つの画像を設定します。
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))))
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
を使用して、ウェブ レシーバーで実行されているメディア プレーヤー アプリの再生、一時停止などの操作を行います。
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())
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
を拡張する新しいクラスを作成します。
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 } }
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
を変更し、ターゲット アクティビティを新しいアクティビティに設定します。
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() }
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
メソッドを更新します。
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() ) }
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 以降を使用するデバイスの場合、送信側デバイスの物理ボタンを使用して、ウェブ レシーバーのキャスト セッションの音量をデフォルトで変更できます。
Jelly Bean 以前の物理ボタンによる音量調節
Jelly Bean より前の Android デバイスで物理音量キーを使用して Web Receiver デバイスの音量を制御するには、送信側アプリでアクティビティの dispatchKeyEvent
をオーバーライドし、CastContext.onDispatchVolumeKeyEventBeforeJellyBean()
を呼び出す必要があります。
class MyActivity : FragmentActivity() { override fun dispatchKeyEvent(event: KeyEvent): Boolean { return (CastContext.getSharedInstance(this) .onDispatchVolumeKeyEventBeforeJellyBean(event) || super.dispatchKeyEvent(event)) } }
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
が使用されます。
// 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()
// 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
を呼び出すことで無効にできます。現在、ロック画面機能は、通知をオンにしている限りオンになります。
// ... 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()
// ... 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 レシーバへの接続を解除するかを決定できます。