スマートグラス(EPSON MOVERIO BT-35E/BT-30E)でWebRTC

# スマートグラスの活用方法は開発者自ら開発しよう

昨年、個人でARCoreを色々と触った中で、「次はスマートグラスかな」と。

とはいえ、スマートグラスはまだまだBtoB向けの「遠隔作業支援ソリューション」として目立つくらい。
一般消費者向けにも[「Vuzix Blade」](http://www.vuzix.jp/smartglasses/blade.html)のような製品も出てきていますが、どうしてもコンテンツ消費型の
使い方が先行している気がしています。

そこで、「スマートグラスの活用方法は開発者自ら開発しよう」と思い、

- 装着のハードルがなるべく低く
- スマートグラスが持つ機能を利用して開発できる・コードが書ける

製品で、色々試してみることにしました。

# まずは技術の写経から

スマートグラスの機能を試しつつ自分が開発できることを知るため、
まずは世の中の模倣をして「技術の写経」を。

「遠隔作業支援ソリューション」ではお馴染みの「現場とセンターをカメラでつなぐ」
アプリを開発からトライしてみることにしました。

## ハードの選定

スマートグラスに求めること

- カメラを搭載している
- センサを搭載している
- SDKを公開している
- 電源供給手段を持っている
    - バッテリ搭載
    - もしくは接続デバイスから供給可能

上記を満たす製品として[「EPSON MOVERIO BT-35E/30E」](https://www.epson.jp/products/moverio/bt35e/)を採用。

合わせて「DisplayPort Alternate Mode」に対応したUSB Type-CのAndroidスマートフォンも
必要なことがわかったので、[「HTC U11」](https://www.htc.com/jp/smartphones/htc-u11/)を別途中古で購入。

## 機能実現のための技術(ソフト)の選定

「現場とセンターをカメラでつなぐ」ために求められること

- 相手の顔や声をスマートグラスから確認できること
- 自分の目の前の景色を相手に表示できること

上記を実現する手段として[「WebRTC」](https://developer.mozilla.org/ja/docs/Web/API/WebRTC_API)を採用。
今回は自前でシグナリングサーバーやSTUNサーバーは準備せず、
NTT CommunicationsさんのWebRTCプラットフォーム[「SkyWay」](https://webrtc.ecl.ntt.com/)を利用することにしました。

## いざ開発

MoverioとWebRTCの組み合わせであっという間にできるな、と軽く考えていたら、
実際はハードルが多かったです・・。

まず前提として、UVCに対応したUSBカメラ(ウェブカメラ)であればWebRTC側で
GetUserMeidaするだけで簡単にMOVERIOのカメラを拾えるだろうと思っていました。

### 技術的なハードル
- AndroidにMOVERIOを接続してもUSBカメラとして認識しない。
    - Androidは9.0以降USBカメラ対応しているはずだが、認識しない。
    - スマホ自体がハードウェアとしてサポートしてなければダメなよう・・。
        - サポートしているかどうかは`PackageManager.hasSystemFeature(FEATURE_CAMERA_EXTERNAL)`で確認できる。
        - 今回購入した「HTC U11」は対応していなかった・・。
    - 「SkyWay」のAndroid SDKは、Android(Java)内で`io.skyway.Peer.Browser.Navigator.getUserMedia`をしてカメラを取得しているが、MOVERIOはカメラとして認識されないので取得されない

### 打開策

AndroidのWebViewを利用してWebアプリケーション(HTML5とJavascript)で実装することにしました。
以降はMOVERIO SDKとSkyWay Javascript SDK を導入済の前提での説明となります。

#### WebRTC用Webページの実装
[SkyWay Javascript SDKのページ](https://webrtc.ecl.ntt.com/documents/javascript-sdk.html)を参考に画面を実装します。
ただし、[2-3.カメラ映像、マイク音声の取得](https://webrtc.ecl.ntt.com/documents/javascript-sdk.html#_2-3-%E3%82%AB%E3%83%A1%E3%83%A9%E6%98%A0%E5%83%8F%E3%80%81%E3%83%9E%E3%82%A4%E3%82%AF%E9%9F%B3%E5%A3%B0%E3%81%AE%E5%8F%96%E5%BE%97)にある「カメラ映像を表示する領域」は`Video`ではなく、`canvas`に変更します。(canvasにMOVERIOの映像データを表示させることになります)

今回は以下のようなシンプルな構成です。
センター側の映像を表示する`video`とMOVERIO側の映像データを表示する`canvas`を`body`内に用意するだけです。

```html:WebRTC用のWebページ
<!DOCTYPE html>
<html>

<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>現場側画面</title>
    <link rel="stylesheet" type="text/css" href="style.css">
    <script src="https://cdn.webrtc.ecl.ntt.com/skyway-latest.js"></script>
</head>

<body>
    <video id="center-video" width="160px" height="160px" autoplay></video>
    <canvas id="canvas" width="1920px" height="1080px"></canvas>
</body>

</html>
```
#### WebRTC用のJavascriptの準備
上記で作成したWebページの末尾にJavascriptを追加していくことになります。

##### MOVERIO側の映像データ表示
まずは、MOVERIOから映像データを取得して`body`内の`canvas`に表示するためのスクリプトを追加します。
(厳密には映像データではなく、画像に変換されたデータを表示することになります。詳細は後述。)

以下のように`img`要素(`moverioImage`)に対して`addEventListener`を用意することで、
`loadImage()`によって画像が読み込まれる度に`canvas`へと描画することができるようになります。

`loadImage()`はダミーのURL(以下では`https://xxxxxxxx/moverio.jpg`)にアクセスすることで
Android(Java)側の処理をフックできるようになります。詳細は後述。

```html:WebRTC用のWebページ
<script>
    const canvas = document.getElementById("canvas");
    const ctx = canvas.getContext('2d');
    var canvasStream = canvas.captureStream();
    var moverioImage = new Image();
    moverioImage.addEventListener("load", function () {
        if (canvas && ctx) {
            ctx.drawImage(moverioImage, 0, 0, canvas.width, canvas.height);
        }
    }, false);

    // 画像からcanvasに描画する
    function loadImage() {
        moverioImage.src = "https://xxxxxxxx/moverio.jpg";
    }
</script>
```

##### SkyWayのサービス利用

SkyWayのサービスを利用するためのJavascript実装は「WebRTC用のWebページの準備」と同様、
[SkyWay Javascript SDKのページ](https://webrtc.ecl.ntt.com/documents/javascript-sdk.html)を参考に実装します。

ただし、[2-6.発信処理](https://webrtc.ecl.ntt.com/documents/javascript-sdk.html#_2-6-%E7%99%BA%E4%BF%A1%E5%87%A6%E7%90%86)内の`peer.call(theirID, localStream);`の`localStream`と、
[2-7.着信処理](https://webrtc.ecl.ntt.com/documents/javascript-sdk.html#_2-7-%E7%9D%80%E4%BF%A1%E5%87%A6%E7%90%86)内の`mediaConnection.answer(localStream);`の`localStream`は
上記スクリプト内で定義している`canvasStream `を渡してあげることになります。
以下は着信処理の場合の例となります。

```html:moverio.html
<script>
    //着信処理
    peer.on('call', mediaConnection => {
        mediaConnection.answer(canvasStream);
        setEventListener(mediaConnection);
    });
</script>
```

#### Androidアプリケーションの実装

WebRTC用Webページを呼び出しはWebViewで実装します。
`onCreate`の中でWebViewインスタンスに各種設定を行います。

```Java
	private WebView myWebView;

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_sample);

		// WebViewClientを設定
		myWebView.setWebViewClient(webViewClient);

		// WebSettingsを設定
		WebSettings settings = myWebView.getSettings();
		settings.setJavaScriptEnabled(true); // Javascript有効化
		});
	}

```

`WebViewClient`は以下のように`shouldInterceptRequest`をオーバーライドしておきます。
`shouldInterceptRequest`を利用することで、Webページ内でレスポンスを差し替えられました。
以下のサイトが非常に参考になりました。

- [Android の WebView 、画像を取得する代わりにローカル画像を表示](http://spinachmedia.hatenablog.com/entry/2014/11/08/233647)

```Java
	private WebViewClient webViewClient = new WebViewClient(){
		@Override
		public WebResourceResponse shouldInterceptRequest (WebView view, WebResourceRequest request){
			// ダミーで指定されたRequestに対してカメラ映像データのInputStreamをResponseとして返却する
			if(request.getUrl().toString().equals("https://xxxxxxxx/moverio.jpg")){
				WebResourceResponse webResourceResponse = new WebResourceResponse("image/jpeg", null, inputStream);
				return webResourceResponse;
			}
			return super.shouldInterceptRequest(view, request);
		}
	};
```

##### カメラ映像データの取得

EPSONさんで公開している[「Moverio SDK & サンプルプロジェクト」](https://tech.moverio.epson.com/ja/bt-35e/sdk_download.html)を利用して実装しました。
MOVERIO本体のカメラへアクセスし、カメラ映像データを取得します。

##### カメラ映像データのWebアプリへの引き渡し

取得したカメラ映像データをWebアプリに渡す際は、byte配列型で渡されるカメラ映像データをBitmapに変換します。
(Bitmapの高さと幅、`Bitmap.Config`は[カメラスペック](https://tech.moverio.epson.com/ja/bt-35e/developers_guide/developing_android_apps.html#ac2-3)から確認しました。)

その後、BitmapをByteArrayOutputStream ⇒ ByteArrayInputStreamへと変換します。
(ここはもしかしたらもう少し処理をシンプルにできるのかもしれません・・)

```Java
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		Bitmap bitmap = Bitmap.createBitmap(1920, 1080, Bitmap.Config.RGB_565);
		bitmap.copyPixelsFromBuffer(ByteBuffer.wrap(data));
		bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos );
		byte[] bitmapData = bos.toByteArray();
		inputStream = new ByteArrayInputStream(bitmapData);
```

変換した直後に画像ロード用のJavascriptを実行することで、`shouldInterceptRequest`をフックできます。
なお、UIスレッド以外からJavascript実行すると以下のように怒られます。

`java.lang.Throwable: A WebView method was called on thread 'XXXXXXX'. All WebView methods must be called on the same thread.`

この場合別途Handlerを用意し、postしてあげることで実行できます。

```Java
private Handler handler = new Handler();
```

今回の場合はBitmapをByteArrayInputStreamに変換した直後に以下の処理を実行します。
直前のキャッシュクリアも重要です。これがないと画像が表示更新されませんでした。

```Java
		// Handlerを使用してUIスレッドのWebViewを更新する
		handler.post(new Runnable() {
			@Override
			public void run() {
				myWebView.clearCache(true); // キャッシュクリア
				myWebView.loadUrl("javascript:loadImage();");
			}
		});
```

WebRTC用のページへの初回アクセスはMOVERIOからカメラ映像データの取得を開始する前であれば、
任意のタイミングで問題ないと思います。

```Java
	myWebView.loadUrl("https://xxxxxxxx/{WebRTC用のページ}");
```

### 動作確認

Androidアプリを起動し、スマホとMOVERIOの両者にカメラ映像が表示されればほぼ完了です。
![Screenshot_20200624-020159.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211817/1933fd54-59a0-b7d5-2224-0f7df8378346.jpeg)

WebRTCの接続先を用意します。
[SkyWay Javascript SDKのページ](https://webrtc.ecl.ntt.com/documents/javascript-sdk.html)のサンプルを利用させてもらうのが楽です。

Webページのデザインは、相手の映像を全画面で表示したり、カメラ映像を表示しているcanvas自体を
非表示にしたり、用途に応じてデザインすればよいかと思います。

私は「現場とセンターをカメラでつなぐ」をイメージして、相手の映像が画面の端に表示されるようにしてみました。
また、自分の目の前を映しているカメラ映像をわざわざ画面に表示することもないと思い、canvasを非表示にしてみました。

## まとめ

### アプリ開発について

USBカメラ対応していないことで思わぬ実装が入りましたが、WebRTCやWebViewを知る良い勉強になりました。
USBカメラ対応している機種なら、ほとんどコーディングすることなくWebRTCの仕組みが
利用できてしまうのではないかと思いました。

MOVERIO SDK自体は癖のない、わかりやすく苦労することのない実装が可能でした。
カメラ機能だけでなく、センサ機能も試してみたいところです。

また、今回は画像だけでしたが、音声データもMediaStreamとして、映像データと合成して
WebRTCに指定できるようようなので、そちらも実装してみたいところです。

### 装着のハードル

ランニングなどの時に腕につけるアームバンドを利用することでコンパクトにまとめられ、
「装着のハードル」は想像よりは低くなりました。
![DSC_1630.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211817/d0b5fc03-5816-883e-88c5-7bf2aec10069.jpeg)

実際に身に付けた時のイメージですw ファッション次第では外でも違和感なくいけるかもしれませんwwww
![DSC_1631.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211817/79354add-e75a-aee8-13df-24d612319ac6.jpeg)

## 課題

スマホからMOVERIOに電源供給を行いながら画像処理も行うため、スマホ側の発熱とバッテリ消費が
激しいのが気になりました。