将 Cast 集成到您的 Android 应用中

本开发者指南介绍了如何为 Android 设备添加 Google Cast 支持 发送应用。

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

发送器框架是指 Cast 类库二进制文件和相关联的 运行时存在的资源发送器应用Cast 应用是指也在此发送器上运行的应用。Web 接收器应用 是指在支持 Cast 的设备上运行的 HTML 应用。

发送者框架使用异步回调设计来告知发送者 事件应用,以及在 Cast 应用生命周期的各种状态之间转换 循环。

应用流程

以下步骤描述了发送者的典型概要执行流程 Android 应用:

  • Cast 框架会自动启动 MediaRouter 基于 Activity 生命周期的设备发现。
  • 当用户点击“投屏”按钮时,框架会显示包含已发现 Cast 设备列表的 Cast 对话框。
  • 当用户选择 Cast 设备时,框架会尝试启动 Cast 设备上的 Web 接收器应用。
  • 框架调用发送者应用中的回调来确认 Web 接收器应用已启动。
  • 该框架在发送者和网络之间创建一个通信通道 接收器应用。
  • 该框架使用通信渠道加载和控制媒体 在网络接收器上播放。
  • 该框架会在发送器和 Web 接收器:当用户执行发送器界面操作时,框架会传递 向网络接收器发送这些媒体控制请求,以及 发送媒体状态更新时,框架会更新发送者界面的状态。
  • 当用户点击“投射”按钮以断开与投射设备的连接时, 框架将断开发送器应用与网络接收器的连接。

有关 Google Cast 中所有类、方法和事件的完整列表 Android SDK,请参阅适用于 Android 设备。 以下各部分介绍了将 Cast 添加到 Android 应用的步骤。

配置 Android 清单

应用的 AndroidManifest.xml 文件要求您配置以下内容 元素:

uses-sdk

设置 Cast SDK 支持的最���和目标 Android API 级别。 目前最低为 API 级别 23,目标是 API 级别 34。

<uses-sdk
        android:minSdkVersion="23"
        android:targetSdkVersion="34" />

android:theme

根据最低 Android SDK 版本设置应用主题。例如,如果 您并没有实现自己的主题,则应使用 如果以最低 Android SDK 版本为目标,则为 Theme.AppCompat Lollipop 之前的版本。

<application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/Theme.AppCompat" >
       ...
</application>

初始化 Cast Context

该框架有一个全局单例对象 CastContext,用于坐标 所有框架的交互

您的应用必须实现 OptionsProvider 接口,用于提供初始化 CastContext 单例。OptionsProvider 提供 CastOptions 其中包含可影响框架行为的选项。最 其中重要的是 Web 接收器应用 ID 发现结果,并在投放会话 。

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 用户体验微件

Cast 框架提供符合 Cast 设计核对清单的微件:

  • 入门叠加层: 该框架提供一个自定义 View, IntroductoryOverlay, 向用户显示的这一图片,以吸引用户注意“投放”按钮 当接收方可用时。“发件人”应用可以 自定义标题的文字和位置 文本

  • Cast Button: 无论投放设备是否可用,“投射”按钮都可见。 用户首次点击“投射”按钮时,系统会显示“投射”对话框 其中会列出已发现的设备当用户点击“投放”按钮时 设备已连接时,系统会显示当前媒体元数据(例如 标题、录音棚名称和缩略图),或允许用户 断开与投放设备的连接。“投射”按钮有时也称为 作为“投射图标”

  • 迷你控制器: 当用户在投放内容时离开了当前 内容页面或展开的控制器转移到发送应用中的其他屏幕时, 迷你控制器显示在屏幕底部 查看当前投放的媒体元数据并控制播放。

  • 展开后的控制器: 当用户在投放内容时点击媒体通知 或迷你控制器时,系统会启动展开的控制器 并提供了多个按钮来控制 媒体播放。

  • 通知: 仅限 Android。当用户在投射内容时离开 发送者应用中,系统会显示一条媒体通知,说明当前正在投射的内容 媒体元数据和播放控件。

  • 锁定屏幕: 仅限 Android。当用户投放内容并进行导航(或设备)时 超时),系统会显示媒体锁定屏幕控件, 显示当前正在投放的媒体元数据和播放控件。

以下指南介绍了如何将这些微件添加到 。

添加投放按钮

Android MediaRouter API 旨在支持在辅助设备上显示和播放媒体内容。使用 MediaRouter API 的 Android 应用应在其界面中包含“投屏”按钮,以便用户选择媒体路由,以便在辅助设备(例如 Cast 设备)上播放媒体。

框架可让添加 MediaRouteButtonCast 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 时,发送器应用会指定网络接收器 并且可以通过设置 supportedNamespaces 英寸 CastOptionsCastContext 在内部持有对 MediaRouter 的引用,并会在以下条件下启动发现过程:

  • 系统会根据旨在平衡设备发现延迟时间和电池用量的算法,在发送方应用进入前台时偶尔自动启动发现功能。
  • “投射”对话框已打开。
  • Cast SDK 正在尝试恢复 Cast 会话。

关闭投放对话框或 发送者应用进入后台。

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 接收器生命周期,请参阅 Web 接收器应用生命周期指南

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

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

会话会自动创建和关闭以响应用户手势 选择 MediaRouter 对话框。

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

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

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
        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)
    }
}
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 中将其关闭。

如果自动合并,此服务可以自动合并到应用的清单中 。

有媒体会话时,框架会启动并停止服务 当媒体会话结束时触发。

媒体控件的工作原理

Cast 框架废弃了 RemoteMediaPlayer 类(从 Cast 2.x 中改为使用新类) RemoteMediaClient, 它通过一组更方便的 API 提供相同的功能,以及 可避免传入 GoogleApiClient

当应用建立 CastSession Web Receiver 应用支持媒体命名空间, RemoteMediaClient 将由框架自动创建;您的应用可以 可通过对 CastSession 调用 getRemoteMediaClient() 方法来访问它 实例。

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

RemoteMediaClient 的实例应该可由 应用的多个部分,以及应用的某些内部组件, 框架,例如永久性迷你控制器通知服务。 为此,此实例支持注册多个 RemoteMediaClient.Listener 实例。

设置媒体元数据

通过 MediaMetadata 类表示您要投射的媒体项的相关信息。通过 以下示例创建了电影的新 MediaMetadata 实例,并将 标题、副标题和两张图片。

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 执行播放、暂停等操作 控制在网络接收器上运行的媒体播放器应用。

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 视频格式

如需查看您的媒体内容是什么视频格式,请使用 getVideoInfo() 以获取 VideoInfo。 此实例包含 HDR TV 格式的类型和显示高度 宽度(以像素为单位)4K 格式的变体由常量 HDR_TYPE_* 表示。

向多台设备发出远程控制通知

当用户投放内容时,连接到同一网络的其他 Android 设备将获得 也可以让他们控制播放任何人的设备 收到此类通知后,可以在“设置”中针对相应设备关闭通知 应用 >Google Cast >显示遥控器通知。 (这些通知包括“设置”应用程序的快捷方式。)如需了解详情,请参阅 投放遥控器通知

添加迷你控制器

根据 Cast Design 核对清单, 发送器应用应提供一个名为迷你头像 (mini) 的持久控件, 控制器 用户离开当前内容网页 发送器应用的另一部分迷你控制器会显示提醒 显示给当前 Cast 会话的用户。点按迷你控制器后,用户可以返回 Cast 全屏展开式控制器视图。

该框架提供了一个自定义视图 MiniControllerFragment,您可以添加 添加到您想要在其中显示 迷你控制器

<fragment
    android:id="@+id/castMiniController"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:visibility="gone"
    class="com.google.android.gms.cast.framework.media.widget.MiniControllerFragment" />

当您的发送器应用正在播放视频或音频直播时,SDK 会 自动显示播放/停止按钮,代替播放/暂停按钮 迷你控制器中

要设置此自定义视图的标题和副标题的文本外观, 并选择按钮,请参阅 自定义迷你控制器

添加展开的控制器

Google Cast 设计核对清单要求发送器应用提供展开后的 控制器 。展开的控制器是 迷你控制器

Cast SDK 为展开的控制器提供了一个名为 ExpandedControllerActivity。 它是一个抽象类,您必须为该类创建子类才能添加“投射”按钮。

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

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

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

</menu>

创建一个扩展 ExpandedControllerActivity 的新类。

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 会 自动显示播放/停止按钮,代替播放/暂停按钮 展开的控制器中

要使用主题设置外观,请选择要显示的按钮, 和添加自定义按钮,请参见 自定义展开后的控制器

音量控制

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

实体按钮音量控制

在 Android 设备上,默认情况下,对于使用 Jelly Bean 或更高版本的任何设备,发送器设备上的实体按钮都可以用于在 Web 接收器上更改 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 设计核对清单要求发送方应用 在 Google Cloud 中实现媒体控件 通知锁形图标 屏幕, 发送器正在投放,但发送器应用没有焦点。通过 框架提供 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();

已开启在通知和锁定屏幕中显示媒体控件的功能 也可通过调用 setNotificationOptions 中含有 null CastMediaOptions.Builder。 目前,只要通知处于通知状态,锁定屏幕功能就会处于开启状态 。

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 生命周期的每个阶段提供最佳响应。应用可以显示 错误对话框,也可以决定断开与 网络接收器。