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

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

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

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

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

アプリケーションの流れ

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

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

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

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

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

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

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

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

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

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

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

送信元アプリでキャスト ウィジェットの外観を構成する方法については、iOS Sender 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];
  ...

}
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