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

本開發人員指南說明如何使用 iOS 傳送端 SDK,在 iOS 傳送端應用程式中加入 Google Cast 支援。

行動裝置或筆記型電腦是傳送者,負責控制播放作業;Google Cast 裝置則是接收者,負責在電視上顯示內容。

傳送端架構是指在傳送端執行階段存在的 Cast 類別庫二進位檔和相關聯的資源。傳送端應用程式Cast 應用程式是指在傳送端執行的應用程式。網頁接收器應用程式是指在網頁接收器上執行的 HTML 應用程式。

傳送端架構採用非同步回呼設計,可將事件通知傳送端應用程式,並在 Cast 應用程式生命週期的各種狀態之間轉換。

應用程式流程

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

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

如要排解傳送者問題,請啟用記錄

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

從主執行緒呼叫方法

初始化 Cast 內容

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

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

-[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 UX 小工具

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

  • 簡介疊加層GCKCastContext 類別有一個方法 presentCastInstructionsViewControllerOnceWithCastButton,可用於在首次提供網頁接收器時,醒目顯示 Cast 按鈕。傳送端應用程式可以自訂文字、標題文字和「關閉」按鈕的位置。

  • Cast 按鈕: 自 Cast iOS 傳送端 SDK 4.6.0 版起,傳送端裝置連上 Wi-Fi 時,系統一律會顯示 Cast 按鈕。使用者首次輕觸「投放」按鈕 (應用程式啟動後) 時,系統會顯示權限對話方塊,讓使用者授予應用程式區域網路存取權,以便存取網路上的裝置。隨後,使用者輕觸「投放」按鈕時,系統會顯示投放對話方塊,列出已探索到的裝置。裝置連線時,使用者輕觸 Cast 按鈕會顯示目前的媒體中繼資料 (例如標題、錄音室名稱和縮圖),或允許使用者與 Cast 裝置中斷連線。如果沒有可用裝置,使用者輕觸 Cast 按鈕時,系統會顯示畫面,說明找不到裝置的原因和疑難排解方式。

  • 迷你遙控器: 使用者投放內容時,如果離開目前內容頁面或展開的遙控器,前往傳送端應用程式中的其他畫面,畫面底部就會顯示迷你遙控器,方便使用者查看目前投放的媒體中繼資料,以及控制播放作業。

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

新增 Cast 按鈕

這個架構會提供 Cast 按鈕元件做為 UIButton 子類別。只要將其包裝在 UIBarButtonItem 中,即可新增至應用程式的標題列。典型的 UIViewController 子類別可安裝 Cast 按鈕,如下所示:

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 也可以直接加入 Storyboard。

設定裝置探索

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

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

工作階段管理功能的運作方式

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

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

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

如果使用 Cast 對話方塊,系統會自動建立及終止工作階段,以回應使用者手勢。否則,應用程式可以透過 GCKSessionManager 的方法明確啟動及結束工作階段。

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

變更串流裝置

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

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

詳情請參閱「在 Web Receiver 上轉移串流」。

自動重新連線

Cast 架構會新增重新連線邏輯,自動處理許多細微的特殊情況,例如:

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

媒體控制功能的運作方式

如果使用支援媒體命名空間的 Web Receiver 應用程式建立 Cast 工作階段,架構會自動建立 GCKRemoteMediaClient 的執行個體,並可做為 GCKCastSession 執行個體的 remoteMediaClient 屬性存取。

GCKRemoteMediaClient 上所有向 Web Receiver 發出要求的方法,都會傳回 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 會自動在迷你控制器中顯示「播放/停止」按鈕,取代「播放/暫停」按鈕。

如要瞭解如何讓發送端應用程式設定 Cast 小工具的外觀,請參閱「自訂 iOS 發送端使用者介面」。

將迷你遙控器新增至傳送端應用程式的方式有兩種:

  • 將現有的檢視畫面控制器包裝在 Cast 架構的檢視畫面控制器中,讓 Cast 架構管理迷你控制器的版面配置。
  • 將迷你控制器小工具新增至現有的檢視畫面控制器,並在情節腳本中提供子檢視畫面,即可自行管理小工具的版面配置。

使用 GCKUICastContainerViewController 包裝

第一種方式是使用 GCKUICastContainerViewController ,這個方法會包裝另一個檢視區塊控制器,並在底部新增 GCKUIMiniMediaControlsViewController 。這種做法的限制在於您無法自訂動畫,也無法設定容器檢視畫面控制器的行為。

第一種方式通常是在應用程式委派的 -[application:didFinishLaunchingWithOptions:] 方法中完成:

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

  // Wrap main view in the GCKUICastContainerViewController and display the mini controller.
  let appStoryboard = UIStoryboard(name: "Main", bundle: nil)
  let navigationController = appStoryboard.instantiateViewController(withIdentifier: "MainNavigation")
  let castContainerVC =
          GCKCastContext.sharedInstance().createCastContainerController(for: navigationController)
  castContainerVC.miniMediaControlsItemEnabled = true
  window = UIWindow(frame: UIScreen.main.bounds)
  window!.rootViewController = castContainerVC
  window!.makeKeyAndVisible()

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

  // Wrap main view in the GCKUICastContainerViewController and display the mini controller.
  UIStoryboard *appStoryboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
  UINavigationController *navigationController =
          [appStoryboard instantiateViewControllerWithIdentifier:@"MainNavigation"];
  GCKUICastContainerViewController *castContainerVC =
          [[GCKCastContext sharedInstance] createCastContainerControllerForViewController:navigationController];
  castContainerVC.miniMediaControlsItemEnabled = YES;
  self.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
  self.window.rootViewController = castContainerVC;
  [self.window makeKeyAndVisible];
  ...

}
Swift
var castControlBarsEnabled: Bool {
  set(enabled) {
    if let castContainerVC = self.window?.rootViewController as? GCKUICastContainerViewController {
      castContainerVC.miniMediaControlsItemEnabled = enabled
    } else {
      print("GCKUICastContainerViewController is not correctly configured")
    }
  }
  get {
    if let castContainerVC = self.window?.rootViewController as? GCKUICastContainerViewController {
      return castContainerVC.miniMediaControlsItemEnabled
    } else {
      print("GCKUICastContainerViewController is not correctly configured")
      return false
    }
  }
}
Objective-C

AppDelegate.h

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (nonatomic, strong) UIWindow *window;
@property (nonatomic, assign) BOOL castControlBarsEnabled;

@end

AppDelegate.m

@implementation AppDelegate

...

- (void)setCastControlBarsEnabled:(BOOL)notificationsEnabled {
  GCKUICastContainerViewController *castContainerVC;
  castContainerVC =
      (GCKUICastContainerViewController *)self.window.rootViewController;
  castContainerVC.miniMediaControlsItemEnabled = notificationsEnabled;
}

- (BOOL)castControlBarsEnabled {
  GCKUICastContainerViewController *castContainerVC;
  castContainerVC =
      (GCKUICastContainerViewController *)self.window.rootViewController;
  return castContainerVC.miniMediaControlsItemEnabled;
}

...

@end

嵌入現有的檢視控制器

第二種方式是使用 createMiniMediaControlsViewController 建立 GCKUIMiniMediaControlsViewController 執行個體,然後將其新增至容器檢視畫面控制器做為子檢視畫面,直接將迷你控制器新增至現有的檢視畫面控制器。

在應用程式委派中設定檢視區塊控制器:

Swift
func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
  ...

  GCKCastContext.sharedInstance().useDefaultExpandedMediaControls = true
  window?.clipsToBounds = true

  let rootContainerVC = (window?.rootViewController as? RootContainerViewController)
  rootContainerVC?.miniMediaControlsViewEnabled = true

  ...

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

  [GCKCastContext sharedInstance].useDefaultExpandedMediaControls = YES;

  self.window.clipsToBounds = YES;

  RootContainerViewController *rootContainerVC;
  rootContainerVC =
      (RootContainerViewController *)self.window.rootViewController;
  rootContainerVC.miniMediaControlsViewEnabled = YES;

  ...

  return YES;
}

在根檢視區塊控制器中,建立 GCKUIMiniMediaControlsViewController 例項,並將其新增至容器檢視區塊控制器做為子檢視區塊:

Swift
let kCastControlBarsAnimationDuration: TimeInterval = 0.20

@objc(RootContainerViewController)
class RootContainerViewController: UIViewController, GCKUIMiniMediaControlsViewControllerDelegate {
  @IBOutlet weak private var _miniMediaControlsContainerView: UIView!
  @IBOutlet weak private var _miniMediaControlsHeightConstraint: NSLayoutConstraint!
  private var miniMediaControlsViewController: GCKUIMiniMediaControlsViewController!
  var miniMediaControlsViewEnabled = false {
    didSet {
      if self.isViewLoaded {
        self.updateControlBarsVisibility()
      }
    }
  }

  var overriddenNavigationController: UINavigationController?

  override var navigationController: UINavigationController? {

    get {
      return overriddenNavigationController
    }

    set {
      overriddenNavigationController = newValue
    }
  }
  var miniMediaControlsItemEnabled = false

  override func viewDidLoad() {
    super.viewDidLoad()
    let castContext = GCKCastContext.sharedInstance()
    self.miniMediaControlsViewController = castContext.createMiniMediaControlsViewController()
    self.miniMediaControlsViewController.delegate = self
    self.updateControlBarsVisibility()
    self.installViewController(self.miniMediaControlsViewController,
                               inContainerView: self._miniMediaControlsContainerView)
  }

  func updateControlBarsVisibility() {
    if self.miniMediaControlsViewEnabled && self.miniMediaControlsViewController.active {
      self._miniMediaControlsHeightConstraint.constant = self.miniMediaControlsViewController.minHeight
      self.view.bringSubview(toFront: self._miniMediaControlsContainerView)
    } else {
      self._miniMediaControlsHeightConstraint.constant = 0
    }
    UIView.animate(withDuration: kCastControlBarsAnimationDuration, animations: {() -> Void in
      self.view.layoutIfNeeded()
    })
    self.view.setNeedsLayout()
  }

  func installViewController(_ viewController: UIViewController?, inContainerView containerView: UIView) {
    if let viewController = viewController {
      self.addChildViewController(viewController)
      viewController.view.frame = containerView.bounds
      containerView.addSubview(viewController.view)
      viewController.didMove(toParentViewController: self)
    }
  }

  func uninstallViewController(_ viewController: UIViewController) {
    viewController.willMove(toParentViewController: nil)
    viewController.view.removeFromSuperview()
    viewController.removeFromParentViewController()
  }

  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "NavigationVCEmbedSegue" {
      self.navigationController = (segue.destination as? UINavigationController)
    }
  }

...
Objective-C

RootContainerViewController.h

static const NSTimeInterval kCastControlBarsAnimationDuration = 0.20;

@interface RootContainerViewController () <GCKUIMiniMediaControlsViewControllerDelegate> {
  __weak IBOutlet UIView *_miniMediaControlsContainerView;
  __weak IBOutlet NSLayoutConstraint *_miniMediaControlsHeightConstraint;
  GCKUIMiniMediaControlsViewController *_miniMediaControlsViewController;
}

@property(nonatomic, weak, readwrite) UINavigationController *navigationController;

@property(nonatomic, assign, readwrite) BOOL miniMediaControlsViewEnabled;
@property(nonatomic, assign, readwrite) BOOL miniMediaControlsItemEnabled;

@end

RootContainerViewController.m

@implementation RootContainerViewController

- (void)viewDidLoad {
  [super viewDidLoad];
  GCKCastContext *castContext = [GCKCastContext sharedInstance];
  _miniMediaControlsViewController =
      [castContext createMiniMediaControlsViewController];
  _miniMediaControlsViewController.delegate = self;

  [self updateControlBarsVisibility];
  [self installViewController:_miniMediaControlsViewController
              inContainerView:_miniMediaControlsContainerView];
}

- (void)setMiniMediaControlsViewEnabled:(BOOL)miniMediaControlsViewEnabled {
  _miniMediaControlsViewEnabled = miniMediaControlsViewEnabled;
  if (self.isViewLoaded) {
    [self updateControlBarsVisibility];
  }
}

- (void)updateControlBarsVisibility {
  if (self.miniMediaControlsViewEnabled &&
      _miniMediaControlsViewController.active) {
    _miniMediaControlsHeightConstraint.constant =
        _miniMediaControlsViewController.minHeight;
    [self.view bringSubviewToFront:_miniMediaControlsContainerView];
  } else {
    _miniMediaControlsHeightConstraint.constant = 0;
  }
  [UIView animateWithDuration:kCastControlBarsAnimationDuration
                   animations:^{
                     [self.view layoutIfNeeded];
                   }];
  [self.view setNeedsLayout];
}

- (void)installViewController:(UIViewController *)viewController
              inContainerView:(UIView *)containerView {
  if (viewController) {
    [self addChildViewController:viewController];
    viewController.view.frame = containerView.bounds;
    [containerView addSubview:viewController.view];
    [viewController didMoveToParentViewController:self];
  }
}

- (void)uninstallViewController:(UIViewController *)viewController {
  [viewController willMoveToParentViewController:nil];
  [viewController.view removeFromSuperview];
  [viewController removeFromParentViewController];
}

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
  if ([segue.identifier isEqualToString:@"NavigationVCEmbedSegue"]) {
    self.navigationController =
        (UINavigationController *)segue.destinationViewController;
  }
}

...

@end

GCKUIMiniMediaControlsViewControllerDelegate 會告知主機檢視畫面控制器何時應顯示迷你控制器:

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

新增展開的控制器

Google Cast 設計檢查清單規定,傳送器應用程式必須為投放的媒體提供擴充控制器。展開的控制器是迷你控制器的全螢幕版本。

展開的遙控器會以全螢幕顯示,方便你全面控制遠端媒體播放。這個檢視畫面應允許投放應用程式管理投放工作階段的所有可管理層面,但 Web Receiver 音量控制和工作階段生命週期 (連線/停止投放) 除外。並提供媒體工作階段的所有狀態資訊 (藝術家、名稱、副標題等)。

這個檢視區塊的功能是由 GCKUIExpandedMediaControlsViewController 類別實作。

首先,您必須在 Cast 內容中啟用預設的展開控制器。修改應用程式委派,啟用預設的展開控制器:

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

  GCKCastContext.sharedInstance().useDefaultExpandedMediaControls = true

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

  [GCKCastContext sharedInstance].useDefaultExpandedMediaControls = YES;

  ..
}

在檢視畫面控制器中加入下列程式碼,即可在使用者開始投放影片時載入擴充控制器:

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

  ...

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

  ...

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

使用者輕觸迷你控制器時,系統也會自動啟動展開的控制器。

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

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

音量控制項

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

使用實體按鈕控制音量

傳送端裝置上的實體音量按鈕可用於變更 Web Receiver 上的 Cast 工作階段音量,方法是在 GCKCastOptions 上設定 physicalVolumeButtonsWillControlDeviceVolume 旗標,而該旗標是在 GCKCastContext 上設定。

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

處理錯誤

傳送端應用程式務必處理所有錯誤回呼,並決定 Cast 生命週期各階段的最佳回應。應用程式可以向使用者顯示錯誤對話方塊,也可以決定結束 Cast 工作階段。

記錄

GCKLogger 是架構用於記錄的單例模式。使用 GCKLoggerDelegate 自訂記錄訊息的處理方式。

使用 GCKLogger 時,SDK 會以偵錯訊息、錯誤和警告的形式產生記錄輸出內容。這些記錄訊息有助於偵錯,可協助排解及找出問題。根據預設,系統會抑制記錄輸出內容,但只要指派 GCKLoggerDelegate,傳送者應用程式就能從 SDK 接收這些訊息,並記錄到系統控制台。

Swift
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, GCKLoggerDelegate {
  let kReceiverAppID = kGCKDefaultMediaReceiverApplicationID
  let kDebugLoggingEnabled = true

  var window: UIWindow?

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

    // Enable logger.
    GCKLogger.sharedInstance().delegate = self

    ...
  }

  // MARK: - GCKLoggerDelegate

  func logMessage(_ message: String,
                  at level: GCKLoggerLevel,
                  fromFunction function: String,
                  location: String) {
    if (kDebugLoggingEnabled) {
      print(function + " - " + message)
    }
  }
}
Objective-C

AppDelegate.h

@interface AppDelegate () <GCKLoggerDelegate>
@end

AppDelegate.m

@implementation AppDelegate

static NSString *const kReceiverAppID = @"AABBCCDD";
static const BOOL kDebugLoggingEnabled = YES;

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

  // Enable logger.
  [GCKLogger sharedInstance].delegate = self;

  ...

  return YES;
}

...

#pragma mark - GCKLoggerDelegate

- (void)logMessage:(NSString *)message
           atLevel:(GCKLoggerLevel)level
      fromFunction:(NSString *)function
          location:(NSString *)location {
  if (kDebugLoggingEnabled) {
    NSLog(@"%@ - %@, %@", function, message, location);
  }
}

@end

如要同時啟用偵錯和詳細訊息,請在設定委派項目後,將這行程式碼新增至程式碼 (如先前所示):

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

您也可以篩選 GCKLogger 產生的記錄訊息。為每個類別設定最低記錄層級,例如:

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

類別名稱可以是常值名稱或 glob 模式,例如 GCKUI\*GCK\*Session