このデベロッパー ガイドでは、Android Sender SDK を使用して Android 送信元アプリに Google Cast のサポートを追加する方法について説明します。
モバイル デバイスやノートパソコンは再生を制御する送信側で、Google Cast デバイスはテレビにコンテンツを表示する受信側です。
送信側フレームワークとは、送信側の実行時に存在する Cast クラス ライブラリのバイナリと関連リソースを指します。センダーアプリまたはキャストアプリとは、センダーでも実行されているアプリを指します。ウェブレシーバー アプリとは、キャスト対応デバイスで実行される HTML アプリケーションを指します。
送信側フレームワークは、非同期コールバック設計を使用して、送信側アプリにイベントを通知し、Cast アプリのライフサイクルのさまざまな状態を遷移します。
アプリケーションの流れ
次の手順は、送信側の Android アプリの一般的な実行フローの概要を示しています。
- Cast フレームワークは、
Activity
ライフサイクルに基づいてMediaRouter
デバイスの検出を自動的に開始します。 - ユーザーがキャスト ボタンをクリックすると、検出されたキャスト デバイスのリストを含むキャスト ダイアログが表示されます。
- ユーザーがキャスト デバイスを選択すると、フレームワークはキャスト デバイスでウェブレシーバー アプリを起動しようとします。
- フレームワークは、送信側アプリのコールバックを呼び出して、ウェブ レシーバー アプリが起動されたことを確認します。
- このフレームワークは、送信者とウェブ受信アプリ間の通信チャネルを作成します。
- フレームワークは、通信チャネルを使用して、ウェブレシーバーでメディアの再生を読み込み、制御します。
- このフレームワークは、送信者とウェブ レシーバー間でメディアの再生状態を同期します。ユーザーが送信者の UI アクションを実行すると、フレームワークはメディア コントロール リクエストをウェブ レシーバーに渡します。ウェブ レシーバーがメディアのステータス更新を送信すると、フレームワークは送信者の UI の状態を更新します。
- ユーザーがキャスト ボタンをクリックしてキャスト デバイスとの接続を解除すると、フレームワークは送信元アプリとウェブレシーバーとの接続を解除します。
Google Cast Android SDK のすべてのクラス、メソッド、イベントの一覧については、Android 用 Google Cast Sender API リファレンスをご覧ください。以降のセクションでは、Android アプリにキャストを追加する手順について説明します。
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 デザイン チェックリストに準拠したウィジェットが用意されています。
導入オーバーレイ: フレームワークには、カスタム ビュー
IntroductoryOverlay
が用意されています。このビューは、レシーバーが初めて使用可能になったときに、キャスト ボタンに注意を喚起するためにユーザーに表示されます。送信元アプリでは、タイトル テキストのテキストと位置をカスタマイズできます。キャスト ボタン: キャスト デバイスの可用性に関係なく、キャスト ボタンが表示されます。ユーザーが初めてキャスト ボタンをクリックすると、検出されたデバイスが一覧表示されるキャスト ダイアログが表示されます。デバイスが接続されているときにユーザーがキャスト ボタンをクリックすると、現在のメディア メタデータ(タイトル、レコーディング スタジオの名前、サムネイル画像など)が表示されます。また、ユーザーはキャスト デバイスとの接続を解除することもできます。「キャストボタン」は「キャストアイコン」とも呼ばれます。
ミニ コントローラ: ユーザーがコンテンツをキャストしていて、現在のコンテンツ ページから移動した場合や、送信元アプリでコントローラを展開して別の画面に移動した場合、ミニ コントローラが画面の下部に表示されます。これにより、ユーザーは現在キャスト中のメディアのメタデータを確認したり、再生を操作したりできます。
拡張コントローラ: ユーザーがコンテンツをキャストしているときに、メディア通知またはミニ コントローラをクリックすると、拡張コントローラが起動します。拡張コントローラには、現在再生中のメディアのメタデータが表示され、メディアの再生を操作するためのボタンがいくつか用意されています。
通知: Android のみ。ユーザーがコンテンツをキャストしているときに送信元アプリから離れると、現在キャスト中のメディアのメタデータと再生コントロールを示すメディア通知が表示されます。
ロック画面: Android のみ。ユーザーがコンテンツをキャストしているときにロック画面に移動すると(またはデバイスのタイムアウトが切れると)、現在キャスト中のメディアのメタデータと再生コントロールを示すメディア ロック画面 コントロールが表示されます。
以下のガイドでは、これらのウィジェットをアプリに追加する方法について説明します。
キャスト ボタンを追加する
Android MediaRouter
API は、セカンダリ デバイスでメディアの表示と再生を行えるように設計されています。MediaRouter
API を使用する Android アプリでは、ユーザーがメディアルートを選択してキャスト デバイスなどのセカンダリ デバイスでメディアを再生できるように、ユーザー インターフェースの一部としてキャスト ボタンを含める必要があります。
このフレームワークでは、Cast button
として MediaRouteButton
を追加するのが非常に簡単です。まず、メニューを定義する 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
を設定して、Namespace フィルタリングをリクエストできます。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 セッションとウェブレシーバーのライフサイクルの詳細については、ウェブレシーバーのアプリケーション ライフサイクル ガイドをご覧ください。
セッションは SessionManager
クラスによって管理され、アプリは CastContext.getSessionManager()
からアクセスできます。個々のセッションは、クラス Session
のサブクラスで表されます。たとえば、CastSession
は Cast デバイスでのセッションを表します。アプリは、SessionManager.getCurrentCastSession()
を介して現在アクティブな Cast セッションにアクセスできます。
アプリは SessionManagerListener
クラスを使用して、作成、停止、再開、終了などのセッション イベントをモニタリングできます。セッションがアクティブな状態で異常な終了または突然の終了が発生した場合、フレームワークは自動的に再開を試みます。
セッションは、MediaRouter
ダイアログでのユーザー操作に応じて自動的に作成、終了されます。
Cast の開始エラーをより正確に把握するため、アプリは 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 アプリ、スマート ディスプレイを使用して、既存の音声ストリームと動画ストリームをデバイス間で移動できます。あるデバイス(ソース)でメディアの再生が停止し、別のデバイス(デスティネーション)で再生が続行されます。最新のファームウェアを搭載した Cast デバイスは、ストリーミング転送の送信元または転送先として機能できます。
ストリームの転送または拡張中に新しい宛先デバイスを取得するには、CastSession#addCastListener
を使用して Cast.Listener
を登録します。次に、onDeviceNameChanged
コールバック中に CastSession#getCastDevice()
を呼び出します。
詳細については、ウェブ レシーバでのストリーミング転送をご覧ください。
自動再接続
このフレームワークには ReconnectionService
が用意されており、送信元アプリで有効にすると、次のような微妙なコーナーケースで再接続を処理できます。
- Wi-Fi が一時的に切断された場合の復旧
- デバイスのスリープ状態から復帰する
- アプリのバックグラウンド処理から復元する
- アプリがクラッシュした場合の復元
このサービスはデフォルトでオンになっていますが、[CastOptions.Builder
] でオフにできます。
Gradle ファイルで自動マージが有効になっている場合、このサービスはアプリのマニフェストに自動的にマージされます。
フレームワークは、メディア セッションがあるときにサービスを開始し、メディア セッションが終了すると停止します。
Media Control の仕組み
Cast フレームワークでは、Cast 2.x の RemoteMediaPlayer
クラスが非推奨になり、新しいクラス RemoteMediaClient
が推奨されます。このクラスは、より便利な API セットで同じ機能を提供します。また、GoogleApiClient を渡す必要がなくなります。
アプリがメディア ネームスペースをサポートするウェブ レシーバー アプリで CastSession
を確立すると、RemoteMediaClient
のインスタンスがフレームワークによって自動的に作成されます。アプリは、CastSession
インスタンスで getRemoteMediaClient()
メソッドを呼び出すことで、このインスタンスにアクセスできます。
ウェブ レシーバーにリクエストを発行する 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 デザイン チェックリストでは、センダーアプリにミニ コントローラと呼ばれる永続的なコントロールを提供することが求められています。このコントロールは、ユーザーが現在のコンテンツ ページからセンダーアプリの別の部分に移動したときに表示されます。ミニ コントローラは、現在のキャスト セッションに関するリマインダーとしてユーザーに表示されます。ミニ コントローラをタップすると、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
を拡張する新しいクラスを作成します。
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()); }
センダーアプリが動画または音声ライブ配信を再生している場合、拡張コントローラで再生/一時停止ボタンの代わりに、再生/停止ボタンが自動的に表示されます。
テーマを使用して外観を設定し、表示するボタンを選択してカスタム ボタンを追加するには、展開コントローラをカスタマイズするをご覧ください。
音量の調整
送信側アプリの音量はフレームワークによって自動的に管理されます。フレームワークは送信側アプリとウェブ受信側アプリを自動的に同期するため、送信側の UI は常にウェブ受信側で指定された音量を報告します。
物理ボタンによる音量調節
Android では、Jelly Bean 以降を搭載したデバイスでは、デフォルトで送信元デバイスの物理ボタンを使用して、ウェブレシーバーのキャスト セッションの音量を変更できます。
Jelly Bean より前の物理ボタンによる音量調節
Jelly Bean より前の Android デバイスで物理的な音量ボタンを使用してウェブレシーバーのデバイスの音量を調整するには、送信側のアプリでアクティビティの 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
が自動的に音声フォーカスをリクエストします。
エラーを処理する
送信元アプリは、すべてのエラー コールバックを処理し、Cast のライフサイクルの各ステージに最適なレスポンスを決定することが非常に重要です。アプリは、エラー ダイアログをユーザーに表示するか、ウェブ レシーバーへの接続を切断するかを決定できます。