iOS用音楽アプリのGoogle Cast対応 | サイバーエージェント 公式エンジニアブログ

はじめに

こんにちは。
AWAのiOS版を開発している岐部と申します。

まず、この記事を書こうと考えた背景から説明します。
AWAには2014年にAWA開発チームが結成されたタイミングでジョインし、それ以来ずっとiOSアプリを開発しています。
Androidアプリを業務で開発した経験はありません。

そんな中、iOS版AWAをGoogle Castに対応させる事になりました。
早速対応方法を調査していたのですが、AndroidアプリのGoogle Cast対応の方法が記載された記事等はそれなりに見つかるものの、iOSエンジニア向けの資料はあまり多くなく、感覚を掴むまでに少々苦労してしまいました。

そこで、シンプルなGoogle Cast対応アプリが作れるようになるところまで、iOSエンジニアの視点で解説する記事を書くことにしました。

Google Castとは

スマホアプリで再生する音楽や動画を他のデバイスに配信する機能の名称です。
主に、家庭にある大きなテレビやスピーカーでコンテンツを楽しむ用途で使用されています。

「Google Cast」という名称をあまり聞いたことが無い方でも、「Chromecast」はご存知かもしれませんね。
ChromecastはGoogle Castに対応している代表的な製品の一つです。

Google Castのシステム構成

Google Castを理解するためには、まず「Sender」と「Receiver」という2つの登場人物を把握しておく必要があります。

Sender = iPhone

Senderには、iPhone等が相当します。
Senderは、Chromecast等のGoogle Cast対応デバイスに、再生すべきコンテンツを指示する役割を持っています。
あくまでSenderは「指示する」のみで、実際に動画や音楽を再生する処理はReceiverに任せます。

Receiver = Chromecast

Receiverには、Chromecast等のGoogle Cast対応デバイスが相当します。
Receiverは、Senderから渡されたコンテンツ情報を基に、動画や音楽を再生する処理を担います。

AirPlayとの違い

Appleにも「AirPlay」という似た仕様があるのをご存知でしょうか。
特にAppleTVをお持ちの方なら、AirPlayを利用した事がある方も多いと思います。

AirPlayはスマートフォン上の動画/音楽をテレビで楽しむための、Appleの規格です。
多少のトラブルはあれど AVPlayer 等のiOSの標準的なAPIを使っていればほぼ自動的に対応が完了するため、対応アプリもたくさん存在します。

Google CastとAirPlayで明確に違いが出るのは、以下の様なシーンです。

機能 Google Cast AirPlay
アプリが終了された後も、テレビ側の再生を継続できるか ×
複数のスマートフォンが同時に接続できるか ×
iTunesライブラリに同期してある楽曲等、ローカル環境にしか無いファイルの再生ができるか ×
アプリのバックグラウンド実行時、iPhoneアプリが再生状態を監視/制御できるか ×


AirPlayはスマートフォンが本体であり、AppleTVを外部ディスプレイとして利用するイメージです。
再生・一時停止等のコントロールは全てスマートフォン側で行います。

一方、Google Castでは再生制御を行うのはあくまでReceiver側であり、SenderはReceiverに対して命令を行うコントローラーのような立場に過ぎません。
更に、実装方法によっては複数のSenderから一つのReceiverに接続するような1:nの接続(!)も可能になります。


この辺りをはじめに理解しておくと、この先も躓きにくいと思います。

注意点

実際に作るときはガイドラインを意識しましょう

User Experience with Google Castのページに、Google Castに対応させる際のガイドラインが記載されています。
この記事ではコード量を必要最低限に抑えるためにガイドラインに沿っていない箇所にも目を瞑って実装していますが、実際にGoogle Castに対応する際はこのガイドラインに準拠するように意識してください。

iOSとAndroidでガイドラインの内容が異なる箇所がある点にも注意が必要です。

Receiverの準備

では早速、実際に制作してみましょう。
まずはReceiver側の準備から行います。

Google Cast SDK Developer Consoleに登録

Google Cast SDK Developer Console に初めてアクセスすると、Welcomeページが表示されます。
この画面で$5(USD)を支払う必要があるので、ご注意ください。

Google Cast SDK Developer ConsoleにReceiverアプリを登録

Google CastはReceiverアプリをカスタマイズすることも可能です。
しかし、今回は話をシンプルにするためにデフォルト状態のReceiverを使用することにします。

Applicationの登録

  1. 「ADD NEW APPLICATION」を選択します。

  2. 「Styled Media Receiver」を選択します。

  3. Receiverアプリに好きな名称をつけて「Save」ボタンを押すと保存されます。

ここまでで、Applicationの登録は完了です。

しかし、まだSenderから接続できる状態にはなっていません。
Senderから接続させるためには、「Publish(※1)」操作を行うか、「デバイスの登録(※2)」操作を行う必要があります。

※1…このReceiverアプリが公開されます。ApplicationIDを知っているSenderであれば誰でも接続できる状態になります。
※2…ApplicationIDを知っているSenderであっても、登録した特定のReceiver端末にしか接続できません。


Publish操作を行うためには、iOSアプリの以下の情報の登録が必要になります。

  • iTunes ID
  • Bundle ID
  • App Launch URI

iTunes IDはアプリをiTunes Connectに登録しないと発行されませんが、今回のアプリはAppStoreに公開するつもりも無いので、Publish操作ではなくデバイスの登録操作を行う事にします。

Receiverのデバイス登録

  1. Developer Console上の「ADD NEW DEVICE」を選択します。

  2. シリアル番号と説明文を入力して、「OK」を選択します。※シリアル番号はChromecast本体の裏面にプリントされています。


以上でReceiverの準備は完了です。

デバイスの登録完了までは、15分程度かかります。
私が試した時は、Statusが「Ready for Testing」になった後、Chromecastを再起動するとSenderから認識されるようになりました。

Senderの実装

次に、いよいよSenderの実装をしていきます。

今回はAppleのSearch APIを使用して、iTunes Storeのランキング情報から再生する楽曲の情報・音源を取得しています。
サンプルコードはこちらです。

sender

※権利関係に配慮してタイトルは「Sound**」としていますが、実際のアプリではトラック名が表示されます。

必要なフレームワークを登録

公式ドキュメント(iOS App Development | Cast | Google Developers)に必要なフレームワークの一覧があるので、それに従って登録していきます。
※GoogleCast.frameworkだけは手動ではなくCocoaPodsでインストールしました。

XcodeのLinked Frameworks and Librariesセクション

Podfile

platform :ios, '8.0'

pod 'google-cast-sdk'

Receiverに接続する

接続では、以下の3ステップを踏んでいきます。

  1. 接続可能なReceiverデバイスを検索
  2. 接続するReceiverデバイスを選択
  3. 接続先のReceiverデバイスにアプリケーションを起動させる

1. 接続可能なReceiverデバイスを検索

// Receiverデバイスを検索(Receiver App Idによるフィルター有)
self.deviceScanner = GCKDeviceScanner(filterCriteria: GCKFilterCriteria(forAvailableApplicationWithID: YOUR_RECEIVER_APP_ID))

// 検索開始
if let deviceScanner = self.deviceScanner {
    deviceScanner.addListener(self)
    deviceScanner.startScan()
    deviceScanner.passiveScan = true
}

アプリ起動と同時に、Receiverデバイスの検索を開始します。
なお、GCKDeviceScannerの初期化には引数の無い GCKDeviceScanner() の方法も存在しますが、こちらは deprecated 扱いなので使用しないようにしてください。

GCKFilterCriteria の初期化に使用している YOUR_RECEIVER_APP_ID は、Google Cast SDK Developer Consoleで先ほど登録したApplicationの「Application ID」の箇所に記載されているIDです。

2. 接続するReceiverデバイスを選択

画面下部の「Switch Casting」ボタンが押されたら、デバイスを選択するアクションシートを表示します。

let alertController = UIAlertController(title: nil, message: "Select device to cast", preferredStyle: .ActionSheet)
deviceScanner.passiveScan = false
for device in devices {
    alertController.addAction(
        UIAlertAction(
            title: device.friendlyName,
            style: .Default,
            handler: { (action: UIAlertAction) -> Void in
                
                // (選択したデバイスへの接続操作/後述)
                
                deviceScanner.passiveScan = true
                self.castButton.hidden = false
        }))
}
alertController.addAction(
    UIAlertAction(
        title: "キャンセル",
        style: .Cancel,
        handler: { (action: UIAlertAction) -> Void in
            deviceScanner.passiveScan = true
            self.castButton.hidden = false
    }))
presentViewController(alertController, animated: true, completion: nil)

ユーザーがデバイスを選択したら、そのデバイスに接続します。

let deviceManager = GCKDeviceManager(device: device, clientPackageName: NSBundle.mainBundle().bundleIdentifier)
self.deviceManager = deviceManager
deviceManager.delegate = self
deviceManager.connect()

これでアプリがReceiverへの接続を試みてくれます。
deviceManager.delegate = self を実行しておくことで、接続の状況に応じてdelegateメソッドが呼ばれるようになります。

接続は、以下の2ステップに分けて行います。

  1. Receiverデバイスへの接続
  2. Receiverデバイス上で動作するアプリケーションへの接続
func deviceManagerDidConnect(deviceManager: GCKDeviceManager!) {
    self.deviceManager?.launchApplication(YOUR_RECEIVER_APP_ID)
}

deviceManagerDidConnectGCKDeviceManagerDelegate のdelegateメソッドで、上記「1. Receiverデバイスへの接続」が正常に完了した時に実行されます。
ここで self.deviceManager?.launchApplication(YOUR_RECEIVER_APP_ID) を実行することで、上記「2. Receiverデバイス上で動作するアプリケーションへの接続」のステップに移行します。

func deviceManager(deviceManager: GCKDeviceManager!, didConnectToCastApplication applicationMetadata: GCKApplicationMetadata!, sessionID: String!, launchedApplication: Bool) {

    // (楽曲のロード操作/後述)

}

deviceManager:didConnectToCastApplication:sessionID:launchedApplicationGCKDeviceManagerDelegate のdelegateメソッドで、上記「2. Receiverデバイス上で動作するアプリケーションへの接続」が正常に完了した時に実行されます。
SenderとReceiverの接続が完了したので、この段階から楽曲情報をReceiverに渡せるようになります。

APIから楽曲情報を取得する箇所を実装する

ここはGoogle Castに直接関係のある実装ではないので、詳細は省きます。
サンプルコードでは、 DataSourceService 内でSearch APIのJSONから楽曲情報を取り出し、それらを扱いやすいように EntityTrack という独自のEntityの配列に変換しています。

再生したい楽曲をロードさせる

楽曲のメディア情報をReceiverに渡すため、楽曲情報を GCKMediaInformation に変換します。

func generateMediaInformation(track: EntityTrack) -> GCKMediaInformation {
    let metadata = GCKMediaMetadata()
    metadata.setString(track.trackName, forKey: kGCKMetadataKeyTitle)
    metadata.setString(track.artistName, forKey: kGCKMetadataKeyArtist)
    metadata.setString(track.albumName, forKey: kGCKMetadataKeyAlbumTitle)
    if let url = NSURL(string: track.imageURLString) {
        metadata.addImage(GCKImage(URL: url, width: track.imageHeight, height: track.imageHeight))
    }
    
    return GCKMediaInformation(
        contentID: track.previewURLString,
        streamType: GCKMediaStreamType.Buffered,
        contentType: track.type,
        metadata: metadata,
        streamDuration: track.durationInSeconds,
        customData: nil)
}

変換し終えた GCKMediaInformation のインスタンスを、 GCKMediaControlChannel のインスタンス経由でReceiverに渡します。

self.deviceManager?.addChannel(self.mediaControlChannel)
self.mediaControlChannel.loadMedia(mediaInfo) // mediaInfo = GCKMediaInformationのインスタンス

これで無事に楽曲のアートワークがテレビに表示され、音源も再生されるはずです!
※権利関係に配慮してアートワーク画像とタイトルは差し替えてありますが、実際のアプリではトラック名とそのアートワークが表示されます。


ただ、この方法で対応出来るのは単一のメディア情報のみで、音楽アプリのような連続再生には対応できません。
連続再生をさせるためには、 GCKMediaQueueItem を使う必要があります。

再生したい楽曲情報のキューを渡す

複数楽曲の情報をSenderからReceiverに渡すときは、 GCKMediaQueueItem の配列を作ります。

var mediaQueueItems: [GCKMediaQueueItem] = []
for track in tracks {
    let queueItemBuilder = GCKMediaQueueItemBuilder()
    queueItemBuilder.startTime = 0
    queueItemBuilder.autoplay = true
    queueItemBuilder.preloadTime = 20
    queueItemBuilder.mediaInformation = self.generateMediaInformation(track)
    mediaQueueItems.append(queueItemBuilder.build())
}

変換し終えた GCKMediaQueueItem の配列を、 GCKMediaControlChannel のインスタンス経由でReceiverに渡します。

self.deviceManager?.addChannel(self.mediaControlChannel)
self.mediaControlChannel.queueLoadItems(
    mediaQueueItems,
    startIndex: startIndex,
    playPosition: 0,
    repeatMode: .Off,
    customData: nil)

これで、連続再生にも対応したSenderアプリが完成しました!

おわりに

私が実装し始めた頃の感想は「AirPlay対応と比べると、やることが多くて大変」でした。。
iOS版のGoogle Cast SDKはAndroid版と比較してもカバーしてくれる範囲が狭く、なかなかハードな開発でした。
(例)Castアイコンの表示切替は自分で実装しないといけない、標準のCast dialogが提供されていない など。

しかし、いざAWAのGoogle Cast対応版がリリースされると、ユーザーの皆様から好評な声が結構な数で挙がっていて、とても嬉しかったのを覚えています。
ChromecastはAppleTVと比べると安価なので、その分普及し易いのかもしれません。
自分で使っていても、大きなテレビ画面や良質なスピーカーで自分のアプリが動作している姿は壮観なので、一度試してみる事をオススメします!

最後まで読んでいただき、ありがとうございました。