將投放功能整合至 iOS 應用程式

本開發人員指南說明如何使用 iOS Sender SDK,在 iOS 傳送器應用程式中新增 Google Cast 支援功能。

行動裝置或筆記型電腦是控制播放的傳送端,而 Google Cast 裝置則是顯示電視內容的接收端

傳送器架構是指在傳送器上執行階段出現的 Cast 類別程式庫二進位檔和相關資源。「傳送端應用程式」或「投放應用程式」是指在傳送端上執行的應用程式。Web Receiver 應用程式是指在 Web Receiver 上執行的 HTML 應用程式。

傳送端架構會使用非同步回呼設計,向傳送端應用程式通知事件,並在 Cast 應用程式生命週期的不同狀態之間進行轉換。

應用程式流程

下列步驟說明傳送端 iOS 應用程式的一般執行流程:

  • Cast 架構會根據 GCKCastOptions 中提供的屬性啟動 GCKDiscoveryManager,開始掃描裝置。
  • 使用者點選「投放」按鈕時,架構會顯示「投放」對話方塊,列出所偵測到的投放裝置。
  • 使用者選取投放裝置時,架構會嘗試在投放裝置上啟動 Web Receiver 應用程式。
  • 此架構會在傳送端應用程式中叫用回呼,確認 Web Receiver 應用程式已啟動。
  • 這個架構會在傳送端和 Web Receiver 應用程式之間建立通訊管道。
  • 這個架構會使用通訊管道,在 Web Receiver 上載入及控制媒體播放。
  • 此架構會在傳送端和 Web Receiver 之間同步媒體播放狀態:當使用者執行傳送端 UI 動作時,架構會將這些媒體控制項要求傳遞至 Web Receiver,而當 Web Receiver 傳送媒體狀態更新時,架構會更新傳送端 UI 的狀態。
  • 當使用者按下投放按鈕以斷開與投放裝置的連線時,架構會將傳送端應用程式與 Web 接收器斷開連線。

如要排解寄件者的問題,請啟用記錄功能。

如需 Google Cast iOS 架構中所有類別、方法和事件的完整清單,請參閱 Google Cast iOS API 參考資料。以下各節將說明將 Cast 整合至 iOS 應用程式的步驟。

從主執行緒呼叫方法

初始化 Cast 背景

Cast 架構具有全域單例模式物件 GCKCastContext,可協調所有架構活動。這個物件必須在應用程式生命週期初期初始化,通常是在應用程式委派程式的 -[application:didFinishLaunchingWithOptions:] 方法中,才能在傳送端應用程式重新啟動時觸發自動工作階段恢復功能。

初始化 GCKCastContext 時,必須提供 GCKCastOptions 物件。這個類別包含會影響架構行為的選項。其中最重要的是 Web Receiver 應用程式 ID,用於篩選探索結果,並在 Cast 工作階段啟動時啟動 Web Receiver 應用程式。

-[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 使用者體驗小工具

Cast iOS SDK 提供符合 Cast 設計檢查清單的這些小工具:

  • 簡介疊加畫面GCKCastContext 類別有一個方法 presentCastInstructionsViewControllerOnceWithCastButton,可用於在第一次提供網路接收器時,將焦點放在投放按鈕上。傳送端應用程式可以自訂文字、標題文字的位置和「關閉」按鈕。

  • 投放按鈕:自 Cast iOS 傳送端 SDK 4.6.0 起,只要傳送端裝置已連上 Wi-Fi,就會一律顯示投放按鈕。使用者在初次啟動應用程式後,第一次輕觸「投放」按鈕時,系統會顯示權限對話方塊,讓使用者授予應用程式在網路上存取裝置的權限。接著,當使用者輕觸投放按鈕時,系統會顯示投放對話方塊,列出所偵測到的裝置。當使用者在裝置連線時輕觸投放按鈕,系統會顯示目前的媒體中繼資料 (例如標題、錄影工作室名稱和縮圖),或讓使用者中斷投放裝置的連線。當使用者在沒有可用裝置的情況下輕觸投放按鈕時,系統會顯示畫面,向使用者說明找不到裝置的原因,以及如何進行疑難排解。

  • 迷你控制器:當使用者投放內容,並已從目前的內容頁面或展開的控制器前往傳送端應用程式中的其他畫面時,系統會在畫面底部顯示迷你控制器,方便使用者查看目前投放的媒體中繼資料,並控制播放作業。

  • 展開式控制器:當使用者投放內容時,如果點選媒體通知或迷你控制器,系統就會啟動展開式控制器,顯示目前播放的媒體中繼資料,並提供多個按鈕來控制媒體播放。

新增投放按鈕

這個架構會提供 Cast 按鈕元件做為 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];

根據預設,輕觸按鈕會開啟架構提供的 Cast 對話方塊。

GCKUICastButton 也可以直接新增至情節板。

設定裝置探索

在架構中,裝置探索會自動發生。除非您實作自訂 UI,否則無須明確啟動或停止探索程序。

架構中的探索功能由 GCKDiscoveryManager 類別管理,這是 GCKCastContext 的屬性。此架構會提供預設的 Cast 對話方塊元件,用於裝置選取和控制。裝置清單會依裝置友善名稱排序。

工作階段管理機制的運作方式

Cast SDK 會介紹 Cast 工作階段的概念,建立工作階段時會結合連線至裝置、啟動 (或加入) Web Receiver 應用程式、連線至該應用程式,以及初始化媒體控制管道的步驟。如要進一步瞭解投放工作階段和 Web Receiver 生命週期,請參閱「應用程式生命週期指南」。

工作階段由 GCKSessionManager 類別管理,這是 GCKCastContext 的屬性。個別工作階段由 GCKSession 類別的子類別表示:例如 GCKCastSession 代表 Cast 裝置的工作階段。您可以存取目前有效的 Cast 工作階段 (如有),做為 GCKSessionManagercurrentCastSession 屬性。

GCKSessionManagerListener 介面可用於監控工作階段事件,例如建立、暫停、恢復和終止工作階段。當傳送端應用程式進入背景時,架構會自動暫停工作階段,並在應用程式返回前景時嘗試恢復工作階段 (或在工作階段處於活動狀態時,應用程式異常/突然終止後重新啟動)。

如果使用投放對話方塊,系統會根據使用者的手勢自動建立及關閉工作階段。否則,應用程式可以透過 GCKSessionManager 上的明確方法啟動及結束工作階段。

如果應用程式需要針對工作階段生命週期事件執行特殊處理,可以使用 GCKSessionManager 註冊一或多個 GCKSessionManagerListener 例項。GCKSessionManagerListener 是一種通訊協定,可定義工作階段開始、工作階段結束等事件的回呼。

串流傳輸

保留工作階段狀態是串流轉移的基礎,使用者可以透過語音指令、Google Home 應用程式或智慧螢幕,在裝置之間移動現有的音訊和視訊串流。媒體在一個裝置 (來源) 上停止播放,並在另一個裝置 (目的地) 上繼續播放。任何搭載最新韌體的 Cast 裝置,都可以在串流轉移中擔任來源或目的地。

如要在串流轉移期間取得新的目的地裝置,請在 [sessionManager:didResumeCastSession:] 回呼期間使用 GCKCastSession#device 屬性。

詳情請參閱「在網路接收器上進行串流轉移」。

自動重新連線

Cast 架構會新增重新連線邏輯,在許多微妙的極端情況下自動處理重新連線,例如:

  • 復原暫時斷線的 Wi-Fi 連線
  • 從裝置休眠狀態復原
  • 從背景應用程式復原
  • 在應用程式當機時復原

媒體控制功能的運作方式

如果透過支援媒體命名空間的 Web Receiver 應用程式建立 Cast 工作階段,架構會自動建立 GCKRemoteMediaClient 的例項;您可以透過 GCKCastSession 例項的 remoteMediaClient 屬性存取該例項。

向 Web Receiver 發出要求的 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 電視格式的類型,以及高度和寬度 (以像素為單位)。4K 格式的變化版本會在 hdrType 屬性中以列舉值 GCKVideoInfoHDRType 表示。

新增迷你控制器

根據投放裝置設計檢查清單,發送端應用程式應提供持續控制項,也就是在使用者離開目前內容頁面時應顯示的迷你控制器。迷你控制器可讓你立即存取目前的 Cast 工作階段,並顯示提醒。

Cast 架構會提供控制列 GCKUIMiniMediaControlsViewController,可新增至您要顯示迷你控制器的場景。

當傳送端應用程式播放影片或音訊直播時,SDK 會自動顯示播放/停止按鈕,取代迷你控制器中的播放/暫停按鈕。

如要瞭解發送端應用程式如何設定投放小工具的外觀,請參閱「自訂 iOS 發送端 UI」。

將 mini 控制器新增至傳送端應用程式的方式有兩種:

  • 讓 Cast 架構透過包裝現有檢視控制器的專屬檢視控制器,管理迷你控制器的版面配置。
  • 您可以自行管理迷你控制器小工具的版面配置,方法是在情節板中提供子檢視畫面,將迷你控制器小工具新增至現有的 View Controller。

使用 GCKUICastContainerViewController 包裝

第一個方法是使用 GCKUICastContainerViewController,這個方法會包裝另一個 View Controller,並在底部新增 GCKUIMiniMediaControlsViewController。此方法的限制在於您無法自訂動畫,也無法設定容器 View Controller 的行為。

第一種方式通常是在應用程式委派作業的 -[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

嵌入現有 View Controller

第二種方法是使用 createMiniMediaControlsViewController 建立 GCKUIMiniMediaControlsViewController 例項,然後將其加入容器檢視控制器做為子檢視區塊,藉此將迷你控制器直接新增至現有的檢視控制器。

在應用程式委派程式中設定 View Controller:

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;

  ..
}

將下列程式碼新增至 View Controller,即可在使用者開始投放影片時載入展開的控制器:

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 會自動在展開的控制器中,將播放/暫停按鈕替換為播放/停止按鈕。

如要瞭解發送端應用程式如何設定投放小工具的外觀,請參閱「將自訂樣式套用至 iOS 應用程式」。

音量控制項

Cast 架構會自動管理傳送端應用程式的音量。架構會自動與提供的 UI 小工具的 Web Receiver 音量同步。如要同步處理應用程式提供的滑桿,請使用 GCKUIDeviceVolumeController

實體按鈕音量控制

您可以使用傳送端裝置上的實體音量按鈕,透過 GCKCastOptions 上的 physicalVolumeButtonsWillControlDeviceVolume 旗標 (已在 GCKCastContext 上設定),變更 Web 接收器上投放工作階段的音量。

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 生命週期各個階段的最佳回應。應用程式可以向使用者顯示錯誤對話方塊,或決定結束投放工作階段。

記錄

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