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

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

行動裝置或筆記型電腦是控製播放的「傳送者」,Google Cast 裝置則是顯示在電視上內容的「接收端」

傳送者架構是指傳送端執行階段中呈現的 Cast 類別程式庫二進位檔和相關資源。傳送者應用程式投放應用程式是指也在傳送端上執行的應用程式。Web Receiver 應用程式是指在網路接收器上執行的 HTML 應用程式。

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

應用程式流程

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

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

如要排解寄件者問題,您必須啟用記錄功能

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

從主執行緒呼叫方法

初始化 Cast 情境

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

初始化 GCKCastContext 時必須提供 GCKCastOptions 物件。這個類別包含會影響架構行為的選項。其中最重要的是網路接收器應用程式 ID,用來篩選探索結果,以及在啟動投放工作階段時啟動 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 時顯示。當使用者初次啟動應用程式後輕觸「投放」按鈕時,系統會顯示權限對話方塊,讓使用者可以授權應用程式存取網路上的裝置。之後當使用者輕觸投放按鈕時,系統會顯示投放對話方塊,列出發現的裝置。如果使用者在裝置連線時輕觸投放按鈕,畫面會顯示目前的媒體中繼資料 (例如標題、錄音室名稱和縮圖圖片),或是允許使用者與投放裝置中斷連線。如果使用者在沒有可用的裝置時輕觸投放按鈕,畫面會顯示畫面,讓使用者瞭解找不到裝置的原因和疑難排解方式。

  • Mini Controller:當使用者投放內容,並離開目前內容頁面,或將控制器展開至傳送端應用程式中的另一個畫面時,畫面底部會顯示迷你控制器,讓使用者可以查看目前的投放媒體中繼資料並控製播放。

  • 展開控制器:當使用者點擊媒體通知或迷你控制器時,展開的控制器會啟動,並顯示正在播放的媒體中繼資料,並提供多個按鈕控制媒體播放。

新增投放按鈕

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

根據預設,輕觸按鈕即可開啟架構提供的「投放」對話方塊。

GCKUICastButton 也可以直接新增至分鏡腳本。

設定裝置探索功能

在這個架構中,系統會自動偵測裝置。除非您實作自訂 UI,否則不需要明確啟動或停止探索程序。

架構中的探索作業是由 GCKDiscoveryManager 類別管理,該類別是 GCKCastContext 的屬性。該架構會提供預設的投放對話方塊元件,方便您選擇及控制裝置。裝置清單會依裝置易記名稱的字母順序排列。

工作階段管理的運作方式

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

工作階段由 GCKSessionManager 類別管理,該類別是 GCKCastContext 的屬性。個別工作階段會以 GCKSession 類別的子類別表示:舉例來說,GCKCastSession 代表有投放裝置的工作階段。您可以存取目前進行中的投放工作階段 (如果有的話),做為 GCKSessionManagercurrentCastSession 屬性。

GCKSessionManagerListener 介面可用於監控工作階段事件,例如建立工作階段、暫停、繼續和終止。架構會在傳送端應用程式進入背景時自動暫停工作階段,並在應用程式返回前景時嘗試繼續工作階段 (或是在工作階段執行期間,應用程式異常/異常終止後重新啟動)。

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

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

變更串流裝置

保留工作階段狀態是串流傳輸的基礎,可讓使用者透過語音指令、Google Home 應用程式或智慧螢幕,在不同裝置上移動現有的音訊和影片串流。媒體會在某部裝置 (來源) 上停止播放,然後在另一部 (目的地) 上繼續播放。任何安裝最新韌體的 Cast 裝置都可以做為串流傳輸的來源或目的地。

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

詳情請參閱「Web Receiver 上的串流傳輸」。

自動重新連線

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

  • 從 Wi-Fi 服務暫時中斷時還原
  • 從裝置睡眠狀態復原
  • 從背景執行應用程式復原
  • 在應用程式當機時復原檔案

媒體控制功能的運作方式

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

向網路接收器發出要求的 GCKRemoteMediaClient 方法會傳回 GCKRequest 物件,可用於追蹤該要求。您可將 GCKRequestDelegate 指派給此物件,以接收作業最終結果的通知。

應用程式的多個部分可能會共用 GCKRemoteMediaClient 的例項,而且該架構的某些內部元件 (例如「投放」對話方塊和迷你媒體控制項) 會共用該執行個體。因此,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 TV 格式的類型,以及高度和寬度 (以像素為單位)。hdrType 屬性的變化版本會由列舉值 GCKVideoInfoHDRType 表示。

新增迷你控制器

根據投放設計檢查清單,傳送者應用程式應提供一個名為「迷你控制器」的永久控制項,也就是使用者離開目前內容頁面時,應顯示的「迷你控制器」。迷你控制器可針對目前的投放工作階段提供立即存取權並顯示提醒。

Cast 架構提供控制列 GCKUIMiniMediaControlsViewController,您可以加到要顯示迷你控制器的場景。

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

請參閱「自訂 iOS 傳送者 UI」,瞭解傳送方應用程式如何設定 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 設計檢查清單需要傳送器應用程式,針對正在投放的媒體提供展開的控制器。展開的控制器是迷你控制器的全螢幕版本。

展開的控制器是全螢幕檢視畫面,可讓您完全控制遠端媒體播放。這個檢視畫面應允許投放應用程式管理投放工作階段的所有可管理層面,但網路接收器音量控制和工作階段生命週期 (連線/停止投放) 除外。還會提供媒體工作階段的所有狀態資訊 (圖片、標題、副標題等)。

這個檢視畫面的功能是由 GCKUIExpandedMediaControlsViewController 類別實作。

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

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 會自動在展開的控制器中顯示播放/暫停按鈕,取代播放/暫停按鈕。

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

音量控制

Cast 架構會自動管理傳送者應用程式的音量。該架構會自動與提供的 UI 小工具網路接收器磁碟區保持同步。如要同步處理應用程式提供的滑桿,請使用 GCKUIDeviceVolumeController

實體按鈕音量控制

只要使用傳送端裝置上的實體音量按鈕,就可以利用 GCKCastOptionsphysicalVolumeButtonsWillControlDeviceVolume 標記 (設定在 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 生命週期各階段的最佳回應方式。應用程式可以向使用者顯示錯誤對話方塊,或決定結束投放工作階段。

記錄

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

SDK 會使用 GCKLogger,以偵錯訊息、錯誤和警告的形式產生記錄輸出內容。這些記錄訊息有助於偵錯,以及用於疑難排解及找出問題。系統預設會隱藏記錄輸出內容,但藉由指派 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