本开发者指南介绍了如何使用 iOS 发送器 SDK 将 Google Cast 支持添加到 iOS 发送器应用。
移动设备或笔记本电脑是控制播放的 发送器 ,而 Google Cast 设备是显示电视内容的 接收器 。
发送器框架 是指在发送器上运行时存在的 Cast 类库二进制文件和相关资源。发送器应用 或 Cast 应用 是指也在发送器上运行的应用。Web 接收器应用 是指在 Web 接收器上运行的 HTML 应用。
发送器框架使用异步回调设计来通知发送器应用事件,并在 Cast 应用生命周期的各种状态之间转换。
应用流程
以下步骤介绍了发送器 iOS 应用的典型概要执行流程:
- Cast 框架根据
GCKCastOptions中提供的属性启动GCKDiscoveryManager,以 开始扫描设备。 - 当用户点击“投放”按钮时,框架会显示 Cast 对话框,其中包含发现的投放设备列表。
- 当用户选择投放设备时,框架会尝试在投放设备上启动 Web 接收器应用。
- 框架会在发送器应用中调用回调,以确认 Web 接收器应用已启动。
- 框架会在发送器应用和 Web 接收器应用之间创建通信渠道。
- 框架使用通信渠道在 Web 接收器上加载和控制媒体播放。
- 框架会在发送器和 Web 接收器之间同步媒体播放状态:当用户执行发送器界面操作时,框架会将这些媒体控制请求传递给 Web 接收器;当 Web 接收器发送媒体状态更新时,框架会更新发送器界面的状态。
- 当用户点击“投放”按钮以断开与投放设备的连接时,框架会将发送器应用与 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 用户体验 widget
Cast iOS SDK 提供了以下符合 Cast 设计核对清单的 widget:
简介叠加层:
GCKCastContext类具有方法presentCastInstructionsViewControllerOnceWithCastButton, 可用于在首次有 Web 接收器 可用时突出显示“投放”按钮。发送器应用可以自定义文本、标题文本的位置和“关闭”按钮。投放按钮: 从 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 会话(如果有)作为 GCKSessionManager 的 currentCastSession 属性进行访问。
The
GCKSessionManagerListener
接口可用于监控会话事件,例如会话创建、
暂停、继续和终止。当发送器应用进入后台时,框架会自动暂停会话,并在应用返回前台(或在会话处于活跃状态时应用异常/突然终止后重新启动)时尝试恢复会话。
如果正在使用 Cast 对话框,则系统会根据用户手势自动创建和关闭会话。否则,应用可以通过
方法显式启动和结束
GCKSessionManager。
如果应用需要针对会话生命周期
事件执行特殊处理,则可以向
GCKSessionManager 注册一个或多个 GCKSessionManagerListener 实例。GCKSessionManagerListener 是一种协议,用于定义会话开始、会话结束等事件的回调。
流传输
保留会话状态是流传输的基础,用户可以使用语音命令、Google Home 应用或智能显示屏在设备之间移动现有的音频和视频流。媒体会在一台设备(源)上停止播放,并在另一台设备(目标)上继续播放。任何安装了最新固件的投放设备都可以作为流传输中的源或目标。
如需在流传输期间获取新的目标设备,请在
[sessionManager:didResumeCastSession:]
回调期间使用
GCKCastSession#device
属性。
如需了解详情,请参阅 Web 接收器上的流传输 。
自动重新连接
Cast 框架添加了重新连接逻辑,以便在许多细微的极端情况下自动处理重新连接,例如:
- 从 Wi-Fi 暂时丢失中恢复
- 从设备休眠中恢复
- 从应用后台运行中恢复
- 如果应用崩溃,则恢复
媒体控制的工作原理
如果使用支持媒体
命名空间的 Web 接收器应用建立 Cast 会话,框架会自动创建
GCKRemoteMediaClient
的实例;该实例可以作为
remoteMediaClient属性进行访问
GCKCastSession
实例。
GCKRemoteMediaClient 上向 Web 接收器发出请求的所有方法
都会返回一个
GCKRequest 对象,该对象
可用于跟踪该请求。您可以为此对象分配
GCKRequestDelegate
,以接收有关操作最终
结果的通知。
预计 GCKRemoteMediaClient 的实例可能会由应用的多个部分共享,事实上,框架的一些内部组件(例如 Cast 对话框和迷你媒体控件)确实会共享该实例。为此,GCKRemoteMediaClient
支持注册多个
GCKRemoteMediaClientListener。
设置媒体元数据
The
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 视频格式
如需确定媒体的视频格式,请使用 videoInfo 属性获取 GCKMediaStatusGCKVideoInfo 的当前实例。此实例包含 HDR TV 格式的类型以及像素的高度和宽度。hdrType 属性中的枚举值 GCKVideoInfoHDRType 指示了 4K 格式的变体。
添加迷你控制器
根据 Cast 设计 核对清单, 发送器应用应提供一个名为 迷你 控制器 的持久控件,该控件应在用户离开当前内容页面时显示。迷你控制器可为当前的 Cast 会话提供即时访问权限和可见提醒。
Cast 框架提供了一个控制条
GCKUIMiniMediaControlsViewController
,可添加到您想要在其中显示迷你控制器的场景。
当发送器应用播放视频或音频直播时,SDK 会自动在迷你控制器中显示播放/停止按钮,而不是播放/暂停按钮。
如需了解发送器应用如何配置 Cast widget 的外观,请参阅自定义 iOS 发送器界面。
您可以通过以下两种方式将迷你控制器添加到发送器应用:
- 让 Cast 框架通过使用自己的视图控制器封装现有视图控制器来管理迷你控制器的布局。
- 通过在情节提要中提供子视图,将迷你控制器 widget 添加到现有视图控制器,从而自行管理迷你控制器 widget 的布局。
使用 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 widget 的外观,请参阅为 iOS 应用 应用自定义样式。
音量控制
Cast 框架会自动管理发送器应用的音量。框架会自动同步提供的界面微件的 Web 接收器音量。如需同步应用提供的滑块,请使用
GCKUIDeviceVolumeController。
实体按钮音量控制
发送器设备上的实体音量按钮可用于更改 Web 接收器上 Cast 会话的音量,方法是使用 physicalVolumeButtonsWillControlDeviceVolume 标志,该标志在 GCKCastOptions 上设置,该标志在 GCKCastContext 上设置。
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 生命周期中的每个阶段确定最佳响应非常重要。应用可以向用户显示错误对话框,也可以决定结束 Cast 会话。
请注意,某些错误(包括
GCKErrorCode
GCKErrorCodeCancelled)是预期行为。
请勿尝试重新连接因 GCKErrorCodeCancelled 而失败的连接。
这样做可能会导致意外行为。
日志记录
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;
类名称可以是字面量名称,也可以是 glob 模式,例如 GCKUI\* 和 GCK\*Session。