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

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

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

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

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

アプリの流れ

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

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

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

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

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

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

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

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

-[application:didFinishLaunchingWithOptions:] メソッドは、フレームワークからロギング メッセージを受信するロギング デリゲートのセットアップにも使用できます。この情報はデバッグやトラブルシューティングに役立ちます。

Swift
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, GCKLoggerDelegate {
  let kReceiverAppID = kGCKDefaultMediaReceiverApplicationID
  let kDebugLoggingEnabled = true

  var window: UIWindow?

  func applicationDidFinishLaunching(_ application: UIApplication) {
    let criteria = GCKDiscoveryCriteria(applicationID: kReceiverAppID)
    let options = GCKCastOptions(discoveryCriteria: criteria)
    GCKCastContext.setSharedInstanceWith(options)

    // Enable logger.
    GCKLogger.sharedInstance().delegate = self

    ...
  }

  // MARK: - GCKLoggerDelegate

  func logMessage(_ message: String,
                  at level: GCKLoggerLevel,
                  fromFunction function: String,
                  location: String) {
    if (kDebugLoggingEnabled) {
      print(function + " - " + message)
    }
  }
}
Objective-C

AppDelegate.h

@interface AppDelegate () <GCKLoggerDelegate>
@end

AppDelegate.m

@implementation AppDelegate

static NSString *const kReceiverAppID = @"AABBCCDD";
static const BOOL kDebugLoggingEnabled = YES;

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  GCKDiscoveryCriteria *criteria = [[GCKDiscoveryCriteria alloc]
                                    initWithApplicationID:kReceiverAppID];
  GCKCastOptions *options = [[GCKCastOptions alloc] initWithDiscoveryCriteria:criteria];
  [GCKCastContext setSharedInstanceWithOptions:options];

  // Enable logger.
  [GCKLogger sharedInstance].delegate = self;

  ...

  return YES;
}

...

#pragma mark - GCKLoggerDelegate

- (void)logMessage:(NSString *)message
           atLevel:(GCKLoggerLevel)level
      fromFunction:(NSString *)function
          location:(NSString *)location {
  if (kDebugLoggingEnabled) {
    NSLog(@"%@ - %@, %@", function, message, location);
  }
}

@end

キャスト UX ウィジェット

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

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

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

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

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

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

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

ストリーミング転送

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

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

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

自動再接続

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

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

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

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

ウェブ レシーバーにリクエストを発行する 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 動画形式

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

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

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

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

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

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

音量の調整

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

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

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

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

エラーを処理する

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

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 など)にできます。