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

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

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

寄件者架構是指 Cast 類別程式庫二進位檔,以及傳送者在執行階段顯示的相關資源。傳送端應用程式投放應用程式是指在傳送端上執行的應用程式。Web Receiver 應用程式是指在網路接收器執行的 HTML 應用程式。

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

應用程式流程

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

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

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

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

透過主執行緒呼叫方法

初始化投放內容

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

初始化 GCKCastContext 時,必須提供 GCKCastOptions 物件。這個類別包含會影響架構行為的選項。其中最重要的是 Web Receiver 應用程式 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)
    }
  }
}
Goal-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 時,畫面上一律會顯示「投放」按鈕。使用者在初次啟動應用程式後第一次輕觸「投放」按鈕時,系統會顯示權限對話方塊,方便使用者授予應用程式區域網路存取權。接下來,當使用者輕觸投放按鈕時,系統會顯示投放對話方塊,其中列出找到的裝置。當使用者在裝置連線時輕觸投放按鈕時,系統會顯示目前的媒體中繼資料 (例如錄音室的標題、錄音室名稱和縮圖),或允許使用者中斷投放裝置。當沒有可用的裝置時,當使用者輕觸投放按鈕時,系統會顯示畫面,讓使用者瞭解找不到裝置的原因,以及如何排解問題。

  • 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)
Goal-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 引進了 Cast 工作階段的概念,其中整合了連線至裝置、啟動 (或加入) 網路接收器應用程式、連線至該應用程式,以及初始化媒體控制管道的步驟。如要進一步瞭解 Cast 工作階段和網路接收器生命週期,請參閱網路接收器應用程式生命週期指南

工作階段由 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))
Goal-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
}
Goal-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 格式的類型和高度和寬度 (以像素為單位)。4K 格式的變化版本是透過列舉值 在 hdrType 屬性中指出。GCKVideoInfoHDRType

新增迷你控制器

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

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

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

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

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

  • 將現有的檢視控制器納入自己的檢視控制器,讓 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()

  ...
}
Goal-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
    }
  }
}
Goal-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
}
Goal-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)
    }
  }

...
Goal-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()
  }
Goal-C
- (void)miniMediaControlsViewController:
            (GCKUIMiniMediaControlsViewController *)miniMediaControlsViewController
                           shouldAppear:(BOOL)shouldAppear {
  [self updateControlBarsVisibility];
}

新增展開的控制器

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

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

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

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

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

  GCKCastContext.sharedInstance().useDefaultExpandedMediaControls = true

  ...
}
Goal-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)
}
Goal-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)
Goal-C
GCKDiscoveryCriteria *criteria = [[GCKDiscoveryCriteria alloc]
                                          initWithApplicationID:kReceiverAppID];
GCKCastOptions *options = [[GCKCastOptions alloc]
                                          initWithDiscoveryCriteria :criteria];
options.physicalVolumeButtonsWillControlDeviceVolume = YES;
[GCKCastContext setSharedInstanceWithOptions:options];

處理錯誤

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

記錄

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

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

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

  var window: UIWindow?

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

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

    ...
  }

  // MARK: - GCKLoggerDelegate

  func logMessage(_ message: String,
                  at level: GCKLoggerLevel,
                  fromFunction function: String,
                  location: String) {
    if (kDebugLoggingEnabled) {
      print(function + " - " + message)
    }
  }
}
Goal-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
Goal-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
Goal-C
GCKLoggerFilter *filter = [[GCKLoggerFilter alloc] init];
[filter setLoggingLevel:GCKLoggerLevelVerbose
             forClasses:@[@"GCKUICastButton",
                          @"GCKUIImageCache",
                          @"NSMutableDictionary"
                          ]];
[GCKLogger sharedInstance].filter = filter;

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