将 Cast 集成到您的 Android 应用中

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

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

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

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

应用流程

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

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

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

配置 Android 清单

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

uses-sdk

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

<uses-sdk
        android:minSdkVersion="21"
        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 Context

框架有一个全局单例对象 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 用户体验微件

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 会话的概念,建立 Cast 会话包括连接到设备、启动(或加入)Web 接收器应用、连接到该应用以及初始化媒体控制渠道的步骤。如需详细了解 Cast 会话和 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
    }

    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,发送者应用可以启用它,以便在许多细微的情况下处理重新连接,例如:

  • 在 Wi-Fi 暂时丢失的情况下恢复
  • 从设备休眠状态中恢复
  • 从应用后台恢复
  • 在应用崩溃时恢复

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

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

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

媒体控件的工作原理

Cast 框架废弃了 Cast 2.x 中的 RemoteMediaPlayer 类,取而代之的是一个新类 RemoteMediaClient,该类通过一组更方便的 API 提供相同的功能,并避免必须传入 GoogleApiClient。

当您的应用使用支持媒体命名空间的 Web 接收器应用建立 CastSession 时,框架将自动创建一个 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 视频格式

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

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

当用户投放内容时,同一网络中的其他 Android 设备也会收到通知,让他们也可以控制播放。任何用户的设备收到此类通知后,都可以在“设置”应用中依次前往“Google”>“Google 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 会自动在展开的控制器中显示播放/停止按钮,而不是播放/暂停按钮。

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

音量控制

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

实体按钮音量控制

对于使用 Jellly Bean 或更高版本的任何设备,默认情况下,发送设备上的实体按钮可用于更改网络接收器上的 Cast 会话的音量。

Jelly Bean 之前的实体按钮音量控制

如需使用实体音量键来控制版本低于 Jelly Bean 的 Android 设备上的网络接收器设备音量,发送器应用应替换其 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 中调用 setNotificationOptions 并指定 null 来停用。目前,只要通知处于开启状态,锁定屏幕功能都会开启。

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