支援投放功能的 iOS 應用程式

1. 總覽

Google Cast 標誌

本程式碼研究室將說明如何修改現有的 iOS 影片應用程式,以便在支援 Google Cast 的裝置上投放內容。

什麼是 Google Cast?

使用者可以透過 Google Cast,將行動裝置中的內容投放到電視上。使用者將行動裝置當成遙控器使用,在電視上播放媒體。

您可以使用 Google Cast SDK 擴充應用程式,以便控制支援 Google Cast 的裝置 (例如電視或音響系統)。您可以使用 Cast SDK 根據 Google Cast 設計檢查清單新增必要的 UI 元件。

我們提供了 Google Cast 設計檢查清單,大幅簡化所有支援平台的 Cast 使用者體驗,並讓使用者享有簡單預測。

我們要建構什麼內容?

完成本程式碼研究室後,您將擁有一個 iOS 影片應用程式,可將影片投放至 Google Cast 裝置。

課程內容

  • 如何在示例影片應用程式中加入 Google Cast SDK。
  • 如何新增「投放」按鈕以選取 Google Cast 裝置。
  • 如何連線至 Cast 裝置並啟動媒體接收器。
  • 如何投放影片。
  • 如何在應用程式中新增 Cast mini 控制器。
  • 如何新增展開的控制器。
  • 如何提供介紹疊加畫面。
  • 如何自訂 Cast 小工具。
  • 如何整合 Cast Connect

軟硬體需求

  • 最新的 Xcode
  • 一部搭載 iOS 9 以上版本的行動裝置 (或 Xcode 模擬器)。
  • USB 資料傳輸線,可將行動裝置連接到開發電腦 (如果使用裝置)。
  • Google Cast 裝置,例如已設定連上網際網路的 ChromecastAndroid TV
  • 具備 HDMI 輸入端的電視或螢幕。
  • 你必須使用 Chromecast (支援 Google TV),才能測試 Cast Connect 整合作業,但對於程式碼研究室的其他部分,你可以選擇是否使用。如果您沒有裝置,可在本教學課程結尾略過「新增 Cast Connect 支援」步驟。

體驗

  • 請注意,您必須具備過往的 iOS 開發知識,
  • 你也需要具備觀看電視的相關知識 :)

您要如何使用這個教學課程?

只閱讀 閱讀並完成練習

針對開發 iOS 應用程式的經驗,你會給予什麼評價?

新手 中級 熟練

請評分你觀看電視的體驗。

新手 中級 還算容易

2. 取得程式碼範例

您可以將所有程式碼範例下載至電腦...

然後將下載的 ZIP 檔案解壓縮。

3. 執行範例應用程式

Apple iOS 標誌

首先,我們來看看完成的範例應用程式是什麼樣子。這款應用程式是基本的影片播放器。使用者可以從清單中選取影片,然後直接在裝置本機播放,或是將影片投放到 Google Cast 裝置。

下載程式碼後,請按照下列操作說明,在 Xcode 中開啟及執行完成的範例應用程式:

常見問題

設定 CocoaPods

如要設定 CocoaPods,請前往控制台,並使用 macOS 提供的預設 Ruby 進行安裝:

sudo gem install cocoapods

如有任何問題,請參閱官方說明文件,下載並安裝依附元件管理員。

專案設定

  1. 前往終端機,然後前往程式碼研究室目錄。
  2. 從 Podfile 安裝依附元件。
cd app-done
pod update
pod install
  1. 開啟 Xcode 並選取「Open another project...」
  2. 在範例程式碼資料夾的 「資料夾」圖示app-done 目錄中選取 CastVideos-ios.xcworkspace 檔案。

執行應用程式

選取目標和模擬工具,然後執行應用程式:

XCode 應用程式模擬器工具列

影片應用程式應該會在幾秒後顯示。

當系統顯示接受來電網路連線的通知時,請務必按一下「允許」。如未接受這個選項,就不會顯示「投放」圖示。

要求接受內送網路連線的確認對話方塊

按一下「投放」按鈕,然後選取 Google Cast 裝置。

選取影片,按一下播放按鈕。

影片就會開始在 Google Cast 裝置上播放。

畫面上會顯示展開的控制器。您可以使用播放/暫停按鈕控製播放。

返回影片清單。

螢幕底部現在會顯示迷你控制器。

插圖:iPhone 正在執行 CastVideos 應用程式,底部顯示迷你控制器

按一下迷你控制器中的暫停按鈕,即可暫停接收端的影片。按一下迷你控制器中的播放按鈕,即可繼續播放影片。

按一下「投放」按鈕,即可停止將內容投放到 Google Cast 裝置。

4. 準備 start 專案

插圖:iPhone 正在執行 CastVideos 應用程式

我們需要在你下載的啟動應用程式中新增 Google Cast 支援功能。以下是本程式碼研究室中會用到的 Google Cast 術語:

  • 寄件者應用程式是在行動裝置或筆記型電腦上執行
  • 接收器應用程式是在 Google Cast 裝置上執行。

專案設定

現在,您可以使用 Xcode 在範例專案上進行建構:

  1. 前往終端機,然後前往程式碼研究室目錄。
  2. 從 Podfile 安裝依附元件。
cd app-start
pod update
pod install
  1. 開啟 Xcode 並選取「Open another project...」
  2. 從範例程式碼資料夾中的 「資料夾」圖示app-start 目錄選取 CastVideos-ios.xcworkspace 檔案。

應用程式設計

應用程式會從遠端網路伺服器擷取影片清單,並提供使用者瀏覽清單。使用者可以選取影片查看詳細資料,或在行動裝置上播放影片。

應用程式包含兩個主要的 View Controller:MediaTableViewControllerMediaViewController.

MediaTableViewController

這個 UITableViewController 會顯示 MediaListModel 執行個體中的影片清單。影片清單及其相關中繼資料會以 JSON 檔案形式託管在遠端伺服器上。MediaListModel 會擷取並處理這個 JSON,以建構 MediaItem 物件清單。

MediaItem 物件會為影片及其相關中繼資料建立模型,例如影片的標題、說明、圖片網址和串流網址。

MediaTableViewController 會建立 MediaListModel 例項,然後將自身註冊為 MediaListModelDelegate,以便在媒體中繼資料下載完成時收到通知,進而載入表格檢視畫面。

系統會向使用者顯示影片縮圖清單,以及每部影片的簡短說明。選取項目時,系統會將對應的 MediaItem 傳遞至 MediaViewController

MediaViewController

這個 View Controller 會顯示特定影片的中繼資料,並讓使用者在行動裝置上本機播放影片。

這個 View Controller 會代管 LocalPlayerView、部分媒體控制項,以及顯示所選影片說明的文字區域。播放器會覆蓋螢幕頂端,並在底下留出空間,以便顯示影片的詳細說明。使用者可以播放/暫停或快轉本地影片。

常見問題

5. 新增投放按鈕

插圖:執行 CastVideos 應用程式的 iPhone 頂端三分之一,右上角顯示「投放」按鈕

支援 Cast 的應用程式會在每個 View Controller 中顯示「投放」按鈕。按一下「投放」按鈕,即可顯示使用者可選取的 Cast 裝置清單。如果使用者在傳送端裝置上播放本機內容,選取投放裝置後,系統就會在該投放裝置上開始或繼續播放內容。在投放工作階段期間,使用者隨時可以按一下「投放」按鈕,停止將應用程式投放到投放裝置。如Google Cast 設計檢查清單所述,使用者必須能在應用程式的任何畫面中,連線至或中斷與 Cast 裝置的連線。

設定

啟動專案需要與完成範例應用程式相同的依附元件和 Xcode 設定。請返回該部分,並按照相同步驟將 GoogleCast.framework 新增至啟動應用程式專案。

初始化

Cast 架構具有全域單例模式物件 GCKCastContext,用來協調所有架構的活動。這個物件必須在應用程式生命週期的早期初始化 (通常是應用程式委派的 application(_:didFinishLaunchingWithOptions:) 方法中),如此一來,傳送方應用程式重新啟動時的自動恢復工作階段即可正確觸發,並可開始掃描裝置。

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

application(_:didFinishLaunchingWithOptions:) 方法也很適合用來設定記錄委派,以便接收來自 Cast 架構的記錄訊息。這些資訊對偵錯和疑難排解相當實用。

開發自己的支援 Cast 的應用程式時,您必須註冊成為 Cast 開發人員,然後為應用程式取得應用程式 ID。在本程式碼研究室中,我們將使用範例應用程式 ID。

將下列程式碼加入 AppDelegate.swift,使用使用者預設值中的應用程式 ID 初始化 GCKCastContext,並為 Google Cast 架構新增記錄器:

import GoogleCast

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  fileprivate var enableSDKLogging = true

  ...

  func application(_: UIApplication,
                   didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    ...
    let options = GCKCastOptions(discoveryCriteria: GCKDiscoveryCriteria(applicationID: kReceiverAppID))
    options.physicalVolumeButtonsWillControlDeviceVolume = true
    GCKCastContext.setSharedInstanceWith(options)

    window?.clipsToBounds = true
    setupCastLogging()
    ...
  }
  ...
  func setupCastLogging() {
    let logFilter = GCKLoggerFilter()
    let classesToLog = ["GCKDeviceScanner", "GCKDeviceProvider", "GCKDiscoveryManager", "GCKCastChannel",
                        "GCKMediaControlChannel", "GCKUICastButton", "GCKUIMediaController", "NSMutableDictionary"]
    logFilter.setLoggingLevel(.verbose, forClasses: classesToLog)
    GCKLogger.sharedInstance().filter = logFilter
    GCKLogger.sharedInstance().delegate = self
  }
}

...

// MARK: - GCKLoggerDelegate

extension AppDelegate: GCKLoggerDelegate {
  func logMessage(_ message: String,
                  at _: GCKLoggerLevel,
                  fromFunction function: String,
                  location: String) {
    if enableSDKLogging {
      // Send SDK's log messages directly to the console.
      print("\(location): \(function) - \(message)")
    }
  }
}

投放按鈕

GCKCastContext 已完成初始化,因此我們需要新增投放按鈕,讓使用者選取投放裝置。Cast SDK 提供名為 GCKUICastButton 的 Cast 按鈕元件,做為 UIButton 子類別。您可以將其包裝在 UIBarButtonItem 中,然後新增至應用程式的標題列。我們需要在 MediaTableViewControllerMediaViewController 中新增投放按鈕。

將下列程式碼新增至 MediaTableViewController.swiftMediaViewController.swift

import GoogleCast

@objc(MediaTableViewController)
class MediaTableViewController: UITableViewController, GCKSessionManagerListener,
  MediaListModelDelegate, GCKRequestDelegate {
  private var castButton: GCKUICastButton!
  ...
  override func viewDidLoad() {
    print("MediaTableViewController - viewDidLoad")
    super.viewDidLoad()

    ...
    castButton = GCKUICastButton(frame: CGRect(x: CGFloat(0), y: CGFloat(0),
                                               width: CGFloat(24), height: CGFloat(24)))
    // Overwrite the UIAppearance theme in the AppDelegate.
    castButton.tintColor = UIColor.white
    navigationItem.rightBarButtonItem = UIBarButtonItem(customView: castButton)

    ...
  }
  ...
}

接著,在 MediaViewController.swift 中加入以下程式碼:

import GoogleCast

@objc(MediaViewController)
class MediaViewController: UIViewController, GCKSessionManagerListener, GCKRemoteMediaClientListener,
  LocalPlayerViewDelegate, GCKRequestDelegate {
  private var castButton: GCKUICastButton!
  ...
  override func viewDidLoad() {
    super.viewDidLoad()
    print("in MediaViewController viewDidLoad")
    ...
    castButton = GCKUICastButton(frame: CGRect(x: CGFloat(0), y: CGFloat(0),
                                               width: CGFloat(24), height: CGFloat(24)))
    // Overwrite the UIAppearance theme in the AppDelegate.
    castButton.tintColor = UIColor.white
    navigationItem.rightBarButtonItem = UIBarButtonItem(customView: castButton)

    ...
  }
  ...
}

接下來請執行應用程式。應用程式的導覽列中應該會顯示「投放」按鈕,點選該按鈕後,系統會列出本機網路上的投放裝置。GCKCastContext 會自動管理裝置探索作業。選取你的投放裝置,範例接收器應用程式就會在投放裝置上載入。您可以瀏覽活動和本機播放器活動之間切換,且投放按鈕狀態會保持同步。

我們尚未提供任何媒體播放支援功能,因此你目前無法在 Cast 裝置上播放影片。按一下「投放」按鈕即可停止投放。

6. 投放影片內容

插圖:搭載 CastVideos 應用程式的 iPhone,顯示特定影片的詳細資料 (《Tears of Steel」)。畫面底部是小型播放器

我們將擴充範例應用程式,以便在投放裝置上遠端播放影片。為此,我們需要監聽 Cast 架構產生的各種事件。

投放媒體

整體來說,如要在投放裝置上播放媒體,必須完成下列操作:

  1. 從 Cast SDK 建立 GCKMediaInformation 物件,用來建立媒體項目模型。
  2. 使用者連線至 Cast 裝置,啟動接收器應用程式。
  3. GCKMediaInformation 物件載入接收器並播放內容。
  4. 追蹤媒體狀態。
  5. 根據使用者互動,將播放指令傳送至接收器。

步驟 1 等同於將一個物件對應至另一個物件;GCKMediaInformation 是 Cast SDK 可理解的物件,而 MediaItem 是應用程式對媒體項目的封裝;我們可以輕鬆將 MediaItem 對應至 GCKMediaInformation。我們已經完成上一節的步驟 2。步驟 3 很容易透過 Cast SDK 完成。

範例應用程式 MediaViewController 已使用這個列舉方式區分本機與遠端播放作業:

enum PlaybackMode: Int {
  case none = 0
  case local
  case remote
}

private var playbackMode = PlaybackMode.none

在本程式碼研究室中,您不必完全瞭解所有玩家邏輯範例的運作方式。請務必瞭解,您的應用程式媒體播放器需要修改,才能以類似的方式掌握兩個播放位置。

目前本機播放器總是處於本機播放狀態,因為它尚不知道投放狀態的任何資訊。我們需要根據 Cast 架構中發生的狀態轉換,更新 UI。舉例來說,如果我們開始投放內容,就必須停止本機播放並停用部分控制項。同樣地,如果在進入這個檢視控制器時停止投放,我們也將需要轉換至本機播放。為了處理這項錯誤,我們需要監聽 Cast 架構產生的各種事件。

投放工作階段管理

對於 Cast 架構而言,Cast 工作階段會結合連線至裝置、啟動 (或加入)、連線至接收器應用程式,以及視情況初始化媒體控制管道的步驟。媒體控制管道是 Cast 架構向接收器媒體播放器傳送及接收訊息的方式。

使用者從「投放」按鈕選取裝置後,「投放」工作階段就會自動開始,並在使用者中斷連線時自動停止。由於網路問題而重新連線至接收器工作階段,Cast 架構也會自動處理。

投放工作階段是由 GCKSessionManager 管理,可透過 GCKCastContext.sharedInstance().sessionManager 存取。GCKSessionManagerListener 回呼可用來監控工作階段事件,例如建立、暫停、恢復和終止。

首先,我們必須註冊工作階段事件監聽器,並初始化部分變數:

class MediaViewController: UIViewController, GCKSessionManagerListener,
  GCKRemoteMediaClientListener, LocalPlayerViewDelegate, GCKRequestDelegate {

  ...
  private var sessionManager: GCKSessionManager!
  ...

  required init?(coder: NSCoder) {
    super.init(coder: coder)

    sessionManager = GCKCastContext.sharedInstance().sessionManager

    ...
  }

  override func viewWillAppear(_ animated: Bool) {
    ...

    let hasConnectedSession: Bool = (sessionManager.hasConnectedSession())
    if hasConnectedSession, (playbackMode != .remote) {
      populateMediaInfo(false, playPosition: 0)
      switchToRemotePlayback()
    } else if sessionManager.currentSession == nil, (playbackMode != .local) {
      switchToLocalPlayback()
    }

    sessionManager.add(self)

    ...
  }

  override func viewWillDisappear(_ animated: Bool) {
    ...

    sessionManager.remove(self)
    sessionManager.currentCastSession?.remoteMediaClient?.remove(self)
    ...
    super.viewWillDisappear(animated)
  }

  func switchToLocalPlayback() {
    ...

    sessionManager.currentCastSession?.remoteMediaClient?.remove(self)

    ...
  }

  func switchToRemotePlayback() {
    ...

    sessionManager.currentCastSession?.remoteMediaClient?.add(self)

    ...
  }


  // MARK: - GCKSessionManagerListener

  func sessionManager(_: GCKSessionManager, didStart session: GCKSession) {
    print("MediaViewController: sessionManager didStartSession \(session)")
    setQueueButtonVisible(true)
    switchToRemotePlayback()
  }

  func sessionManager(_: GCKSessionManager, didResumeSession session: GCKSession) {
    print("MediaViewController: sessionManager didResumeSession \(session)")
    setQueueButtonVisible(true)
    switchToRemotePlayback()
  }

  func sessionManager(_: GCKSessionManager, didEnd _: GCKSession, withError error: Error?) {
    print("session ended with error: \(String(describing: error))")
    let message = "The Casting session has ended.\n\(String(describing: error))"
    if let window = appDelegate?.window {
      Toast.displayMessage(message, for: 3, in: window)
    }
    setQueueButtonVisible(false)
    switchToLocalPlayback()
  }

  func sessionManager(_: GCKSessionManager, didFailToStartSessionWithError error: Error?) {
    if let error = error {
      showAlert(withTitle: "Failed to start a session", message: error.localizedDescription)
    }
    setQueueButtonVisible(false)
  }

  func sessionManager(_: GCKSessionManager,
                      didFailToResumeSession _: GCKSession, withError _: Error?) {
    if let window = UIApplication.shared.delegate?.window {
      Toast.displayMessage("The Casting session could not be resumed.",
                           for: 3, in: window)
    }
    setQueueButtonVisible(false)
    switchToLocalPlayback()
  }

  ...
}

MediaViewController,我們希望在與投放裝置連線或中斷連線時收到相關通知,以便切換或退出本機播放器。請注意,在行動裝置上執行的應用程式執行個體,不僅可能會中斷連線,而且在另一部行動裝置上執行的另一個應用程式執行個體也可能會中斷連線。

您可以透過 GCKCastContext.sharedInstance().sessionManager.currentCastSession 存取目前的有效工作階段。系統可根據「投放」對話方塊中的使用者手勢,自動建立並刪除工作階段。

載入媒體

在 Cast SDK 中,GCKRemoteMediaClient 提供一組便利的 API,可用來管理接收器上的遠端媒體播放作業。如果 GCKCastSession 支援媒體播放,SDK 會自動建立 GCKRemoteMediaClient 的例項。它可以做為 GCKCastSession 執行個體的 remoteMediaClient 屬性存取。

將下列程式碼新增至 MediaViewController.swift,即可在接收端載入目前選取的影片:

@objc(MediaViewController)
class MediaViewController: UIViewController, GCKSessionManagerListener,
  GCKRemoteMediaClientListener, LocalPlayerViewDelegate, GCKRequestDelegate {
  ...

  @objc func playSelectedItemRemotely() {
    loadSelectedItem(byAppending: false)
  }

  /**
   * Loads the currently selected item in the current cast media session.
   * @param appending If YES, the item is appended to the current queue if there
   * is one. If NO, or if
   * there is no queue, a new queue containing only the selected item is created.
   */
  func loadSelectedItem(byAppending appending: Bool) {
    print("enqueue item \(String(describing: mediaInfo))")
    if let remoteMediaClient = sessionManager.currentCastSession?.remoteMediaClient {
      let mediaQueueItemBuilder = GCKMediaQueueItemBuilder()
      mediaQueueItemBuilder.mediaInformation = mediaInfo
      mediaQueueItemBuilder.autoplay = true
      mediaQueueItemBuilder.preloadTime = TimeInterval(UserDefaults.standard.integer(forKey: kPrefPreloadTime))
      let mediaQueueItem = mediaQueueItemBuilder.build()
      if appending {
        let request = remoteMediaClient.queueInsert(mediaQueueItem, beforeItemWithID: kGCKMediaQueueInvalidItemID)
        request.delegate = self
      } else {
        let queueDataBuilder = GCKMediaQueueDataBuilder(queueType: .generic)
        queueDataBuilder.items = [mediaQueueItem]
        queueDataBuilder.repeatMode = remoteMediaClient.mediaStatus?.queueRepeatMode ?? .off

        let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder()
        mediaLoadRequestDataBuilder.mediaInformation = mediaInfo
        mediaLoadRequestDataBuilder.queueData = queueDataBuilder.build()

        let request = remoteMediaClient.loadMedia(with: mediaLoadRequestDataBuilder.build())
        request.delegate = self
      }
    }
  }
  ...
}

請更新各種現有方法,使用 Cast 工作階段邏輯支援遠端播放功能:

required init?(coder: NSCoder) {
  super.init(coder: coder)
  ...
  castMediaController = GCKUIMediaController()
  ...
}

func switchToLocalPlayback() {
  print("switchToLocalPlayback")
  if playbackMode == .local {
    return
  }
  setQueueButtonVisible(false)
  var playPosition: TimeInterval = 0
  var paused: Bool = false
  var ended: Bool = false
  if playbackMode == .remote {
    playPosition = castMediaController.lastKnownStreamPosition
    paused = (castMediaController.lastKnownPlayerState == .paused)
    ended = (castMediaController.lastKnownPlayerState == .idle)
    print("last player state: \(castMediaController.lastKnownPlayerState), ended: \(ended)")
  }
  populateMediaInfo((!paused && !ended), playPosition: playPosition)
  sessionManager.currentCastSession?.remoteMediaClient?.remove(self)
  playbackMode = .local
}

func switchToRemotePlayback() {
  print("switchToRemotePlayback; mediaInfo is \(String(describing: mediaInfo))")
  if playbackMode == .remote {
    return
  }
  // If we were playing locally, load the local media on the remote player
  if playbackMode == .local, (_localPlayerView.playerState != .stopped), (mediaInfo != nil) {
    print("loading media: \(String(describing: mediaInfo))")
    let paused: Bool = (_localPlayerView.playerState == .paused)
    let mediaQueueItemBuilder = GCKMediaQueueItemBuilder()
    mediaQueueItemBuilder.mediaInformation = mediaInfo
    mediaQueueItemBuilder.autoplay = !paused
    mediaQueueItemBuilder.preloadTime = TimeInterval(UserDefaults.standard.integer(forKey: kPrefPreloadTime))
    mediaQueueItemBuilder.startTime = _localPlayerView.streamPosition ?? 0
    let mediaQueueItem = mediaQueueItemBuilder.build()

    let queueDataBuilder = GCKMediaQueueDataBuilder(queueType: .generic)
    queueDataBuilder.items = [mediaQueueItem]
    queueDataBuilder.repeatMode = .off

    let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder()
    mediaLoadRequestDataBuilder.queueData = queueDataBuilder.build()

    let request = sessionManager.currentCastSession?.remoteMediaClient?.loadMedia(with: mediaLoadRequestDataBuilder.build())
    request?.delegate = self
  }
  _localPlayerView.stop()
  _localPlayerView.showSplashScreen()
  setQueueButtonVisible(true)
  sessionManager.currentCastSession?.remoteMediaClient?.add(self)
  playbackMode = .remote
}

/* Play has been pressed in the LocalPlayerView. */
func continueAfterPlayButtonClicked() -> Bool {
  let hasConnectedCastSession = sessionManager.hasConnectedCastSession
  if mediaInfo != nil, hasConnectedCastSession() {
    // Display an alert box to allow the user to add to queue or play
    // immediately.
    if actionSheet == nil {
      actionSheet = ActionSheet(title: "Play Item", message: "Select an action", cancelButtonText: "Cancel")
      actionSheet?.addAction(withTitle: "Play Now", target: self,
                             selector: #selector(playSelectedItemRemotely))
    }
    actionSheet?.present(in: self, sourceView: _localPlayerView)
    return false
  }
  return true
}

接著,請在行動裝置上執行應用程式。連線至投放裝置,然後開始播放影片。你應該會在接收端看到播放的影片。

7. 迷你控制器

根據投放設計檢查清單,所有投放應用程式都必須在使用者離開目前內容頁面時,提供迷你控制器。迷你控制器可讓你即時存取目前的投放工作階段,並顯示一則顯示提醒。

插圖:執行 CastVideos 應用程式的 iPhone 底部部分,並聚焦在迷你控制器

Cast SDK 提供控制列 GCKUIMiniMediaControlsViewController,您可以將這個控制列加到要顯示永久控制項的場景中。

在範例應用程式中,我們會使用 GCKUICastContainerViewController,包裝另一個檢視控制器,並在底部新增 GCKUIMiniMediaControlsViewController

修改 AppDelegate.swift 檔案,並透過以下方法為 if useCastContainerViewController 條件新增下列程式碼:

func application(_: UIApplication,
                 didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
  ...
  let appStoryboard = UIStoryboard(name: "Main", bundle: nil)
  guard let navigationController = appStoryboard.instantiateViewController(withIdentifier: "MainNavigation")
    as? UINavigationController else { return false }
  let castContainerVC = GCKCastContext.sharedInstance().createCastContainerController(for: navigationController)
    as GCKUICastContainerViewController
  castContainerVC.miniMediaControlsItemEnabled = true
  window = UIWindow(frame: UIScreen.main.bounds)
  window?.rootViewController = castContainerVC
  window?.makeKeyAndVisible()
  ...
}

新增這個屬性和 setter/getter 來控制迷你控制器的顯示設定 (我們會在後續章節中用到這些項目):

var isCastControlBarsEnabled: Bool {
    get {
      if useCastContainerViewController {
        let castContainerVC = (window?.rootViewController as? GCKUICastContainerViewController)
        return castContainerVC!.miniMediaControlsItemEnabled
      } else {
        let rootContainerVC = (window?.rootViewController as? RootContainerViewController)
        return rootContainerVC!.miniMediaControlsViewEnabled
      }
    }
    set(notificationsEnabled) {
      if useCastContainerViewController {
        var castContainerVC: GCKUICastContainerViewController?
        castContainerVC = (window?.rootViewController as? GCKUICastContainerViewController)
        castContainerVC?.miniMediaControlsItemEnabled = notificationsEnabled
      } else {
        var rootContainerVC: RootContainerViewController?
        rootContainerVC = (window?.rootViewController as? RootContainerViewController)
        rootContainerVC?.miniMediaControlsViewEnabled = notificationsEnabled
      }
    }
  }

執行應用程式並投放影片。接收器開始播放時,每個場景的底部應該都會顯示迷你控制器。您可以使用迷你控制器控制遠端播放內容。如果您在瀏覽活動和本機播放器活動之間切換,迷你控制器狀態應與接收器媒體播放狀態保持同步。

8. 介紹重疊畫面

Google Cast 設計檢查清單規定,發送端應用程式必須向現有使用者介紹投放按鈕,讓他們知道發送端應用程式現在支援投放功能,並協助新使用者瞭解 Google Cast。

插圖:iPhone 執行 CastVideos 應用程式,顯示投放按鈕疊加層,並以醒目顯示投放按鈕,以及顯示「Touch to cast media to your TV and Speakers」訊息

GCKCastContext 類別包含 presentCastInstructionsViewControllerOnce 方法,可用於在使用者首次向使用者顯示「投放」按鈕時醒目顯示。將下列程式碼加入 MediaViewController.swiftMediaTableViewController.swift

override func viewDidLoad() {
  ...

  NotificationCenter.default.addObserver(self, selector: #selector(castDeviceDidChange),
                                         name: NSNotification.Name.gckCastStateDidChange,
                                         object: GCKCastContext.sharedInstance())
}

@objc func castDeviceDidChange(_: Notification) {
  if GCKCastContext.sharedInstance().castState != .noDevicesAvailable {
    // You can present the instructions on how to use Google Cast on
    // the first time the user uses you app
    GCKCastContext.sharedInstance().presentCastInstructionsViewControllerOnce(with: castButton)
  }
}

在行動裝置上執行應用程式,您應該會看到介紹疊加畫面。

9. 展開的控制器

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

插圖:iPhone 執行 CastVideos 應用程式,播放影片,底部顯示展開的控制器

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

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

首先,您必須在投放內容中啟用預設的展開控制器。修改 AppDelegate.swift,啟用預設展開控制器:

import GoogleCast

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  ...

  func application(_: UIApplication,
                   didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    ...
    // Add after the setShareInstanceWith(options) is set.
    GCKCastContext.sharedInstance().useDefaultExpandedMediaControls = true
    ...
  }
  ...
}

將下列程式碼新增至 MediaViewController.swift,在使用者開始投放影片時載入展開的控制器:

@objc func playSelectedItemRemotely() {
  ...
  appDelegate?.isCastControlBarsEnabled = false
  GCKCastContext.sharedInstance().presentDefaultExpandedMediaControls()
}

使用者輕觸迷你控制器時,系統也會自動啟動展開的控制器。

執行應用程式並投放影片。您應該會看到展開的控制器。回到影片清單,當您按一下迷你控制器時,會再次載入展開的控制器。

10. 新增 Cast Connect 支援

Cast Connect 程式庫可讓現有的發送端應用程式透過 Cast 通訊協定與 Android TV 應用程式通訊。Cast Connect 建構於 Cast 基礎架構上,並將 Android TV 應用程式當做接收器。

依附元件

Podfile 中,請確認 google-cast-sdk 指向 4.4.8 以上版本,如下所列。如果您修改了檔案,請從控制台執行 pod update,將變更內容與專案保持同步。

pod 'google-cast-sdk', '>=4.4.8'

GCKLaunchOptions

如要啟動 Android TV 應用程式 (也稱為 Android 接收器),我們需要在 GCKLaunchOptions 物件中將 androidReceiverCompatible 標記設為 true。這個 GCKLaunchOptions 物件會決定接收器的啟動方式,並將其傳遞至 GCKCastOptions,這些都是使用 GCKCastContext.setSharedInstanceWith 在共用例項中設定的。

AppDelegate.swift 中新增下列程式碼行:

let options = GCKCastOptions(discoveryCriteria:
                          GCKDiscoveryCriteria(applicationID: kReceiverAppID))
...
/** Following code enables CastConnect */
let launchOptions = GCKLaunchOptions()
launchOptions.androidReceiverCompatible = true
options.launchOptions = launchOptions

GCKCastContext.setSharedInstanceWith(options)

設定啟動憑證

在傳送者端,您可以指定 GCKCredentialsData 來代表加入工作階段的使用者。credentials 是可由使用者定義的字串,只要 ATV 應用程式能瞭解即可。GCKCredentialsData 只會在啟動或加入時傳送至 Android TV 應用程式。如果在連線後再次設定 PIN 碼,就不會傳送到 Android TV 應用程式。

如要設定啟動憑證,GCKCredentialsData 必須在 GCKLaunchOptions 設定後的任何時間定義。為說明這一點,我們將為「Creds」按鈕新增邏輯,在建立工作階段時設定要傳遞的憑證。在 MediaTableViewController.swift 中加入下列程式碼:

class MediaTableViewController: UITableViewController, GCKSessionManagerListener, MediaListModelDelegate, GCKRequestDelegate {
  ...
  private var credentials: String? = nil
  ...
  override func viewDidLoad() {
    ...
    navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Creds", style: .plain,
                                                       target: self, action: #selector(toggleLaunchCreds))
    ...
    setLaunchCreds()
  }
  ...
  @objc func toggleLaunchCreds(_: Any){
    if (credentials == nil) {
        credentials = "{\"userId\":\"id123\"}"
    } else {
        credentials = nil
    }
    Toast.displayMessage("Launch Credentials: "+(credentials ?? "Null"), for: 3, in: appDelegate?.window)
    print("Credentials set: "+(credentials ?? "Null"))
    setLaunchCreds()
  }
  ...
  func setLaunchCreds() {
    GCKCastContext.sharedInstance()
        .setLaunch(GCKCredentialsData(credentials: credentials))
  }
}

在載入要求中設定憑證

如要在網頁和 Android TV 接收器應用程式中處理 credentials,請在 MediaTableViewController.swift 類別的 loadSelectedItem 函式下方加入下列程式碼:

let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder()
...
mediaLoadRequestDataBuilder.credentials = credentials
...

視傳送端投放至哪個接收端應用程式而定,SDK 會自動將上述憑證套用到進行中的工作階段。

測試 Cast Connect

在 Chromecast (支援 Google TV) 上安裝 Android TV APK 的步驟

  1. 找出 Android TV 裝置的 IP 位址。通常會在「設定」>「網路與網際網路」> (裝置連線的網路名稱) 下方。畫面右側會顯示詳細資料,以及裝置在網路上的 IP 位址。
  2. 使用裝置的 IP 位址,以便透過終端機透過 ADB 連線:
$ adb connect <device_ip_address>:5555
  1. 在終端機視窗中,前往您在本程式碼研究室一開始下載的程式碼研究室範例的頂層資料夾。例如:
$ cd Desktop/ios_codelab_src
  1. 執行以下命令,將這個資料夾中的 .apk 檔案安裝到 Android TV:
$ adb -s <device_ip_address>:5555 install android-tv-app.apk
  1. 現在,您應該會在 Android TV 裝置上,透過「您的應用程式」選單的「投放影片」名稱看到應用程式。
  2. 完成後,請在模擬器或行動裝置上建構並執行應用程式。使用 Android TV 裝置建立投放工作階段時,系統應會在 Android TV 上啟動 Android 接收器應用程式。透過 iOS 行動裝置傳送端播放影片時,應該會在 Android 接收器中啟動影片,讓你使用 Android TV 裝置的遙控器控制播放。

11. 自訂 Cast 小工具

初始化

從「App-Done」資料夾開始。請在 AppDelegate.swift 檔案的 applicationDidFinishLaunchingWithOptions 方法中新增以下內容。

func application(_: UIApplication,
                 didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
  ...
  let styler = GCKUIStyle.sharedInstance()
  ...
}

按照本程式碼研究室其他章節所述,套用一或多個自訂項目後,請呼叫以下程式碼提交這些樣式

styler.apply()

自訂投放檢視畫面

您可以設定檢視區塊的預設樣式指南,自訂 Cast 應用程式架構管理的所有檢視區塊。例如,變更圖示的色調顏色。

styler.castViews.iconTintColor = .lightGray

您可以視需要覆寫每個畫面的預設值。例如,只針對展開的媒體控制器,覆寫圖示色調顏色的 lightGrayColor。

styler.castViews.mediaControl.expandedController.iconTintColor = .green

變更顏色

您可以自訂所有檢視畫面的背景顏色,或個別自訂每個檢視畫面的背景顏色。下方程式碼會將所有 Cast 應用程式架構所提供檢視畫面的背景顏色設為藍色。

styler.castViews.backgroundColor = .blue
styler.castViews.mediaControl.miniController.backgroundColor = .yellow

變更字型

你可以為投放介面中的不同標籤自訂字型。為了方便說明,我們將所有字型都設為「Courier-Oblique」。

styler.castViews.headingTextFont = UIFont.init(name: "Courier-Oblique", size: 16) ?? UIFont.systemFont(ofSize: 16)
styler.castViews.mediaControl.headingTextFont = UIFont.init(name: "Courier-Oblique", size: 6) ?? UIFont.systemFont(ofSize: 6)

變更預設按鈕圖片

將您自己的自訂圖片新增至專案,並將圖片指派給按鈕以設定樣式。

let muteOnImage = UIImage.init(named: "yourImage.png")
if let muteOnImage = muteOnImage {
  styler.castViews.muteOnImage = muteOnImage
}

變更「投放」按鈕的主題

你也可以使用 UIAppearance 通訊協定,為 Cast 小工具設定主題。下列程式碼是 GCKUICastButton 顯示在所有檢視畫面上的主題:

GCKUICastButton.appearance().tintColor = UIColor.gray

12. 恭喜

您現在已瞭解如何在 iOS 上使用 Cast SDK 小工具,為影片應用程式啟用 Cast 功能。

詳情請參閱 iOS Sender 開發人員指南。