将 Cast 集成到您的 Android 应用中

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

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

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

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

应用流程

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

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

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

配置 Android 清单

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

uses-sdk

设置 Cast SDK 支持的最低 Android API 级别和目标 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 上下文

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

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

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

Cast 框架提供了符合 Cast 设计核对清单的 widget:

  • 入门级叠加层:该框架提供了一个自定义视图 IntroductoryOverlay,系统会向用户显示该视图,以便在接收器首次可用时提醒您注意“投放”按钮。发送者应用可以自定义标题文字的文字和位置

  • “投放”按钮:无论投放设备是否可用,“投放”按钮都会显示出来。 当用户首次点击“投射”按钮时,系统会显示一个“投射”对话框,其中会列出发现的设备。当用户在设备连接的情况下点击“投射”按钮时,系统会显示当前的媒体元数据(例如标题、录音室名称和缩略图),或者允许用户断开与 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,并且可以选择性地通过在 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 会话和网络接收器生命周期,请参阅网络接收器应用生命周期指南

会话由 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,可以由发送方应用启用它,以便在许多细微的角落处理重新连接,例如:

  • 在 WLAN 暂时丢失时恢复
  • 从设备休眠状态恢复
  • 将应用置于后台后恢复
  • 在应用崩溃时恢复

此服务默认处于启用状态,您可以在 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 可播放、暂停或以其他方式控制 Web 接收器上运行的媒体播放器应用。

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

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

4K 视频格式

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

向多台设备发送遥控器通知

当用户投放内容时,同一网络上的其他 Android 设备将会收到通知,让他们也能控制播放。任何收到此类通知的设备的用户均可在“设置”应用中依次前往“Google”>“Google Cast”>“显示遥控器通知”,为该设备关闭此类通知。(通知包括“设置”应用程序的快捷方式。)如需了解详情,请参阅投射遥控器通知

添加迷你控制器

根据 Cast 设计核对清单,发送器应用应提供一个称为“迷你控制器”的持久性控件,当用户从当前内容页面导航到发送器应用的其他部分时,该控件应显示。迷你控制器会向用户显示有关当前 Cast 会话的可见提醒。用户可以通过点按迷你控制器返回到“投放”全屏展开的控制器视图。

该框架提供了一个自定义 View 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 会自动显示播放/停止按钮,代替展开的控制器中的播放/暂停按钮。

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

音量控制

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

实体按钮音量控件

在 Android 上,对于任何使用 Jely 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);
    }
}

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

根据 Google Cast 设计核对清单的要求,发送设备应用在通知中锁定屏幕(发送设备正在投射但发送设备没有焦点)中实现媒体控件(仅限 Android 系统)。该框架提供了 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 接收器的连接。