Cast を iOS アプリに統合する

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

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

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

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

アプリケーションの流れ

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

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

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

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

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

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

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

GCKCastContext を初期化するときに、GCKCastOptions オブジェクトを指定する必要があります。このクラスには、フレームワークの動作に影響を与えるオプションが含まれています。最も重要なのは Web Receiver アプリケーション ID です。この 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

キャスト 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 を実装しない限り、検出プロセスを明示的に開始または停止する必要はありません。

フレームワーク内の検出は、GCKDiscoveryManager クラス(GCKCastContext のプロパティ)によって管理されます。フレームワークには、デバイスの選択と操作用にデフォルトのキャスト ダイアログ コンポーネントが用意されています。デバイスリストは、デバイス フレンドリーな名前で辞書順に並べ替えられます。

セッション管理の仕組み

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

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

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

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

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

ストリーミング転送

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

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

詳細については、Web Receiver でのストリーム転送をご覧ください。

自動再接続

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

  • 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 が用意されています。このコントロールは、ミニ コントローラを表示するシーンに追加できます。

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

送信側アプリがキャスト ウィジェットの外観を設定する方法について詳しくは、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 の設計チェックリストでは、送信側メディアの拡張コントローラをセンダーアプリで指定する必要があります。拡張コントローラは、ミニ コントローラの全画面バージョンです。

展開されたコントローラは全画面ビューであり、リモート メディアの再生を完全に制御できます。このビューでは、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];
}

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

送信側アプリが動画または音声のライブ ストリームを再生しているとき、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)。