将 Cast 集成到您的 Android 应用中

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

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

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

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

应用流程

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

  • Cast 框架会根据 Activity 生命周期自动启动 MediaRouter 设备发现。
  • 当用户点击“投放”按钮时,框架会显示 Cast 对话框,其中列出了发现的投放设备。
  • 当用户选择投放设备时,框架会尝试在投放设备上启动 Web 接收设备应用。
  • 框架会在发送设备应用中调用回调,以确认 Web 接收设备应用已启动。
  • 框架会在发送设备应用和 Web 接收设备应用之间创建通信渠道。
  • 框架使用通信渠道在 Web 接收设备上加载和控制媒体播放。
  • 框架会在发送设备和 Web 接收设备之间同步媒体播放状态:当用户执行发送设备界面操作时,框架会将这些媒体控制请求传递给 Web 接收设备;当 Web 接收设备发送媒体状态更新时,框架会更新发送设备界面的状态。
  • 当用户点击“投放”按钮以断开与投放设备的连接时,框架会将发送设备应用与 Web 接收设备断开连接。

如需查看 Google Cast Android SDK 中所有类、方法和事件的完整列表,请参阅Google Cast Sender API 参考( Android 版)。 以下部分介绍了将 Cast 添加到 Android 应用的步骤。

配置 Android 清单

您需要为 Cast SDK 配置应用的 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 版本为目标平台(Lollipop 之前)时,应使用 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 接收设备应用。

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;
    }
}

您必须在发送设备应用的 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 当调用 CastContext.getSharedInstance() 时,系统会延迟初始化。

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, 该视图会在首次有接收设备可用时向用户显示,以引起用户对“投放”按钮的注意。发送设备应用可以 自定义标题 文本的文本和位置

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

  • 迷你控制器: 当用户投射内容并从当前 内容页面或展开的控制器导航到发送设备应用中的另一个屏幕时, 屏幕底部会显示迷你控制器,以便用户 查看当前投射的媒体元数据并控制播放。

  • 展开的控制器: 当用户投射内容时,如果他们点击媒体通知 或迷你控制器,系统会启动展开的控制器,其中会显示 当前播放的媒体元数据,并提供多个按钮来控制 媒体播放。

  • 通知: 仅限 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,并且可以选择通过在 supportedNamespaces中设置 CastOptions来请求命名空间过滤。 CastContext 在内部保留对 MediaRouter 的引用,并且会在以下情况下启动发现过程:

  • 根据旨在平衡设备发现延迟时间和 电池用量的算法,当发送设备应用进入前台时,系统会偶尔自动启动发现。
  • Cast 对话框处于打开状态。
  • Cast SDK 正在尝试恢复 Cast 会话。

当 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 表示与 Cast 设备的会话。您的应用可以通过 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
        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 应用或智能显示屏在设备之间移动现有的音频和视频流。媒体在一个设备(来源)上停止播放,并在另一个设备(目标)上继续播放。任何具有最新固件的投放设备都可以作为流传输中的来源或目标。

如需在流传输或扩展期间获取新的目标设备, 请使用 CastSession#addCastListener 注册 Cast.Listener。 然后在此回调期间调用 CastSession#getCastDevice()onDeviceNameChanged

如需了解详情,请参阅 Web 接收设备上的流传输

自动重新连接

框架提供了一个 ReconnectionService ,发送设备应用可以启用该服务,以处理许多细微的 极端情况下的重新连接,例如:

  • 从暂时性 Wi-Fi 丢失中恢复
  • 从设备休眠中恢复
  • 从应用后台运行中恢复
  • 如果应用崩溃,则恢复

此服务默认处于开启状态,可以在 CastOptions.Builder中关闭。

如果您的 Gradle 文件中启用了自动合并,则此服务可以自动合并到您应用的清单中。

框架会在媒体会话存在时启动该服务,并在媒体会话结束时停止该服务。

媒体控制的工作原理

Cast 框架弃用了 Cast 2.x 中的 RemoteMediaPlayer 类,转而使用新类 RemoteMediaClient, 该类在一组更便捷的 API 中提供相同的功能,并 避免了必须传入 GoogleApiClient。

当您的应用与支持媒体命名空间的 Web 接收设备应用建立 CastSession 时,框架会自动创建 RemoteMediaClient的实例;您的应用可以通过对CastSession 实例调用getRemoteMediaClient()方法来 访问该实例。

RemoteMediaClient 的所有向 Web 接收设备发出请求的方法都会返回一个 PendingResult 对象,该对象可用于跟踪该请求。

预计 RemoteMediaClient 的实例可能会被应用的多个部分共享,实际上,框架的一些内部组件(例如持久 迷你控制器通知服务)也是如此。为此,此实例支持注册 RemoteMediaClient.Listener 的多个实例。

设置媒体元数据

The 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 播放、暂停和以其他方式控制在 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 视频格式

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

向多台设备发送遥控通知

当用户投射内容时,同一网络上的其他 Android 设备会收到通知,以便他们也能控制播放。任何收到此类通知的设备都可以在“设置”应用中依次前往 Google > Google Cast > 显示遥控通知 ,为该设备关闭通知。 (通知包含指向“设置”应用的快捷方式。)如需了解详情,请参阅 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的 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 或更新版本的任何设备,发送设备上的实体按钮可用于更改 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 设计核对清单要求发送设备应用 在 通知锁定 屏幕中实现媒体控件, 其中发送设备正在投射内容,但发送设备应用没有焦点。框架提供了 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 接收设备的连接。