使 iOS 应用支持 Cast

1. 概览

Google Cast 徽标

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

什么是 Google Cast?

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

借助 Google Cast SDK,您可以扩展应用以控制支持 Google Cast 的设备(例如:电视或音响系统)。使用 Cast SDK,您可以基于 Google Cast 设计核对清单添加必需的界面组件。

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

构建目标

完成此 Codelab 后,您将拥有一个能够将视频投放到 Google Cast 投放设备上的 iOS 视频应用。

学习内容

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

所需条件

  • 最新版本的 Xcode
  • 一部搭载 iOS 9 或更高版本(或者 Xcode 模拟器)的移动设备。
  • 一根用于将移动设备连接到开发计算机的 USB 数据线(如果使用的是设备)。
  • 一台可连接到互联网的 Google Cast 投放设备,例如 ChromecastAndroid TV
  • 一台带 HDMI 输入端口的电视或显示器。
  • 您需要使用 Chromecast with Google TV 来测试 Cast Connect 集成,但对于此 Codelab 的其余部分,这并非必需。如果您没有,可以跳过本教程末尾的添加 Cast Connect 支持步骤。

体验

  • 您需要有 iOS 开发的经验。
  • 您还需要有观看电视的经验 :)

您打算如何使用本教程?

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

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

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

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

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

2. 获取示例代码

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

然后解压下载的 zip 文件。

3. 运行示例应用

Apple iOS 徽标

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

下载代码后,请按照以下说明操作,在 Xcode 中打开并运行完成后的示例应用:

常见问题解答

CocoaPods 设置

如需设置 CocoaPods,请前往控制台并使用 macOS 上提供的默认 Ruby 进行安装:

sudo gem install cocoapods

如有任何问题,请参阅官方文档,以下载并安装依赖项管理器。

项目设置

  1. 前往终端,然后找到 Codelab 目录。
  2. 安装 Podfile 中的依赖项。
cd app-done
pod update
pod install
  1. 打开 Xcode,然后选择 Open another project...
  2. 从示例代码文件夹的 文件夹图标app-done 目录中选择 CastVideos-ios.xcworkspace 文件。

运行应用

选择目标和模拟器,然后运行应用:

XCode 应用模拟器工具栏

视频应用应该会在几秒钟后显示。

在显示有关接受收到的网络连接的通知时,请务必点击“允许”。如果您不接受此选项,Cast 图标将不会显示。

确认对话框,询问是否允许接受入站网络连接

点击“投放”按钮,然后选择您的 Google Cast 投放设备。

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

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

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

返回到视频列表。

现在您会在屏幕底部看到一个迷你控制器。

插图:iPhone 运行 CastVideos 应用,底部显示迷你控制器

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

点击“投放”按钮可停止投放到 Google Cast 设备。

4. 准备起始项目

iPhone 运行 CastVideos 应用的图示

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

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

项目设置

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

  1. 前往终端,然后找到 Codelab 目录。
  2. 安装 Podfile 中的依赖项。
cd app-start
pod update
pod install
  1. 打开 Xcode,然后选择 Open another project...
  2. 从示例代码文件夹的 文件夹图标app-start 目录中选择 CastVideos-ios.xcworkspace 文件。

应用设计

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

该应用包含两个主要视图控制器:MediaTableViewControllerMediaViewController.

MediaTableViewController

此 UITableViewController 会显示 MediaListModel 实例中的视频列表。该视频列表及其关联的元数据以 JSON 文件的形式托管在远程服务器上。MediaListModel 会提取此 JSON 文件并进行处理,以构建一份 MediaItem 对象列表。

MediaItem 对象会为视频及其关联的元数据建模,例如视频的标题、说明、图片的网址以及视频流的网址。

MediaTableViewController 会创建一个 MediaListModel 实例,然后将其作为 MediaListModelDelegate 进行注册,以便在媒体元数据下载完毕后收到通知,从而可以加载表格视图。

用户会看到一个视频缩略图列表,其中每个视频都有一份简短说明。选择某项内容后,对应的 MediaItem 便会传递给 MediaViewController

MediaViewController

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

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

常见问题解答

5. 添加“投放”按钮

插图:iPhone 上运行 CastVideos 应用的顶部三分之一,显示右上角的“投放按钮”

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

配置

入门级项目需要的依赖项和 Xcode 设置与完成后的示例应用相同。返回到对应部分,然后按照相同的步骤操作,即可将 GoogleCast.framework 添加到入门级应用项目中。

初始化

Cast 框架有一个全局单例对象 GCKCastContext,用于协调框架的所有 activity。此对象必须在应用生命周期的早期阶段(通常是在应用委托的 application(_:didFinishLaunchingWithOptions:) 方法中)进行初始化,以便在发送设备应用重启时可以正确触发自动会话恢复,并开始扫描设备。

在初始化 GCKCastContext 时必须提供 GCKCastOptions 对象。此类包含会影响该框架行为的选项。其中最重要的是接收设备应用 ID,该 ID 用于过滤 Cast 设备发现结果,以及在 Cast 会话启动时启动接收设备应用。

此外,您还可以使用 application(_:didFinishLaunchingWithOptions:) 方法设置日志记录代理,以便从 Cast 框架接收日志消息。这些对于调试和问题排查非常有用。

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

将以下代码添加到 AppDelegate.swift,以使用用户默认设置中的应用 ID 初始化 GCKCastContext,并为 Google Cast 框架添加日志记录器:

import GoogleCast

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  fileprivate var enableSDKLogging = true

  ...

  func application(_: UIApplication,
                   didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    ...
    let options = GCKCastOptions(discoveryCriteria: GCKDiscoveryCriteria(applicationID: kReceiverAppID))
    options.physicalVolumeButtonsWillControlDeviceVolume = true
    GCKCastContext.setSharedInstanceWith(options)

    window?.clipsToBounds = true
    setupCastLogging()
    ...
  }
  ...
  func setupCastLogging() {
    let logFilter = GCKLoggerFilter()
    let classesToLog = ["GCKDeviceScanner", "GCKDeviceProvider", "GCKDiscoveryManager", "GCKCastChannel",
                        "GCKMediaControlChannel", "GCKUICastButton", "GCKUIMediaController", "NSMutableDictionary"]
    logFilter.setLoggingLevel(.verbose, forClasses: classesToLog)
    GCKLogger.sharedInstance().filter = logFilter
    GCKLogger.sharedInstance().delegate = self
  }
}

...

// MARK: - GCKLoggerDelegate

extension AppDelegate: GCKLoggerDelegate {
  func logMessage(_ message: String,
                  at _: GCKLoggerLevel,
                  fromFunction function: String,
                  location: String) {
    if enableSDKLogging {
      // Send SDK's log messages directly to the console.
      print("\(location): \(function) - \(message)")
    }
  }
}

投放按钮

现在,GCKCastContext 已初始化,接下来需要添加“投射”按钮,以便用户选择 Cast 设备。Cast SDK 提供了一个名为 GCKUICastButton 的投放按钮组件作为 UIButton 子类。只需在 UIBarButtonItem 中对其进行封装,即可将该组件添加到应用的标题栏中。我们需要将“投放”按钮添加到 MediaTableViewControllerMediaViewController

将以下代码添加到 MediaTableViewController.swiftMediaViewController.swift

import GoogleCast

@objc(MediaTableViewController)
class MediaTableViewController: UITableViewController, GCKSessionManagerListener,
  MediaListModelDelegate, GCKRequestDelegate {
  private var castButton: GCKUICastButton!
  ...
  override func viewDidLoad() {
    print("MediaTableViewController - viewDidLoad")
    super.viewDidLoad()

    ...
    castButton = GCKUICastButton(frame: CGRect(x: CGFloat(0), y: CGFloat(0),
                                               width: CGFloat(24), height: CGFloat(24)))
    // Overwrite the UIAppearance theme in the AppDelegate.
    castButton.tintColor = UIColor.white
    navigationItem.rightBarButtonItem = UIBarButtonItem(customView: castButton)

    ...
  }
  ...
}

接下来,将以下代码添加到您的 MediaViewController.swift 中:

import GoogleCast

@objc(MediaViewController)
class MediaViewController: UIViewController, GCKSessionManagerListener, GCKRemoteMediaClientListener,
  LocalPlayerViewDelegate, GCKRequestDelegate {
  private var castButton: GCKUICastButton!
  ...
  override func viewDidLoad() {
    super.viewDidLoad()
    print("in MediaViewController viewDidLoad")
    ...
    castButton = GCKUICastButton(frame: CGRect(x: CGFloat(0), y: CGFloat(0),
                                               width: CGFloat(24), height: CGFloat(24)))
    // Overwrite the UIAppearance theme in the AppDelegate.
    castButton.tintColor = UIColor.white
    navigationItem.rightBarButtonItem = UIBarButtonItem(customView: castButton)

    ...
  }
  ...
}

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

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

6. 投射视频内容

插图:iPhone 正在运行 CastVideos 应用,其中显示了特定视频(“钢之泪”)的详细信息。底部是迷你播放器

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

投射媒体

大体而言,如果您想在投放设备上播放媒体内容,需要满足以下条件:

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

第 1 步就是将一个对象映射到另一个对象;GCKMediaInformation 是 Cast SDK 可理解的对象,MediaItem 是应用对媒体项的封装;我们可以轻松地将 MediaItem 映射到 GCKMediaInformation。我们已经在上一部分中完成了第 2 步。使用 Cast SDK 可轻松执行第 3 步。

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

enum PlaybackMode: Int {
  case none = 0
  case local
  case remote
}

private var playbackMode = PlaybackMode.none

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

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

投射会话管理

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

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

Cast 会话由 GCKSessionManager 管理,后者可通过 GCKCastContext.sharedInstance().sessionManager 进行访问。GCKSessionManagerListener 回调可用于监控会话事件,例如创建、暂停、继续和终止。

首先,我们需要注册会话监听器并初始化一些变量:

class MediaViewController: UIViewController, GCKSessionManagerListener,
  GCKRemoteMediaClientListener, LocalPlayerViewDelegate, GCKRequestDelegate {

  ...
  private var sessionManager: GCKSessionManager!
  ...

  required init?(coder: NSCoder) {
    super.init(coder: coder)

    sessionManager = GCKCastContext.sharedInstance().sessionManager

    ...
  }

  override func viewWillAppear(_ animated: Bool) {
    ...

    let hasConnectedSession: Bool = (sessionManager.hasConnectedSession())
    if hasConnectedSession, (playbackMode != .remote) {
      populateMediaInfo(false, playPosition: 0)
      switchToRemotePlayback()
    } else if sessionManager.currentSession == nil, (playbackMode != .local) {
      switchToLocalPlayback()
    }

    sessionManager.add(self)

    ...
  }

  override func viewWillDisappear(_ animated: Bool) {
    ...

    sessionManager.remove(self)
    sessionManager.currentCastSession?.remoteMediaClient?.remove(self)
    ...
    super.viewWillDisappear(animated)
  }

  func switchToLocalPlayback() {
    ...

    sessionManager.currentCastSession?.remoteMediaClient?.remove(self)

    ...
  }

  func switchToRemotePlayback() {
    ...

    sessionManager.currentCastSession?.remoteMediaClient?.add(self)

    ...
  }


  // MARK: - GCKSessionManagerListener

  func sessionManager(_: GCKSessionManager, didStart session: GCKSession) {
    print("MediaViewController: sessionManager didStartSession \(session)")
    setQueueButtonVisible(true)
    switchToRemotePlayback()
  }

  func sessionManager(_: GCKSessionManager, didResumeSession session: GCKSession) {
    print("MediaViewController: sessionManager didResumeSession \(session)")
    setQueueButtonVisible(true)
    switchToRemotePlayback()
  }

  func sessionManager(_: GCKSessionManager, didEnd _: GCKSession, withError error: Error?) {
    print("session ended with error: \(String(describing: error))")
    let message = "The Casting session has ended.\n\(String(describing: error))"
    if let window = appDelegate?.window {
      Toast.displayMessage(message, for: 3, in: window)
    }
    setQueueButtonVisible(false)
    switchToLocalPlayback()
  }

  func sessionManager(_: GCKSessionManager, didFailToStartSessionWithError error: Error?) {
    if let error = error {
      showAlert(withTitle: "Failed to start a session", message: error.localizedDescription)
    }
    setQueueButtonVisible(false)
  }

  func sessionManager(_: GCKSessionManager,
                      didFailToResumeSession _: GCKSession, withError _: Error?) {
    if let window = UIApplication.shared.delegate?.window {
      Toast.displayMessage("The Casting session could not be resumed.",
                           for: 3, in: window)
    }
    setQueueButtonVisible(false)
    switchToLocalPlayback()
  }

  ...
}

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

当前处于活跃状态的会话可通过 GCKCastContext.sharedInstance().sessionManager.currentCastSession 访问。系统会根据 Cast 对话框中的用户手势,自动创建和关闭会话。

加载媒体

在 Cast SDK 中,GCKRemoteMediaClient 提供了一组方便的 API,用于管理接收设备上的远程媒体播放。对于支持媒体播放的 GCKCastSession,该 SDK 会自动创建 GCKRemoteMediaClient 的实例。该实例可以作为 GCKCastSession 实例的 remoteMediaClient 属性进行访问。

将以下代码添加到 MediaViewController.swift,以在接收设备上加载当前所选的视频:

@objc(MediaViewController)
class MediaViewController: UIViewController, GCKSessionManagerListener,
  GCKRemoteMediaClientListener, LocalPlayerViewDelegate, GCKRequestDelegate {
  ...

  @objc func playSelectedItemRemotely() {
    loadSelectedItem(byAppending: false)
  }

  /**
   * Loads the currently selected item in the current cast media session.
   * @param appending If YES, the item is appended to the current queue if there
   * is one. If NO, or if
   * there is no queue, a new queue containing only the selected item is created.
   */
  func loadSelectedItem(byAppending appending: Bool) {
    print("enqueue item \(String(describing: mediaInfo))")
    if let remoteMediaClient = sessionManager.currentCastSession?.remoteMediaClient {
      let mediaQueueItemBuilder = GCKMediaQueueItemBuilder()
      mediaQueueItemBuilder.mediaInformation = mediaInfo
      mediaQueueItemBuilder.autoplay = true
      mediaQueueItemBuilder.preloadTime = TimeInterval(UserDefaults.standard.integer(forKey: kPrefPreloadTime))
      let mediaQueueItem = mediaQueueItemBuilder.build()
      if appending {
        let request = remoteMediaClient.queueInsert(mediaQueueItem, beforeItemWithID: kGCKMediaQueueInvalidItemID)
        request.delegate = self
      } else {
        let queueDataBuilder = GCKMediaQueueDataBuilder(queueType: .generic)
        queueDataBuilder.items = [mediaQueueItem]
        queueDataBuilder.repeatMode = remoteMediaClient.mediaStatus?.queueRepeatMode ?? .off

        let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder()
        mediaLoadRequestDataBuilder.mediaInformation = mediaInfo
        mediaLoadRequestDataBuilder.queueData = queueDataBuilder.build()

        let request = remoteMediaClient.loadMedia(with: mediaLoadRequestDataBuilder.build())
        request.delegate = self
      }
    }
  }
  ...
}

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

required init?(coder: NSCoder) {
  super.init(coder: coder)
  ...
  castMediaController = GCKUIMediaController()
  ...
}

func switchToLocalPlayback() {
  print("switchToLocalPlayback")
  if playbackMode == .local {
    return
  }
  setQueueButtonVisible(false)
  var playPosition: TimeInterval = 0
  var paused: Bool = false
  var ended: Bool = false
  if playbackMode == .remote {
    playPosition = castMediaController.lastKnownStreamPosition
    paused = (castMediaController.lastKnownPlayerState == .paused)
    ended = (castMediaController.lastKnownPlayerState == .idle)
    print("last player state: \(castMediaController.lastKnownPlayerState), ended: \(ended)")
  }
  populateMediaInfo((!paused && !ended), playPosition: playPosition)
  sessionManager.currentCastSession?.remoteMediaClient?.remove(self)
  playbackMode = .local
}

func switchToRemotePlayback() {
  print("switchToRemotePlayback; mediaInfo is \(String(describing: mediaInfo))")
  if playbackMode == .remote {
    return
  }
  // If we were playing locally, load the local media on the remote player
  if playbackMode == .local, (_localPlayerView.playerState != .stopped), (mediaInfo != nil) {
    print("loading media: \(String(describing: mediaInfo))")
    let paused: Bool = (_localPlayerView.playerState == .paused)
    let mediaQueueItemBuilder = GCKMediaQueueItemBuilder()
    mediaQueueItemBuilder.mediaInformation = mediaInfo
    mediaQueueItemBuilder.autoplay = !paused
    mediaQueueItemBuilder.preloadTime = TimeInterval(UserDefaults.standard.integer(forKey: kPrefPreloadTime))
    mediaQueueItemBuilder.startTime = _localPlayerView.streamPosition ?? 0
    let mediaQueueItem = mediaQueueItemBuilder.build()

    let queueDataBuilder = GCKMediaQueueDataBuilder(queueType: .generic)
    queueDataBuilder.items = [mediaQueueItem]
    queueDataBuilder.repeatMode = .off

    let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder()
    mediaLoadRequestDataBuilder.queueData = queueDataBuilder.build()

    let request = sessionManager.currentCastSession?.remoteMediaClient?.loadMedia(with: mediaLoadRequestDataBuilder.build())
    request?.delegate = self
  }
  _localPlayerView.stop()
  _localPlayerView.showSplashScreen()
  setQueueButtonVisible(true)
  sessionManager.currentCastSession?.remoteMediaClient?.add(self)
  playbackMode = .remote
}

/* Play has been pressed in the LocalPlayerView. */
func continueAfterPlayButtonClicked() -> Bool {
  let hasConnectedCastSession = sessionManager.hasConnectedCastSession
  if mediaInfo != nil, hasConnectedCastSession() {
    // Display an alert box to allow the user to add to queue or play
    // immediately.
    if actionSheet == nil {
      actionSheet = ActionSheet(title: "Play Item", message: "Select an action", cancelButtonText: "Cancel")
      actionSheet?.addAction(withTitle: "Play Now", target: self,
                             selector: #selector(playSelectedItemRemotely))
    }
    actionSheet?.present(in: self, sourceView: _localPlayerView)
    return false
  }
  return true
}

现在,在移动设备上运行应用。连接到您的 Cast 设备,然后开始播放视频。您应该会看到视频在接收设备上播放。

7. 迷你控制器

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

图示:运行 CastVideos 应用的 iPhone 的底部,重点显示迷你控制器

Cast SDK 提供了一个控制条 GCKUIMiniMediaControlsViewController,可添加到您想要在其中显示持久控件的场景。

对于示例应用,我们将使用 GCKUICastContainerViewController,其中封装了另一个视图控制器,并会在底部添加 GCKUIMiniMediaControlsViewController

修改 AppDelegate.swift 文件,并在以下方法中为 if useCastContainerViewController 条件添加以下代码:

func application(_: UIApplication,
                 didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
  ...
  let appStoryboard = UIStoryboard(name: "Main", bundle: nil)
  guard let navigationController = appStoryboard.instantiateViewController(withIdentifier: "MainNavigation")
    as? UINavigationController else { return false }
  let castContainerVC = GCKCastContext.sharedInstance().createCastContainerController(for: navigationController)
    as GCKUICastContainerViewController
  castContainerVC.miniMediaControlsItemEnabled = true
  window = UIWindow(frame: UIScreen.main.bounds)
  window?.rootViewController = castContainerVC
  window?.makeKeyAndVisible()
  ...
}

添加此属性和 setter/getter,以控制迷你控制器的可见性(我们将在稍后的部分中使用这些内容):

var isCastControlBarsEnabled: Bool {
    get {
      if useCastContainerViewController {
        let castContainerVC = (window?.rootViewController as? GCKUICastContainerViewController)
        return castContainerVC!.miniMediaControlsItemEnabled
      } else {
        let rootContainerVC = (window?.rootViewController as? RootContainerViewController)
        return rootContainerVC!.miniMediaControlsViewEnabled
      }
    }
    set(notificationsEnabled) {
      if useCastContainerViewController {
        var castContainerVC: GCKUICastContainerViewController?
        castContainerVC = (window?.rootViewController as? GCKUICastContainerViewController)
        castContainerVC?.miniMediaControlsItemEnabled = notificationsEnabled
      } else {
        var rootContainerVC: RootContainerViewController?
        rootContainerVC = (window?.rootViewController as? RootContainerViewController)
        rootContainerVC?.miniMediaControlsViewEnabled = notificationsEnabled
      }
    }
  }

运行应用并投射视频。接收设备上开始播放内容时,您应该会看到迷你控制器显示在每个场景的底部。您可以使用迷你控制器控制远程播放。如果您在浏览 Activity 和本地播放器 Activity 之间导航,迷你控制器状态应与接收设备媒体播放状态保持同步。

8. 介绍性叠加层

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

插图:iPhone 运行 CastVideos 应用,其中叠加了“投放”按钮,该按钮高亮显示,并显示消息“点按即可将媒体投放到电视和音箱”

GCKCastContext 类具有 presentCastInstructionsViewControllerOnce 方法,可用于在首次向用户显示“投放”按钮时突出显示此按钮。将以下代码添加到 MediaViewController.swiftMediaTableViewController.swift

override func viewDidLoad() {
  ...

  NotificationCenter.default.addObserver(self, selector: #selector(castDeviceDidChange),
                                         name: NSNotification.Name.gckCastStateDidChange,
                                         object: GCKCastContext.sharedInstance())
}

@objc func castDeviceDidChange(_: Notification) {
  if GCKCastContext.sharedInstance().castState != .noDevicesAvailable {
    // You can present the instructions on how to use Google Cast on
    // the first time the user uses you app
    GCKCastContext.sharedInstance().presentCastInstructionsViewControllerOnce(with: castButton)
  }
}

在移动设备上运行应用,您应该会看到介绍性叠加层。

9. 展开的控制器

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

插图:iPhone 运行 CastVideos 应用,播放视频,底部显示展开的控制器

展开的控制器是一个全屏视图,可完全控制远程媒体播放。此视图应允许投射应用管理 Cast 会话的每个可管理方面,但接收设备音量控件和会话生命周期(连接/停止投射)除外。此外,该视图还提供了与媒体会话有关的所有状态信息(海报图片、标题、副标题等)。

此视图的功能通过 GCKUIExpandedMediaControlsViewController 类实现。

首先,您必须在投射上下文中启用默认的展开的控制器。修改 AppDelegate.swift,以启用默认的展开的控制器:

import GoogleCast

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  ...

  func application(_: UIApplication,
                   didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    ...
    // Add after the setShareInstanceWith(options) is set.
    GCKCastContext.sharedInstance().useDefaultExpandedMediaControls = true
    ...
  }
  ...
}

将以下代码添加到 MediaViewController.swift 以在用户开始投射视频时加载展开的控制器:

@objc func playSelectedItemRemotely() {
  ...
  appDelegate?.isCastControlBarsEnabled = false
  GCKCastContext.sharedInstance().presentDefaultExpandedMediaControls()
}

当用户点按迷你控制器时,展开的控制器也会自动启动。

运行应用并投射视频。您应该会看到展开的控制器。返回到视频列表,当您点击迷你控制器时,系统会再次加载展开的控制器。

10. 添加 Cast Connect 支持

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

依赖项

Podfile 中,确保 google-cast-sdk 指向 4.4.8 或更高版本(如下所示)。如果您对文件进行了修改,请从控制台运行 pod update,以将更改与项目同步。

pod 'google-cast-sdk', '>=4.4.8'

GCKLaunchOptions

为了启动 Android TV 应用(也称为 Android 接收器),我们需要在 GCKLaunchOptions 对象中将 androidReceiverCompatible 标志设置为 true。此 GCKLaunchOptions 对象用于指定接收器的启动方式,并通过 GCKCastContext.setSharedInstanceWith 传递给在共享实例中设置的 GCKCastOptions

将以下行添加到 AppDelegate.swift 中:

let options = GCKCastOptions(discoveryCriteria:
                          GCKDiscoveryCriteria(applicationID: kReceiverAppID))
...
/** Following code enables CastConnect */
let launchOptions = GCKLaunchOptions()
launchOptions.androidReceiverCompatible = true
options.launchOptions = launchOptions

GCKCastContext.setSharedInstanceWith(options)

设置启动凭据

在发送方,您可以指定 GCKCredentialsData 来表示加入会话的用户。credentials 是一个可以由用户定义的字符串,只要您的 ATV 应用可以理解即可。GCKCredentialsData 仅在启动或加入时传递给 Android TV 应用。如果您在连接时再次设置,则不会传递到 Android TV 应用。

为了设置启动凭据,需要在设置 GCKLaunchOptions 后的任意时间定义 GCKCredentialsData。为了演示这一点,我们来为 Creds 按钮添加逻辑,以设置在会话建立时要传递的凭据。将以下代码添加到您的 MediaTableViewController.swift 中:

class MediaTableViewController: UITableViewController, GCKSessionManagerListener, MediaListModelDelegate, GCKRequestDelegate {
  ...
  private var credentials: String? = nil
  ...
  override func viewDidLoad() {
    ...
    navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Creds", style: .plain,
                                                       target: self, action: #selector(toggleLaunchCreds))
    ...
    setLaunchCreds()
  }
  ...
  @objc func toggleLaunchCreds(_: Any){
    if (credentials == nil) {
        credentials = "{\"userId\":\"id123\"}"
    } else {
        credentials = nil
    }
    Toast.displayMessage("Launch Credentials: "+(credentials ?? "Null"), for: 3, in: appDelegate?.window)
    print("Credentials set: "+(credentials ?? "Null"))
    setLaunchCreds()
  }
  ...
  func setLaunchCreds() {
    GCKCastContext.sharedInstance()
        .setLaunch(GCKCredentialsData(credentials: credentials))
  }
}

在加载请求中设置凭据

为了在 Web 应用和 Android TV 接收器应用中处理 credentials,请在 loadSelectedItem 函数下的 MediaTableViewController.swift 类中添加以下代码:

let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder()
...
mediaLoadRequestDataBuilder.credentials = credentials
...

根据发送方投屏到的接收方应用,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/ios_codelab_src
  1. 运行以下命令,将相应文件夹中的 .apk 文件安装到 Android TV 上:
$ adb -s <device_ip_address>:5555 install android-tv-app.apk
  1. 现在,您应该能够在 Android TV 设备的您的应用菜单中看到名为 Cast Videos 的应用。
  2. 完成后,在模拟器或移动设备上构建并运行应用。在与 Android TV 设备建立投屏会话时,它现在应该会在 Android TV 上启动 Android 接收器应用。从 iOS 移动发送器播放视频时,应该会在 Android 接收器中启动视频,并允许您使用 Android TV 设备的遥控器控制播放。

11. 自定义 Cast 微件

初始化

首先,打开 App-Done 文件夹。将以下代码添加到 AppDelegate.swift 文件的 applicationDidFinishLaunchingWithOptions 方法中。

func application(_: UIApplication,
                 didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
  ...
  let styler = GCKUIStyle.sharedInstance()
  ...
}

在应用此 Codelab 其余部分中介绍的一项或多项自定义设置后,调用以下代码即可提交样式

styler.apply()

自定义 Cast 视图

您可以通过在视图之间使用默认样式准则,自定义 Cast 应用框架管理的所有视图。例如,更改图标色调颜色。

styler.castViews.iconTintColor = .lightGray

如果需要,您可以替换每个屏幕的默认设置。例如,仅为展开的媒体控制器替换图标色调颜色的 lightGrayColor。

styler.castViews.mediaControl.expandedController.iconTintColor = .green

更改颜色

您可以为所有视图自定义背景颜色,也可以为各个视图单独自定义背景颜色。以下代码可将所有 Cast 应用框架提供的视图的背景颜色设为蓝色。

styler.castViews.backgroundColor = .blue
styler.castViews.mediaControl.miniController.backgroundColor = .yellow

更改字体

您可以为在 Cast 视图中显示的不同标签自定义字体。为了便于说明,我们将所有字体都设为“Courier-Oblique”。

styler.castViews.headingTextFont = UIFont.init(name: "Courier-Oblique", size: 16) ?? UIFont.systemFont(ofSize: 16)
styler.castViews.mediaControl.headingTextFont = UIFont.init(name: "Courier-Oblique", size: 6) ?? UIFont.systemFont(ofSize: 6)

更改默认按钮图片

将您自己的自定义映像添加到项目中,然后将这些图片分配给按钮,以为其设置样式。

let muteOnImage = UIImage.init(named: "yourImage.png")
if let muteOnImage = muteOnImage {
  styler.castViews.muteOnImage = muteOnImage
}

更改“投放”按钮主题

您还可以使用 UIAppearance 协议为 Cast 微件设置主题。以下代码会在其出现的所有视图上以 GCKUICastButton 为主题:

GCKUICastButton.appearance().tintColor = UIColor.gray

12. 恭喜

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

如需了解详情,请参阅 iOS 发送方开发者指南。