本开发者指南将介绍如何使用 iOS 发送器 SDK 为 iOS 发送器应用添加 Google Cast 支持。
移动设备或笔记本电脑是控制播放的发送方,Google Cast 设备是在电视上显示内容的接收方。
发送器框架是指 Cast 类库二进制文件以及运行时在发送器上存在的相关资源。发送器应用或 Cast 应用是指在发送器上运行的应用。网络接收器应用是指在网络接收器上运行的 HTML 应用。
发送器框架采用异步回调设计,向发送器应用通知事件,并在 Cast 应用生命周期的各种状态之间转换。
应用流程
以下步骤介绍了发送者 iOS 应用的典型执行流程:
- Cast 框架会根据
GCKCastOptions
中提供的属性启动GCKDiscoveryManager
,以开始扫描设备。 - 当用户点击“投射”按钮时,框架会显示 Cast 对话框,其中会列出发现的 Cast 设备。
- 当用户选择 Cast 设备时,框架会尝试在 Cast 设备上启动 Web 接收器应用。
- 框架在发送器应用中调用回调,以确认 Web 接收器应用已启动。
- 该框架会在发送器应用和 Web 接收器应用之间创建一个通信通道。
- 该框架使用信道在 Web 接收器上加载和控制媒体播放。
- 框架会在发送器和网络接收器之间同步媒体播放状态:当用户执行发送器界面操作时,框架会将这些媒体控制请求传递给网络接收器;当网络接收器发送媒体状态更新时,框架会更新发送器界面的状态。
- 当用户点击 Cast 按钮断开与 Cast 设备的连接时,框架会断开发送器应用与网络接收器的连接。
要对发件人进行问题排查,您需要启用日志记录。
如需查看 Google Cast iOS 框架中所有类、方法和事件的完整列表,请参阅 Google Cast iOS API 参考文档。下面几部分介绍了将 Cast 集成到 iOS 应用中的步骤。
从主线程调用方法
初始化 Cast 上下文
Cast 框架有一个全局单例对象 GCKCastContext
,用于协调框架的所有 activity。此对象必须在应用生命周期的早期进行初始化(通常在应用委托的 -[application:didFinishLaunchingWithOptions:]
方法中进行),以便在发送方应用重启时可以正确触发自动会话恢复。
初始化 GCKCastContext
时必须提供 GCKCastOptions
对象。此类包含会影响框架行为的选项。其中最重要的是网络接收器应用 ID,该 ID 用于过滤发现结果,以及在 Cast 会话启动时启动网络接收器应用。
此外,如需设置日志记录代理以从框架接收日志记录消息,也可使用 -[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 设计核对清单的微件:
入门叠加层:
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 会话的概念,其建立过程结合了执行以下操作的步骤:连接到设备、启动(或加入)网络接收器应用、连接到该应用,以及初始化媒体控制通道。如需详细了解 Cast 会话和网络接收器生命周期,请参阅网络接收器应用生命周期指南。
会话由 GCKSessionManager
类管理,该类是 GCKCastContext
的一个属性。各个会话由 GCKSession
类的子类表示:例如,GCKCastSession
表示具有 Cast 设备的会话。您可以通过 GCKSessionManager
的 currentCastSession
属性访问当前处于活动状态的 Cast 会话(如果有)。
GCKSessionManagerListener
接口可用于监控会话事件,例如会话创建、暂停、恢复和终止。当发送方应用进入后台时,框架会自动暂停会话,并在应用返回前台时尝试恢复会话(或者在会话处于活动状态时,应用异常/突然终止后重新启动)。
如果使用 Cast 对话框,则系统会自动创建和关闭会话,以响应用户手势。否则,应用可以通过 GCKSessionManager
上的方法明确开始和结束会话。
如果应用需要执行特殊处理来响应会话生命周期事件,它可以向 GCKSessionManager
注册一个或多个 GCKSessionManagerListener
实例。GCKSessionManagerListener
是一种协议,用于定义会话开始、会话结束等事件的回调。
流式传输
保留会话状态是数据流传输的基础,在这种情况下,用户可以使用语音指令、Google Home 应用或智能显示屏在设备之间移动现有的音频和视频串流。媒体在一台设备(来源)上停止播放,然后在另一台设备(目标)上继续播放。任何具有最新固件的 Cast 设备都可以作为流式传输传输中的来源或目的地。
如需在数据流传输期间获取新的目标设备,请在 [sessionManager:didResumeCastSession:]
回调期间使用 GCKCastSession#device
属性。
如需了解详情,请参阅在 Web 接收器上传输数据流。
自动重新连接
Cast 框架添加了重新连接逻辑,以便在许多细微的极端情况下自动处理重新连接,例如:
- 在 WLAN 暂时丢失时恢复
- 从设备休眠状态恢复
- 将应用置于后台后恢复
- 在应用崩溃时恢复
媒体控件的运作方式
如果使用支持媒体命名空间的 Web 接收器应用建立 Cast 会话,框架将自动创建 GCKRemoteMediaClient
的实例;该实例可作为 GCKCastSession
实例的 remoteMediaClient
属性进行访问。
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 视频格式
如需确定媒体是什么视频格式,请使用 GCKMediaStatus
的 videoInfo
属性获取 GCKVideoInfo
的当前实例。此实例包含 HDR 电视格式的类型以及高度和宽度(以像素为单位)。4K 格式的变体在 hdrType
属性中由枚举值 GCKVideoInfoHDRType
表示。
添加迷你控制器
根据 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 设计核对清单要求发送设备应用为要投放的媒体提供展开后的控制器。展开的控制器是迷你控制器的全屏版本。
展开的控制器是一个全屏视图,可让用户完全控制远程媒体播放。此视图应允许投射应用管理投射会话的每个可管理方面,但网络接收器音量控制和会话生命周期(连接/停止投射)除外。它还提供有关媒体会话的所有状态信息(海报图片、标题、副标题等)。
此视图的功能由 GCKUIExpandedMediaControlsViewController
类实现。
首先,您需要在 Cast 上下文中启用默认的展开的控制器。修改应用委托以启用默认的展开控制器:
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 框架会自动管理发送器应用的音量。该框架会自动与所提供的界面 widget 的 Web 接收器音量同步。如需同步应用提供的滑块,请使用 GCKUIDeviceVolumeController
。
实体按钮音量控件
发送设备上的实体音量按钮可用于通过 GCKCastOptions
(在 GCKCastContext
中设置)上的 physicalVolumeButtonsWillControlDeviceVolume
标志来更改网络接收器上的 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 生命周期的每个阶段确定最佳响应非常重要。应用可以向用户显示错误对话框,也可以决定结束 Cast 会话。
日志记录
GCKLogger
是框架用于日志记录的单例。您可以使用 GCKLoggerDelegate
自定义处理日志消息的方式。
SDK 使用 GCKLogger
以调试消息、错误和警告的形式生成日志记录输出。这些日志消息有助于进行调试,并且有助于排查和识别问题。默认情况下,系统会阻止日志输出,但通过分配 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
。