将 Cast 集成到您的 iOS 应用中

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

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

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

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

应用流程

以下步骤介绍了发件人 iOS 应用的典型概要执行流程:

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

如需对发件人进行问题排查,您需要启用日志记录

如需查看 Google Cast iOS 框架中所有类、方法和事件的完整列表,请参阅 Google Cast iOS API 参考文档。以下部分介绍了将 Cast 集成到 iOS 应用中的步骤。

从主线程调用方法

初始化 Cast 上下文

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

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

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

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, GCKLoggerDelegate {
  let kReceiverAppID = kGCKDefaultMediaReceiverApplicationID
  let kDebugLoggingEnabled = true

  var window: UIWindow?

  func applicationDidFinishLaunching(_ application: UIApplication) {
    let criteria = GCKDiscoveryCriteria(applicationID: kReceiverAppID)
    let options = GCKCastOptions(discoveryCriteria: criteria)
    GCKCastContext.setSharedInstanceWith(options)

    // Enable logger.
    GCKLogger.sharedInstance().delegate = self

    ...
  }

  // MARK: - GCKLoggerDelegate

  func logMessage(_ message: String,
                  at level: GCKLoggerLevel,
                  fromFunction function: String,
                  location: String) {
    if (kDebugLoggingEnabled) {
      print(function + " - " + message)
    }
  }
}

AppDelegate.h

@interface AppDelegate () <GCKLoggerDelegate>
@end

AppDelegate.m

@implementation AppDelegate

static NSString *const kReceiverAppID = @"AABBCCDD";
static const BOOL kDebugLoggingEnabled = YES;

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  GCKDiscoveryCriteria *criteria = [[GCKDiscoveryCriteria alloc]
                                    initWithApplicationID:kReceiverAppID];
  GCKCastOptions *options = [[GCKCastOptions alloc] initWithDiscoveryCriteria:criteria];
  [GCKCastContext setSharedInstanceWithOptions:options];

  // Enable logger.
  [GCKLogger sharedInstance].delegate = self;

  ...

  return YES;
}

...

#pragma mark - GCKLoggerDelegate

- (void)logMessage:(NSString *)message
           atLevel:(GCKLoggerLevel)level
      fromFunction:(NSString *)function
          location:(NSString *)location {
  if (kDebugLoggingEnabled) {
    NSLog(@"%@ - %@, %@", function, message, location);
  }
}

@end

Cast 用户体验微件

Cast iOS SDK 提供了符合 Cast 设计核对清单的以下微件:

  • 初始叠加层GCKCastContext 类有一个方法 presentCastInstructionsViewControllerOnceWithCastButton,可用于在首次有可用的网络接收器时突出显示“投屏”按钮。发件人应用可以自定义文本、标题文本的位置和“关闭”按钮。

  • 投屏按钮:从 Cast iOS 发送器 SDK 4.6.0 开始,当发送器设备连接到 Wi-Fi 时,投屏按钮始终可见。用户首次启动应用后首次点按“投放”按钮时,系统会显示权限对话框,以便用户向应用授予对网络上设备的本地网络访问权限。随后,当用户点按投放按钮时,系统会显示一个列出已发现设备的投放对话框。当用户在设备处于连接状态时点按投放按钮时,系统会显示当前媒体元数据(例如标题、录音工作室的名称和缩略图),或允许用户断开与投放设备的连接。当用户点按投放按钮时,如果没有可用设备,系统会显示一个屏幕,告知用户找不到设备的原因以及如何排查问题。

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

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

添加“投屏”按钮

该框架提供了一个“投屏”按钮组件作为 UIButton 子类。只需在 UIBarButtonItem 中对其进行封装,即可将该组件添加到应用的标题栏中。典型的 UIViewController 子类可以按如下方式安装投放按钮:

let castButton = GCKUICastButton(frame: CGRect(x: 0, y: 0, width: 24, height: 24))
castButton.tintColor = UIColor.gray
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: castButton)
GCKUICastButton *castButton = [[GCKUICastButton alloc] initWithFrame:CGRectMake(0, 0, 24, 24)];
castButton.tintColor = [UIColor grayColor];
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:castButton];

默认情况下,点按该按钮会打开框架提供的 Cast 对话框。

您还可以直接将 GCKUICastButton 添加到故事板中。

配置设备发现

在该框架中,设备发现会自动进行。除非您实现自定义界面,否则无需明确启动或停止发现流程。

框架中的发现功能由 GCKDiscoveryManager 类管理,该类是 GCKCastContext 的属性。该框架提供了用于设备选择和控制的默认 Cast 对话框组件。设备列表按设备友好名称的字典顺序排序。

会话管理的运作方式

Cast SDK 引入了 Cast 会话的概念,其建立包含以下步骤:连接到设备、启动(或加入)Web 接收器应用、连接到该应用以及初始化媒体控制渠道。如需详细了解 Cast 会话和 Web 接收器生命周期,请参阅 Web 接收器应用生命周期指南

会话由 GCKSessionManager 类管理,该类是 GCKCastContext 的属性。各个会话由类 GCKSession 的子类表示:例如,GCKCastSession 表示与 Cast 设备的会话。您可以将当前处于活跃状态的 Cast 会话(如果有)作为 GCKSessionManagercurrentCastSession 属性进行访问。

GCKSessionManagerListener 接口可用于监控会话事件,例如会话创建、暂停、继续和终止。当发送方应用进入后台时,该框架会自动暂停会话,并在应用返回前台时尝试恢复会话(或在会话处于活跃状态时应用异常/突然终止后重新启动)。

如果使用的是 Cast 对话框,系统会根据用户手势自动创建和关闭会话。否则,应用可以通过 GCKSessionManager 的方法显式启动和结束会话。

如果应用需要对会话生命周期事件执行特殊处理,则可以向 GCKSessionManager 注册一个或多个 GCKSessionManagerListener 实例。GCKSessionManagerListener 是一种协议,用于为会话开始、会话结束等事件定义回调。

流式传输

保留会话状态是流式传输的基础,用户可以使用语音指令、Google Home 应用或智能显示屏在设备之间移动现有音频和视频流。媒体在一部设备(来源)上停止播放,并在另一部设备(目标设备)上继续播放。任何搭载最新固件的 Cast 设备都可以作为流式传输的源或目标。

如需在流式传输期间获取新的目标设备,请在 [sessionManager:didResumeCastSession:] 回调期间使用 GCKCastSession#device 属性。

如需了解详情,请参阅在 Web 接收器上进行流式传输

自动重新连接

Cast 框架添加了重新连接逻辑,以便在许多细微的极端情况下自动处理重新连接,例如:

  • 从暂时断开 Wi-Fi 连接中恢复
  • 从设备休眠状态恢复
  • 从将应用切换到后台状态恢复
  • 在应用崩溃时进行恢复

媒体控件的运作方式

如果使用支持媒体命名空间的 Web 接收器应用建立 Cast 会话,框架会自动创建 GCKRemoteMediaClient 的实例;该实例可作为 GCKCastSession 实例的 remoteMediaClient 属性进行访问。

向 Web 接收器发出请求的 GCKRemoteMediaClient 上的所有方法都会返回一个 GCKRequest 对象,该对象可用于跟踪该请求。您可以向此对象分配 GCKRequestDelegate,以接收有关操作最终结果的通知。

预计 GCKRemoteMediaClient 的实例可能会由应用的多个部分共享,事实上,框架的某些内部组件(例如 Cast 对话框和迷你媒体控件)确实会共享该实例。为此,GCKRemoteMediaClient 支持注册多个 GCKRemoteMediaClientListener

设置媒体元数据

GCKMediaMetadata 类表示要投放的媒体内容的相关信息。以下示例会创建电影的新 GCKMediaMetadata 实例,并设置影视内容、字幕、录音工作室的名称和两张图片。

let metadata = GCKMediaMetadata()
metadata.setString("Big Buck Bunny (2008)", forKey: kGCKMetadataKeyTitle)
metadata.setString("Big Buck Bunny tells the story of a giant rabbit with a heart bigger than " +
  "himself. When one sunny day three rodents rudely harass him, something " +
  "snaps... and the rabbit ain't no bunny anymore! In the typical cartoon " +
  "tradition he prepares the nasty rodents a comical revenge.",
                   forKey: kGCKMetadataKeySubtitle)
metadata.addImage(GCKImage(url: URL(string: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg")!,
                           width: 480,
                           height: 360))
GCKMediaMetadata *metadata = [[GCKMediaMetadata alloc]
                                initWithMetadataType:GCKMediaMetadataTypeMovie];
[metadata setString:@"Big Buck Bunny (2008)" forKey:kGCKMetadataKeyTitle];
[metadata setString:@"Big Buck Bunny tells the story of a giant rabbit with a heart bigger than "
 "himself. When one sunny day three rodents rudely harass him, something "
 "snaps... and the rabbit ain't no bunny anymore! In the typical cartoon "
 "tradition he prepares the nasty rodents a comical revenge."
             forKey:kGCKMetadataKeySubtitle];
[metadata addImage:[[GCKImage alloc]
                    initWithURL:[[NSURL alloc] initWithString:@"https://commondatastorage.googleapis.com/"
                                 "gtv-videos-bucket/sample/images/BigBuckBunny.jpg"]
                    width:480
                    height:360]];

如需了解如何将图片与媒体元数据搭配使用,请参阅图片选择和缓存部分。

加载媒体

如需加载媒体内容,请使用媒体的元数据创建 GCKMediaInformation 实例。然后,获取当前的 GCKCastSession,并使用其 GCKRemoteMediaClient 在接收器应用中加载媒体。然后,您可以使用 GCKRemoteMediaClient 控制在接收器上运行的媒体播放器应用,例如播放、暂停和停止。

let url = URL.init(string: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4")
guard let mediaURL = url else {
  print("invalid mediaURL")
  return
}

let mediaInfoBuilder = GCKMediaInformationBuilder.init(contentURL: mediaURL)
mediaInfoBuilder.streamType = GCKMediaStreamType.none;
mediaInfoBuilder.contentType = "video/mp4"
mediaInfoBuilder.metadata = metadata;
mediaInformation = mediaInfoBuilder.build()

guard let mediaInfo = mediaInformation else {
  print("invalid mediaInformation")
  return
}

if let request = sessionManager.currentSession?.remoteMediaClient?.loadMedia(mediaInfo) {
  request.delegate = self
}
GCKMediaInformationBuilder *mediaInfoBuilder =
  [[GCKMediaInformationBuilder alloc] initWithContentURL:
   [NSURL URLWithString:@"https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"]];
mediaInfoBuilder.streamType = GCKMediaStreamTypeNone;
mediaInfoBuilder.contentType = @"video/mp4";
mediaInfoBuilder.metadata = metadata;
self.mediaInformation = [mediaInfoBuilder build];

GCKRequest *request = [self.sessionManager.currentSession.remoteMediaClient loadMedia:self.mediaInformation];
if (request != nil) {
  request.delegate = self;
}

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

4K 视频格式

如需确定媒体的视频格式,请使用 GCKMediaStatusvideoInfo 属性获取 GCKVideoInfo 的当前实例。此实例包含 HDR TV 格式的类型以及高度和宽度(以像素为单位)。4K 格式的变体在 hdrType 属性中由枚举值 GCKVideoInfoHDRType 表示。

添加迷你控制器

根据 Cast 设计核对清单,发送器应用应提供一个称为“迷你控制器”的永久性控件,该控件应在用户离开当前内容页面时显示。迷你控制器可为当前的 Cast 会话提供即时访问权限和可见提醒。

Cast 框架提供了一个控制条 GCKUIMiniMediaControlsViewController,可添加到您想要在其中显示迷你控制器的场景。

当发送器应用播放视频或音频直播时,SDK 会在迷你控制器中自动显示播放/停止按钮,而不是播放/暂停按钮。

如需了解发送器应用如何配置 Cast 微件的外观,请参阅自定义 iOS 发送器界面

您可以通过以下两种方式将迷你控制器添加到发送器应用中:

  • 通过使用自己的视图控制器封装现有视图控制器,让 Cast 框架管理迷你控制器的布局。
  • 通过在故事板中提供子视图,将迷你控制器微件添加到现有视图控制器,以便自行管理迷你控制器微件的布局。

使用 GCKUICastContainerViewController 进行封装

第一种方法是使用 GCKUICastContainerViewController,其中封装了另一个视图控制器,并会在底部添加 GCKUIMiniMediaControlsViewController。此方法的局限性在于,您无法自定义动画,也无法配置容器视图控制器的行为。

这种第一种方式通常在应用委托的 -[application:didFinishLaunchingWithOptions:] 方法中完成:

func applicationDidFinishLaunching(_ application: UIApplication) {
  ...

  // Wrap main view in the GCKUICastContainerViewController and display the mini controller.
  let appStoryboard = UIStoryboard(name: "Main", bundle: nil)
  let navigationController = appStoryboard.instantiateViewController(withIdentifier: "MainNavigation")
  let castContainerVC =
          GCKCastContext.sharedInstance().createCastContainerController(for: navigationController)
  castContainerVC.miniMediaControlsItemEnabled = true
  window = UIWindow(frame: UIScreen.main.bounds)
  window!.rootViewController = castContainerVC
  window!.makeKeyAndVisible()

  ...
}
- (BOOL)application:(UIApplication *)application
        didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  ...

  // Wrap main view in the GCKUICastContainerViewController and display the mini controller.
  UIStoryboard *appStoryboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
  UINavigationController *navigationController =
          [appStoryboard instantiateViewControllerWithIdentifier:@"MainNavigation"];
  GCKUICastContainerViewController *castContainerVC =
          [[GCKCastContext sharedInstance] createCastContainerControllerForViewController:navigationController];
  castContainerVC.miniMediaControlsItemEnabled = YES;
  self.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
  self.window.rootViewController = castContainerVC;
  [self.window makeKeyAndVisible];
  ...

}
var castControlBarsEnabled: Bool {
  set(enabled) {
    if let castContainerVC = self.window?.rootViewController as? GCKUICastContainerViewController {
      castContainerVC.miniMediaControlsItemEnabled = enabled
    } else {
      print("GCKUICastContainerViewController is not correctly configured")
    }
  }
  get {
    if let castContainerVC = self.window?.rootViewController as? GCKUICastContainerViewController {
      return castContainerVC.miniMediaControlsItemEnabled
    } else {
      print("GCKUICastContainerViewController is not correctly configured")
      return false
    }
  }
}

AppDelegate.h

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (nonatomic, strong) UIWindow *window;
@property (nonatomic, assign) BOOL castControlBarsEnabled;

@end

AppDelegate.m

@implementation AppDelegate

...

- (void)setCastControlBarsEnabled:(BOOL)notificationsEnabled {
  GCKUICastContainerViewController *castContainerVC;
  castContainerVC =
      (GCKUICastContainerViewController *)self.window.rootViewController;
  castContainerVC.miniMediaControlsItemEnabled = notificationsEnabled;
}

- (BOOL)castControlBarsEnabled {
  GCKUICastContainerViewController *castContainerVC;
  castContainerVC =
      (GCKUICastContainerViewController *)self.window.rootViewController;
  return castContainerVC.miniMediaControlsItemEnabled;
}

...

@end

嵌入到现有视图控制器中

第二种方法是使用 createMiniMediaControlsViewController 创建 GCKUIMiniMediaControlsViewController 实例,然后将其作为子视图添加到容器视图控制器,从而将迷你控制器直接添加到现有视图控制器。

在应用委托中设置视图控制器:

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

  GCKCastContext.sharedInstance().useDefaultExpandedMediaControls = true
  window?.clipsToBounds = true

  let rootContainerVC = (window?.rootViewController as? RootContainerViewController)
  rootContainerVC?.miniMediaControlsViewEnabled = true

  ...

  return true
}
- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  ...

  [GCKCastContext sharedInstance].useDefaultExpandedMediaControls = YES;

  self.window.clipsToBounds = YES;

  RootContainerViewController *rootContainerVC;
  rootContainerVC =
      (RootContainerViewController *)self.window.rootViewController;
  rootContainerVC.miniMediaControlsViewEnabled = YES;

  ...

  return YES;
}

在根视图控制器中,创建一个 GCKUIMiniMediaControlsViewController 实例,并将其作为子视图添加到容器视图控制器:

let kCastControlBarsAnimationDuration: TimeInterval = 0.20

@objc(RootContainerViewController)
class RootContainerViewController: UIViewController, GCKUIMiniMediaControlsViewControllerDelegate {
  @IBOutlet weak private var _miniMediaControlsContainerView: UIView!
  @IBOutlet weak private var _miniMediaControlsHeightConstraint: NSLayoutConstraint!
  private var miniMediaControlsViewController: GCKUIMiniMediaControlsViewController!
  var miniMediaControlsViewEnabled = false {
    didSet {
      if self.isViewLoaded {
        self.updateControlBarsVisibility()
      }
    }
  }

  var overriddenNavigationController: UINavigationController?

  override var navigationController: UINavigationController? {

    get {
      return overriddenNavigationController
    }

    set {
      overriddenNavigationController = newValue
    }
  }
  var miniMediaControlsItemEnabled = false

  override func viewDidLoad() {
    super.viewDidLoad()
    let castContext = GCKCastContext.sharedInstance()
    self.miniMediaControlsViewController = castContext.createMiniMediaControlsViewController()
    self.miniMediaControlsViewController.delegate = self
    self.updateControlBarsVisibility()
    self.installViewController(self.miniMediaControlsViewController,
                               inContainerView: self._miniMediaControlsContainerView)
  }

  func updateControlBarsVisibility() {
    if self.miniMediaControlsViewEnabled && self.miniMediaControlsViewController.active {
      self._miniMediaControlsHeightConstraint.constant = self.miniMediaControlsViewController.minHeight
      self.view.bringSubview(toFront: self._miniMediaControlsContainerView)
    } else {
      self._miniMediaControlsHeightConstraint.constant = 0
    }
    UIView.animate(withDuration: kCastControlBarsAnimationDuration, animations: {() -> Void in
      self.view.layoutIfNeeded()
    })
    self.view.setNeedsLayout()
  }

  func installViewController(_ viewController: UIViewController?, inContainerView containerView: UIView) {
    if let viewController = viewController {
      self.addChildViewController(viewController)
      viewController.view.frame = containerView.bounds
      containerView.addSubview(viewController.view)
      viewController.didMove(toParentViewController: self)
    }
  }

  func uninstallViewController(_ viewController: UIViewController) {
    viewController.willMove(toParentViewController: nil)
    viewController.view.removeFromSuperview()
    viewController.removeFromParentViewController()
  }

  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "NavigationVCEmbedSegue" {
      self.navigationController = (segue.destination as? UINavigationController)
    }
  }

...

RootContainerViewController.h

static const NSTimeInterval kCastControlBarsAnimationDuration = 0.20;

@interface RootContainerViewController () <GCKUIMiniMediaControlsViewControllerDelegate> {
  __weak IBOutlet UIView *_miniMediaControlsContainerView;
  __weak IBOutlet NSLayoutConstraint *_miniMediaControlsHeightConstraint;
  GCKUIMiniMediaControlsViewController *_miniMediaControlsViewController;
}

@property(nonatomic, weak, readwrite) UINavigationController *navigationController;

@property(nonatomic, assign, readwrite) BOOL miniMediaControlsViewEnabled;
@property(nonatomic, assign, readwrite) BOOL miniMediaControlsItemEnabled;

@end

RootContainerViewController.m

@implementation RootContainerViewController

- (void)viewDidLoad {
  [super viewDidLoad];
  GCKCastContext *castContext = [GCKCastContext sharedInstance];
  _miniMediaControlsViewController =
      [castContext createMiniMediaControlsViewController];
  _miniMediaControlsViewController.delegate = self;

  [self updateControlBarsVisibility];
  [self installViewController:_miniMediaControlsViewController
              inContainerView:_miniMediaControlsContainerView];
}

- (void)setMiniMediaControlsViewEnabled:(BOOL)miniMediaControlsViewEnabled {
  _miniMediaControlsViewEnabled = miniMediaControlsViewEnabled;
  if (self.isViewLoaded) {
    [self updateControlBarsVisibility];
  }
}

- (void)updateControlBarsVisibility {
  if (self.miniMediaControlsViewEnabled &&
      _miniMediaControlsViewController.active) {
    _miniMediaControlsHeightConstraint.constant =
        _miniMediaControlsViewController.minHeight;
    [self.view bringSubviewToFront:_miniMediaControlsContainerView];
  } else {
    _miniMediaControlsHeightConstraint.constant = 0;
  }
  [UIView animateWithDuration:kCastControlBarsAnimationDuration
                   animations:^{
                     [self.view layoutIfNeeded];
                   }];
  [self.view setNeedsLayout];
}

- (void)installViewController:(UIViewController *)viewController
              inContainerView:(UIView *)containerView {
  if (viewController) {
    [self addChildViewController:viewController];
    viewController.view.frame = containerView.bounds;
    [containerView addSubview:viewController.view];
    [viewController didMoveToParentViewController:self];
  }
}

- (void)uninstallViewController:(UIViewController *)viewController {
  [viewController willMoveToParentViewController:nil];
  [viewController.view removeFromSuperview];
  [viewController removeFromParentViewController];
}

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
  if ([segue.identifier isEqualToString:@"NavigationVCEmbedSegue"]) {
    self.navigationController =
        (UINavigationController *)segue.destinationViewController;
  }
}

...

@end

GCKUIMiniMediaControlsViewControllerDelegate 会告知托管视图控制器何时应显示迷你控制器:

  func miniMediaControlsViewController(_: GCKUIMiniMediaControlsViewController,
                                       shouldAppear _: Bool) {
    updateControlBarsVisibility()
  }
- (void)miniMediaControlsViewController:
            (GCKUIMiniMediaControlsViewController *)miniMediaControlsViewController
                           shouldAppear:(BOOL)shouldAppear {
  [self updateControlBarsVisibility];
}

添加展开的控制器

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

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

此视图的功能由 GCKUIExpandedMediaControlsViewController 类实现。

首先,您必须在投放上下文中启用默认的展开的控制器。修改应用代理以启用默认的展开的控制器:

func applicationDidFinishLaunching(_ application: UIApplication) {
  ..

  GCKCastContext.sharedInstance().useDefaultExpandedMediaControls = true

  ...
}
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  ...

  [GCKCastContext sharedInstance].useDefaultExpandedMediaControls = YES;

  ..
}

将以下代码添加到您的视图控制器,以便在用户开始投放视频时加载展开的控制器:

func playSelectedItemRemotely() {
  GCKCastContext.sharedInstance().presentDefaultExpandedMediaControls()

  ...

  // Load your media
  sessionManager.currentSession?.remoteMediaClient?.loadMedia(mediaInformation)
}
- (void)playSelectedItemRemotely {
  [[GCKCastContext sharedInstance] presentDefaultExpandedMediaControls];

  ...

  // Load your media
  [self.sessionManager.currentSession.remoteMediaClient loadMedia:mediaInformation];
}

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

当发送器应用播放视频或音频直播时,SDK 会在展开式控制器中自动显示播放/停止按钮,而不是播放/暂停按钮。

如需了解发送器应用如何配置 Cast 微件的外观,请参阅向 iOS 应用应用自定义样式

音量控制

Cast 框架会自动管理发送器应用的音量。该框架会自动与所提供的界面 widget 的 Web 接收器音量同步。如需同步应用提供的滑块,请使用 GCKUIDeviceVolumeController

实体按钮音量控制

发送器设备上的实体音量按钮可用于使用 GCKCastContext 上设置的 GCKCastOptions 上的 physicalVolumeButtonsWillControlDeviceVolume 标志来更改 Web 接收器上 Cast 会话的音量。

let criteria = GCKDiscoveryCriteria(applicationID: kReceiverAppID)
let options = GCKCastOptions(discoveryCriteria: criteria)
options.physicalVolumeButtonsWillControlDeviceVolume = true
GCKCastContext.setSharedInstanceWith(options)
GCKDiscoveryCriteria *criteria = [[GCKDiscoveryCriteria alloc]
                                          initWithApplicationID:kReceiverAppID];
GCKCastOptions *options = [[GCKCastOptions alloc]
                                          initWithDiscoveryCriteria :criteria];
options.physicalVolumeButtonsWillControlDeviceVolume = YES;
[GCKCastContext setSharedInstanceWithOptions:options];

处理错误

发送方应用必须处理所有错误回调,并为 Cast 生命周期的每个阶段确定最佳响应,这一点非常重要。应用可以向用户显示错误对话框,也可以决定结束投放会话。

日志记录

GCKLogger 是框架用于日志记录的单例。使用 GCKLoggerDelegate 自定义日志消息的处理方式。

使用 GCKLogger,SDK 会以调试消息、错误和警告的形式生成日志输出。这些日志消息有助于调试,并对排查问题和识别问题很有用。默认情况下,系统会抑制日志输出,但通过分配 GCKLoggerDelegate,发件人应用可以从 SDK 接收这些消息并将其记录到系统控制台。

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, GCKLoggerDelegate {
  let kReceiverAppID = kGCKDefaultMediaReceiverApplicationID
  let kDebugLoggingEnabled = true

  var window: UIWindow?

  func applicationDidFinishLaunching(_ application: UIApplication) {
    ...

    // Enable logger.
    GCKLogger.sharedInstance().delegate = self

    ...
  }

  // MARK: - GCKLoggerDelegate

  func logMessage(_ message: String,
                  at level: GCKLoggerLevel,
                  fromFunction function: String,
                  location: String) {
    if (kDebugLoggingEnabled) {
      print(function + " - " + message)
    }
  }
}

AppDelegate.h

@interface AppDelegate () <GCKLoggerDelegate>
@end

AppDelegate.m

@implementation AppDelegate

static NSString *const kReceiverAppID = @"AABBCCDD";
static const BOOL kDebugLoggingEnabled = YES;

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  ...

  // Enable logger.
  [GCKLogger sharedInstance].delegate = self;

  ...

  return YES;
}

...

#pragma mark - GCKLoggerDelegate

- (void)logMessage:(NSString *)message
           atLevel:(GCKLoggerLevel)level
      fromFunction:(NSString *)function
          location:(NSString *)location {
  if (kDebugLoggingEnabled) {
    NSLog(@"%@ - %@, %@", function, message, location);
  }
}

@end

如需同时启用调试消息和详细消息,请在设置代理(如上所示)后将以下代码行添加到代码中:

let filter = GCKLoggerFilter.init()
filter.minimumLevel = GCKLoggerLevel.verbose
GCKLogger.sharedInstance().filter = filter
GCKLoggerFilter *filter = [[GCKLoggerFilter alloc] init];
[filter setMinimumLevel:GCKLoggerLevelVerbose];
[GCKLogger sharedInstance].filter = filter;

您还可以过滤 GCKLogger 生成的日志消息。设置每个类的最低日志记录级别,例如:

let filter = GCKLoggerFilter.init()
filter.setLoggingLevel(GCKLoggerLevel.verbose, forClasses: ["GCKUICastButton",
                                                            "GCKUIImageCache",
                                                            "NSMutableDictionary"])
GCKLogger.sharedInstance().filter = filter
GCKLoggerFilter *filter = [[GCKLoggerFilter alloc] init];
[filter setLoggingLevel:GCKLoggerLevelVerbose
             forClasses:@[@"GCKUICastButton",
                          @"GCKUIImageCache",
                          @"NSMutableDictionary"
                          ]];
[GCKLogger sharedInstance].filter = filter;

类名称可以是字面量名称或通配模式,例如 GCKUI\*GCK\*Session