将 Cast 集成到您的 iOS 应用中

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

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

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

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

应用流程

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

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

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

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

从主线程调用方法

初始化 Cast 上下文

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

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

-[application:didFinishLaunchingWithOptions:] 方法也非常适合设置日志记录委托,以从框架接收日志记录消息。它们对于调试和问题排查非常有用。

Swift
@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)
    }
  }
}
Objective-C

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

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

  • 介绍性叠加层GCKCastContext 类具有 presentCastInstructionsViewControllerOnceWithCastButton 方法,可用于在网络接收器首次可用时突出显示“投射”按钮。发送者应用可以自定义文本、标题文本的位置和“关闭”按钮。

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

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

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

添加“投放”按钮

该框架提供了一个“投射”按钮组件作为 UIButton 子类。如需将其添加到应用的标题栏中,请将其封装在 UIBarButtonItem 中。典型的 UIViewController 子类可以安装“投射”按钮,如下所示:

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

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

GCKUICastButton 也可以直接添加到故事板中。

配置设备发现

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

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

会话管理的运作方式

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

会话由 GCKSessionManager 类管理,该类是 GCKCastContext 的一个属性。各个会话由 GCKSession 类的子类表示:例如,GCKCastSession 表示与 Cast 设备的会话。您可以通过 GCKSessionManagercurrentCastSession 属性访问当前活跃的 Cast 会话(如果有)。

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

如果正在使用“投射”对话框,则系统会自动创建和关闭会话以响应用户手势。否则,应用可以通过 GCKSessionManager 中的方法明确开始和结束会话。

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

流式传输

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

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

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

自动重新连接

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

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

媒体控件的工作原理

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

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

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

设置媒体元数据

GCKMediaMetadata 类表示您要投射的媒体内容的相关信息。以下示例将创建电影的新 GCKMediaMetadata 实例,并设置标题、字幕、录音棚名称和两张图片。

Swift
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))
Objective-C
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 控制在接收端上运行的媒体播放器应用,例如播放、暂停和停止。

Swift
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
}
Objective-C
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 格式的类型以及高度和宽度(以像素为单位)。在 hdrType 属性中,4K 格式的变体由枚举值 GCKVideoInfoHDRType 表示。

添加迷你控制器

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

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

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

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

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

  • 通过将现有的视图控制器封装到其自己的视图控制器中,让 Cast 框架管理迷你控制器的布局。
  • 通过在 Storyboard 中提供子视图,将迷你控制器 widget 添加到现有的视图控制器,从而自行管理该 widget 的布局。

使用 GCKUICastContainerViewController 进行封装

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

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

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

  ...
}
Objective-C
- (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];
  ...

}
Swift
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
    }
  }
}
Objective-C

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 实例,然后将其作为子视图添加到容器视图控制器,从而将迷你控制器直接添加到现有视图控制器。

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

Swift
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
}
Objective-C
- (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 实例,并将其作为子视图添加到容器视图控制器:

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

...
Objective-C

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 会告知主机视图控制器迷你控制器应何时可见:

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

添加展开的控制器

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

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

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

首先,您需要在投放上下文中启用默认的展开控制器。修改应用委托以启用默认的展开控制器:

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

  GCKCastContext.sharedInstance().useDefaultExpandedMediaControls = true

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

  [GCKCastContext sharedInstance].useDefaultExpandedMediaControls = YES;

  ..
}

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

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

  ...

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

  ...

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

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

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

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

音量控制

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

实体按钮音量控制

发送设备设备上的实体音量按钮可用于通过在 GCKCastContext 上设置的 GCKCastOptions 上的 physicalVolumeButtonsWillControlDeviceVolume 标记更改网络接收器上 Cast 会话的音量。

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

处理错误

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

日志记录

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

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

Swift
@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)
    }
  }
}
Objective-C

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

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

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

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

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

类名称可以是字面量名称,也可以是 glob 模式,例如 GCKUI\*GCK\*Session