本开发者指南介绍了如何使用 Android Sender SDK 向 Android 发送端应用添加 Google Cast 支持。
移动设备或笔记本电脑是控制播放的发送器,Google Cast 设备是将内容显示在电视上的接收器。
发送方框架是指发送方在运行时存在的 Cast 类库二进制文件和关联资源。发送器应用或 Cast 应用是指也运行在发送器上的应用。Web 接收器应用是指在支持 Cast 的设备上运行的 HTML 应用。
发送器框架使用异步回调设计来通知发送器应用事件,并在 Cast 应用生命周期的各个状态之间转换。
应用流程
以下步骤介绍了发件人 Android 应用的典型概要执行流程:
- Cast 框架会根据
Activity
生命周期自动启动MediaRouter
设备发现。 - 当用户点击“投屏”按钮时,框架会显示包含已发现 Cast 设备列表的 Cast 对话框。
- 当用户选择 Cast 设备时,该框架会尝试在 Cast 设备上启动 Web 接收器应用。
- 该框架会在发送器应用中调用回调,以确认 Web 接收器应用已启动。
- 该框架会在发送器应用和 Web 接收器应用之间创建通信通道。
- 该框架使用通信通道在 Web 接收器上加载和控制媒体播放。
- 该框架会在发送器和 Web 接收器之间同步媒体播放状态:当用户执行发送器界面操作时,该框架会将这些媒体控制请求传递给 Web 接收器;当 Web 接收器发送媒体状态更新时,该框架会更新发送器界面的状态。
- 当用户点击“投屏”按钮以断开与 Cast 设备的连接时,该框架会断开发送器应用与 Web 接收器的连接。
如需查看 Google Cast Android SDK 中所有类、方法和事件的完整列表,请参阅 适用于 Android 的 Google Cast Sender API 参考文档。以下部分介绍了向 Android 应用添加 Cast 的步骤。
配置 Android 清单
您需要在应用的 AndroidManifest.xml 文件中为 Cast SDK 配置以下元素:
uses-sdk
设置 Cast SDK 支持的最低和目标 Android API 级别。 目前,最低 API 级别为 23,目标 API 级别为 34。
<uses-sdk
android:minSdkVersion="23"
android:targetSdkVersion="34" />
android:theme
根据最低 Android SDK 版本设置应用的主题。例如,如果您不实现自己的主题,则应在以 Android Lollipop 之前的最低 SDK 版本为目标平台时使用 Theme.AppCompat
的变体。
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat" >
...
</application>
初始化 Cast 上下文
该框架有一个全局单例对象 CastContext
,用于协调框架的所有互动。
您的应用必须实现 OptionsProvider
接口,以提供初始化 CastContext
单例所需的选项。OptionsProvider
提供 CastOptions
的实例,其中包含会影响该框架行为的选项。其中最重要的是 Web 接收器应用 ID,该 ID 用于过滤发现结果,以及在 Cast 会话启动时启动 Web 接收器应用。
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; } }
您必须在发送方应用的 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.getSharedInstance()
时,系统会延迟初始化 CastContext
。
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); } }
Cast 用户体验微件
Cast 框架提供了符合 Cast 设计核对清单的微件:
初始叠加层:该框架提供了一个自定义视图
IntroductoryOverlay
,用于在首次有接收器可用时向用户显示,以吸引用户注意“投屏”按钮。Sender 应用可以自定义标题文本和标题文本的位置。投放按钮:无论是否有可用的 Cast 设备,系统都会显示投放按钮。当用户首次点击“投屏”按钮时,系统会显示一个列出已发现设备的“投屏”对话框。当设备处于连接状态时,用户点击“投屏”按钮,系统会显示当前媒体元数据(例如标题、录音工作室的名称和缩略图),或允许用户断开与 Cast 设备的连接。“投放按钮”有时也称为“投放图标”。
迷你控制器:当用户投放内容并离开当前内容页面或将展开的控制器移至发送器应用中的另一个屏幕时,系统会在屏幕底部显示迷你控制器,以便用户查看当前投放的媒体元数据并控制播放。
展开式控制器:当用户投放内容时,如果点击媒体通知或迷你控制器,系统会启动展开式控制器,其中会显示当前正在播放的媒体元数据,并提供用于控制媒体播放的多个按钮。
通知:仅限 Android。当用户投放内容并离开发送器应用时,系统会显示媒体通知,其中显示当前投放的媒体元数据和播放控件。
锁定屏幕:仅限 Android 设备。当用户投放内容并导航到(或设备超时)锁定屏幕时,系统会显示媒体锁定屏幕控件,其中会显示当前投放的媒体元数据和播放控件。
以下指南介绍了如何将这些 widget 添加到您的应用中。
添加“投屏”按钮
Android MediaRouter
API 旨在支持在辅助设备上显示和播放媒体内容。使用 MediaRouter
API 的 Android 应用应在其界面中包含“投屏”按钮,以便用户选择媒体路由,以便在辅助设备(例如 Cast 设备)上播放媒体。
借助该框架,您可以非常轻松地将 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 时,发送方应用会指定 Web 接收器应用 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 接收器应用、连接到该应用以及初始化媒体控制渠道。如需详细了解 Cast 会话和 Web 接收器生命周期,请参阅 Web 接收器应用生命周期指南。
会话由 SessionManager
类管理,您的应用可以通过 CastContext.getSessionManager()
访问该类。各个会话由 Session
类的子类表示。例如,CastSession
表示与 Cast 设备的会话。您的应用可以通过 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 应用或智能显示屏在设备之间移动现有音频和视频流。媒体在一部设备(来源)上停止播放,并在另一部设备(目标设备)上继续播放。任何搭载最新固件的 Cast 设备都可以作为流式传输的源或目标。
如需在串流传输或扩展期间获取新的目标设备,请使用 CastSession#addCastListener
注册 Cast.Listener
。然后,在 onDeviceNameChanged
回调期间调用 CastSession#getCastDevice()
。
如需了解详情,请参阅在 Web 接收器上进行流式传输。
自动重新连接
该框架提供了 ReconnectionService
,发送方应用可以启用该接口,以便在许多细微的极端情况下处理重新连接,例如:
- 从暂时断开 Wi-Fi 连接中恢复
- 从设备休眠状态恢复
- 从将应用切换到后台状态恢复
- 在应用崩溃时进行恢复
此服务默认处于开启状态,您可以在 CastOptions.Builder
中将其关闭。
如果您的 Gradle 文件中启用了自动合并,系统可以自动将此服务合并到应用的清单中。
该框架会在有媒体会话时启动服务,并在媒体会话结束时停止服务。
媒体控制功能的运作方式
Cast 框架废弃了 Cast 2.x 中的 RemoteMediaPlayer
类,改用新类 RemoteMediaClient
,后者通过一组更便捷的 API 提供相同的功能,并且避免了必须传入 GoogleApiClient。
当您的应用与支持媒体命名空间的 Web 接收器应用建立 CastSession
时,框架会自动创建 RemoteMediaClient
的实例;您的应用可以通过对 CastSession
实例调用 getRemoteMediaClient()
方法来访问它。
向 Web 接收器发出请求的所有 RemoteMediaClient
方法都会返回一个 PendingResult 对象,该对象可用于跟踪该请求。
预计 RemoteMediaClient
实例可能会由应用的多个部分共享,实际上框架的某些内部组件(例如持久性迷你控制器和通知服务)也会共享该实例。为此,此实例支持注册多个 RemoteMediaClient.Listener
实例。
设置媒体元数据
MediaMetadata
类表示您要投放的媒体内容的相关信息。以下示例会创建电影的新 MediaMetadata 实例,并设置影视内容、字幕和两张图片。
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
,然后将 MediaInfo
加载到该 RemoteMediaClient
中。使用 RemoteMediaClient
可播放、暂停和以其他方式控制在 Web 接收器上运行的媒体播放器应用。
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 会话的可见提醒。点按迷你控制器后,用户可以返回 Cast 全屏展开式控制器视图。
该框架提供了一个自定义视图 MiniControllerFragment,您可以将其添加到您想要显示迷你控制器的每个 activity 的布局文件底部。
<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
标记内声明您的新 activity:
<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
,以将目标 activity 设置为您的新 activity:
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
方法,以在加载远程媒体时显示您的新 activity:
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 会在展开式控制器中自动显示播放/停止按钮,而不是播放/暂停按钮。
如需使用主题设置外观、选择要显示的按钮以及添加自定义按钮,请参阅自定义展开式控制器。
音量控制
该框架会自动管理发送器应用的音量。该框架会自动同步发送器应用和 Web 接收器应用,以便发送器界面始终报告 Web 接收器指定的音量。
实体按钮音量控制
在 Android 上,对于使用 Jelly Bean 或更高版本的任何设备,发送器设备上的实体按钮默认可用于在 Web 接收器上更改 Cast 会话的音量。
Jelly Bean 之前的物理按钮音量控制
如需在低于 Jelly Bean 的 Android 设备上使用实体音量按键控制 Web 接收器设备音量,发送器应用应替换其 activity 中的 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 生命周期的每个阶段确定最佳响应,这一点非常重要。应用可以向用户显示错误对话框,也可以决定断开与 Web 接收器的连接。