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

1. 總覽

Google Cast 標誌

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

什麼是 Google Cast?

Google Cast 可讓使用者將行動裝置上的內容投放到電視上。讓使用者能將行動裝置當做電視播放媒體的遙控器。

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

我們歸納了 Google Cast 設計檢查清單,目的是讓所有支援的平台都能簡單且可預測的 Cast 使用者體驗。

我們要建構什麼?

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

課程內容

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

軟硬體需求

  • 最新的 Xcode
  • 一部搭載 iOS 9 以上版本的行動裝置 (或 Xcode 模擬器)。
  • USB 資料傳輸線,可將行動裝置連接到開發電腦 (如果使用的是裝置)。
  • 一部 Google Cast 裝置,例如已設定網際網路連線的 ChromecastAndroid TV
  • 具備 HDMI 輸入端的電視或顯示器。
  • 如要測試 Cast Connect 整合功能,必須使用 Chromecast (支援 Google TV),但程式碼研究室的其餘部分為選用。如果您沒有裝置,可以略過本教學課程結尾的「新增 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 專案

插圖:搭載 CastVideos 應用程式的 iPhone

對於你下載的啟動應用程式,我們需要新增 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 檔案。

應用程式設計

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

應用程式包含兩個主要檢視區塊控制器:MediaTableViewControllerMediaViewController.

MediaTableViewController

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

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

MediaTableViewController 會建立 MediaListModel 執行個體,然後將其註冊為 MediaListModelDelegate,以便在媒體中繼資料下載完成後收到通知,以便載入資料表檢視畫面。

使用者會看到一份影片縮圖清單,其中包含每部影片的簡短說明。選取項目後,系統會將對應的 MediaItem 傳遞至 MediaViewController

MediaViewController

這個檢視畫面控制器會顯示特定影片的中繼資料,並允許使用者在行動裝置上播放影片。

檢視控制器會代管 LocalPlayerView、部分媒體控制項,以及顯示所選影片說明的文字區域。播放器覆蓋畫面最上方,留下影片的詳細說明。使用者可以播放/暫停或搜尋本機影片播放。

常見問題

5. 新增「投放」按鈕

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

支援 Cast 的應用程式會在每個檢視控制器中顯示「投放」按鈕。按一下「投放」按鈕,就會顯示使用者可選取的投放裝置清單。如果使用者是透過傳送者的裝置本機播放內容,選取投放裝置時,該投放裝置就會開始或繼續播放內容。在投放過程中,使用者隨時可以按一下「投放」按鈕,停止將應用程式投放到投放裝置。使用者必須能在應用程式的任何畫面中,與投放裝置連線或中斷連線,詳情請參閱 Google Cast 設計檢查清單

設定

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

初始化

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

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

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 的投放按鈕元件,做為 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 自動管理。選取你的投放裝置,範例接收器應用程式隨即會在投放裝置上載入。您可以在瀏覽活動和本機播放器活動之間瀏覽,而「投放」按鈕狀態會保持同步。

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

6. 投放影片內容

插圖:執行 CastVideos 應用程式的 iPhone,顯示特定影片的詳細資料 (例如「鋼製的撕裂」)。畫面底部是迷你播放器

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

投放媒體

大致上,如果您想在投放裝置上播放媒體,必須滿足以下條件:

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

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

範例應用程式 MediaViewController 已藉由使用此列舉來區分本機和遠端播放:

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

private var playbackMode = PlaybackMode.none

想瞭解所有範例播放器邏輯的運作方式,不是本程式碼研究室中的重點。請務必瞭解,應用程式的媒體播放器需要經過修改,才能以類似的方式瞭解這兩個播放位置。

目前,本機播放器一律會在本機播放狀態,因為其尚無「投放狀態」的相關資訊。我們需要根據 Cast 架構中的狀態轉換作業更新 UI。舉例來說,如果我們開始投放,就必須停止本機播放,並停用部分控制項。同樣地,如果我們在使用這個檢視控制器時停止投放,就必須轉換為本機播放。為了處理這種情況,我們需要監聽 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
      }
    }
  }
  ...
}

現在更新各種現有方法,以使用投放工作階段邏輯支援遠端播放:

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. 迷你控制器

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

執行 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 搭載「投放」按鈕,當中有「投放」按鈕重疊,醒目顯示「投放」按鈕,且顯示「輕觸即可將媒體投放到電視和音箱」訊息

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 物件會指定接收器的啟動方式,並傳遞至使用 GCKCastContext.setSharedInstanceWith 之共用執行個體中設定的 GCKCastOptions

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 應用程式。如果你在連線時再次設定 Wi-Fi,系統就不會將這部裝置傳送到 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 位址。通常可以在「設定」>「網路和網際網路」>「(裝置連上的網路名稱)」底下找到這個 ID。畫面右側會顯示詳細資料和裝置這個網路 IP。
  2. 使用終端機透過 ADB 使用裝置的 IP 位址連線至該裝置:
$ 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 Protocol 設定 Cast 小工具的主題。下列程式碼主題是 GCKUICastButton 上顯示的所有檢視畫面主題:

GCKUICastButton.appearance().tintColor = UIColor.gray

12. 恭喜

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

詳情請參閱「iOS 寄件者」開發人員指南。