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

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

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

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

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

應用程式流程

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

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

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

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

主執行緒的呼叫方法

初始化 Cast 環境

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

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

-[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 時,一律顯示「投放」按鈕。如果使用者在初次啟動應用程式後首次輕觸「投放」按鈕,系統會顯示權限對話方塊,方便使用者將應用程式區域網路存取權授予網路上的裝置。之後使用者輕觸投放按鈕時,系統會顯示投放對話方塊,列出發現的裝置。如果使用者在裝置連線時輕觸「投放」按鈕,就會顯示目前的媒體中繼資料 (例如標題、影音工作室名稱和縮圖),或允許使用者中斷與投放裝置的連線。如果使用者在沒有可用裝置的情況下輕觸投放按鈕,畫面上將顯示一個畫面,讓使用者瞭解找不到裝置的原因以及疑難排解的方法。

  • 小控制器:當使用者投放內容,並離開目前內容頁面或展開控制器前往傳送端應用程式的其他畫面時,螢幕底部會顯示迷你控制器,讓使用者查看目前投放的媒體中繼資料並控製播放內容。

  • 展開控制器:當使用者投放內容時,只要按一下媒體通知或迷你控制器,即可啟動展開的控制器,顯示目前正在播放的媒體中繼資料,並提供多個可控制媒體播放的按鈕。

新增「投放」按鈕

這個架構提供投放按鈕元件做為 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 介紹投放工作階段的概念,其建立方式結合了連線至裝置、啟動 (或加入) 網路接收器應用程式、連線至該應用程式,以及初始化媒體控制管道的步驟。如要進一步瞭解投放工作階段和網路接收器的生命週期,請參閱網路接收器應用程式生命週期指南

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

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

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

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

變更串流裝置

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

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

詳情請參閱在網路接收器上進行串流傳輸

自動重新連線

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

  • 在 Wi-Fi 連線暫時中斷的情況下復原
  • 從裝置睡眠狀態復原
  • 從背景執行復原程序
  • 在應用程式當機時復原

媒體控制選項的運作方式

如果您使用支援媒體命名空間的網頁接收器應用程式建立投放工作階段,架構會自動建立 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 電視格式的類型,以及高度和寬度 (以像素為單位)。4K 格式的變體在 hdrType 屬性中是以列舉值 GCKVideoInfoHDRType 表示。

新增迷你控制器

根據「Cast 設計檢查清單」規定,傳送方應用程式應提供一個永久控制項 (稱為小控制器),當使用者離開目前內容頁面時,這個控制項應該就會顯示。迷你控制器可為目前的投放工作階段提供即時存取權和可見提醒。

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

實體按鈕音量控制

傳送端裝置上的實體音量按鈕,可透過 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 生命週期中每個階段的最佳回應,這點十分重要。應用程式可以向使用者顯示錯誤對話方塊,或是決定結束投放工作階段。

Logging

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