支援投放功能的 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,可以直接略過本教學課程結尾處的「新增 Cast Connect 支援」步驟。

功能

  • 您必須具有先前的 iOS 開發知識。
  • 此外,您還必須具備觀看電視節目的相關知識 :)

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

僅詳讀 唸出並完成練習

根據您打造 iOS 應用程式的經驗,您會給予什麼評價?

11 月 中級 專業

根據您看電視的經驗,您會給予什麼評價?

11 月 中級 專業

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 裝置上播放。

系統會顯示展開的控制器。您可以使用播放/暫停按鈕控製播放作業。

返回影片清單。

畫面底部隨即會顯示迷你控制器。

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

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

按一下「投放」按鈕,停止將內容投放到 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,這個 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. 投放影片內容

iPhone 執行 CastVideos 應用程式的插圖,顯示特定影片的詳細資料 (「鋼眼)。底部是迷你播放器

我們將擴充範例應用程式,以便透過投放裝置遠端播放影片。為了達到這個目的,我們需要監聽 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 工作階段結合了連線至裝置、啟動 (或加入)、連線至接收器應用程式的步驟,以及視情況初始化媒體控制頻道的步驟。媒體控制管道是指投放架構從接收端媒體播放器收發訊息的方式。

當使用者透過「投放」按鈕選取裝置時,「投放」工作階段會自動啟動,並在使用者中斷連線時自動停止。由於網路問題導致重新連線至接收器工作階段,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 Session 邏輯支援遠端播放功能:

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 上透過「投放」按鈕重疊執行 CastVideos 應用程式,醒目顯示「投放」按鈕,並顯示「輕觸即可將媒體投放到電視和揚聲器」訊息

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 應用程式。如果在連線時重新設定,該裝置就不會傳送到 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 小工具

初始化

從「應用程式完成」資料夾開始。將以下內容新增至 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 通訊協定,為投放小工具設定主題。下列程式碼會在顯示的所有檢視畫面上設定 GCKUICastButton 的主題:

GCKUICastButton.appearance().tintColor = UIColor.gray

12. 恭喜

您現在已瞭解如何使用 iOS 上的 Cast SDK 小工具,來投放影片應用程式。

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