将投射功能集成到您的 Android 应用中

本开发者指南将介绍如何使用 Android Sender SDK 为 Google 发送器应用添加 Google Cast 支持。

移动设备或笔记本电脑是控制播放的发送器,Google Cast 设备则是在电视上显示内容的接收器。

发送器框架是指 Cast 类库二进制文件以及发送者在运行时存在的相关资源。“发送器应用”或“投射应用”是指也在发送者上运行的应用。Web 接收器应用是指在支持 Cast 的设备上运行的 HTML 应用。

发送器框架使用异步回调设计来通知发送器应用相关事件,并在 Cast 应用生命周期的不同状态之间转换。

应用流程

以下步骤介绍了 Android 发件人应用的典型概要执行流程:

  • Cast 框架会根据 Activity 生命周期自动启动 MediaRouter 设备发现。
  • 当用户点击“投放”按钮时,框架会显示“投放”对话框,其中包含发现的投放设备列表。
  • 当用户选择投放设备时,框架会尝试在投放设备上启动 Web 接收器应用。
  • 框架会调用发送者应用中的回调,以确认 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>

初始化 Cast 上下文

框架有一个全局单例对象 CastContext,用于协调框架的所有交互。

您的应用必须实现 OptionsProvider 接口,以提供初始化 CastContext 单例所需的选项。OptionsProvider 提供了一个 CastOptions 实例,其中包含会影响框架行为的选项。其中最重要的是 Web Receiver 应用 ID,它用于过滤发现结果并在 Cast 会话启动时启动 Web Receiver 应用。

Kotlin
class CastOptionsProvider : OptionsProvider {
    override fun getCastOptions(context: Context): CastOptions {
        return Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .build()
    }

    override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? {
        return null
    }
}
Java
public class CastOptionsProvider implements OptionsProvider {
    @Override
    public CastOptions getCastOptions(Context context) {
        CastOptions castOptions = new CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .build();
        return castOptions;
    }
    @Override
    public List<SessionProvider> getAdditionalSessionProviders(Context context) {
        return null;
    }
}

您必须将已实现的 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.getSharedInstance() 时,CastContext 会延迟初始化。

Kotlin
class MyActivity : FragmentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        val castContext = CastContext.getSharedInstance(this)
    }
}
Java
public class MyActivity extends FragmentActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        CastContext castContext = CastContext.getSharedInstance(this);
    }
}

Cast 用户体验 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" />
Kotlin
// Then override the onCreateOptionMenu() for each of your activities.
// MyActivity.kt
override fun onCreateOptionsMenu(menu: Menu): Boolean {
    super.onCreateOptionsMenu(menu)
    menuInflater.inflate(R.menu.main, menu)
    CastButtonFactory.setUpMediaRouteButton(
        applicationContext,
        menu,
        R.id.media_route_menu_item
    )
    return true
}
Java
// Then override the onCreateOptionMenu() for each of your activities.
// MyActivity.java
@Override public boolean onCreateOptionsMenu(Menu menu) {
    super.onCreateOptionsMenu(menu);
    getMenuInflater().inflate(R.menu.main, menu);
    CastButtonFactory.setUpMediaRouteButton(getApplicationContext(),
                                            menu,
                                            R.id.media_route_menu_item);
    return true;
}

然后,如果您的 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>
Kotlin
// MyActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_layout)

    mMediaRouteButton = findViewById<View>(R.id.media_route_button) as MediaRouteButton
    CastButtonFactory.setUpMediaRouteButton(applicationContext, mMediaRouteButton)

    mCastContext = CastContext.getSharedInstance(this)
}
Java
// MyActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_layout);

   mMediaRouteButton = (MediaRouteButton) findViewById(R.id.media_route_button);
   CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), mMediaRouteButton);

   mCastContext = CastContext.getSharedInstance(this);
}

如需使用主题设置“投放”按钮的外观,请参阅自定义投放按钮

配置设备发现

设备发现完全由 CastContext 管理。初始化 CastContext 时,发送者应用指定 Web 接收器应用 ID,并且可以选择在 CastOptions 中设置 supportedNamespaces 来请求命名空间过滤。CastContext 会在内部保存对 MediaRouter 的引用,并在发送者应用进入前台时启动发现进程,并在发送者应用进入后台时停止发现进程。

Kotlin
class CastOptionsProvider : OptionsProvider {
    companion object {
        const val CUSTOM_NAMESPACE = "urn:x-cast:custom_namespace"
    }

    override fun getCastOptions(appContext: Context): CastOptions {
        val supportedNamespaces: MutableList<String> = ArrayList()
        supportedNamespaces.add(CUSTOM_NAMESPACE)

        return CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .setSupportedNamespaces(supportedNamespaces)
            .build()
    }

    override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? {
        return null
    }
}
Java
class CastOptionsProvider implements OptionsProvider {
    public static final String CUSTOM_NAMESPACE = "urn:x-cast:custom_namespace";

    @Override
    public CastOptions getCastOptions(Context appContext) {
        List<String> supportedNamespaces = new ArrayList<>();
        supportedNamespaces.add(CUSTOM_NAMESPACE);

        CastOptions castOptions = new CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .setSupportedNamespaces(supportedNamespaces)
            .build();
        return castOptions;
    }

    @Override
    public List<SessionProvider> getAdditionalSessionProviders(Context context) {
        return null;
    }
}

会话管理的工作原理

Cast SDK 引入了 Cast 会话的概念,它结合了连接设备、启动(或加入)Web 接收器应用、连接到该应用以及初始化媒体控制渠道的步骤。如需详细了解 Cast 会话和 Web Receiver 生命周期,请参阅 Web Receiver 应用生命周期指南

会话由 SessionManager 类管理,您的应用可通过 CastContext.getSessionManager() 访问该会话。各个会话均由 Session 类的子类表示。例如,CastSession 表示与投放设备的会话。您的应用可以通过 SessionManager.getCurrentCastSession() 访问当前活跃的 Cast 会话。

您的应用可以使用 SessionManagerListener 类来监控会话事件,例如创建、暂停、恢复和终止。会话处于活跃状态时,框架会自动尝试从异常/突然终止中恢复。

系统会自动创建和关闭会话,以响应 MediaRouter 对话框中的用户手势。

为了更好地了解 Cast 启动错误,应用可以使用 CastContext#getCastReasonCodeForCastStatusCode(int) 将会话启动错误转换为 CastReasonCodes。请注意,某些会话启动错误(例如 CastReasonCodes#CAST_CANCELLED)属于预期行为,不应记录为错误。

如果您需要了解会话的状态变化,可以实现 SessionManagerListener。此示例将监听 ActivityCastSession 的可用性。

Kotlin
class MyActivity : Activity() {
    private var mCastSession: CastSession? = null
    private lateinit var mCastContext: CastContext
    private lateinit var mSessionManager: SessionManager
    private val mSessionManagerListener: SessionManagerListener<CastSession> =
        SessionManagerListenerImpl()

    private inner class SessionManagerListenerImpl : SessionManagerListener<CastSession?> {
        override fun onSessionStarting(session: CastSession?) {}

        override fun onSessionStarted(session: CastSession?, sessionId: String) {
            invalidateOptionsMenu()
        }

        override fun onSessionStartFailed(session: CastSession?, error: Int) {
            val castReasonCode = mCastContext.getCastReasonCodeForCastStatusCode(error)
            // Handle error
        }

        override fun onSessionSuspended(session: CastSession?, reason Int) {}

        override fun onSessionResuming(session: CastSession?, sessionId: String) {}

        override fun onSessionResumed(session: CastSession?, wasSuspended: Boolean) {
            invalidateOptionsMenu()
        }

        override fun onSessionResumeFailed(session: CastSession?, error: Int) {}

        override fun onSessionEnding(session: CastSession?) {}

        override fun onSessionEnded(session: CastSession?, error: Int) {
            finish()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mCastContext = CastContext.getSharedInstance(this)
        mSessionManager = mCastContext.sessionManager
    }

    override fun onResume() {
        super.onResume()
        mCastSession = mSessionManager.currentCastSession
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession::class.java)
    }

    override fun onPause() {
        super.onPause()
        mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession::class.java)
        mCastSession = null
    }
}
Java
public class MyActivity extends Activity {
    private CastContext mCastContext;
    private CastSession mCastSession;
    private SessionManager mSessionManager;
    private SessionManagerListener<CastSession> mSessionManagerListener =
            new SessionManagerListenerImpl();

    private class SessionManagerListenerImpl implements SessionManagerListener<CastSession> {
        @Override
        public void onSessionStarting(CastSession session) {}
        @Override
        public void onSessionStarted(CastSession session, String sessionId) {
            invalidateOptionsMenu();
        }
        @Override
        public void onSessionStartFailed(CastSession session, int error) {
            int castReasonCode = mCastContext.getCastReasonCodeForCastStatusCode(error);
            // Handle error
        }
        @Override
        public void onSessionSuspended(CastSession session, int reason) {}
        @Override
        public void onSessionResuming(CastSession session, String sessionId) {}
        @Override
        public void onSessionResumed(CastSession session, boolean wasSuspended) {
            invalidateOptionsMenu();
        }
        @Override
        public void onSessionResumeFailed(CastSession session, int error) {}
        @Override
        public void onSessionEnding(CastSession session) {}
        @Override
        public void onSessionEnded(CastSession session, int error) {
            finish();
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mCastContext = CastContext.getSharedInstance(this);
        mSessionManager = mCastContext.getSessionManager();
    }
    @Override
    protected void onResume() {
        super.onResume();
        mCastSession = mSessionManager.getCurrentCastSession();
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession.class);
    }
    @Override
    protected void onPause() {
        super.onPause();
        mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession.class);
        mCastSession = null;
    }
}

视频流传输

保留会话状态是信息流传输的基础,在这种情况下,用户可以使用语音指令、Google Home 应用或智能显示屏在设备间移动现有音频和视频流。媒体在一台设备(来源)上停止播放,然后在另一台设备(目标)上继续播放。具有最新固件的 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 Receiver 应用建立 CastSession 时,框架会自动创建 RemoteMediaClient 的实例;您的应用可通过对 CastSession 实例调用 getRemoteMediaClient() 方法来访问该实例。

向网络接收器发出请求的所有 RemoteMediaClient 方法都将返回一个 PendingResult 对象,可用于跟踪该请求。

RemoteMediaClient 的实例应可供应用的多个部分共享,并且确实是框架的一些内部组件,例如永久性迷你控制器通知服务。为此,此实例支持注册多个 RemoteMediaClient.Listener 实例。

设置媒体元数据

MediaMetadata 类表示要投射的媒体项的相关信息。以下示例会创建一个新的影片 MediaMediaMetadata 实例,并设置标题、副标题和两张图片。

Kotlin
val movieMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE)

movieMetadata.putString(MediaMetadata.KEY_TITLE, mSelectedMedia.getTitle())
movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, mSelectedMedia.getStudio())
movieMetadata.addImage(WebImage(Uri.parse(mSelectedMedia.getImage(0))))
movieMetadata.addImage(WebImage(Uri.parse(mSelectedMedia.getImage(1))))
Java
MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);

movieMetadata.putString(MediaMetadata.KEY_TITLE, mSelectedMedia.getTitle());
movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, mSelectedMedia.getStudio());
movieMetadata.addImage(new WebImage(Uri.parse(mSelectedMedia.getImage(0))));
movieMetadata.addImage(new WebImage(Uri.parse(mSelectedMedia.getImage(1))));

如需了解如何将图片与媒体元数据搭配使用,请参阅图片选择

加载媒体

您的应用可以加载媒体项,如以下代码所示。首先,使用 MediaInfo.Builder 与媒体元数据来构建 MediaInfo 实例。从当前的 CastSession 获取 RemoteMediaClient,然后将 MediaInfo 加载到该 RemoteMediaClient 中。使用 RemoteMediaClient 播放、暂停和控制 Web 接收器上运行的媒体播放器应用。

Kotlin
val mediaInfo = MediaInfo.Builder(mSelectedMedia.getUrl())
    .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
    .setContentType("videos/mp4")
    .setMetadata(movieMetadata)
    .setStreamDuration(mSelectedMedia.getDuration() * 1000)
    .build()
val remoteMediaClient = mCastSession.getRemoteMediaClient()
remoteMediaClient.load(MediaLoadRequestData.Builder().setMediaInfo(mediaInfo).build())
Java
MediaInfo mediaInfo = new MediaInfo.Builder(mSelectedMedia.getUrl())
        .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
        .setContentType("videos/mp4")
        .setMetadata(movieMetadata)
        .setStreamDuration(mSelectedMedia.getDuration() * 1000)
        .build();
RemoteMediaClient remoteMediaClient = mCastSession.getRemoteMediaClient();
remoteMediaClient.load(new MediaLoadRequestData.Builder().setMediaInfo(mediaInfo).build());

另请参阅使用媒体轨道部分。

4K 视频格式

如需检查媒体的视频格式,请使用 MediaStatus 中的 getVideoInfo() 获取 VideoInfo 的当前实例。此实例包含 HDR TV 格式的类型以及显示高度和宽度(以像素为单位)。4K 格式的变体由常量 HDR_TYPE_* 表示。

针对多台设备的遥控器通知

当用户投放内容时,同一网络中的其他 Android 设备会收到一条通知,告知他们也可控制播放。用户的设备收到此类通知后,可以通过“设置”应用中的 Google > Google Cast > 显示遥控器通知为相应设备关闭此类通知。(通知中包含一个指向“设置”应用的快捷方式。)如需了解详情,请参阅投射遥控器通知

添加迷你控制器

根据 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 的 widget。它是一个抽象类,您必须为该类创建子类才能添加“投射”按钮。

首先,为展开后的控制器创建一个新的菜单资源文件,以提供“投放”按钮:

<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
            android:id="@+id/media_route_menu_item"
            android:title="@string/media_route_menu_title"
            app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
            app:showAsAction="always"/>

</menu>

创建一个扩展 ExpandedControllerActivity 的新类。

Kotlin
class ExpandedControlsActivity : ExpandedControllerActivity() {
    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        super.onCreateOptionsMenu(menu)
        menuInflater.inflate(R.menu.expanded_controller, menu)
        CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item)
        return true
    }
}
Java
public class ExpandedControlsActivity extends ExpandedControllerActivity {
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        getMenuInflater().inflate(R.menu.expanded_controller, menu);
        CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item);
        return true;
    }
}

现在,在应用清单中声明 application 标记内的新 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 并更改 NotificationOptionsCastMediaOptions,以将目标 activity 设置为新 activity:

Kotlin
override fun getCastOptions(context: Context): CastOptions? {
    val notificationOptions = NotificationOptions.Builder()
        .setTargetActivityClassName(ExpandedControlsActivity::class.java.name)
        .build()
    val mediaOptions = CastMediaOptions.Builder()
        .setNotificationOptions(notificationOptions)
        .setExpandedControllerActivityClassName(ExpandedControlsActivity::class.java.name)
        .build()

    return CastOptions.Builder()
        .setReceiverApplicationId(context.getString(R.string.app_id))
        .setCastMediaOptions(mediaOptions)
        .build()
}
Java
public CastOptions getCastOptions(Context context) {
    NotificationOptions notificationOptions = new NotificationOptions.Builder()
            .setTargetActivityClassName(ExpandedControlsActivity.class.getName())
            .build();
    CastMediaOptions mediaOptions = new CastMediaOptions.Builder()
            .setNotificationOptions(notificationOptions)
            .setExpandedControllerActivityClassName(ExpandedControlsActivity.class.getName())
            .build();

    return new CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .setCastMediaOptions(mediaOptions)
            .build();
}

更新 LocalPlayerActivity loadRemoteMedia 方法,以在加载远程媒体时显示新的 activity:

Kotlin
private fun loadRemoteMedia(position: Int, autoPlay: Boolean) {
    val remoteMediaClient = mCastSession?.remoteMediaClient ?: return

    remoteMediaClient.registerCallback(object : RemoteMediaClient.Callback() {
        override fun onStatusUpdated() {
            val intent = Intent(this@LocalPlayerActivity, ExpandedControlsActivity::class.java)
            startActivity(intent)
            remoteMediaClient.unregisterCallback(this)
        }
    })

    remoteMediaClient.load(
        MediaLoadRequestData.Builder()
            .setMediaInfo(mSelectedMedia)
            .setAutoplay(autoPlay)
            .setCurrentTime(position.toLong()).build()
    )
}
Java
private void loadRemoteMedia(int position, boolean autoPlay) {
    if (mCastSession == null) {
        return;
    }
    final RemoteMediaClient remoteMediaClient = mCastSession.getRemoteMediaClient();
    if (remoteMediaClient == null) {
        return;
    }
    remoteMediaClient.registerCallback(new RemoteMediaClient.Callback() {
        @Override
        public void onStatusUpdated() {
            Intent intent = new Intent(LocalPlayerActivity.this, ExpandedControlsActivity.class);
            startActivity(intent);
            remoteMediaClient.unregisterCallback(this);
        }
    });
    remoteMediaClient.load(new MediaLoadRequestData.Builder()
            .setMediaInfo(mSelectedMedia)
            .setAutoplay(autoPlay)
            .setCurrentTime(position).build());
}

在发送器应用播放视频或音频直播时,SDK 会自动在展开的控制器中显示播放/停止按钮来代替播放/暂停按钮。

如需使用主题设置外观、选择要显示的按钮以及添加自定义按钮,请参阅自定义展开后的控制器

音量控制

框架会自动管理发送者应用的音量。框架会自动同步发送者和 Web 接收器应用,以便发送者界面始终报告 Web 接收器指定的音量。

实体按钮音量控制

在 Android 上,发送器设备上的物理按钮可用于更改任何使用 Jelly Bean 或更高版本的设备在网络接收器上的 Cast 会话音量。

Jelly Bean 之前的物理按钮音量控制

如需使用物理音量键控制 Jelly Bean 之前的 Android 设备上的 Web 接收器设备音量,发送者应用应在其 activity 中替换 dispatchKeyEvent,然后调用 CastContext.onDispatchVolumeKeyEventBeforeJellyBean()

Kotlin
class MyActivity : FragmentActivity() {
    override fun dispatchKeyEvent(event: KeyEvent): Boolean {
        return (CastContext.getSharedInstance(this)
            .onDispatchVolumeKeyEventBeforeJellyBean(event)
                || super.dispatchKeyEvent(event))
    }
}
Java
class MyActivity extends FragmentActivity {
    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        return CastContext.getSharedInstance(this)
            .onDispatchVolumeKeyEventBeforeJellyBean(event)
            || super.dispatchKeyEvent(event);
    }
}

向通知和锁定屏幕添加媒体控件

仅在 Android 设备上,Google Cast 设计核对清单要求发送者应用在通知中以及锁定屏幕(其中发送者正在投放,但发送者应用没有获得焦点)中实现媒体控件。该框架提供 MediaNotificationServiceMediaIntentReceiver,可帮助发送者应用构建通知和锁定屏幕中的媒体控件。

MediaNotificationService 在发送设备时运行,并显示一条通知,其中包含图片缩略图以及有关当前投放项的信息、播放/暂停按钮和停止按钮。

MediaIntentReceiver 是负责处理通知中的用户操作的 BroadcastReceiver

您的应用可以通过 NotificationOptions 从锁定屏幕配置通知和媒体控件。您的应用可以配置要在通知中显示哪些控制按钮,以及当用户点按通知时要打开哪个 Activity。如果未明确提供操作,系统将使用默认值 MediaIntentReceiver.ACTION_TOGGLE_PLAYBACKMediaIntentReceiver.ACTION_STOP_CASTING

Kotlin
// Example showing 4 buttons: "rewind", "play/pause", "forward" and "stop casting".
val buttonActions: MutableList<String> = ArrayList()
buttonActions.add(MediaIntentReceiver.ACTION_REWIND)
buttonActions.add(MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK)
buttonActions.add(MediaIntentReceiver.ACTION_FORWARD)
buttonActions.add(MediaIntentReceiver.ACTION_STOP_CASTING)

// Showing "play/pause" and "stop casting" in the compat view of the notification.
val compatButtonActionsIndices = intArrayOf(1, 3)

// Builds a notification with the above actions. Each tap on the "rewind" and "forward" buttons skips 30 seconds.
// Tapping on the notification opens an Activity with class VideoBrowserActivity.
val notificationOptions = NotificationOptions.Builder()
    .setActions(buttonActions, compatButtonActionsIndices)
    .setSkipStepMs(30 * DateUtils.SECOND_IN_MILLIS)
    .setTargetActivityClassName(VideoBrowserActivity::class.java.name)
    .build()
Java
// Example showing 4 buttons: "rewind", "play/pause", "forward" and "stop casting".
List<String> buttonActions = new ArrayList<>();
buttonActions.add(MediaIntentReceiver.ACTION_REWIND);
buttonActions.add(MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK);
buttonActions.add(MediaIntentReceiver.ACTION_FORWARD);
buttonActions.add(MediaIntentReceiver.ACTION_STOP_CASTING);

// Showing "play/pause" and "stop casting" in the compat view of the notification.
int[] compatButtonActionsIndices = new int[]{1, 3};

// Builds a notification with the above actions. Each tap on the "rewind" and "forward" buttons skips 30 seconds.
// Tapping on the notification opens an Activity with class VideoBrowserActivity.
NotificationOptions notificationOptions = new NotificationOptions.Builder()
    .setActions(buttonActions, compatButtonActionsIndices)
    .setSkipStepMs(30 * DateUtils.SECOND_IN_MILLIS)
    .setTargetActivityClassName(VideoBrowserActivity.class.getName())
    .build();

从通知和锁定屏幕显示媒体控件默认处于开启状态,但您可以在 CastMediaOptions.Builder 中调用 null 以调用 setNotificationOptions。目前,只要通知功能处于开启状态,锁定屏幕功能就会开启。

Kotlin
// ... continue with the NotificationOptions built above
val mediaOptions = CastMediaOptions.Builder()
    .setNotificationOptions(notificationOptions)
    .build()
val castOptions: CastOptions = Builder()
    .setReceiverApplicationId(context.getString(R.string.app_id))
    .setCastMediaOptions(mediaOptions)
    .build()
Java
// ... continue with the NotificationOptions built above
CastMediaOptions mediaOptions = new CastMediaOptions.Builder()
        .setNotificationOptions(notificationOptions)
        .build();
CastOptions castOptions = new CastOptions.Builder()
        .setReceiverApplicationId(context.getString(R.string.app_id))
        .setCastMediaOptions(mediaOptions)
        .build();

当您的发送者应用播放视频或音频直播时,SDK 会自动在通知控件(而不是锁定屏幕控件)上显示播放/停止按钮来代替播放/暂停按钮。

注意:如需在 Lollipop 之前版本的设备上显示锁定屏幕控件,RemoteMediaClient 会自动代表您请求获得音频焦点。

处理错误

对于发送器应用来说,处理所有错误回调并针对 Cast 生命周期的每个阶段确定最佳响应非常重要。应用可以向用户显示错误对话框,也可以决定断开与 Web 接收器的连接。