本开发者指南将介绍如何使用 Android 发送者 SDK 为 Android 发送器应用添加 Google Cast 支持。
移动设备或笔记本电脑是控制播放的“发送器”,而 Google Cast 设备是“在电视上显示内容的接收器”。
发送器框架是指 Cast 类库二进制文件和运行时在运行时存在于发送者上的相关资源。“发送器应用”或“投射应用”是指也在发送者上运行的应用。Web 接收器应用是指在支持 Cast 的设备上运行的 HTML 应用。
发送者框架使用异步回调设计来向发送者应用通知事件,并在 Cast 应用生命周期的各种状态之间过渡。
应用流程
以下步骤介绍了 Android 应用的典型概要执行流程:
- Cast 框架会根据
Activity
生命周期自动启动MediaRouter
设备发现。 - 当用户点击“投放”按钮时,框架会显示“投放”对话框以及发现的投放设备列表。
- 当用户选择 Cast 设备时,框架会尝试在 Cast 设备上启动 Web 接收器应用。
- 框架会在发送者应用中调用回调,以确认 Web 接收器应用是否已启动。
- 该框架在发送器应用和网络接收器应用之间创建一个通信通道。
- 该框架使用通信通道在网络接收器上加载和控制媒体播放。
- 框架会在发送者和网络接收器之间同步媒体播放状态:当用户进行发送者界面操作时,框架会将这些媒体控制请求传递给网络接收器,当网络接收器发送媒体状态更新时,框架会更新发送者界面的状态。
- 当用户点击“投放”按钮断开与投放设备的连接时,框架会断开发送者应用与网络接收器的连接。
如需查看 Google Cast Android SDK 中所有类、方法和事件的完整列表,请参阅 Android 版 Google Cast Sender API 参考文档。 下面几部分介绍了将 Cast 添加到 Android 应用的步骤。
配置 Android 清单
您的应用的 AndroidManifest.xml 文件要求您为 Cast SDK 配置以下元素:
uses-sdk
设置 Cast SDK 支持的最低和目标 Android API 级别。 目前最低为 API 级别 19,目标为 API 级别 28。
<uses-sdk
android:minSdkVersion="19"
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 接收器应用 ID,它用于过滤发现结果并在启动会话时启动 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 用户体验 widget
Cast 框架提供了符合 Cast 设计核对清单的 widget:
简介叠加层:该框架提供了一个自定义视图
IntroductoryOverlay
,它会在首次提供接收器时向用户显示,以吸引注意“投放”按钮。发件人应用可以自定义文本和标题文本的位置。“投放”按钮:当发现支持应用的接收器时,系统会显示“投放”按钮。当用户首次点击“投放”按钮时,系统会显示一个“投放”对话框,其中会列出发现的设备。如果用户在设备处于连接状态时点击“投放”按钮,系统会显示当前媒体元数据(例如标题、录音棚的名称和缩略图)或允许用户断开与投放设备的连接。
迷你控制器:当用户投射内容并已离开当前内容页面或展开的控制器并转到发送者应用中的另一个屏幕时,迷你控制器会显示在屏幕底部,以便用户看到当前投放的媒体元数据并控制播放。
展开控制器:当用户投射内容时,如果用户点击媒体通知或迷你控制器,则展开的控制器会启动,其中显示了当前播放的媒体元数据并提供多种按钮来控制媒体播放。
通知:仅限 Android。当用户投射内容并离开发送者应用时,系统会显示媒体通知,显示当前投放的媒体元数据和播放控件。
锁定屏幕:仅限 Android。当用户投射内容并导航到(或设备超时)锁定屏幕时,系统会显示媒体锁定屏幕控件,其中显示了当前投放的媒体元数据和播放控件。
以下指南介绍了如何将这些 widget 添加到您的应用。
添加“投放”按钮
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 时,发送者应用会指定 Web 接收器应用 ID,并且可以选择通过在 CastOptions
中设置 supportedNamespaces
来请求命名空间过滤。CastContext
在内部保存对 MediaRouter
的引用,将在发送者应用进入前台时启动发现进程,并在发送者应用进入后台时停止发现进程。
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 接收器生命周期,请参阅网络接收器应用生命周期指南。
会话由 SessionManager
类管理,您的应用可通过 CastContext.getSessionManager()
访问该类。各个会话由 Session
类的子类表示。例如,CastSession
表示与投放设备的会话。您的应用可以通过 SessionManager.getCurrentCastSession()
访问当前活跃的 Cast 会话。
您的应用可以使用 SessionManagerListener
类来监控会话事件,例如创建、暂停、恢复和终止。当会话处于活跃状态时,框架会自动尝试从异常/突然终止中恢复。
系统会自动创建和关闭会话,以响应 MediaRouter
对话框中的用户手势。
如果您需要了解会话的状态变化,可以实现 SessionManagerListener
。此示例监听 Activity
中 CastSession
的可用性。
class MyActivity : Activity() { private var mCastSession: CastSession? = null private lateinit var mSessionManager: SessionManager private val mSessionManagerListener: SessionManagerListener<CastSession> = SessionManagerListenerImpl() private inner class SessionManagerListenerImpl : SessionManagerListener<CastSession?> { override fun onSessionStarted(session: CastSession?, sessionId: String) { invalidateOptionsMenu() } override fun onSessionResumed(session: CastSession?, wasSuspended: Boolean) { invalidateOptionsMenu() } override fun onSessionEnded(session: CastSession?, error: Int) { finish() } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) mSessionManager = CastContext.getSharedInstance(this).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 } }
public class MyActivity extends Activity { private CastSession mCastSession; private SessionManager mSessionManager; private SessionManagerListener<CastSession> mSessionManagerListener = new SessionManagerListenerImpl(); private class SessionManagerListenerImpl implements SessionManagerListener<CastSession> { @Override public void onSessionStarted(CastSession session, String sessionId) { invalidateOptionsMenu(); } @Override public void onSessionResumed(CastSession session, boolean wasSuspended) { invalidateOptionsMenu(); } @Override public void onSessionEnded(CastSession session, int error) { finish(); } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mSessionManager = CastContext.getSharedInstance(this).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 应用或智能显示屏在不同设备上移动现有的音频和视频流。媒体在一台设备(来源)上停止播放,然后在另一台设备(目标)上继续播放。具有最新固件的任何 Cast 设备都可以在数据流传输中充当来源或目标。
如需在流传输或扩展期间获取新的目标设备,请使用 CastSession#addCastListener
注册 Cast.Listener
。然后,在 onDeviceNameChanged
回调期间调用 CastSession#getCastDevice()
。
如需了解详情,请参阅在 Web 接收器上传输流。
自动重新连接
该框架提供了一个 ReconnectionService
,发送者应用可启用该 API 来处理许多细微情况下的重新连接,例如:
- 暂时失去 Wi-Fi 连接
- 从设备休眠状态恢复
- 从应用后台恢复
- 在应用崩溃时恢复
此服务默认启用,可在 CastOptions.Builder
中停用。
如果您在 Gradle 文件中启用了自动合并功能,则可将该服务自动合并到应用的清单中。
框架会在存在媒体会话时启动服务,并在媒体会话结束时停止服务。
媒体控件的工作原理
Cast 框架废弃了 Cast 2.x 中的 RemoteMediaPlayer
类,取而代之的是新类 RemoteMediaClient
,后者在一组更方便的 API 中提供了相同的功能,且无需传入 GoogleApiClient。
当您的应用与支持媒体命名空间的 Web 接收器应用建立 CastSession
时,框架会自动创建 RemoteMediaClient
的实例;您的应用可以通过对 CastSession
实例调用 getRemoteMediaClient()
方法来对其进行访问。
向网络接收器发出请求的所有 RemoteMediaClient
方法都将返回一个 PendingResult 对象,该对象可用于跟踪该请求。
RemoteMediaClient
的实例预计可由应用的多个部分(实际上是框架的一些内部组件)共享,例如永久性迷你控制器和通知服务。为此,此实例支持注册多个 RemoteMediaClient.Listener
实例。
设置媒体元数据
MediaMetadata
类表示您要投射的媒体项的相关信息。以下示例将创建新的影片 MediaMediaMetadata 实例,并设置标题、副标题和两张图片。
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
播放、暂停和以其他方式控制网络接收器上运行的媒体播放器应用。
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 Design 核对清单,发送器应用应提供一种称为迷你控制器的持久性控件,当用户离开当前内容页面并进入发送者应用的其他部分时,迷你控制器应向其显示可见的迷你提醒。点按迷你控制器后,用户可以返回到 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 会自动在展开的控制器中显示播放/停止按钮来代替播放/暂停按钮。
如需使用主题设置外观、选择要显示的按钮以及添加自定义按钮,请参阅自定义展开控制器。
音量控制
框架会自动管理发送者应用的音量。框架会自动同步发送者和网络接收器应用,以便发送者界面始终报告网络接收器指定的音量。
实体按钮音量控制
在 Android 上,发送器设备上的物理按钮可用于更改任何使用 Jellly Bean 或更高版本的设备的网络接收器上的投放会话音量。
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 生命周期的每个阶段确定最佳响应,这一点非常重要。应用可以向用户显示错误对话框,也可以决定断开与网络接收器的连接。