iOS アプリをキャスト対応にする

1. 概要

Google Cast のロゴ

この Codelab では、既存の iOS 動画アプリを変更して、Google Cast 対応デバイスでコンテンツをキャストできるようにする方法を説明します。

Google Cast とは

Google Cast では、ユーザーはモバイル デバイスからテレビにコンテンツをキャストできます。ユーザーは自分のモバイル デバイスをリモコンとして使い、テレビでのメディア再生を行うことが可能です。

Google Cast SDK を使用すると、Google Cast 対応デバイス(テレビやサウンド システムなど)の操作機能をアプリに組み込むことができます。Cast SDK では、Google Cast デザイン チェックリストに沿って、必要な UI コンポーネントを追加できます。

Google Cast デザイン チェックリストは、サポートされているすべてのプラットフォームにわたって、Cast ユーザー エクスペリエンスをシンプルで予測可能なものにするために使用します。

達成目標

この Codelab を完了すると、iOS 動画アプリで Google Cast デバイスに動画をキャストできるようになります。

学習内容

  • サンプル動画アプリに Google Cast SDK を追加する方法
  • Google Cast デバイスを選択するキャスト アイコンを追加する方法
  • キャスト デバイスに接続してメディア レシーバーを起動する方法
  • 動画をキャストする方法
  • Cast ミニ コントローラをアプリに追加する方法
  • 拡張コントローラを追加する方法
  • 案内用のオーバーレイを提供する方法
  • キャスト ウィジェットをカスタマイズする方法
  • Cast Connect を統合する方法

必要なもの

  • 最新の Xcode
  • iOS 9 以降を搭載したモバイル デバイス(または Xcode Simulator)
  • モバイル デバイスを開発用のパソコンに接続するための USB データケーブル(デバイスを使用している場合)
  • インターネットに接続できる ChromecastAndroid TV などの Google Cast デバイス。
  • HDMI 入力対応のテレビまたはモニター
  • Cast Connect の統合をテストするには Chromecast with Google TV が必要ですが、この Codelab の残りの部分では必須ではありません。お持ちでない場合は、このチュートリアルの最後にある Cast Connect のサポートを追加するの手順をスキップしてください。

エクスペリエンス

  • iOS アプリ開発の知識が必要です。
  • 一般的なテレビの視聴経験も必要です。

このチュートリアルの利用方法をお選びください。

通読するのみ 通読し、演習を行う

iOS アプリ作成のご経験についてお答えください。

初心者 中級者 上級者

テレビ視聴のご経験についてお答えください。

初心者 中級者 上級者

2. サンプルコードを取得する

すべてのサンプルコードをパソコンにダウンロードできます。

ダウンロードした ZIP ファイルを解凍します。

3. サンプルアプリを実行する

Apple iOS のロゴ

まず、完成したサンプルアプリがどのようなものか見てみましょう。このアプリは基本的な動画プレーヤーです。ユーザーはリストから動画を選択し、デバイス上でローカルに再生するか、Google Cast デバイスにキャストできます。

コードをダウンロードしたら、以下で説明する手順に沿って、完成したサンプルアプリを Xcode で開いて実行してください。

よくある質問

CocoaPods のセットアップ

CocoaPods をセットアップするには、コンソールを開き、macOS で利用可能なデフォルトの Ruby を使用してインストールします。

sudo gem install cocoapods

問題が生じる場合は、公式ドキュメントを参照し、依存関係管理ツールをダウンロードしてインストールしてください。

プロジェクトの設定

  1. ターミナルを開いて、Codelab ディレクトリに移動します。
  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. 開始用プロジェクトを準備する

iPhone で CastVideos アプリを実行しているイラスト

ダウンロードした開始用アプリに Google Cast のサポートを追加する必要があります。この Codelab では次のような Google Cast の用語を使用します。

  • 送信側アプリはモバイル デバイスやノートパソコンで動作します。
  • レシーバー アプリは Google Cast デバイスで動作します。

プロジェクトの設定

Xcode で開始用プロジェクトを基に作業する準備を行います。

  1. ターミナルを開いて、Codelab ディレクトリに移動します。
  2. Podfile から依存関係をインストールします。
cd app-start
pod update
pod install
  1. Xcode を開いて、[Open another project...] を選択します。
  2. サンプルコード フォルダの フォルダ アイコンapp-start ディレクトリにある CastVideos-ios.xcworkspace ファイルを選択します。

アプリの設計

アプリはリモートのウェブサーバーから動画のリストを取得し、ユーザーがブラウジングできるようにリストを提供します。動画を選択すると、その詳細情報を表示したり、モバイル デバイスで動画をローカルに再生したりできます。

アプリは、MediaTableViewControllerMediaViewController. の 2 つのメインビュー コントローラで構成されます。

MediaTableViewController

この UITableViewController には、MediaListModel インスタンスの動画のリストが表示されます。動画とその関連するメタデータのリストは、リモート サーバーで JSON ファイルとしてホストされます。MediaListModel はこの JSON を取得して処理し、MediaItem オブジェクトのリストを作成します。

MediaItem オブジェクトは、動画とその関連メタデータ(タイトル、説明、画像の URL、ストリームの URL など)をモデル化します。

MediaTableViewControllerMediaListModel インスタンスを作成してから、自身を MediaListModelDelegate として登録し、メディア メタデータがダウンロードされたときに通知を受けて、テーブルビューを読み込めるようにします。

動画のサムネイルのリストと、各動画の簡単な説明が表示されます。アイテムを選択すると、対応する MediaItemMediaViewController に渡されます。

MediaViewController

このビュー コントローラは、特定の動画に関するメタデータを表示し、ユーザーがモバイル デバイスで動画をローカルに再生できるようにします。

ビュー コントローラは、LocalPlayerView、一部のメディア コントローラ、テキストエリアをホストし、ユーザーが選択した動画の説明を表示します。プレーヤーは画面の最上部に表示され、動画の詳細な説明を表示するスペースがその下に表示されます。ユーザーは動画をローカルで再生、一時停止したり、再生位置を移動したりできます。

よくある質問

5. キャスト アイコンの追加

iPhone の上部 3 分の 1 のイラスト。CastVideos アプリが実行されており、右上隅にキャスト ボタンが表示されている

Cast 対応アプリでは、それぞれのビュー コントローラにキャスト アイコンが表示されます。キャスト アイコンをクリックすると、ユーザーが選択できるキャスト デバイスのリストが表示されます。送信側のデバイスでコンテンツをローカルで再生している場合は、キャスト デバイスを選択すると、そのキャスト デバイスで再生が開始または再開されます。キャスト セッション中、ユーザーはいつでもキャスト アイコンをクリックして、キャスト デバイスへのアプリのキャストを停止できます。Google Cast デザイン チェックリストで規定されているとおり、アプリのどの画面でもキャスト デバイスに接続または接続を解除できるようにする必要があります。

構成

開始用プロジェクトでは、完成したサンプルアプリと同じように、依存関係の設定と Xcode のセットアップを行う必要があります。前述のセクションを参照して、同じ手順で GoogleCast.framework を開始用アプリ プロジェクトに追加してください。

初期化

キャスト フレームワークには、グローバル シングルトン オブジェクトの GCKCastContext があり、これによりフレームワークのすべてのアクティビティが調整されます。このオブジェクトは、アプリのライフサイクルの早い段階で初期化する必要があります。通常はアプリのデリゲートの application(_:didFinishLaunchingWithOptions:) メソッドで初期化し、センダーアプリの再起動時に、セッションの自動再開が適切にトリガーされ、デバイスのスキャンを開始できるようにします。

GCKCastContext を初期化するときは、GCKCastOptions オブジェクトを指定する必要があります。このクラスには、フレームワークの動作に影響するオプションが含まれています。最も重要なオプションはレシーバー アプリの ID で、キャスト デバイスの検出結果をフィルタし、キャスト セッションの開始時にレシーバー アプリを起動するために使用されます。

application(_:didFinishLaunchingWithOptions:) メソッドは、キャスト フレームワークからロギング メッセージを受信するロギング デリゲートのセットアップにも使用できます。この情報はデバッグやトラブルシューティングに役立ちます。

独自の Cast 対応アプリを開発する場合は、Cast デベロッパーとして登録してからアプリのアプリ ID を取得する必要があります。この Codelab では、サンプルのアプリ 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 アプリを実行し、特定の動画(「Tears of Steel」)の詳細を表示している様子を示すイラスト。下部にミニプレーヤーが表示されます。

キャスト デバイスでも動画をリモートで再生できるようにサンプルアプリを拡張します。そのためには、キャスト フレームワークによって生成された各種イベントをリッスンする必要があります。

メディアのキャスト

大まかに説明すると、キャスト デバイスでメディアを再生するプロセスは、次のようになります。

  1. Cast SDK を使用して、メディア アイテムをモデル化する GCKMediaInformation オブジェクトを作成します。
  2. ユーザーがキャスト デバイスに接続してレシーバー アプリを起動します。
  3. GCKMediaInformation オブジェクトをレシーバーに読み込み、コンテンツを再生します。
  4. メディアのステータスを追跡します。
  5. ユーザーの操作に基づいて再生コマンドをレシーバーに送信します。

ステップ 1 では、オブジェクトを別のオブジェクトにマッピングします。GCKMediaInformation は Cast SDK が解釈できるもので、MediaItem はアプリでメディア アイテムをカプセル化したものです。MediaItemGCKMediaInformation に簡単にマッピングできます。前のセクションのステップ 2 はすでに完了しています。ステップ 3 は Cast SDK で簡単に行うことができます。

サンプルアプリ MediaViewController では、すでに次の列挙型を使って、ローカル再生とリモート再生を区別しています。

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

private var playbackMode = PlaybackMode.none

この Codelab では、すべてのサンプル プレーヤーにおけるロジックの仕組みを正確に理解する必要はありません。ここで重要なのは、2 つの再生場所を同じように認識するようにアプリのメディア プレーヤーを変更する必要があるということです。

現時点では、ローカル プレーヤーはキャスト状態について何も認識していないため、常にローカルの再生状態になっています。キャスト フレームワークで発生する状態遷移に基づいて UI を更新する必要があります。たとえば、キャストを開始するときは、ローカルの再生を停止して、いくつかのコントロールを無効にする必要があります。同様に、このビュー コントローラを使用しているときにキャストを停止した場合は、ローカルの再生に移行する必要があります。これに対応するには、キャスト フレームワークによって生成された各種イベントをリッスンする必要があります。

キャスト セッションの管理

キャスト フレームワークの場合、キャスト セッションは、デバイスへの接続、起動(または参加)、レシーバー アプリへの接続、メディア コントロール チャネルの初期化(該当する場合)の 2 つの手順で構成されます。メディア コントロール チャネルは、キャスト フレームワークがレシーバー メディア プレーヤーからメッセージを送受信する方法です。

キャスト アイコンからデバイスを選択すると自動的にキャスト セッションが開始され、ユーザーが切断すると自動的に停止します。ネットワークの問題によるレシーバー セッションへの再接続も、キャスト フレームワークによって自動的に処理されます。

キャスト セッションは 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 には、レシーバーでのリモート メディア再生を管理できる便利な API GCKRemoteMediaClient が用意されています。メディア再生をサポートする GCKCastSession の場合、GCKRemoteMediaClient のインスタンスは SDK によって自動的に作成されます。これは、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()
  ...
}

このプロパティとセッター/ゲッターを追加して、ミニ コントローラの表示を制御します(後のセクションで使用します)。

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 アプリには渡されません。

起動認証情報を設定するには、GCKLaunchOptions の設定後にいつでも GCKCredentialsData を定義する必要があります。これを説明するために、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))
  }
}

Load リクエストで認証情報を設定する

ウェブアプリと Android TV レシーバー アプリの両方で credentials を処理するには、loadSelectedItem 関数の下の MediaTableViewController.swift クラスに次のコードを追加します。

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

送信側がキャストしている受信側アプリに応じて、SDK は上記の認証情報を進行中のセッションに自動的に適用します。

Cast Connect のテスト

Chromecast with Google TV に Android TV APK をインストールする手順

  1. Android TV デバイスの IP アドレスを確認します。通常は、[設定] > [ネットワークとインターネット] > [(デバイスが接続されているネットワーク名)] で確認できます。右側に詳細とネットワーク上のデバイスの IP が表示されます。
  2. デバイスの IP アドレスを使用して、ターミナルから ADB 経由でデバイスに接続します。
$ adb connect <device_ip_address>:5555
  1. ターミナル ウィンドウで、この Codelab の開始時にダウンロードした Codelab サンプルの最上位フォルダに移動します。次に例を示します。
$ cd Desktop/ios_codelab_src
  1. 次のコマンドを実行して、このフォルダ内の .apk ファイルを Android TV にインストールします。
$ adb -s <device_ip_address>:5555 install android-tv-app.apk
  1. Android TV デバイスの [アプリ] メニューに「Cast Videos」というアプリが表示されるようになります。
  2. 完了したら、エミュレータまたはモバイル デバイスでアプリをビルドして実行します。Android TV デバイスとのキャスト セッションを確立すると、Android TV で Android レシーバー アプリケーションが起動するようになります。iOS モバイル センダーから動画を再生すると、Android レシーバーで動画が起動し、Android TV デバイスのリモコンを使用して再生を操作できるようになります。

11. キャスト ウィジェットをカスタマイズする

初期化

「app-done」フォルダにある AppDelegate.swift ファイルの applicationDidFinishLaunchingWithOptions メソッドに次のコードを追加します。

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

この Codelab の他のセクションで紹介するカスタマイズが完了したら、次のコードを呼び出してスタイルを適用します。

styler.apply()

キャストビューのカスタマイズ

Cast Application Framework で管理するすべてのビューは、ビュー全体にデフォルトのスタイル設定ガイドラインを適用してカスタマイズできます。試しにアイコンの色合いを変更してみましょう。

styler.castViews.iconTintColor = .lightGray

必要に応じて、デフォルトの設定を画面ごとにオーバーライドできます。たとえば、展開されたメディア コントローラでのみアイコンの lightGrayColor をオーバーライドするには、次のように記述します。

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

色の変更

すべてのビュー(またはビューごと)の背景色をカスタマイズできます。次のコードは、Cast Application Framework で表示されるすべてのビューの背景色を青色に設定します。

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 送信者のデベロッパー ガイドをご覧ください。