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

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

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

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

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

アプリケーションの流れ

次の手順は、送信側の iOS アプリの一般的な実行フローの概要を示しています。

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

送信元のトラブルシューティングを行うには、ロギングを有効にする必要があります。

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

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

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

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

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

-[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

キャスト UX ウィジェット

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

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

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

  • ミニ コントローラ: ユーザーがコンテンツをキャストしていて、現在のコンテンツ ページから移動した場合や、送信元アプリでコントローラを展開して別の画面に移動した場合、ミニ コントローラが画面の下部に表示されます。これにより、ユーザーは現在キャスト中のメディアのメタデータを確認したり、再生を操作したりできます。

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

キャスト ボタンを追加する

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

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 セッションとウェブレシーバーのライフサイクルの詳細については、ウェブレシーバーのアプリケーション ライフサイクル ガイドをご覧ください。

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

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

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

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

ストリーミング転送

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

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

詳細については、ウェブ レシーバでのストリーミング転送をご覧ください。

自動再接続

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

  • Wi-Fi が一時的に切断された場合の復旧
  • デバイスのスリープ状態から復帰する
  • アプリのバックグラウンド処理から復元する
  • アプリがクラッシュした場合の復元

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

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

ウェブ レシーバにリクエストを発行する 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 TV 形式のタイプと、高さと幅(ピクセル単位)が含まれます。4K 形式のバリエーションは、hdrType プロパティの列挙値 GCKVideoInfoHDRType で示されます。

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

Cast デザイン チェックリストでは、送信側アプリにミニ コントローラと呼ばれる永続的なコントロールを用意し、ユーザーが現在のコンテンツ ページから離れたときに表示することが求められています。ミニ コントローラは、すばやくアクセスでき、現在のキャスト セッションの情報が表示されます。

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

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

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

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

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

GCKUICastContainerViewController を使用してラップする

最初の方法は、GCKUICastContainerViewController を使用して別のビュー コントローラをラップし、最下部に GCKUIMiniMediaControlsViewController を追加します。このアプローチには、アニメーションをカスタマイズできないこと、コンテナ ビュー コントローラの動作を構成できないという制限があります。

この最初の方法は通常、アプリ デリゲートの -[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 デザイン チェックリストでは、センダーアプリに、キャストするメディアの拡張コントローラを表示するよう規定されています。拡張コントローラは、ミニ コントローラの全画面バージョンです。

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

このビューの機能は、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];
}

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

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

送信元アプリで Cast ウィジェットの外観を構成する方法については、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 のライフサイクルの各ステージに最適なレスポンスを決定することが非常に重要です。アプリは、エラー ダイアログをユーザーに表示したり、キャスト セッションを終了したりできます。

ロギング

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

SDK は GCKLogger を使用して、デバッグ メッセージ、エラー、警告の形式でロギング出力を生成します。これらのログ メッセージはデバッグに役立ち、トラブルシューティングと問題の特定に役立ちます。デフォルトでは、ログ出力は抑制されますが、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;

クラス名は、GCKUI\*GCK\*Session などのリテラル名またはグロブパターンにすることができます。