让 Android 应用支持 Cast

1. 概览

Google Cast 徽标

在此 Codelab 中,您将学习如何修改现有 Android 视频应用,使其可在支持 Google Cast 的设备上投放内容。

什么是 Google Cast?

Google Cast 可让用户将移动设备上的内容投射到电视上。然后,用户可以将其移动设备用作遥控器,来控制电视上的媒体播放。

借助 Google Cast SDK,您可以扩展应用以控制电视或音响系统。借助 Cast SDK,您可以根据 Google Cast 设计核对清单添加必要的界面组件。

Google Cast 设计核对清单用于在所有支持的平台上实现简单、可预测的 Cast 用户体验。

构建目标

完成此 Codelab 后,您将拥有一个 Android 视频应用,该应用能够将视频投射到支持 Google Cast 的设备上。

学习内容

  • 如何将 Google Cast SDK 添加到示例视频应用中。
  • 如何添加“投射”按钮以选择 Google Cast 设备。
  • 如何连接到 Cast 设备并启动媒体接收器。
  • 如何投射视频。
  • 如何将 Cast 迷你控制器添加到您的应用中。
  • 如何支持媒体通知和锁定屏幕控件。
  • 如何添加展开的控制器。
  • 如何提供介绍性叠加层。
  • 如何自定义 Cast widget。
  • 如何与 Cast Connect 集成

所需条件

  • 最新的 Android SDK
  • Android Studio 3.2 或更高版本
  • 一部搭载 Android 4.1 Jelly Bean(API 级别 16)或更高版本的移动设备。
  • 一根用于将移动设备连接到开发计算机的 USB 数据线。
  • 一台可连接到互联网的 Google Cast 设备,例如 ChromecastAndroid TV
  • 一台带 HDMI 输入端口的电视或显示器。
  • 若要测试 Cast Connect 集成,必须搭配 Chromecast(支持 Google TV),不过对于此 Codelab 的其余部分,这是可选组件。如果您没有此类设备,可在本教程结尾处跳过添加 Cast Connect 支持步骤。

体验

  • 您需要具备 Kotlin 和 Android 开发方面的知识。
  • 您还需要有观看电视的经验 :)

您打算如何使用本教程?

仅阅读教程内容 阅读并完成练习

您如何评价自己在构建 Android 应用方面的经验水平?

新手水平 中等水平 熟练水平

您如何评价自己在观看电视方面的经验水平?

新手 中等 熟练

2. 获取示例代码

您可以将所有示例代码下载到您的计算机…

然后解压下载的 zip 文件。

3. 运行示例应用

一对罗盘的图标

首先,我们来看看完成后的示例应用的外观。该应用是一个基础视频播放器。用户可以从列表中选择一个视频,然后在设备上本地播放该视频,或者将该视频投射到 Google Cast 设备上。

下载代码后,以下说明介绍了如何在 Android Studio 中打开并运行完成后的示例应用:

在欢迎屏幕上选择 Import Project,或依次选择 File > New > Import Project... 菜单选项。

从示例代码文件夹中选择 文件夹图标app-done 目录,然后点击“OK”。

依次点击 File > Android Studio 的“Sync Project with Gradle”按钮 Sync Project with Gradle Files

在您的 Android 设备上启用 USB 调试 - 在搭载 Android 4.2 及更高版本的设备上,“开发者选项”屏幕默认处于隐藏状态。如需显示开发者选项,请依次转到设置 > 关于手机,然后点按 build 号七次。返回上一屏幕,转到系统 > 高级,点按底部附近的开发者选项,然后点按 USB 调试将其开启。

为您的 Android 设备接通电源,然后点击 Android Studio 中的 Android Studio 的“Run”按钮,一个指向右侧的绿色三角形Run 按钮。您应该会在几秒钟后看到名为 Cast Videos 的视频应用。

点击视频应用中的“投射”按钮,然后选择您的 Google Cast 设备。

选择一个视频,然后点击播放按钮。

该视频便会开始在您的 Google Cast 设备上播放。

此时系统会显示展开的控制器。您可以使用播放/暂停按钮来控制播放。

返回到视频列表。

现在您会在屏幕底部看到一个迷你控制器。一台运行“Cast Videos”应用的 Android 手机图示,其中迷你控制器显示在屏幕底部

点击迷你控制器中的暂停按钮以在接收设备上暂停视频。点击迷你控制器中的播放按钮以继续播放视频。

点击移动设备主屏幕按钮。下拉通知,现在您应该会看到一条 Cast 会话通知。

锁定手机后进行解锁时,您应该会在锁定屏幕上看到一条可用于控制媒体播放或停止投射的通知。

返回到视频应用,点击“投射”按钮,即可停止在 Google Cast 设备上投射。

常见问题解答

4. 准备起始项目

运行“Cast Videos”应用的 Android 手机的插图

我们需要在您下载的入门级应用中添加 Google Cast 支持。下面是一些我们会在此 Codelab 中使用的 Google Cast 术语:

  • 发送设备应用是指在移动设备或笔记本电脑上运行的应用;
  • 接收设备应用是指在 Google Cast 设备上运行的应用。

现在,您可以使用 Android Studio 在入门级项目的基础上进行构建了:

  1. 从下载的示例代码中选择 文件夹图标app-start 目录(在欢迎屏幕上选择 Import Project,或依次选择 File > New > Import Project... 菜单选项)。
  2. 点击 Android Studio 的“Sync Project with Gradle”按钮 Sync Project with Gradle Files 按钮。
  3. 点击 Android Studio 的“Run”按钮,一个指向右侧的绿色三角形Run 按钮以运行应用并浏览界面。

应用设计

该应用从远程网络服务器中提取视频列表,并提供列表供用户浏览。用户可以选择视频查看相关详情,也可以在移动设备上本地播放视频。

该应用包含两个主要 activity:VideoBrowserActivityLocalPlayerActivity。为了集成 Google Cast 功能,Activity 需要从 AppCompatActivity 或其父级 FragmentActivity 继承。存在此限制,因为我们需要将 MediaRouteButton(在 MediaRouter 支持库中提供)添加为 MediaRouteActionProvider,并且这仅在 activity 继承自上述类时才有效。MediaRouter 支持库依赖于 AppCompat 支持库,后者提供了所需的类。

VideoBrowserActivity

此 activity 包含一个 Fragment (VideoBrowserFragment)。此列表由 ArrayAdapter (VideoListAdapter) 提供支持。该视频列表及其关联的元数据以 JSON 文件的形式托管在远程服务器上。AsyncTaskLoader (VideoItemLoader) 会提取此 JSON 文件并对其进行处理,以构建 MediaItem 对象的列表。

MediaItem 对象会为视频及其关联的元数据建模,例如视频的标题、说明、视频流的网址、支持图片的网址以及关联的用于字幕的文字轨道(如有)。MediaItem 对象在 activity 之间传递,因此 MediaItem 具有可将其转换为 Bundle 的实用方法,反之亦然。

当加载器构建 MediaItems 的列表时,它会将该列表传递给 VideoListAdapter,后者随后便会在 VideoBrowserFragment 中显示 MediaItems 列表。用户会看到一个视频缩略图列表,其中每个视频都有一份简短说明。选择某项内容后,对应的 MediaItem 便会转换为 Bundle 并传递给 LocalPlayerActivity

LocalPlayerActivity

此 Activity 显示关于某个特定视频的元数据,并允许用户在移动设备上本地播放该视频。

该 activity 托管了一个 VideoView、一些媒体控件以及一个用于显示所选视频说明的文本区域。播放器位于屏幕顶部区域,从而在下方为视频的详细说明留出空间。用户可以播放/暂停视频或者跳转到视频的本地播放位置。

依赖项

由于我们使用的是 AppCompatActivity,因此需要 AppCompat 支持库。我们使用 Volley 库来管理视频列表并异步获取列表的图片。

常见问题解答

5. 添加“投射”按钮

正在运行 Cast Video 应用的 Android 手机顶部插图;屏幕右上角显示“投射”按钮

支持 Cast 的应用会在其每个 Activity 中显示“投射”按钮。点击“投射”按钮会显示用户可以选择的 Cast 设备列表。如果用户正在发送设备上本地播放内容,则选择 Cast 设备即会在相应 Cast 设备上开始播放或继续播放。在 Cast 会话期间,用户随时可以点击“投射”按钮,停止将应用投射到 Cast 设备。如 Google Cast 设计核对清单中所述,在应用的任何 activity 中,用户都必须能够与 Cast 设备连接或断开连接。

依赖项

更新应用的 build.gradle 文件以包含必要的库依赖项:

dependencies {
    implementation 'androidx.appcompat:appcompat:1.5.0'
    implementation 'androidx.mediarouter:mediarouter:1.3.1'
    implementation 'androidx.recyclerview:recyclerview:1.2.1'
    implementation 'com.google.android.gms:play-services-cast-framework:21.1.0'
    implementation 'com.android.volley:volley:1.2.1'
    implementation "androidx.core:core-ktx:1.8.0"
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
}

同步项目以确认项目构建没有错误。

初始化

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

您必须实现 OptionsProvider 接口,以提供初始化 CastContext 单例所需的 CastOptions。最重要的选项是接收设备应用 ID,该 ID 用于过滤 Cast 设备发现结果,以及在 Cast 会话启动时启动接收设备应用。

您在开发自己的支持 Cast 的应用时,必须注册为 Cast 开发者,然后为您的应用获取应用 ID。在此 Codelab 中,我们将使用一个示例应用 ID。

将以下新的 CastOptionsProvider.kt 文件添加到项目的 com.google.sample.cast.refplayer 软件包中:

package com.google.sample.cast.refplayer

import android.content.Context
import com.google.android.gms.cast.framework.OptionsProvider
import com.google.android.gms.cast.framework.CastOptions
import com.google.android.gms.cast.framework.SessionProvider

class CastOptionsProvider : OptionsProvider {
    override fun getCastOptions(context: Context): CastOptions {
        return CastOptions.Builder()
                .setReceiverApplicationId(context.getString(R.string.app_id))
                .build()
    }

    override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? {
        return null
    }
}

现在,在应用 AndroidManifest.xml 文件的“application”标记内声明 OptionsProvider

<meta-data
    android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
    android:value="com.google.sample.cast.refplayer.CastOptionsProvider" />

VideoBrowserActivity onCreate 方法中延迟初始化 CastContext

import com.google.android.gms.cast.framework.CastContext

private var mCastContext: CastContext? = null

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.video_browser)
    setupActionBar()

    mCastContext = CastContext.getSharedInstance(this)
}

将相同的初始化逻辑添加到 LocalPlayerActivity

投放按钮

现在,CastContext 已初始化,接下来需要添加“投射”按钮,以便用户选择 Cast 设备。“投射”按钮由 MediaRouter 支持库中的 MediaRouteButton 实现。与可以添加到 activity 中的任何操作图标(使用 ActionBarToolbar)一样,您首先需要将相应的菜单项添加到菜单中。

修改 res/menu/browse.xml 文件,并将 MediaRouteActionProvider 项添加到菜单中的设置项前面:

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

使用 CastButtonFactoryMediaRouteButton 连接到 Cast 框架,从而替换 VideoBrowserActivityonCreateOptionsMenu() 方法:

import com.google.android.gms.cast.framework.CastButtonFactory

private var mediaRouteMenuItem: MenuItem? = null

override fun onCreateOptionsMenu(menu: Menu): Boolean {
     super.onCreateOptionsMenu(menu)
     menuInflater.inflate(R.menu.browse, menu)
     mediaRouteMenuItem = CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), menu,
                R.id.media_route_menu_item)
     return true
}

以类似的方式替换 LocalPlayerActivity 中的 onCreateOptionsMenu

点击 Android Studio 的“Run”按钮,一个指向右侧的绿色三角形Run 按钮以在移动设备上运行应用。您应该会在应用的操作栏中看到“投射”按钮,而且当您点击该按钮时,它会列出连接到您本地网络的 Cast 设备。设备发现由 CastContext 自动管理。选择您的 Cast 设备,然后示例接收设备应用便会在 Cast 设备上加载。您可以在浏览 Activity 和本地播放器 Activity 之间导航,而且“投射”按钮状态会保持同步。

我们尚未挂接任何对媒体播放的支持,因此您目前还无法在 Cast 设备上播放视频。点击“投射”按钮断开连接。

6. 投射视频内容

运行“Cast Videos”应用的 Android 手机的插图

我们将扩展示例应用,以便还可以在 Cast 设备上远程播放视频。为此,我们需要监听 Cast 框架生成的各种事件。

投射媒体

大体而言,如果您想在 Cast 设备上播放媒体内容,需要执行以下操作:

  1. 创建用于为媒体内容建模的 MediaInfo 对象。
  2. 连接到 Cast 设备并启动接收设备应用。
  3. MediaInfo 对象加载到接收设备中,然后播放内容。
  4. 跟踪媒体状态。
  5. 根据用户互动情况向接收设备发送播放命令。

我们已经在上一部分中完成了第 2 步。借助 Cast 框架可以轻松执行第 3 步。第 1 步是将一个对象映射到另一个对象;MediaInfo 是 Cast 框架理解的内容,MediaItem 是应用对媒体内容的封装;我们可以轻松将 MediaItem 映射到 MediaInfo

示例应用 LocalPlayerActivity 已可以通过使用以下枚举来区分本地播放与远程播放:

private var mLocation: PlaybackLocation? = null

enum class PlaybackLocation {
    LOCAL, REMOTE
}

enum class PlaybackState {
    PLAYING, PAUSED, BUFFERING, IDLE
}

在此 Codelab 中,您无需准确了解所有示例播放器逻辑的运作方式。但请务必了解,您必须修改应用的媒体播放器,才能让其以类似的方式感知这两个播放位置。

目前,本地播放器始终处于本地播放状态,因为它还不知道任何关于投射状态的信息。我们需要根据 Cast 框架中发生的状态转换来更新界面。例如,如果我们开始投射,则需要停止本地播放并停用一些控件。同样,如果我们在处于此 Activity 中时停止投射,则需要转换为本地播放。为处理此情况,我们需要监听 Cast 框架生成的各种事件。

投射会话管理

对于 Cast 框架,Cast 会话包含以下步骤:连接到设备、启动(或加入)、连接到接收设备应用以及初始化媒体控制通道(如果适用)。媒体控制通道是指 Cast 框架从接收设备媒体播放器发送和接收消息的方式。

当用户通过“投射”按钮选择设备时,Cast 会话将自动启动,而当用户断开连接后,会话便会自动停止。Cast SDK 还会自动处理由于网络问题而重新连接到接收设备会话的操作。

我们来向 LocalPlayerActivity 添加一个 SessionManagerListener

import com.google.android.gms.cast.framework.CastSession
import com.google.android.gms.cast.framework.SessionManagerListener
...

private var mSessionManagerListener: SessionManagerListener<CastSession>? = null
private var mCastSession: CastSession? = null
...

private fun setupCastListener() {
    mSessionManagerListener = object : SessionManagerListener<CastSession> {
        override fun onSessionEnded(session: CastSession, error: Int) {
            onApplicationDisconnected()
        }

        override fun onSessionResumed(session: CastSession, wasSuspended: Boolean) {
            onApplicationConnected(session)
        }

        override fun onSessionResumeFailed(session: CastSession, error: Int) {
            onApplicationDisconnected()
        }

        override fun onSessionStarted(session: CastSession, sessionId: String) {
            onApplicationConnected(session)
        }

        override fun onSessionStartFailed(session: CastSession, error: Int) {
            onApplicationDisconnected()
        }

        override fun onSessionStarting(session: CastSession) {}
        override fun onSessionEnding(session: CastSession) {}
        override fun onSessionResuming(session: CastSession, sessionId: String) {}
        override fun onSessionSuspended(session: CastSession, reason: Int) {}
        private fun onApplicationConnected(castSession: CastSession) {
            mCastSession = castSession
            if (null != mSelectedMedia) {
                if (mPlaybackState == PlaybackState.PLAYING) {
                    mVideoView!!.pause()
                    loadRemoteMedia(mSeekbar!!.progress, true)
                    return
                } else {
                    mPlaybackState = PlaybackState.IDLE
                    updatePlaybackLocation(PlaybackLocation.REMOTE)
                }
            }
            updatePlayButton(mPlaybackState)
            invalidateOptionsMenu()
        }

        private fun onApplicationDisconnected() {
            updatePlaybackLocation(PlaybackLocation.LOCAL)
            mPlaybackState = PlaybackState.IDLE
            mLocation = PlaybackLocation.LOCAL
            updatePlayButton(mPlaybackState)
            invalidateOptionsMenu()
       }
   }
}

LocalPlayerActivity Activity 中,我们希望在与 Cast 设备连接或断开连接时收到通知,以便我们可以来回切换本地播放器。请注意,连接不仅可以被在您的移动设备上运行的应用(您的)的实例中断,还可以被在另一台移动设备上运行的应用(您的或其他)的其他实例中断。

当前处于活跃状态的会话可通过 SessionManager.getCurrentSession() 访问。系统会自动创建和关闭会话,以响应用户与 Cast 对话框的互动。

我们需要注册会话监听器并初始化将在该 Activity 中使用的一些变量。将 LocalPlayerActivity onCreate 方法更改为:

import com.google.android.gms.cast.framework.CastContext
...

private var mCastContext: CastContext? = null
...

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    mCastContext = CastContext.getSharedInstance(this)
    mCastSession = mCastContext!!.sessionManager.currentCastSession
    setupCastListener()
    ...
    loadViews()
    ...
    val bundle = intent.extras
    if (bundle != null) {
        ....
        if (shouldStartPlayback) {
              ....

        } else {
            if (mCastSession != null && mCastSession!!.isConnected()) {
                updatePlaybackLocation(PlaybackLocation.REMOTE)
            } else {
                updatePlaybackLocation(PlaybackLocation.LOCAL)
            }
            mPlaybackState = PlaybackState.IDLE
            updatePlayButton(mPlaybackState)
        }
    }
    ...
}

加载媒体

在 Cast SDK 中,RemoteMediaClient 提供了一组方便的 API,用于在接收设备上管理远程媒体播放。对于支持媒体播放的 CastSession,该 SDK 会自动创建 RemoteMediaClient 的实例。您可以通过在 CastSession 实例上调用 getRemoteMediaClient() 方法对其进行访问。将以下方法添加到 LocalPlayerActivity,以在接收设备上加载当前所选的视频:

import com.google.android.gms.cast.framework.media.RemoteMediaClient
import com.google.android.gms.cast.MediaInfo
import com.google.android.gms.cast.MediaLoadOptions
import com.google.android.gms.cast.MediaMetadata
import com.google.android.gms.common.images.WebImage
import com.google.android.gms.cast.MediaLoadRequestData

private fun loadRemoteMedia(position: Int, autoPlay: Boolean) {
    if (mCastSession == null) {
        return
    }
    val remoteMediaClient = mCastSession!!.remoteMediaClient ?: return
    remoteMediaClient.load( MediaLoadRequestData.Builder()
                .setMediaInfo(buildMediaInfo())
                .setAutoplay(autoPlay)
                .setCurrentTime(position.toLong()).build())
}

private fun buildMediaInfo(): MediaInfo? {
    val movieMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE)
    mSelectedMedia?.studio?.let { movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, it) }
    mSelectedMedia?.title?.let { movieMetadata.putString(MediaMetadata.KEY_TITLE, it) }
    movieMetadata.addImage(WebImage(Uri.parse(mSelectedMedia!!.getImage(0))))
    movieMetadata.addImage(WebImage(Uri.parse(mSelectedMedia!!.getImage(1))))
    return mSelectedMedia!!.url?.let {
        MediaInfo.Builder(it)
            .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
            .setContentType("videos/mp4")
            .setMetadata(movieMetadata)
            .setStreamDuration((mSelectedMedia!!.duration * 1000).toLong())
            .build()
    }
}

现在,更新各种现有方法,以使用 Cast 会话逻辑来支持远程播放:

private fun play(position: Int) {
    startControllersTimer()
    when (mLocation) {
        PlaybackLocation.LOCAL -> {
            mVideoView!!.seekTo(position)
            mVideoView!!.start()
        }
        PlaybackLocation.REMOTE -> {
            mPlaybackState = PlaybackState.BUFFERING
            updatePlayButton(mPlaybackState)
            //seek to a new position within the current media item's new position 
            //which is in milliseconds from the beginning of the stream
            mCastSession!!.remoteMediaClient?.seek(position.toLong())
        }
        else -> {}
    }
    restartTrickplayTimer()
}
private fun togglePlayback() {
    ...
    PlaybackState.IDLE -> when (mLocation) {
        ...
        PlaybackLocation.REMOTE -> {
            if (mCastSession != null && mCastSession!!.isConnected) {
                loadRemoteMedia(mSeekbar!!.progress, true)
            }
        }
        else -> {}
    }
    ...
}
override fun onPause() {
    ...
    mCastContext!!.sessionManager.removeSessionManagerListener(
                mSessionManagerListener!!, CastSession::class.java)
}
override fun onResume() {
    Log.d(TAG, "onResume() was called")
    mCastContext!!.sessionManager.addSessionManagerListener(
            mSessionManagerListener!!, CastSession::class.java)
    if (mCastSession != null && mCastSession!!.isConnected) {
        updatePlaybackLocation(PlaybackLocation.REMOTE)
    } else {
        updatePlaybackLocation(PlaybackLocation.LOCAL)
    }
    super.onResume()
}

对于 updatePlayButton 方法,更改 isConnected 变量的值:

private fun updatePlayButton(state: PlaybackState?) {
    ...
    val isConnected = (mCastSession != null
                && (mCastSession!!.isConnected || mCastSession!!.isConnecting))
    ...
}

现在,点击 Android Studio 的“Run”按钮,一个指向右侧的绿色三角形Run 按钮,以在移动设备上运行应用。连接到您的 Cast 设备,然后开始播放视频。您应该会看到视频在接收设备上播放。

7. 迷你控制器

Cast 设计核对清单要求所有 Cast 应用都提供一个会在用户离开当前内容页面时显示的迷你控制器。迷你控制器可为当前的 Cast 会话提供即时访问和可见提醒。

Android 手机底部的插图,显示了“投射视频”应用中的迷你播放器

Cast SDK 提供了一个自定义视图 MiniControllerFragment,您可以将该视图添加到要在其中显示迷你控制器的 activity 的应用布局文件。

将以下 Fragment 定义添加到 res/layout/player_activity.xmlres/layout/video_browser.xml 底部:

<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"/>

点击 Android Studio 的“Run”按钮,一个指向右侧的绿色三角形Run 按钮以运行应用并投射视频。接收设备上开始播放内容时,您应该会看到迷你控制器显示在每个 Activity 的底部。您可以使用迷你控制器控制远程播放。如果您在浏览 Activity 和本地播放器 Activity 之间导航,迷你控制器状态应与接收设备媒体播放状态保持同步。

8. 通知和锁定屏幕

Google Cast 设计核对清单要求发送设备应用实现来自通知锁定屏幕的媒体控件。

一部 Android 手机图示,显示了通知区域中的媒体控件

Cast SDK 提供了一个 MediaNotificationService,可帮助发送方应用构建通知和锁定屏幕的媒体控件。Gradle 会自动将该服务合并到您应用的清单中。

在发送设备投射过程中,MediaNotificationService 会在后台运行,并且会显示一个通知,其中包含有关当前投射内容的图片缩略图和元数据、播放/暂停按钮以及停止按钮。

在初始化 CastContext 时,可以使用 CastOptions 启用通知和锁定屏幕控件。通知和锁定屏幕的媒体控件默认处于开启状态。只要通知处于启用状态,锁定屏幕功能就处于启用状态。

修改 CastOptionsProvider 并更改 getCastOptions 实现,以与以下代码保持一致:

import com.google.android.gms.cast.framework.media.CastMediaOptions
import com.google.android.gms.cast.framework.media.NotificationOptions

override fun getCastOptions(context: Context): CastOptions {
   val notificationOptions = NotificationOptions.Builder()
            .setTargetActivityClassName(VideoBrowserActivity::class.java.name)
            .build()
    val mediaOptions = CastMediaOptions.Builder()
            .setNotificationOptions(notificationOptions)
            .build()
   return CastOptions.Builder()
                .setReceiverApplicationId(context.getString(R.string.app_id))
                .setCastMediaOptions(mediaOptions)
                .build()
}

点击 Android Studio 的“Run”按钮,一个指向右侧的绿色三角形Run 按钮以在移动设备上运行应用。投射视频并离开示例应用。此时接收设备上应该会显示一条关于当前播放视频的通知。锁定您的移动设备,锁定屏幕现在应该会显示 Cast 设备上媒体播放的控件。

Android 手机在锁定屏幕上显示媒体控件的插图

9. 介绍性叠加层

Google Cast 设计核对清单要求发送设备应用向现有用户引入“投射”按钮,让他们知道发送设备应用现在支持投射,并且可为刚开始使用 Google Cast 的用户提供帮助。

插图:在 Cast Videos Android 应用上的“投放”按钮周围显示介绍性“投放”叠加层

Cast SDK 提供了一个自定义视图 IntroductoryOverlay,可用于在首次向用户显示“投放”按钮时突出显示该按钮。将以下代码添加到 VideoBrowserActivity

import com.google.android.gms.cast.framework.IntroductoryOverlay
import android.os.Looper

private var mIntroductoryOverlay: IntroductoryOverlay? = null

private fun showIntroductoryOverlay() {
    mIntroductoryOverlay?.remove()
    if (mediaRouteMenuItem?.isVisible == true) {
       Looper.myLooper().run {
           mIntroductoryOverlay = com.google.android.gms.cast.framework.IntroductoryOverlay.Builder(
                    this@VideoBrowserActivity, mediaRouteMenuItem!!)
                   .setTitleText("Introducing Cast")
                   .setSingleTime()
                   .setOnOverlayDismissedListener(
                           object : IntroductoryOverlay.OnOverlayDismissedListener {
                               override fun onOverlayDismissed() {
                                   mIntroductoryOverlay = null
                               }
                          })
                   .build()
          mIntroductoryOverlay!!.show()
        }
    }
}

现在,添加 CastStateListener 并在 Cast 设备可用时调用 showIntroductoryOverlay 方法,方法是修改 onCreate 方法并替换 onResumeonPause 方法以匹配以下内容:

import com.google.android.gms.cast.framework.CastState
import com.google.android.gms.cast.framework.CastStateListener

private var mCastStateListener: CastStateListener? = null

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.video_browser)
    setupActionBar()
    mCastStateListener = object : CastStateListener {
            override fun onCastStateChanged(newState: Int) {
                if (newState != CastState.NO_DEVICES_AVAILABLE) {
                    showIntroductoryOverlay()
                }
            }
        }
    mCastContext = CastContext.getSharedInstance(this)
}

override fun onResume() {
    super.onResume()
    mCastContext?.addCastStateListener(mCastStateListener!!)
}

override fun onPause() {
    super.onPause()
    mCastContext?.removeCastStateListener(mCastStateListener!!)
}

清除应用数据或从设备中移除应用。然后,点击 Android Studio 的“Run”按钮,一个指向右侧的绿色三角形Run 按钮以在移动设备上运行应用,您应该会看到介绍性叠加层(如果叠加层未显示,请清除应用数据)。

10. 展开的控制器

Google Cast 设计核对清单要求发送设备应用为要投放的媒体提供展开的控制器。展开的控制器是迷你控制器的全屏版本。

Android 手机上播放视频的插图,其中展开的控制器叠加在它上面

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

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

<?xml version="1.0" encoding="utf-8"?>

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

com.google.sample.cast.refplayer 软件包中创建一个新的软件包 expandedcontrols。接下来,在 com.google.sample.cast.refplayer.expandedcontrols 软件包中创建一个名为 ExpandedControlsActivity.kt 的新文件。

package com.google.sample.cast.refplayer.expandedcontrols

import android.view.Menu
import com.google.android.gms.cast.framework.media.widget.ExpandedControllerActivity
import com.google.sample.cast.refplayer.R
import com.google.android.gms.cast.framework.CastButtonFactory

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

现在,在 OPTIONS_PROVIDER_CLASS_NAME 上方的 application 标记的 AndroidManifest.xml 中声明 ExpandedControlsActivity

<application>
    ...
    <activity
        android:name="com.google.sample.cast.refplayer.expandedcontrols.ExpandedControlsActivity"
        android:label="@string/app_name"
        android:launchMode="singleTask"
        android:theme="@style/Theme.CastVideosDark"
        android:screenOrientation="portrait"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.MAIN"/>
        </intent-filter>
        <meta-data
            android:name="android.support.PARENT_ACTIVITY"
            android:value="com.google.sample.cast.refplayer.VideoBrowserActivity"/>
    </activity>
    ...
</application>

修改 CastOptionsProvider 并更改 NotificationOptionsCastMediaOptions,以将目标 Activity 设置为 ExpandedControlsActivity

import com.google.sample.cast.refplayer.expandedcontrols.ExpandedControlsActivity

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()
}

更新 LocalPlayerActivity loadRemoteMedia 方法,以在加载远程媒体时显示 ExpandedControlsActivity

import com.google.sample.cast.refplayer.expandedcontrols.ExpandedControlsActivity

private fun loadRemoteMedia(position: Int, autoPlay: Boolean) {
    if (mCastSession == null) {
        return
    }
    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(buildMediaInfo())
                .setAutoplay(autoPlay)
                .setCurrentTime(position.toLong()).build())
}

点击 Android Studio 的“Run”按钮,一个指向右侧的绿色三角形Run 按钮,以在移动设备上运行应用并投射视频。您应该会看到展开的控制器。返回到视频列表,当您点击迷你控制器时,系统会再次加载展开的控制器。离开应用以查看通知。点击通知图片以加载展开的控制器。

11. 添加 Cast Connect 支持

借助 Cast Connect 库,现有的发送器应用可通过 Cast 协议与 Android TV 应用进行通信。Cast Connect 基于 Cast 基础架构构建,并以 Android TV 应用作为接收器。

依赖项

注意:如需实现 Cast Connect,play-services-cast-framework 必须为 19.0.0 或更高版本。

LaunchOptions

为了启动 Android TV 应用(也称为 Android 接收器),我们需要将 LaunchOptions 对象中的 setAndroidReceiverCompatible 标志设置为 true。此 LaunchOptions 对象决定了接收器的启动方式,并传递给 CastOptionsProvider 类返回的 CastOptions。将上述标志设置为 false 后,系统会在 Cast Developer Console 中启动所指定应用 ID 的网络接收器。

CastOptionsProvider.kt 文件中,将以下代码添加到 getCastOptions 方法中:

import com.google.android.gms.cast.LaunchOptions
...
val launchOptions = LaunchOptions.Builder()
            .setAndroidReceiverCompatible(true)
            .build()
return new CastOptions.Builder()
        .setLaunchOptions(launchOptions)
        ...
        .build()

设置启动凭据

在发送者端,您可以指定 CredentialsData 来表示谁正在加入会话。credentials 是一个可以由用户定义的字符串,但前提是您的 ATV 应用能够理解该字符串。CredentialsData 仅在启动或加入时传递给 Android TV 应用。如果您在连接时重新设置,系统不会将它传递给 Android TV 应用。

若要设置启动凭据,您需要定义 CredentialsData 并将其传递给 LaunchOptions 对象。将以下代码添加到 CastOptionsProvider.kt 文件中的 getCastOptions 方法:

import com.google.android.gms.cast.CredentialsData
...

val credentialsData = CredentialsData.Builder()
        .setCredentials("{\"userId\": \"abc\"}")
        .build()
val launchOptions = LaunchOptions.Builder()
       ...
       .setCredentialsData(credentialsData)
       .build()

为 LoadRequest 设置凭据

如果您的 Web Receiver 应用和 Android TV 应用以不同方式处理 credentials,您可能需要为它们分别定义 credentials。为此,请在 LocalPlayerActivity.kt 文件中的 loadRemoteMedia 函数下添加以下代码:

remoteMediaClient.load(MediaLoadRequestData.Builder()
       ...
       .setCredentials("user-credentials")
       .setAtvCredentials("atv-user-credentials")
       .build())

现在,SDK 会自动处理要用于当前会话的凭据,具体取决于您的发送内容投放到的接收者应用。

测试 Cast Connect

在 Chromecast(支持 Google TV)上安装 Android TV APK 的步骤

  1. 找到 Android TV 设备的 IP 地址。通常,此设置位于设置 > 网络和互联网 >(您的设备连接到的网络名称)下。右侧会显示详细信息以及您设备在网络上的 IP。
  2. 使用设备的 IP 地址,通过终端通过 ADB 连接到该设备:
$ adb connect <device_ip_address>:5555
  1. 在终端窗口中,找到您在此 Codelab 开始时下载的 Codelab 示例的顶层文件夹。例如:
$ cd Desktop/android_codelab_src
  1. 运行以下命令,将此文件夹中的 .apk 文件安装到 Android TV 上:
$ adb -s <device_ip_address>:5555 install android-tv-app.apk
  1. 现在,您应该能在 Android TV 设备的您的应用菜单中,通过“投射视频”这一名称看到某个应用。
  2. 返回 Android Studio 项目,然后点击“Run”按钮,在您的实体移动设备上安装并运行发送器应用。点击右上角的投射图标,然后从可用选项中选择您的 Android TV 设备。现在,您应该会看到 Android TV 设备上启动了 Android TV 应用,播放视频应该可让您使用 Android TV 遥控器控制视频播放。

12. 自定义 Cast widget

您可以通过设置颜色、设置按钮、文字和缩略图外观的样式,以及选择要显示的按钮类型来自定义 Cast widget

更新 res/values/styles_castvideo.xml

<style name="Theme.CastVideosTheme" parent="Theme.AppCompat.Light.NoActionBar">
    ...
    <item name="mediaRouteTheme">@style/CustomMediaRouterTheme</item>
    <item name="castIntroOverlayStyle">@style/CustomCastIntroOverlay</item>
    <item name="castMiniControllerStyle">@style/CustomCastMiniController</item>
    <item name="castExpandedControllerStyle">@style/CustomCastExpandedController</item>
    <item name="castExpandedControllerToolbarStyle">
        @style/ThemeOverlay.AppCompat.ActionBar
    </item>
    ...
</style>

声明以下自定义主题背景:

<!-- Customize Cast Button -->
<style name="CustomMediaRouterTheme" parent="Theme.MediaRouter">
    <item name="mediaRouteButtonStyle">@style/CustomMediaRouteButtonStyle</item>
</style>
<style name="CustomMediaRouteButtonStyle" parent="Widget.MediaRouter.Light.MediaRouteButton">
    <item name="mediaRouteButtonTint">#EEFF41</item>
</style>

<!-- Customize Introductory Overlay -->
<style name="CustomCastIntroOverlay" parent="CastIntroOverlay">
    <item name="castButtonTextAppearance">@style/TextAppearance.CustomCastIntroOverlay.Button</item>
    <item name="castTitleTextAppearance">@style/TextAppearance.CustomCastIntroOverlay.Title</item>
</style>
<style name="TextAppearance.CustomCastIntroOverlay.Button" parent="android:style/TextAppearance">
    <item name="android:textColor">#FFFFFF</item>
</style>
<style name="TextAppearance.CustomCastIntroOverlay.Title" parent="android:style/TextAppearance.Large">
    <item name="android:textColor">#FFFFFF</item>
</style>

<!-- Customize Mini Controller -->
<style name="CustomCastMiniController" parent="CastMiniController">
    <item name="castShowImageThumbnail">true</item>
    <item name="castTitleTextAppearance">@style/TextAppearance.AppCompat.Subhead</item>
    <item name="castSubtitleTextAppearance">@style/TextAppearance.AppCompat.Caption</item>
    <item name="castBackground">@color/accent</item>
    <item name="castProgressBarColor">@color/orange</item>
</style>

<!-- Customize Expanded Controller -->
<style name="CustomCastExpandedController" parent="CastExpandedController">
    <item name="castButtonColor">#FFFFFF</item>
    <item name="castPlayButtonDrawable">@drawable/cast_ic_expanded_controller_play</item>
    <item name="castPauseButtonDrawable">@drawable/cast_ic_expanded_controller_pause</item>
    <item name="castStopButtonDrawable">@drawable/cast_ic_expanded_controller_stop</item>
</style>

13. 恭喜

现在,您已了解如何在 Android 设备上使用 Cast SDK 微件让视频应用支持 Cast。

有关详情,请参阅 Android 发件人开发者指南。