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

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

モバイル デバイスまたはノートパソコンが再生を制御する送信側となり、Google Cast デバイスが受信側としてテレビにコンテンツを表示します。

送信側フレームワークとは、送信側で実行される、キャスト クラス ライブラリ バイナリと関連リソースを指します。「送信側アプリ」または「キャストアプリ」とは、送信側アプリでも実行されているアプリのことです。Web Receiver アプリとは、Web Receiver で実行される HTML アプリケーションのことです。

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

アプリケーションの流れ

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

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

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

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

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

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

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

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

-[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 Design チェックリストに準拠した以下のウィジェットが用意されています。

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

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

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

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

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

ストリーミング転送

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

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

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

自動再接続

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

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

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

メディア名前空間をサポートする Web Receiver アプリでキャスト セッションが確立されると、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 があり、ミニ コントローラを表示するシーンに追加できます。

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

送信側アプリで Cast ウィジェットの外観を設定する方法は、iOS の送信者 UI をカスタマイズするをご覧ください。

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

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

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 ウィジェットの Web Receiver の音量と自動的に同期されます。アプリが提供するスライダーを同期するには、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;

クラス名は、リテラル名または glob パターンのいずれかにできます(例: GCKUI\*GCK\*Session)。