記憶回路断線中。。

SE→情シス→SE→また情シス(笑) IT系の困った事を徒然なるままに。。。

iOSからchromecastへRemote Display #4

Chromecast-2015

 

(前回記事)

yo-tan.hatenablog.com

 

remote display APIをプロジェクトに追加

前回は、google cast APIのプロジェクトへの取り込みは行なったが、remote display APIはまだでした。このAPIのプロジェクトへの追加から始める。

 

追加なんて簡単。google cast APIの時と同様に、以下の手順でframeworkを追加。

  • プロジェクトルートにGoogleCastRemoteDisplay.framework ディレクトリをコピー
  • finder上でGoogleCastRemoteDisplay.frameworkを選択し、xcodeにD&D

 

あらら。エラーが出る出る。

出てきたエラーをググると、ここにあたった。

plus.google.com

書いてある通り、Other Linker Flagsに以下を追加
("Undefined symbols"エラーを、Other Linker Flagsで解決することが可能なのね・・・)

-lsqlite3
-lz

 

これで、エラーが30個くらいから4個まで減ったが、まだある。今後はこっち。 

stackoverflow.com

書いてある通り、CoreMedia.frameworkをプロジェクトに追加。ビルドエラーなくなった!

 

ん?こんな、マニュアルに書いてもいない適当な手順で進めていいのかって?

いいんです!悪いのはマニュアル(笑) 動けばOK^^
appleさんが標準で持っているframeworkを使っているだけだしね)

 

これでエラーは無くなったが、 警告がまだある。

GCKRemoteDisplaySessionのヘッダ読み込み部分で、

Missing submodule 'AVFoundation.AVAudioTypes'

と言う警告が出る。気持ち悪いので対処。

stackoverflow.com

 

umbrella headerだけあればいいとのこと(そりゃそうだよね。昔は存在してなかったと言うことか・・・?)なので、警告発生行を、

 

before:

#import <AVFoundation/AVAudioTypes.h>

after:

#import <AVFoundation/AVFoundation.h>

 

に変更。

そのとき、「frameworkの中のソース、変更しちゃっていいの?」と言う警告のポップアップが出るが、unlockを押す。

GCKRemoteDisplaySession.h is locked for editing and you may not be able to save your changes. Do you want to unlock it?

 

2箇所変更したら、警告0になりましたー。

 

 

remote display のimport時のエラー

remoteDisplayを実装するには、"GCKRemoteDisplayChannelDelegate"と言うプロトコルを使用するのだが、これを下記のように書くと、エラーになってしまう。

import UIKit
import GoogleCast

@objc
class ViewController: UIViewController, GCKDeviceScannerListener, GCKDeviceManagerDelegate, GCKMediaControlChannelDelegate, GCKRemoteDisplayChannelDelegate {

 

普通に、 "import GoogleCastRemoteDisplay" のimport文がないからそうなってると思うのだが、普通に"import GoogleCastRemoteDisplay" と書こうとしても、「GoogleCastRemoteDisplayなんてものはありません」と怒られる。。。

なんでGoogleCast はimportできてるのに、こっちはできないんだ・・・

 

サンプルソースを見ると、objective-cなのだが、

#import <GoogleCast/GoogleCast.h>

#import <GoogleCastRemoteDisplay/GoogleCastRemoteDisplay.h>

 と書いてある。普通にimportできてるよねぇ。

 

よくわからないながら、いろいろやっていると、以下をやったらエラーがなくなった!

1. Test.h / Test.mのobjective-cのファイルを作成

2. 作成時に「Bridging Headerを作るか?」と聞かれるのでYES

3. 作成されたTest.hに、上記2つのimport文を記述

 

 

remote display のコード実装

ついに、全ての準備は整った。コードです。

remote displayでは、receiverに表示するframeを管理する、frameInputと言うobjectを使うのだが、公式サイトのマニュアルでは、それがmetalをベースとしたGCKMetalVideoFrameInputと言う実装クラスを使って実現している。

今回やろうとしているサイネージアプリでは、単純にUIViewの上に乗っているものをcastしてくれれば良いのだが、そんな仕組みではないのか!?

 

・・・ありました。

GCKMetalVideoFrameInputの親クラス:GCKVideoFrameInputには、3つの実装クラスがあって、前述のmetalベースのものの他に、以下がある。

GCKOpenGLESVideoFrameInput
GCKViewVideoFrameInput

名前だけで判断すると、GCKViewVideoFrameInputを使用すれば、なんだかうまく行きそう^^;

 

 

と言うわけで、もろもろ調べてやってみましたー。

import UIKit
import GoogleCast

@objc
class ViewController: UIViewController, GCKDeviceScannerListener, GCKDeviceManagerDelegate, GCKMediaControlChannelDelegate, GCKRemoteDisplayChannelDelegate {

	let APP_ID = "AAAA1111"; //取得したAPP_IDを設定
//	let APP_ID = kGCKMediaDefaultReceiverApplicationID;
	
	var deviceScanner: GCKDeviceScanner?;
	var deviceManager: GCKDeviceManager?;
	
	var sessionId: String?;
	
	var remoteDisplaySession: GCKRemoteDisplaySession?;
	var remoteDisplayChannel: GCKRemoteDisplayChannel?;
	
	var frameInput: GCKViewVideoFrameInput!

	// MARK:- UIViewController

	override func viewDidLoad() {
		super.viewDidLoad()
	}

	override func didReceiveMemoryWarning() {
		super.didReceiveMemoryWarning()
	}
	
	// MARK:- GCKDeviceScannerListener

	func deviceDidComeOnline(_ device: GCKDevice!) {
		print("Device found: \(device.friendlyName)");

		//connect
		deviceManager = GCKDeviceManager(device: device, clientPackageName: Bundle.main.bundleIdentifier)
		deviceManager!.delegate = self
		deviceManager!.connect()
	}
	
	func deviceDidGoOffline(_ device: GCKDevice!) {
		print("Device went away: \(device.friendlyName)");
	}
	
	func deviceDidChange(_ device: GCKDevice!) {
		print("Device did Change: \(device.friendlyName)");
	}

	// MARK:- GCKDeviceManagerDelegate

	func deviceManagerDidConnect(_ deviceManager: GCKDeviceManager!) {
		print("didConnect")
		let options = GCKLaunchOptions(relaunchIfRunning: true)
		deviceManager.launchApplication(APP_ID, with: options)
	}
	
	func deviceManager(_ deviceManager: GCKDeviceManager!, didConnectToCastApplication applicationMetadata: GCKApplicationMetadata!, sessionID: String!, launchedApplication: Bool) {
		print("Application has launched.")
		
		sessionId = sessionID;

//		normalCast();
		remoteDisplayCast();
	}
	
	// MARK:- GCKRemoteDisplayChannelDelegate
	
	func remoteDisplayChannelDidConnect(_ channel: GCKRemoteDisplayChannel) {
		let configuration = GCKRemoteDisplayConfiguration()
		configuration.videoStreamDescriptor.frameRate = .rate15p
		// Customize the configuration as needed.
		do {
			try channel.beginSession(with: configuration);
		} catch {
			print("test")
		}
	}
	
	func remoteDisplayChannel(_ channel: GCKRemoteDisplayChannel, didBegin session: GCKRemoteDisplaySession) {
		print("begin");
		remoteDisplaySession = session;
		frameInput = GCKViewVideoFrameInput(session: session);
		frameInput.view = self.view;
	}
	
	func remoteDisplayChannel(_ channel: GCKRemoteDisplayChannel, deviceRejectedConfiguration configuration: GCKRemoteDisplayConfiguration, error: Error?) {
		print("rejected");
	}

	// MARK:- other

	func remoteDisplayCast() {
		remoteDisplayChannel = GCKRemoteDisplayChannel();
		remoteDisplayChannel?.delegate = self;
		deviceManager?.add(remoteDisplayChannel);
	}
	
	@IBAction func scan() {
		// Establish filter criteria.
		let filterCriteria = GCKFilterCriteria(forAvailableApplicationWithID: APP_ID);
		
		// Initialize device scanner.
		deviceScanner = GCKDeviceScanner(filterCriteria: filterCriteria)
		if let deviceScanner = deviceScanner {
			deviceScanner.add(self)
			deviceScanner.startScan()
			deviceScanner.passiveScan = true
		}
	}
	
	@IBAction func animate() {
		
		let f: () -> () = {
			if self.view.backgroundColor == UIColor.white {
				self.view.backgroundColor = UIColor.red;
			} else {
				self.view.backgroundColor = UIColor.white;
			}
		}
		
		UIView.animate(withDuration: 3, animations: f);
	}
}

 

できた!

ちゃんと画面にviewが表示されました!

animateの処理は、ボタンを押すことで呼び出すようにしていて、単純にviewの色が徐々に変わるアニメーションをつけてます。chromecastでどのくらい滑らかに表示されるかを見るために。

 

現在テストしているchromecast環境は、Wi-Fiルータとテレビが離れていて、Wi-Fiの扇マークが3つ中2つって感じ。

携帯もほぼ同じ場所にいるので、あまりネットワーク環境としては良くないのだが、それでもそれなりに色の変化はそれっぽく表示されてくれた。

 

今回のソースは、(自分で)わかりやすくするために、エラー処理を敢えて抜いて描いてますが、きちんとしたアプリにはエラー処理は不可欠なので、そこはreferenceなりみながらやってみてください^^;

 

では、良いgooglecast lifeを!^^

 

iOSからchromecastへRemote Display #3

Chromecast-2015

 

(前回記事) 

yo-tan.hatenablog.com

 

 

swiftプロジェクトの準備

Develop iOS Sender App with Cast v2  |  Cast  |  Google Developers

引き続き、このサイトに書いてある通りに進める。

真ん中くらいにある"iOS Development"から。

First, follow the guide, iOS Sender App Development to get started with building your app for remote display. Follow the procedures in that document up to, but not including, Launch application before following the procedures in this guide.

 まずは、iOS Sender App Developmentのリンク先の手順をやりなさいと。Launch applicationの前の手順まで。

 

cocoapodsを使用するのが推奨、と書いてあるが、正しいPodfileの記述の例が見当たらなかったので、sampleに倣って進めてみる。

sampleタブから以下のリンクに行くことで、"CastRemoteDisplay-iOS"のソースを取得。

Develop iOS Sender App with Cast v2  |  Cast  |  Google Developers

 

ダウンロードしたプロジェクトに対し、以下の設定を変更。

1. pod install
2. Build SettingsのSigningを正しく設定
3. Bundle Identifierを適当に変更

 

・・・その他、いろいろ足掻いてみるが、なかなかビルドエラーが取れない。

xcconfigの設定とか複雑で、知識不足のたなやんにはよくわからない・・・

 

 

というわけで、マニュアルに書いてある、手動設定の方の手順で行きます^^;

ただし、使用する外部モジュールは、cocoapodでdownloadしたものにする。

※バージョンは以下。

Installing google-cast-remote-display-sdk (2.10.4)
Installing google-cast-sdk (2.10.4.1)

 

 
(必要なモジュールのインストールと各種初期設定)

    1. create a new xcode project から新規プロジェクト作成(single view app, swift)

    2. Build SettingsのSigningを正しく設定

    3. Bundle IDを、Google Cast SDK Developer Console で設定したApplication IDに対応するものに設定(google cast sdkは、Bundle IDを見ておらず、applicationIDのみで判断しているように見えるが念のため)

    4.  Other Linker Flagsに追加
      In your Xcode project, set the Other Linker Flags to -ObjC
      f:id:yo--tan:20180706083724p:plain
       
    5. 必要なframeworkの追加

      f:id:yo--tan:20180708164008p:plain

      とりあえず、GoogleCast.framework以外のものを一通り指定。
      その後、以下の通りにGoogleCast.frameworkを追加。

      5-1. このxcodeプロジェクトのルートディレクトリに、GoogleCast.frameworkをコピー(コンソールなどから)

      5-2. GoogleCast.frameworkの中にある"GoogleCastResources.bundle"を、framework直下のディレクトリから、その親ディレクトリ(framework外)に移動。framework内にそのファイルがあると、以下のリンク先のエラーが発生する。
      stackoverflow.com
      5-3. このframeworkをD&Dでxcode上に設置。コピーはせずreferenceで。

      5-4. 5-3までで、Target > GeneralのLinked Frameworks and Librariesの中にGoogleCast.frameworkが入っていると思うが、なければ追加(Embedded Binariesには入れない)

 

swiftコード記述(google cast sender API部分。remote display APIは次で^^;)

やっと、コードを書きます。本来のゴールはremote displayなのだが、ベースとしてgoogle cast sdk自体をわかっておかないと辛そうなので、まずはそこから。

 

事前準備として、scanするのをボタントリガーとするためボタンを追加。

f:id:yo--tan:20180709075344p:plain

  

そして、そのボタンを押すとscan() メソッドが呼ばれるように設定し、以下のコードを記述。

import UIKit
import GoogleCast

class ViewController: UIViewController, GCKDeviceScannerListener, GCKDeviceManagerDelegate, GCKMediaControlChannelDelegate {

	let APP_ID = kGCKMediaDefaultReceiverApplicationID;
	
	var deviceScanner: GCKDeviceScanner?;
	var deviceManager: GCKDeviceManager?;
	var mediaControlChannel: GCKMediaControlChannel?;
	var mediaInformation: GCKMediaInformation?;
	
	// MARK:- UIViewController

	override func viewDidLoad() {
		super.viewDidLoad()
	}

	override func didReceiveMemoryWarning() {
		super.didReceiveMemoryWarning()
	}
	
	// MARK:- GCKDeviceScannerListener

	func deviceDidComeOnline(_ device: GCKDevice!) {
		print("Device found: \(device.friendlyName)");

		//connect
		deviceManager = GCKDeviceManager(device: device, clientPackageName: Bundle.main.bundleIdentifier)
		deviceManager!.delegate = self
		deviceManager!.connect()
	}
	
	func deviceDidGoOffline(_ device: GCKDevice!) {
		print("Device went away: \(device.friendlyName)");
	}
	
	func deviceDidChange(_ device: GCKDevice!) {
		print("Device did Change: \(device.friendlyName)");
	}

	// MARK:- GCKDeviceManagerDelegate

	func deviceManagerDidConnect(_ deviceManager: GCKDeviceManager!) {
		print("didConnect")
		let options = GCKLaunchOptions(relaunchIfRunning: true)
		deviceManager.launchApplication(APP_ID, with: options)
	}
	
	func deviceManager(_ deviceManager: GCKDeviceManager!, didConnectToCastApplication applicationMetadata: GCKApplicationMetadata!, sessionID: String!, launchedApplication: Bool) {
		print("Application has launched.")
		mediaControlChannel = GCKMediaControlChannel()
		mediaControlChannel!.delegate = self
		deviceManager.add(mediaControlChannel)
		mediaControlChannel!.requestStatus()
		
		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)
		
		let url = NSURL(string:"https://commondatastorage.googleapis.com/gtv-videos-bucket/" +
			"sample/images/BigBuckBunny.jpg")
		metadata?.addImage(GCKImage(url: url! as URL, width: 480, height: 360));

		mediaInformation = GCKMediaInformation(
			contentID:
			"https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
			streamType: GCKMediaStreamType.none,
			contentType: "video/mp4",
			metadata: metadata,
			streamDuration: 0,
			mediaTracks: [],
			textTrackStyle: nil,
			customData: nil
		)
		
		// Cast the media
		mediaControlChannel!.loadMedia(mediaInformation, autoplay: true)
	}
	
	// MARK:- for this app
	
	@IBAction func scan() {
		// Establish filter criteria.
		let filterCriteria = GCKFilterCriteria(forAvailableApplicationWithID: APP_ID);
		
		// Initialize device scanner.
		deviceScanner = GCKDeviceScanner(filterCriteria: filterCriteria)
		if let deviceScanner = deviceScanner {
			deviceScanner.add(self)
			deviceScanner.startScan()
			deviceScanner.passiveScan = true
		}
	}
}

  

おお!できた!

googleさんが用意しているサンプル動画が、テレビに流れましたー。

複数のchromecast端末から対象を選択するところとかはとりあえず省いちゃったけど、これくらいの方が入り口としてはわかりやすいよね^^;

 

処理の流れとしては、以下のイメージ。

  1. deviceScanner.startScan() で、存在する端末を取得。
    (ただし、存在することがわかっているのであれば、省略も可能。)

  2. chromecastとonlineになった後に呼ばれるGCKDeviceScannerListener#deviceDidComeOnline() で、onlineになった(connectとはまた別なので用語に注意)端末とconnect

  3. 接続後に呼ばれるGCKDeviceManagerDelegate#deviceManagerDidConnect() で、アプリ起動

  4. アプリ起動後に呼ばれるGCKDeviceManagerDelegate#deviceManager(didConnectToCastApplication:) で、アプリからchromecastに渡すデータを処理して、渡す

 

では、次はついにremote displayの実装行きます!

 

 

yo-tan.hatenablog.com

 

iOSからchromecastへRemote Display #2

Chromecast-2015

 

(前回記事)yo-tan.hatenablog.com

 

 

本家サイトの読み込み

さて、能書きはこれくらいにして、実際の作業に入って行きましょー。

今回やりたいのは「Remote Display」。
どうやら、自分で自由に、テレビに表示するものを作れるようだ。

Remote Display  |  Cast  |  Google Developers

 

f:id:yo--tan:20180701075305p:plain

いきなりこんなこと書かれると、やる気削がれるねぇ・・・既に更新が止まっていて、今後消えゆくAPIなのか・・・?

まぁ、理解できていない「CAF」は使わなくていいみたいだから、一旦気にしないことにしよう^^;

 

f:id:yo--tan:20180702081612p:plain

そして、まだベータ版なの・・・大丈夫かねこれ。。

んっ!でも(まだ)くじけないっ!くじけそうっ(笑)

 

Google Cast SDK Developer Console

順番的にはあとから気づいたんだけど、先に書いておきます。

remote display APIを使用するためには、"Google Cast SDK Developer Console"への登録が必要となる。登録料$5です。

前述のremote displayのサイトの一番下に書いてあるんですが、$5とはいえ金がかかることは一番最初に書いてくれ・・・

  Google Cast SDK Developer Console

 

こんな画面です。

f:id:yo--tan:20180705075835p:plain

で、Applicationは、登録するとApplication IDが発行される(画面キャプチャ上は一応Application IDをマスクしてます)。

そのIDは、ソースコードに埋め込んで使用する。これはOK。

 

その下にある、Device。どうやらchromecastでテストするためには、その機器のserialを登録しておく必要があるらしい。

しかし、"ADD NEW DEVICES"ボタンがdisabledになっていて押せない・・・

画面上部には"NOTE: Developer devices cannot be added at this time."と書いてある。。

 

plus.google.com

f:id:yo--tan:20180705075609p:plain

 

どうやら、3日前とかの投稿だから、最近になって発生している模様。。。

これは解決するまで待つしかないか・・・

 

(2018/7/11)直った!

 

plus.google.com

 

Seems Google has resolved this. I am no longer seeing this message on Registration page 

「なんだかgoogleが直したみたい。その(エラー)メッセージ、もう表示されてないね」

って、、、天下のGoogleさん、有料のサイトを2週間も使えなくしておいて、直りましたもごめんなさいも何もなし・・・?

「直したみたい」の一般人コメントで終わりとは・・・このサイト自体、googleが管理するサイトなのに・・・ほんとひどいなgoogle

 

と言うわけで、以下サイトの下の"Devices"のところをみてSerialを取得、登録は、サイト見ればできましたー。

Registration  |  Cast  |  Google Developers

 

さて、次は実装。続くっ

 

 

yo-tan.hatenablog.com

 

iOSからchromecastへRemote Display #1

Chromecast-2015

 

iPhoneアプリで、サイネージ系のアプリを作ってるのだが、そのアプリに、ディスプレイとの接続をcastで行う機能を追加したいという要望があった。

その対応で、色々困ったので後ろを歩く方のためにメモメモ。 

 

castの種類と概要

さて、たなやんは、そもそもcastってなんだ?からスタート^^;
以下、いろんなサイト見てかじった情報。 

スマホからディスプレイに、無線接続で画面表示をする規格(or 機器)は、主に以下。
 1. chromecast
 2. amazon fire TV
 3. Apple TV
 4. miracast

・1.は、プログラムすることで、任意の画面をディスプレイに映すことができる模様
 https://developers.google.com/cast/docs/remote

・2.は、AirPlay前提でのミラーのようなので、アプリとしてプログラムする話ではない模様(間違ってたらご指摘を〜)

・4.は、一般的な規格だがiOSは未対応。 

 

そんなわけで、当該アプリは既に3.には対応しているので、追加で対応できるのはchromecastのみだと悟る^^; 

 

chromecast 購入!

 実は、chromecastはよく聞いてはいたけど、実物見たことなかったんです。
流行りに疎いたなやんも、たまには新し物(では全然ないよねもはや・・)にも触れようかなと言うことで、早速購入! 

今は、第二世代ってやつらしいです。HDMI接続で、先に丸いのがついてるやつ。ultraじゃない方買いました。

store.google.com

 

これ、電源別途必要なんだ・・・ネット上の写真にないからよくわからんよ・・ヨドバシ吉祥寺店のおねいさんはきちんと教えてくれたけど。ありがとうヨドバシおねいさん! 

 

さて、早速繋いでみた。 

youtubeがテレビで観れる。
とりあえず、芸人のネタ動画を数時間視聴。 「ちょっと何言ってるかわからないんですけど」を数十回聞いてるうちに、気を失って昼寝(笑)

テレビで映すと画像は荒いけど、なぜかテレビだとじっくりみちゃうねーyoutube^^

これで5000円なら全然ありだなー・・・



・・・って、そんなことをするために買ったんじゃない。アプリからchromecastに繋ぎたいんだよぉぉぉぉぉ!(笑) 

 

Google Cast SDKとは

というわけで、この辺からやっと真面目な話^^; 

chromecastには、アプリから操作するためのAPIがある。

developers.google.com

 

でもね、ここを見ても、いまいちピンとこないのです。APIの体系がよくわからない。
なぜって、同じようで微妙に異なる用語が散在していて整理されていないから。 

なので、ちゃんと整理。

用語

説明

Google Cast SDK

スマホやPCと、chromecastやスピーカーなどとを接続するAPI全体の名前。
2018/7現在、v4というのが最新らしい。
Get Started  |  Cast  |  Google Developers
この中に、後述のSender API、Receiver APIも含まれている。

SenderとReceiver

コンテンツ送信元であるスマホなどをSender、コンテンツを受け取って表示、音声出力する機器側をReceiverと呼ぶ。
Senderは当然ObjC / swiftで作るのだが、Receiver側は、html5 / javascriptで作るらしい。ただ、Receiver側を作るのは特殊な場合のみの模様。今回は対象外。

Sender API / Sender Applicationなどと表現したりする。Receiverも然り。

iOS Sender API

iOS用のSender API
November 14, 2017 に、v4というのが出てます。このv4系が現時点の最新。

CAF

これが理解に苦しむ。
CAF = Cast Application Framework なのはいいのだが・・・
Cast Application Framework (CAF) Receiver Overview  |  Cast  |  Google Developers

"CAF Receiver SDK is a major upgrade from the Receiver v2 SDK."
とある。Receiver v2 SDKというのは、Google Cast SDK v2の一部のことだが、それをmajor upgradeしたら、Receiver v3 SDKになるのではなく、CAF Receiver SDKになっている(笑)意味不明。。

しかも、Google Cast SDK自身も、v3, v4と出ているしねぇ・・^^;

Remote Display API

通常のSender API、Receiver APIだと、Youtubeなど、chromecastが対応している、ネット上の動画や音声の出力しかできない。
chromecastで任意の画面を表示するには、このAPIの使用が必要になる。

 

ちなみに、Sender / Receiverっていうか、そもそも、スマホなどからテレビにコンテンツを渡すことを「cast」と呼んでるんだから、casterとcasteeでいいんでないの?と思うのは俺だけ・・・?全体的に、Google Cast SDK回りの用語に一貫性がない。。。

 

そんなわけで、よくわからないけど、Remote Display APIを使用する前提で前に進んでみよう・・・続くっ

 

 

yo-tan.hatenablog.com