将投射功能集成到您的 iOS 应用中

使用集合让一切井井有条 根据您的偏好保存内容并对其进行分类。

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

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

发送器框架是指 Cast 类库二进制文件和运行时在运行时存在于发送者上的相关资源。“发送器应用”或“投射应用”是指也在发送者上运行的应用。Web 接收器应用是指在 Web 接收器上运行的 HTML 应用。

发送者框架使用异步回调设计来向发送者应用通知事件,并在 Cast 应用生命周期的各种状态之间过渡。

应用流程

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

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

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

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

从主线程调用方法

初始化 Cast 上下文

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

初始化 GCKCastContext 时,必须提供 GCKCastOptions 对象。此类包含会影响框架行为的选项。其中最重要的是 Web 接收器应用 ID,它用于过滤发现结果并在启动会话时启动 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)
    }
  }
}
目标 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)
目标 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 会话的概念,其定义结合了连接到设备、启动(或加入)Web 接收器应用、连接到该应用以及初始化媒体控制通道的步骤。如需详细了解 Cast 会话和 Web 接收器生命周期,请参阅网络接收器应用生命周期指南

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

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

如果使用 Cast 对话框,那么系统会根据用户手势自动创建和关闭会话。否则,应用可以通过 GCKSessionManager 上的方法明确开始和结束会话。

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

流式传输

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

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

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

自动重新连接

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

  • 暂时失去 Wi-Fi 连接
  • 从设备休眠状态恢复
  • 从应用后台恢复
  • 在应用崩溃时恢复

媒体控件的运作方式

如果使用支持媒体命名空间的网络接收器应用建立了 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))
目标 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
}
目标 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 格式的类型以及高度和宽度(以像素为单位)。4K 格式的变体在 hdrType 属性中通过枚举值 GCKVideoInfoHDRType 表示。

添加迷你控制器

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

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

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

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

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

  • 使用现有视图控制器封装现有视图控制器,让 Cast 框架管理迷你控制器的布局。
  • 通过在故事板中提供子视图,将迷你控制器 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()

  ...
}
目标 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
    }
  }
}
目标 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
}
目标 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)
    }
  }

...
目标 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()
  }
目标 C
- (void)miniMediaControlsViewController:
            (GCKUIMiniMediaControlsViewController *)miniMediaControlsViewController
                           shouldAppear:(BOOL)shouldAppear {
  [self updateControlBarsVisibility];
}

添加展开的控制器

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

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

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

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

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

  GCKCastContext.sharedInstance().useDefaultExpandedMediaControls = true

  ...
}
目标 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)
}
目标 C
- (void)playSelectedItemRemotely {
  [[GCKCastContext sharedInstance] presentDefaultExpandedMediaControls];

  ...

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

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

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

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

音量控制

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

实体按钮音量控制

您可以使用发送器设备上的物理音量按钮,通过 GCKCastOptions(在 GCKCastContext 上设置)上的 physicalVolumeButtonsWillControlDeviceVolume 标志来更改网络接收器上的投放会话的音量。

Swift
let criteria = GCKDiscoveryCriteria(applicationID: kReceiverAppID)
let options = GCKCastOptions(discoveryCriteria: criteria)
options.physicalVolumeButtonsWillControlDeviceVolume = true
GCKCastContext.setSharedInstanceWith(options)
目标 C
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 接收这些消息并将其记录到系统控制台中。

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)
    }
  }
}
目标 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
目标 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
目标 C
GCKLoggerFilter *filter = [[GCKLoggerFilter alloc] init];
[filter setLoggingLevel:GCKLoggerLevelVerbose
             forClasses:@[@"GCKUICastButton",
                          @"GCKUIImageCache",
                          @"NSMutableDictionary"
                          ]];
[GCKLogger sharedInstance].filter = filter;

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