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

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

モバイル デバイスまたはノートパソコンが再生を制御するセンダーで、Google Cast デバイスがテレビにコンテンツを表示するレシーバーです。

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

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

アプリケーションの流れ

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

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

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

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

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

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

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

GCKCastContext を初期化するときに、GCKCastOptions オブジェクトを指定する必要があります。このクラスには、フレームワークの動作に影響するオプションが含まれています。最も重要なのは、ウェブ レシーバー アプリケーション ID です。この 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 には、Cast デザイン チェックリストに準拠したウィジェットが用意されています。

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

  • キャストボタン: 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 SDK では、キャスト セッションのコンセプトが導入されています。キャスト セッションは、デバイスへの接続、ウェブ レシーバー アプリの起動(または参加)、そのアプリへの接続、メディア コントロール チャンネルの初期化の各ステップで構成されます。キャスト セッションと Web Receiver のライフサイクルについて詳しくは、Web Receiver のアプリケーション ライフサイクル ガイドをご覧ください。

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

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

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

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

ストリーミング転送

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

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

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

自動再接続

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

  • 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 動画フォーマット

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

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

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

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

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

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

センダーアプリにミニ コントローラを追加するには、次の 2 つの方法があります。

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

GCKUICastContainerViewController を使用してラップする

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

通常、この 1 つの方法はアプリ デリゲートの -[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 Receiver の音量調整とセッションのライフサイクル(接続/キャストの停止)を除いて、キャスト セッションの管理可能なすべての要素を管理できるようにする必要があります。また、メディア セッションに関するすべてのステータス情報(アートワーク、タイトル、サブタイトルなど)も表示されます。

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

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

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

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

音量調節

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

物理ボタンの音量調節

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

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];

エラーを処理する

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

ロギング

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 のように、リテラル名または glob パターンを使用できます。