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

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

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

「傳送者架構」是指 Cast 類別程式庫二進位檔,以及傳送者在執行階段提供的相關資源。傳送者應用程式投放應用程式也有在傳送者上執行的應用程式。網路接收器應用程式是指在 Web Receiver 中執行的 HTML 應用程式。

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

應用程式流程

下列步驟說明寄件者 iOS 應用程式的一般高階執行流程:

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

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

如需 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 方法,可用來在首次使用網路接收器時醒目顯示「投放」按鈕。傳送者應用程式可以自訂文字、標題文字的位置和關閉按鈕。

  • 投放按鈕:從投放 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 連線
  • 從裝置睡眠時復原
  • 從背景應用程式復原
  • 在應用程式停止運作時復原

媒體控制選項的運作方式

如果使用支援媒體命名空間的 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 電視格式的類型,以及高度和寬度 (以像素為單位)。在 hdrType 屬性中,系統透過列舉 GCKVideoInfoHDRType 標明 4K 格式的變化版本。

新增迷你控制器

根據 Cast 設計檢查清單,寄件者應用程式應提供稱為迷你控制器的持續性控制項,當使用者離開目前的內容頁面時,您應該會看到這個控制項。迷你控制器提供即時投放功能,以及針對目前投放工作階段顯示的提醒。

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

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

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

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

  • 讓 Wear 架構管理自己的檢視控制器控制器和自己的檢視控制器,藉此管理迷你控制器的版面配置。
  • 只要在分鏡腳本中提供子檢視畫面,將迷你控制器小工具加入現有的檢視畫面控制器,即可自行管理版面配置。

使用 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

實體按鈕音量控制

您可以使用傳送端上的實體音量按鈕,透過 GCKCastContext 上的 GCKCastOptions 上的 physicalVolumeButtonsWillControlDeviceVolume 標記變更網頁接收器的投放工作階段音量。

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];

處理錯誤

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

Logging

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