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

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

行動裝置或筆電是控制播放的「傳送端」,Google Cast 裝置則是將內容顯示在電視上的「接收端」。

「傳送端架構」是指 Cast 類別程式庫二進位檔,以及傳送端在執行階段提供的相關聯資源。「傳送端應用程式」或「Cast 應用程式」是指在傳送端上執行的應用程式。Web 接收端應用程式是指在 Web 接收端上執行的 HTML 應用程式。

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

應用程式流程

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

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

如要排解傳送端問題,請啟用記錄功能。

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

從主執行緒呼叫方法

初始化 Cast 情境

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

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

-[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 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 對話方塊元件,可用於選取及控制裝置。裝置清單會依裝置易記名稱的字母順序排列。

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

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

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

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

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

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

變更串流裝置

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

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

詳情請參閱「在 Web 接收端上變更串流裝置」。

自動重新連線

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

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

媒體遙控器的運作方式

如果使用支援媒體命名空間的 Web 接收器應用程式建立投放工作階段,架構會自動建立 GCKRemoteMediaClient 的例項,並可做為 GCKCastSession 例項的 remoteMediaClient 屬性存取。

GCKRemoteMediaClient 上向 Web 接收端發出要求的所有方法,都會傳回 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 影片格式

如要判斷媒體的影片格式,請使用 GCKMediaStatus 的 videoInfo 屬性,取得 GCKVideoInfo 的目前例項。這個例項包含 HDR 電視格式類型,以及高度和寬度 (以像素為單位)。4K 格式的變化版本會以列舉值 GCKVideoInfoHDRType 標示在 hdrType 屬性中。

新增迷你控制器

根據 Cast 設計檢查清單,傳送端應用程式應提供名為迷你控制器的永久控制項,當使用者離開目前的內容頁面時,該控制項應會顯示。迷你控制器可讓使用者立即存取目前的 Cast 工作階段,並提供醒目的提醒。

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

如果傳送端應用程式正在播放影片或音訊直播,SDK 會自動在小型控制器中顯示播放/停止按鈕,取代播放/暫停按鈕。

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

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

  • 使用 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 接收端音量控制和工作階段生命週期 (連線/停止投放) 除外。此外,也會提供媒體工作階段的所有狀態資訊 (圖片、標題、字幕等)。

這個檢視畫面的功能是由 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 小工具的 Web 接收端音量同步。如要同步處理應用程式提供的滑桿,請使用 GCKUIDeviceVolumeController。

實體按鈕音量控制

在 GCKCastContext 上設定 GCKCastOptions 的 physicalVolumeButtonsWillControlDeviceVolume 旗標後,即可使用傳送端裝置的實體音量鍵,變更 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 生命週期各階段的最佳回應。應用程式可以向使用者顯示錯誤對話方塊,或決定結束投放工作階段。

請注意,某些錯誤 (包括 GCKErrorCode GCKErrorCodeCancelled) 是預期行為。

請勿嘗試重新連線,因為連線失敗,並顯示 GCKErrorCodeCancelled。否則可能會發生非預期的行為。

記錄

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。