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

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

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

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

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

アプリケーションの流れ

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

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

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

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

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

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

Cast フレームワークには、フレームワークのすべてのアクティビティを調整するグローバル シングルトン オブジェクト 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

Cast UX ウィジェット

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

  • 紹介オーバーレイ: 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 セッションというコンセプトが導入されています。Cast セッションを確立するには、デバイスへの接続、ウェブ レシーバー アプリの起動(または参加)、そのアプリへの接続、メディア コントロール チャンネルの初期化という手順を組み合わせます。キャスト セッションとウェブ レシーバーのライフサイクルの詳細については、ウェブ レシーバーのアプリケーション ライフサイクル ガイドをご覧ください。

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

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

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

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

ストリーミング転送

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

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

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

自動再接続

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

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

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

メディアの名前空間をサポートするウェブ レシーバー アプリでキャスト セッションが確立されると、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 動画形式

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

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

Cast デザイン チェックリストによると、送信側アプリは、ユーザーが現在のコンテンツ ページから移動したときに表示されるミニ コントローラと呼ばれる永続的なコントロールを提供する必要があります。ミニ コントローラを使用すると、現在のキャスト セッションにすぐにアクセスでき、セッションの状況を視覚的に確認できます。

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

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

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

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

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

GCKUICastContainerViewController を使用してラップする

1 つ目の方法は、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];
}

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

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

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

音量の調整

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

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

送信側デバイスの物理的な音量ボタンを使用して、ウェブ レシーバーのキャスト セッションの音量を変更できます。これには、GCKCastContext に設定されている GCKCastOptions の physicalVolumeButtonsWillControlDeviceVolume フラグを使用します。

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 など)のいずれかです。