将 Cast 集成到您的 Android 应用中

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

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

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

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

应用流程

以下步骤介绍了发送方 Android 应用的典型高级别执行流程:

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

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

配置 Android 清单

您应用的 AndroidManifest.xml 文件需要您为 Cast SDK 配置以下元素:

uses-sdk

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

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

android:theme

根据最低 Android SDK 版本设置应用的主题。例如,如果您未实现自己的主题,则在以 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 接收器应用 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.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,当接收器首次可用时,该视图会显示给用户,以引起用户对“投屏”按钮的注意。发送方应用可以自定义标题文字和标题文字的位置

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

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

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

  • 通知: 仅限 Android。当用户投放内容并离开发送方应用时,系统会显示一个媒体通知,其中包含当前投放的媒体元数据和播放控件。

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

以下指南介绍了如何将这些 widget 添加到您的应用中。

添加“投放”按钮

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

该框架可让您轻松添加 MediaRouteButton 作为 Cast button。您应先在定义菜单的 XML 文件中添加菜单项或 MediaRouteButton,然后使用 CastButtonFactory 将其与框架关联起来。

// To add a Cast button, add the following snippet.
// menu.xml
<item
    android:id="@+id/media_route_menu_item"
    android:title="@string/media_route_menu_title"
    app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
    app:showAsAction="always" />
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 的引用,并在以下条件下启动发现过程:

  • 根据旨在平衡设备发现延迟时间和电池使用情况的算法,当发送方应用进入前台时,系统会偶尔自动启动发现。
  • “投放”对话框已打开。
  • 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 Receiver 生命周期,请参阅 Web Receiver 应用生命周期指南

会话由 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 应用或智能显示屏在设备之间转移现有的音频和视频流。媒体内容在一部设备(源设备)上停止播放,并在另一部设备(目标设备)上继续播放。任何搭载最新固件的 Cast 设备都可以作为流传输中的来源或目的地。

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

如需了解详情,请参阅在 Web 接收器上进行流转移

自动重新连接

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

  • 从暂时性 Wi-Fi 丢失中恢复
  • 从设备休眠状态恢复
  • 从应用进入后台状态中恢复
  • 在应用崩溃时恢复

此服务默认处于启用状态,可在 CastOptions.Builder 中停用。

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

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

媒体控制功能的运作方式

Cast 框架弃用了 Cast 2.x 中的 RemoteMediaPlayer 类,转而使用新类 RemoteMediaClient,该类通过一组更便捷的 API 提供相同的功能,并且无需传入 GoogleApiClient。

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

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

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

设置媒体元数据

MediaMetadata 类表示您要投放的媒体项目的信息。以下示例创建了一个新的电影 MediaMetadata 实例,并设置了标题、副标题和两张图片。

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 电视格式的类型以及显示屏的高度和宽度(以像素为单位)。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 Receiver 的连接。