iOS アプリにキャストを統合する

このデベロッパー ガイドでは、iOS Sender SDK を使用して iOS 送信側アプリに Google Cast のサポートを追加する方法について説明します。

モバイル デバイスやノートパソコンは再生を制御する送信側、Google Cast デバイスはテレビにコンテンツを表示する受信側です。

送信側フレームワークは、送信側で実行時に存在する Cast クラス ライブラリ バイナリと関連リソースを指します。センダーアプリまたはキャストアプリは、センダーでも実行されているアプリを指します。ウェブ レシーバー アプリとは、ウェブ レシーバーで実行される HTML アプリケーションのことです。

送信側フレームワークは、非同期コールバック設計を使用して、送信側アプリにイベントを通知し、Cast アプリのライフサイクルのさまざまな状態間を遷移します。

アプリケーションの流れ

送信側 iOS アプリの一般的な実行フローの概要は次のとおりです。

  • Cast フレームワークは、GCKCastOptions で提供されたプロパティに基づいて GCKDiscoveryManager を開始し、デバイスのスキャンを開始します。
  • ユーザーがキャスト アイコンをクリックすると、フレームワークは検出されたキャスト デバイスのリストを含むキャスト ダイアログを表示します。
  • ユーザーがキャスト デバイスを選択すると、フレームワークはキャスト デバイスで Web レシーバー アプリを起動しようとします。
  • フレームワークは、送信側アプリでコールバックを呼び出して、ウェブ レシーバー アプリが起動したことを確認します。
  • フレームワークは、送信側アプリと Web レシーバー アプリの間に通信チャネルを作成します。
  • フレームワークは、通信チャネルを使用して Web レシーバーでメディア再生を読み込んで制御します。
  • フレームワークは、送信側とウェブ レシーバーの間でメディア再生状態を同期します。ユーザーが送信側の UI アクションを行うと、フレームワークはメディア コントロール リクエストをウェブ レシーバーに渡し、ウェブ レシーバーがメディア ステータスの更新を送信すると、フレームワークは送信側の UI の状態を更新します。
  • ユーザーがキャスト アイコンをクリックしてキャスト デバイスから切断すると、フレームワークはセンダーアプリとウェブ レシーバーの接続を解除します。

送信者の問題を解決するには、ロギングを有効にする必要があります。

Google Cast iOS フレームワークのすべてのクラス、メソッド、イベントの一覧については、Google Cast iOS API リファレンスをご覧ください。以下のセクションでは、iOS アプリに Cast を統合する手順について説明します。

メインスレッドからメソッドを呼び出す

キャスト コンテキストを初期化する

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

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

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

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

  var window: UIWindow?

  func applicationDidFinishLaunching(_ application: UIApplication) {
    let criteria = GCKDiscoveryCriteria(applicationID: kReceiverAppID)
    let options = GCKCastOptions(discoveryCriteria: criteria)
    GCKCastContext.setSharedInstanceWith(options)

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

    ...
  }

  // MARK: - GCKLoggerDelegate

  func logMessage(_ message: String,
                  at level: GCKLoggerLevel,
                  fromFunction function: String,
                  location: String) {
    if (kDebugLoggingEnabled) {
      print(function + " - " + message)
    }
  }
}
Objective-C

AppDelegate.h

@interface AppDelegate () <GCKLoggerDelegate>
@end

AppDelegate.m

@implementation AppDelegate

static NSString *const kReceiverAppID = @"AABBCCDD";
static const BOOL kDebugLoggingEnabled = YES;

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  GCKDiscoveryCriteria *criteria = [[GCKDiscoveryCriteria alloc]
                                    initWithApplicationID:kReceiverAppID];
  GCKCastOptions *options = [[GCKCastOptions alloc] initWithDiscoveryCriteria:criteria];
  [GCKCastContext setSharedInstanceWithOptions:options];

  // Enable logger.
  [GCKLogger sharedInstance].delegate = self;

  ...

  return YES;
}

...

#pragma mark - GCKLoggerDelegate

- (void)logMessage:(NSString *)message
           atLevel:(GCKLoggerLevel)level
      fromFunction:(NSString *)function
          location:(NSString *)location {
  if (kDebugLoggingEnabled) {
    NSLog(@"%@ - %@, %@", function, message, location);
  }
}

@end

Cast UX ウィジェット

Cast iOS SDK には、Cast デザイン チェックリストに準拠した次のウィジェットが用意されています。

  • 導入オーバーレイ: GCKCastContext クラスには presentCastInstructionsViewControllerOnceWithCastButton メソッドがあります。このメソッドを使用して、ウェブ レシーバが初めて利用可能になったときにキャスト アイコンをスポットライト表示できます。送信側アプリは、テキスト、タイトル テキストの位置、[閉じる] ボタンをカスタマイズできます。

  • キャスト アイコン: Cast iOS Sender SDK 4.6.0 以降では、送信側デバイスが Wi-Fi に接続されている場合、キャスト アイコンは常に表示されます。アプリを最初に起動した後で、ユーザーがキャスト アイコンを初めてタップすると、権限ダイアログが表示され、ユーザーはアプリにネットワーク上のデバイスへのローカル ネットワーク アクセス権を付与できます。その後、ユーザーがキャスト アイコンをタップすると、検出されたデバイスのリストが表示されるキャスト ダイアログが表示されます。デバイスが接続されているときにユーザーがキャスト アイコンをタップすると、現在のメディア メタデータ(タイトル、録音スタジオの名前、サムネイル画像など)が表示されるか、キャスト デバイスから切断できます。利用可能なデバイスがない状態でユーザーがキャスト アイコンをタップすると、デバイスが見つからない理由とトラブルシューティングの方法に関する情報が画面に表示されます。

  • ミニ コントローラ: ユーザーがコンテンツをキャストしているときに、現在のコンテンツ ページから離れたり、送信側アプリの別の画面にコントローラを拡大したりすると、画面の下部にミニ コントローラが表示され、現在キャスト中のメディアのメタデータを確認したり、再生を制御したりできます。

  • 拡張コントローラ: ユーザーがコンテンツをキャストしているときに、メディア通知またはミニコントローラをクリックすると、拡張コントローラが起動します。拡張コントローラには、現在再生中のメディアのメタデータが表示され、メディアの再生を制御するためのボタンがいくつか用意されています。

キャスト アイコンを追加する

フレームワークでは、キャスト アイコン コンポーネントが UIButton サブクラスとして提供されています。アプリのタイトルバーに追加する場合は、UIBarButtonItem でラップします。一般的な UIViewController サブクラスは、次のように Cast ボタンをインストールできます。

Swift
let castButton = GCKUICastButton(frame: CGRect(x: 0, y: 0, width: 24, height: 24))
castButton.tintColor = UIColor.gray
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: castButton)
Objective-C
GCKUICastButton *castButton = [[GCKUICastButton alloc] initWithFrame:CGRectMake(0, 0, 24, 24)];
castButton.tintColor = [UIColor grayColor];
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:castButton];

デフォルトでは、ボタンをタップすると、フレームワークによって提供されるキャスト ダイアログが開きます。

GCKUICastButton は、ストーリーボードに直接追加することもできます。

デバイス検出を構成する

フレームワークでは、デバイスの検出は自動的に行われます。カスタム UI を実装する場合を除き、検出プロセスを明示的に開始または停止する必要はありません。

フレームワーク内の検出は、GCKCastContext のプロパティである GCKDiscoveryManager クラスによって管理されます。フレームワークは、デバイスの選択と制御のためのデフォルトの Cast ダイアログ コンポーネントを提供します。デバイスのリストは、デバイスのわかりやすい名前の辞書順に並べ替えられます。

セッション管理の仕組み

Cast SDK では、キャスト セッションというコンセプトが導入されています。キャスト セッションの確立では、デバイスへの接続、ウェブ レシーバー アプリの起動(または参加)、そのアプリへの接続、メディア コントロール チャネルの初期化というプロセスが組み合わされます。Cast セッションと Web Receiver のライフサイクルについて詳しくは、Web Receiver のアプリケーション ライフサイクル ガイドをご覧ください。

セッションは GCKSessionManager クラスによって管理されます。このクラスは GCKCastContext のプロパティです。個々のセッションは GCKSession クラスのサブクラスで表されます。たとえば、GCKCastSession は Cast デバイスとのセッションを表します。現在アクティブな Cast セッション(存在する場合)には、GCKSessionManagercurrentCastSession プロパティとしてアクセスできます。

GCKSessionManagerListener インターフェースは、セッションの作成、停止、再開、終了などのセッション イベントのモニタリングに使用できます。フレームワークは、送信側アプリがバックグラウンドに移行するとセッションを自動的に一時停止し、アプリがフォアグラウンドに戻ると(または、セッションがアクティブな状態でアプリが異常終了または突然終了した後に再起動されると)、セッションの再開を試みます。

キャスト ダイアログが使用されている場合、セッションはユーザー操作に応じて自動的に作成、終了されます。それ以外の場合、アプリは GCKSessionManager のメソッドを介してセッションを明示的に開始および終了できます。

セッション ライフサイクル イベントに応答して特別な処理を行う必要がある場合、アプリは GCKSessionManager に 1 つ以上の GCKSessionManagerListener インスタンスを登録できます。GCKSessionManagerListener は、セッションの開始や終了などのイベントのコールバックを定義するプロトコルです。

ストリーミング転送

セッションの状態の保持は、ストリーム転送の基本です。ストリーム転送では、音声コマンド、Google Home アプリ、スマート ディスプレイを使用して、既存の音声ストリームと動画ストリームをデバイス間で移動できます。メディアの再生が 1 つのデバイス(ソース)で停止し、別のデバイス(宛先)で続行されます。最新のファームウェアを搭載した Cast デバイスは、ストリーム転送の転送元または転送先として機能します。

ストリーム転送中に新しい宛先デバイスを取得するには、[sessionManager:didResumeCastSession:] コールバック中に GCKCastSession#device プロパティを使用します。

詳しくは、ウェブ レシーバでのストリーム転送をご覧ください。

自動再接続

キャスト フレームワークは、再接続ロジックを追加して、次のような多くの微妙なコーナーケースで再接続を自動的に処理します。

  • Wi-Fi の一時的な損失から復元する
  • デバイスのスリープから復元する
  • アプリのバックグラウンド化から復元する
  • アプリがクラッシュした場合に復元する

メディア コントロールの仕組み

メディア名前空間をサポートする Web レシーバ アプリで Cast セッションが確立されると、GCKRemoteMediaClient のインスタンスがフレームワークによって自動的に作成されます。これは GCKCastSession インスタンスの remoteMediaClient プロパティとしてアクセスできます。

Web レシーバにリクエストを発行する GCKRemoteMediaClient のすべてのメソッドは、そのリクエストの追跡に使用できる GCKRequest オブジェクトを返します。このオブジェクトに GCKRequestDelegate を割り当てて、オペレーションの最終結果に関する通知を受け取ることができます。

GCKRemoteMediaClient のインスタンスはアプリの複数の部分で共有されることが想定されており、実際、フレームワークの内部コンポーネント(キャスト ダイアログやミニ メディア コントロールなど)はインスタンスを共有しています。そのため、GCKRemoteMediaClient は複数の GCKRemoteMediaClientListener の登録をサポートしています。

メディアのメタデータを設定する

GCKMediaMetadata クラスは、キャストするメディア アイテムに関する情報を表します。次の例では、映画の新しい GCKMediaMetadata インスタンスを作成し、タイトル、サブタイトル、録音スタジオの名前、2 つの画像を設定します。

Swift
let metadata = GCKMediaMetadata()
metadata.setString("Big Buck Bunny (2008)", forKey: kGCKMetadataKeyTitle)
metadata.setString("Big Buck Bunny tells the story of a giant rabbit with a heart bigger than " +
  "himself. When one sunny day three rodents rudely harass him, something " +
  "snaps... and the rabbit ain't no bunny anymore! In the typical cartoon " +
  "tradition he prepares the nasty rodents a comical revenge.",
                   forKey: kGCKMetadataKeySubtitle)
metadata.addImage(GCKImage(url: URL(string: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg")!,
                           width: 480,
                           height: 360))
Objective-C
GCKMediaMetadata *metadata = [[GCKMediaMetadata alloc]
                                initWithMetadataType:GCKMediaMetadataTypeMovie];
[metadata setString:@"Big Buck Bunny (2008)" forKey:kGCKMetadataKeyTitle];
[metadata setString:@"Big Buck Bunny tells the story of a giant rabbit with a heart bigger than "
 "himself. When one sunny day three rodents rudely harass him, something "
 "snaps... and the rabbit ain't no bunny anymore! In the typical cartoon "
 "tradition he prepares the nasty rodents a comical revenge."
             forKey:kGCKMetadataKeySubtitle];
[metadata addImage:[[GCKImage alloc]
                    initWithURL:[[NSURL alloc] initWithString:@"https://commondatastorage.googleapis.com/"
                                 "gtv-videos-bucket/sample/images/BigBuckBunny.jpg"]
                    width:480
                    height:360]];

メディア メタデータを含む画像の使用については、画像の選択とキャッシュ保存のセクションをご覧ください。

メディアを読み込む

メディア アイテムを読み込むには、メディアのメタデータを使用して GCKMediaInformation インスタンスを作成します。次に、現在の GCKCastSession を取得し、その GCKRemoteMediaClient を使用して受信側アプリにメディアを読み込みます。その後、GCKRemoteMediaClient を使用して、受信側で実行されているメディア プレーヤー アプリを制御できます(再生、一時停止、停止など)。

Swift
let url = URL.init(string: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4")
guard let mediaURL = url else {
  print("invalid mediaURL")
  return
}

let mediaInfoBuilder = GCKMediaInformationBuilder.init(contentURL: mediaURL)
mediaInfoBuilder.streamType = GCKMediaStreamType.none;
mediaInfoBuilder.contentType = "video/mp4"
mediaInfoBuilder.metadata = metadata;
mediaInformation = mediaInfoBuilder.build()

guard let mediaInfo = mediaInformation else {
  print("invalid mediaInformation")
  return
}

if let request = sessionManager.currentSession?.remoteMediaClient?.loadMedia(mediaInfo) {
  request.delegate = self
}
Objective-C
GCKMediaInformationBuilder *mediaInfoBuilder =
  [[GCKMediaInformationBuilder alloc] initWithContentURL:
   [NSURL URLWithString:@"https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"]];
mediaInfoBuilder.streamType = GCKMediaStreamTypeNone;
mediaInfoBuilder.contentType = @"video/mp4";
mediaInfoBuilder.metadata = metadata;
self.mediaInformation = [mediaInfoBuilder build];

GCKRequest *request = [self.sessionManager.currentSession.remoteMediaClient loadMedia:self.mediaInformation];
if (request != nil) {
  request.delegate = self;
}

メディア トラックの使用に関するセクションもご覧ください。

4K 動画形式

メディアの動画形式を特定するには、GCKMediaStatusvideoInfo プロパティを使用して、GCKVideoInfo の現在のインスタンスを取得します。このインスタンスには、HDR テレビ形式のタイプと、ピクセル単位の高さと幅が含まれています。4K 形式のバリエーションは、hdrType プロパティで列挙値 GCKVideoInfoHDRType によって示されます。

ミニ コントローラを追加する

Cast デザイン チェックリストによると、センダーアプリは、ユーザーが現在のコンテンツ ページから移動するときに表示されるミニ コントローラと呼ばれる永続的なコントロールを提供する必要があります。ミニ コントローラは、すばやくアクセスでき、現在のキャスト セッションの情報が表示されます。

Cast フレームワークには、コントロール バー GCKUIMiniMediaControlsViewController があり、ミニ コントローラを表示するシーンに追加できます。

センダーアプリが動画または音声のライブ配信を再生している場合、SDK はミニ コントローラの再生/一時停止ボタンの代わりに再生/停止ボタンを自動的に表示します。

センダーアプリで Cast ウィジェットの外観を構成する方法については、iOS センダー UI のカスタマイズをご覧ください。

ミニ コントローラを送信側アプリに追加する方法は 2 つあります。

  • 既存のビュー コントローラを独自のビュー コントローラでラップして、キャスト フレームワークにミニ コントローラのレイアウトを管理させます。
  • ミニ コントローラ ウィジェットのレイアウトを自分で管理するには、ストーリーボードでサブビューを指定して、既存のビュー コントローラに追加します。

GCKUICastContainerViewController を使用してラップする

1 つ目の方法は、別のビュー コントローラをラップし、最下部に GCKUIMiniMediaControlsViewController を追加する GCKUICastContainerViewController を使用することです。このアプローチでは、アニメーションをカスタマイズしたり、コンテナ ビュー コントローラの動作を構成したりすることはできません。

通常、この方法はアプリ デリゲートの -[application:didFinishLaunchingWithOptions:] メソッドで行います。

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

  // Wrap main view in the GCKUICastContainerViewController and display the mini controller.
  let appStoryboard = UIStoryboard(name: "Main", bundle: nil)
  let navigationController = appStoryboard.instantiateViewController(withIdentifier: "MainNavigation")
  let castContainerVC =
          GCKCastContext.sharedInstance().createCastContainerController(for: navigationController)
  castContainerVC.miniMediaControlsItemEnabled = true
  window = UIWindow(frame: UIScreen.main.bounds)
  window!.rootViewController = castContainerVC
  window!.makeKeyAndVisible()

  ...
}
Objective-C
- (BOOL)application:(UIApplication *)application
        didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  ...

  // Wrap main view in the GCKUICastContainerViewController and display the mini controller.
  UIStoryboard *appStoryboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
  UINavigationController *navigationController =
          [appStoryboard instantiateViewControllerWithIdentifier:@"MainNavigation"];
  GCKUICastContainerViewController *castContainerVC =
          [[GCKCastContext sharedInstance] createCastContainerControllerForViewController:navigationController];
  castContainerVC.miniMediaControlsItemEnabled = YES;
  self.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
  self.window.rootViewController = castContainerVC;
  [self.window makeKeyAndVisible];
  ...

}
Swift
var castControlBarsEnabled: Bool {
  set(enabled) {
    if let castContainerVC = self.window?.rootViewController as? GCKUICastContainerViewController {
      castContainerVC.miniMediaControlsItemEnabled = enabled
    } else {
      print("GCKUICastContainerViewController is not correctly configured")
    }
  }
  get {
    if let castContainerVC = self.window?.rootViewController as? GCKUICastContainerViewController {
      return castContainerVC.miniMediaControlsItemEnabled
    } else {
      print("GCKUICastContainerViewController is not correctly configured")
      return false
    }
  }
}
Objective-C

AppDelegate.h

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (nonatomic, strong) UIWindow *window;
@property (nonatomic, assign) BOOL castControlBarsEnabled;

@end

AppDelegate.m

@implementation AppDelegate

...

- (void)setCastControlBarsEnabled:(BOOL)notificationsEnabled {
  GCKUICastContainerViewController *castContainerVC;
  castContainerVC =
      (GCKUICastContainerViewController *)self.window.rootViewController;
  castContainerVC.miniMediaControlsItemEnabled = notificationsEnabled;
}

- (BOOL)castControlBarsEnabled {
  GCKUICastContainerViewController *castContainerVC;
  castContainerVC =
      (GCKUICastContainerViewController *)self.window.rootViewController;
  return castContainerVC.miniMediaControlsItemEnabled;
}

...

@end

既存のビュー コントローラに埋め込む

2 つ目の方法は、createMiniMediaControlsViewController を使用して GCKUIMiniMediaControlsViewController インスタンスを作成し、それをサブビューとしてコンテナ ビュー コントローラに追加することで、ミニ コントローラを既存のビュー コントローラに直接追加する方法です。

アプリ デリゲートでビュー コントローラを設定します。

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

  GCKCastContext.sharedInstance().useDefaultExpandedMediaControls = true
  window?.clipsToBounds = true

  let rootContainerVC = (window?.rootViewController as? RootContainerViewController)
  rootContainerVC?.miniMediaControlsViewEnabled = true

  ...

  return true
}
Objective-C
- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  ...

  [GCKCastContext sharedInstance].useDefaultExpandedMediaControls = YES;

  self.window.clipsToBounds = YES;

  RootContainerViewController *rootContainerVC;
  rootContainerVC =
      (RootContainerViewController *)self.window.rootViewController;
  rootContainerVC.miniMediaControlsViewEnabled = YES;

  ...

  return YES;
}

ルート ビュー コントローラで GCKUIMiniMediaControlsViewController インスタンスを作成し、サブビューとしてコンテナ ビュー コントローラに追加します。

Swift
let kCastControlBarsAnimationDuration: TimeInterval = 0.20

@objc(RootContainerViewController)
class RootContainerViewController: UIViewController, GCKUIMiniMediaControlsViewControllerDelegate {
  @IBOutlet weak private var _miniMediaControlsContainerView: UIView!
  @IBOutlet weak private var _miniMediaControlsHeightConstraint: NSLayoutConstraint!
  private var miniMediaControlsViewController: GCKUIMiniMediaControlsViewController!
  var miniMediaControlsViewEnabled = false {
    didSet {
      if self.isViewLoaded {
        self.updateControlBarsVisibility()
      }
    }
  }

  var overriddenNavigationController: UINavigationController?

  override var navigationController: UINavigationController? {

    get {
      return overriddenNavigationController
    }

    set {
      overriddenNavigationController = newValue
    }
  }
  var miniMediaControlsItemEnabled = false

  override func viewDidLoad() {
    super.viewDidLoad()
    let castContext = GCKCastContext.sharedInstance()
    self.miniMediaControlsViewController = castContext.createMiniMediaControlsViewController()
    self.miniMediaControlsViewController.delegate = self
    self.updateControlBarsVisibility()
    self.installViewController(self.miniMediaControlsViewController,
                               inContainerView: self._miniMediaControlsContainerView)
  }

  func updateControlBarsVisibility() {
    if self.miniMediaControlsViewEnabled && self.miniMediaControlsViewController.active {
      self._miniMediaControlsHeightConstraint.constant = self.miniMediaControlsViewController.minHeight
      self.view.bringSubview(toFront: self._miniMediaControlsContainerView)
    } else {
      self._miniMediaControlsHeightConstraint.constant = 0
    }
    UIView.animate(withDuration: kCastControlBarsAnimationDuration, animations: {() -> Void in
      self.view.layoutIfNeeded()
    })
    self.view.setNeedsLayout()
  }

  func installViewController(_ viewController: UIViewController?, inContainerView containerView: UIView) {
    if let viewController = viewController {
      self.addChildViewController(viewController)
      viewController.view.frame = containerView.bounds
      containerView.addSubview(viewController.view)
      viewController.didMove(toParentViewController: self)
    }
  }

  func uninstallViewController(_ viewController: UIViewController) {
    viewController.willMove(toParentViewController: nil)
    viewController.view.removeFromSuperview()
    viewController.removeFromParentViewController()
  }

  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "NavigationVCEmbedSegue" {
      self.navigationController = (segue.destination as? UINavigationController)
    }
  }

...
Objective-C

RootContainerViewController.h

static const NSTimeInterval kCastControlBarsAnimationDuration = 0.20;

@interface RootContainerViewController () <GCKUIMiniMediaControlsViewControllerDelegate> {
  __weak IBOutlet UIView *_miniMediaControlsContainerView;
  __weak IBOutlet NSLayoutConstraint *_miniMediaControlsHeightConstraint;
  GCKUIMiniMediaControlsViewController *_miniMediaControlsViewController;
}

@property(nonatomic, weak, readwrite) UINavigationController *navigationController;

@property(nonatomic, assign, readwrite) BOOL miniMediaControlsViewEnabled;
@property(nonatomic, assign, readwrite) BOOL miniMediaControlsItemEnabled;

@end

RootContainerViewController.m

@implementation RootContainerViewController

- (void)viewDidLoad {
  [super viewDidLoad];
  GCKCastContext *castContext = [GCKCastContext sharedInstance];
  _miniMediaControlsViewController =
      [castContext createMiniMediaControlsViewController];
  _miniMediaControlsViewController.delegate = self;

  [self updateControlBarsVisibility];
  [self installViewController:_miniMediaControlsViewController
              inContainerView:_miniMediaControlsContainerView];
}

- (void)setMiniMediaControlsViewEnabled:(BOOL)miniMediaControlsViewEnabled {
  _miniMediaControlsViewEnabled = miniMediaControlsViewEnabled;
  if (self.isViewLoaded) {
    [self updateControlBarsVisibility];
  }
}

- (void)updateControlBarsVisibility {
  if (self.miniMediaControlsViewEnabled &&
      _miniMediaControlsViewController.active) {
    _miniMediaControlsHeightConstraint.constant =
        _miniMediaControlsViewController.minHeight;
    [self.view bringSubviewToFront:_miniMediaControlsContainerView];
  } else {
    _miniMediaControlsHeightConstraint.constant = 0;
  }
  [UIView animateWithDuration:kCastControlBarsAnimationDuration
                   animations:^{
                     [self.view layoutIfNeeded];
                   }];
  [self.view setNeedsLayout];
}

- (void)installViewController:(UIViewController *)viewController
              inContainerView:(UIView *)containerView {
  if (viewController) {
    [self addChildViewController:viewController];
    viewController.view.frame = containerView.bounds;
    [containerView addSubview:viewController.view];
    [viewController didMoveToParentViewController:self];
  }
}

- (void)uninstallViewController:(UIViewController *)viewController {
  [viewController willMoveToParentViewController:nil];
  [viewController.view removeFromSuperview];
  [viewController removeFromParentViewController];
}

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
  if ([segue.identifier isEqualToString:@"NavigationVCEmbedSegue"]) {
    self.navigationController =
        (UINavigationController *)segue.destinationViewController;
  }
}

...

@end

GCKUIMiniMediaControlsViewControllerDelegate は、ミニ コントローラを表示するタイミングをホスト ビュー コントローラに伝えます。

Swift
  func miniMediaControlsViewController(_: GCKUIMiniMediaControlsViewController,
                                       shouldAppear _: Bool) {
    updateControlBarsVisibility()
  }
Objective-C
- (void)miniMediaControlsViewController:
            (GCKUIMiniMediaControlsViewController *)miniMediaControlsViewController
                           shouldAppear:(BOOL)shouldAppear {
  [self updateControlBarsVisibility];
}

拡張コントローラを追加する

Google Cast デザイン チェックリストでは、センダーアプリに、キャストするメディアの拡張コントローラを表示するよう規定されています。拡張コントローラは、ミニ コントローラの全画面バージョンです。

拡張コントローラは全画面表示され、リモートのメディア再生をすべて操作できます。このビューを使用して、キャストアプリでキャスト セッションの管理可能なすべての操作を制御できるようにする必要があります(Web レシーバーの音量調節と、接続やキャストの停止などセッションのライフサイクル制御を除く)。このコントローラには、メディア セッションに関するすべてのステータス情報(アートワーク、タイトル、サブタイトルなど)が表示されます。

このビューの機能は、GCKUIExpandedMediaControlsViewController クラスによって実装されます。

まず、キャスト コンテキストでデフォルトの拡張コントローラを有効にする必要があります。アプリのデリゲートを変更して、デフォルトの拡張コントローラを有効にします。

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

  GCKCastContext.sharedInstance().useDefaultExpandedMediaControls = true

  ...
}
Objective-C
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  ...

  [GCKCastContext sharedInstance].useDefaultExpandedMediaControls = YES;

  ..
}

次のコードをビュー コントローラに追加して、ユーザーが動画のキャストを開始したときに拡張コントローラを読み込みます。

Swift
func playSelectedItemRemotely() {
  GCKCastContext.sharedInstance().presentDefaultExpandedMediaControls()

  ...

  // Load your media
  sessionManager.currentSession?.remoteMediaClient?.loadMedia(mediaInformation)
}
Objective-C
- (void)playSelectedItemRemotely {
  [[GCKCastContext sharedInstance] presentDefaultExpandedMediaControls];

  ...

  // Load your media
  [self.sessionManager.currentSession.remoteMediaClient loadMedia:mediaInformation];
}

拡張コントローラは、ユーザーがミニ コントローラをタップしたときも自動的に起動します。

センダーアプリが動画または音声のライブ配信を再生している場合、SDK は拡張コントローラの再生/一時停止ボタンの代わりに再生/停止ボタンを自動的に表示します。

送信側アプリでキャスト ウィジェットの外観を構成する方法については、iOS アプリにカスタム スタイルを適用するをご覧ください。

音量の調整

Cast フレームワークは、送信側アプリの音量を自動的に管理します。フレームワークは、指定された UI ウィジェットのウェブ レシーバーの音量と自動的に同期します。アプリが提供するスライダーを同期するには、GCKUIDeviceVolumeController を使用します。

物理ボタンによる音量調節

送信側デバイスの物理的な音量ボタンを使用して、GCKCastContext で設定された GCKCastOptionsphysicalVolumeButtonsWillControlDeviceVolume フラグを使用して、ウェブ レシーバの Cast セッションの音量を変更できます。

Swift
let criteria = GCKDiscoveryCriteria(applicationID: kReceiverAppID)
let options = GCKCastOptions(discoveryCriteria: criteria)
options.physicalVolumeButtonsWillControlDeviceVolume = true
GCKCastContext.setSharedInstanceWith(options)
Objective-C
GCKDiscoveryCriteria *criteria = [[GCKDiscoveryCriteria alloc]
                                          initWithApplicationID:kReceiverAppID];
GCKCastOptions *options = [[GCKCastOptions alloc]
                                          initWithDiscoveryCriteria :criteria];
options.physicalVolumeButtonsWillControlDeviceVolume = YES;
[GCKCastContext setSharedInstanceWithOptions:options];

エラーを処理する

送信側アプリがすべてのエラー コールバックを処理し、Cast ライフサイクルの各ステージに最適なレスポンスを決定することは非常に重要です。アプリは、ユーザーにエラー ダイアログを表示するか、キャスト セッションを終了するかを決定できます。

GCKErrorCode GCKErrorCodeCancelled などの一部のエラーは、意図された動作です。

GCKErrorCodeCancelled で失敗した接続の再試行は行わないでください。上記のように設定しないと予期しない動作が発生する恐れがあります。

ロギング

GCKLogger は、フレームワークによるロギングに使用されるシングルトンです。GCKLoggerDelegate を使用して、ログ メッセージの処理方法をカスタマイズします。

GCKLogger を使用すると、SDK はデバッグ メッセージ、エラー、警告の形式でロギング出力を生成します。これらのログ メッセージはデバッグに役立ち、トラブルシューティングや問題の特定に役立ちます。デフォルトでは、ログ出力は抑制されますが、GCKLoggerDelegate を割り当てることで、送信側アプリは SDK からこれらのメッセージを受信し、システム コンソールに記録できます。

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

  var window: UIWindow?

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

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

    ...
  }

  // MARK: - GCKLoggerDelegate

  func logMessage(_ message: String,
                  at level: GCKLoggerLevel,
                  fromFunction function: String,
                  location: String) {
    if (kDebugLoggingEnabled) {
      print(function + " - " + message)
    }
  }
}
Objective-C

AppDelegate.h

@interface AppDelegate () <GCKLoggerDelegate>
@end

AppDelegate.m

@implementation AppDelegate

static NSString *const kReceiverAppID = @"AABBCCDD";
static const BOOL kDebugLoggingEnabled = YES;

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  ...

  // Enable logger.
  [GCKLogger sharedInstance].delegate = self;

  ...

  return YES;
}

...

#pragma mark - GCKLoggerDelegate

- (void)logMessage:(NSString *)message
           atLevel:(GCKLoggerLevel)level
      fromFunction:(NSString *)function
          location:(NSString *)location {
  if (kDebugLoggingEnabled) {
    NSLog(@"%@ - %@, %@", function, message, location);
  }
}

@end

デバッグ メッセージと詳細メッセージも有効にするには、デリゲートの設定(前述)後に次の行をコードに追加します。

Swift
let filter = GCKLoggerFilter.init()
filter.minimumLevel = GCKLoggerLevel.verbose
GCKLogger.sharedInstance().filter = filter
Objective-C
GCKLoggerFilter *filter = [[GCKLoggerFilter alloc] init];
[filter setMinimumLevel:GCKLoggerLevelVerbose];
[GCKLogger sharedInstance].filter = filter;

GCKLogger によって生成されたログメッセージをフィルタすることもできます。クラスごとに最小ロギング レベルを設定します。次に例を示します。

Swift
let filter = GCKLoggerFilter.init()
filter.setLoggingLevel(GCKLoggerLevel.verbose, forClasses: ["GCKUICastButton",
                                                            "GCKUIImageCache",
                                                            "NSMutableDictionary"])
GCKLogger.sharedInstance().filter = filter
Objective-C
GCKLoggerFilter *filter = [[GCKLoggerFilter alloc] init];
[filter setLoggingLevel:GCKLoggerLevelVerbose
             forClasses:@[@"GCKUICastButton",
                          @"GCKUIImageCache",
                          @"NSMutableDictionary"
                          ]];
[GCKLogger sharedInstance].filter = filter;

クラス名は、リテラル名または glob パターン(GCKUI\*GCK\*Session など)にできます。