Web でカメラを使おう – WebRTC (getUserMedia) on WebView

こんにちは、今回は WebRTC を使って Web 上でカメラを制御するお話です。
最近の話ですが、プラットフォームに依存しない形で QRコードを読み取れないか、という要求がありまして
いろいろと検討した結果、WebRTC で実現できそうだということで実際に使ってみたのでそのレポートも兼ねて紹介したいと思います。
お品書き
・WebRTC とは(簡単な紹介)
・基本的な使い方
・細かい制御
・WebView から使う

WebRTC とは(簡単な紹介)

WebRTC (Web Real Time Communication) はウェブブラウザ間でリアルタイム通信を実現するための API で、 Google 社によってオープンソースとして公開され、その後 W3C によって標準化が進められています。
この API はブラウザに対してプラグインを追加したり設定を変更する(例えばchrome://flagsのような)必要なしに利用することができます。以下で紹介しているチュートリアルによれば、すでに Facebook Messenger などで実際に使用されているようです。
API は主に以下の3つのモジュールから成ります。

    • MediaStream (別名 getUserMedia)
      マイクとカメラに対するストリームアクセス
    • RTCPeerConnection
      ブラウザ間の Audio/Video データストリーミング
    • RTCDataChannel
      Audio/Video データストリーミング以外のデータ通信(例えばゲーミング、リモートデスクトップ、ファイル転送など)

WebRTC のチュートリアルについては本家サイトで紹介されているこちらのサイトがオススメです。
https://www.html5rocks.com/en/tutorials/webrtc/basics/
API仕様(Working Draft)についてはこちらで公開されています。
https://w3c.github.io/mediacapture-main/
今回の目的は QRコードの読み取りですので以降は MediaStream (以下 getUserMedia) を見ていきます。

サポート状況

getUserMedia に対する各主要ブラウザとWebコンポーネントのサポート状況です。

ブラウザバージョンサポート状況
Google Chrome74.0.3729.157yes(Android WebView 含む)
Safari12.1 (14607.1.40.1.4)yes
WKWebView / SFSafariViewControlleriOS12no
Microsoft Edge41.16299.1004.0yes
Mozilla Firefox66.0.5yes

Android の WebView は Chrome から提供されている(Android 7.0 以降)ので Chrome で動けば WebView でも動きます。iOS については WebView は Safari と実装が別になっており、残念ながらまだ動作しませんでした。(RTCPeerConnection と RTCDataChannel については iOS11からサポートされているようです。)
というわけでプラットフォーム非依存という当初の目的には早くもミソが付いてしまいました。
ここまで読んでくださった iOS エンジニアの方々には申し訳ないのですが、以降は Safari をターゲットとしてご覧いただけますと幸いです。

基本的な使い方

前提

実装に入る前にまず getUserMedia を使用するための前提としてAPI を呼び出すページは HTTPS通信である必要があります。 唯一例外として localhost のみ HTTP通信での利用が許可されています。
Androidアプリの場合、HTMLファイルを assets としてお腹に持たせる方法も考えられますが、試したところやはりだめでした。。。
技術的にはアプリ内で 80番ポートを listen するなどすれば localhost での開発も可能ですが、素直に SSL/TLS を有効にした Webサーバを用意することをお勧めします。
また一般的に非推奨ではありますが、self-signed certificate いわゆるオレオレ証明書でも API は利用可能です。

基本サンプル

getUserMedia を用いて カメラのスナップショットを取得するためのコードは Google が公開していますのでこちらを参考に QRコード読み取りプログラムを作っていきます。
それを踏まえて QRコード読み取り用に手を加えたものが以下のコードです。

大まかな流れを説明します。(各API の説明については後述します。)

    1. カメラのストリームを取得しそれをプレイヤーのデータソースに設定することでカメラを起動しプレビューを表示する
    2. カメラプレビューからスナップショットを取得するためにキャンバスに書き込みそこからイメージデータを取得する
    3. 取得したイメージデータを QRコード読み取りライブラリに与え、結果が得られればそれをコールバックする

2 と 3 の処理は繰り返し行う必要があるためタイマーを利用します。
QRコード読み取りについてはこちらのライブラリを利用させていただきました。

また上記のコードには出てきていませんが、API に指定する各種パラメータを決定するための助けとなるメソッドやそれらを取得するためのメソッドがあります。
以下で合わせて見ていきます。

細かい制御

処理順に沿って見ていきます。

getSupportedConstraints

getUserMediaメソッドを呼ぶ前にまずこちらのメソッドを呼んでその結果を取得することをお勧めします。
getUserMediaメソッドの結果として取得する MediaStream オブジェクトについてブラウザがサポートする制約の一覧を取得できます。
取得した制約一覧は getUserMedia に指定するパラメータを決定するために利用できます。
呼び出し方

例えば次のような結果が得られた場合、getUserMedia メソッドでそれらの項目を指定できることを意味します。

制約一覧について W3C の仕様(Working Draft)では以下が定められているようです。

参考までに各ブラウザでの実行結果を載せておきます。
Google Chrome

さすがです。 言い出しっぺだけにそこまで使おうとは思ってなかったというようなものまで含めかなりの項目がサポートされていますね。 仕様がワーキングドラフトとはいえ、すでに拡張しまくりです。
拡張項目についてはどのような値が指定できるのか、そもそも利用可能なのか、残念ながらまだよくわかっていません。
Safari

Chrome の次に持ってきたことに特に意味はないのですが、多くないですね。。。
Microsoft Edge. Mozilla Firefox

こちらの2ブラウザは同じ結果でした。比較的まずまずのサポート状況でしょうか。

getUserMedia

MediaStreamConstraints を指定して Audio または Video ストリームを取得します。
MediaStreamConstraints は Audio と Video それぞれについて有効/無効または MediaTrackConstraints を指定できます。
MediaTrackConstraints は 上述の getSupportedConstraints で得られる Constraints に対する具体的な数値項目です。
次のように使います。

MediaTrackConstraints の具体的な min, max値は後述する getCapabilitiesメソッドで取得した範囲値のものを指定できます。
facingMode制約はデバイスに付属するカメラの向きを表します。
値の‘environment’は周辺環境を撮影するつまり背面カメラを指します。
他の値については API仕様のVideoFacingModeEnum を確認してください。
また ノートPC のように正面カメラしかない場合は‘environment’を指定しても正面カメラが起動します。
API仕様の Examples では getSupportedConstraints と合わせて次のような使い方が紹介されています。

より安全にカメラを起動するにはこのようにアプリケーションとして必要な制約についてそれぞれチェックをかけましょうということですね。
またこの例では各制約値に対してexact指定がされていますが、これを指定することで必ずその設定を使うことを強制できます。 この場合もし正面カメラを持たない環境で実行した場合はエラーになります。
次に stream 取得後の処理です。

getCapabilities, getSettings

これらのメソッドは stream オブジェクトから取得できる MediaStreamTrack オブジェクトのものです。
それぞれ

  • getCapabilities
    getUserMedia で指定した制約に従って決定された MediaStreamTrack の具体的な設定可能な値の範囲
  • getSettings
    MediaStreamTrack の現在の設定値

を取得できます。
以下は Xperia ZXs(SO-03J) での実行結果です。

  • getCapabilities

  • getSettings

今回はスマートフォン上でも動作させるのでその際はカメラのスクリーンサイズをアプリの表示レイアウト内にフィルさせることにしました。
レイアウトサイズはある程度デバイスに依存するため、カメラの Width と Height は document.body.clientWidth/Height などから 取得したものを設定し、Canvas と QRコード読み取りライブラリそれぞれに指定する Width と Height はこの getSettings メソッドから 取得した値を指定しています。
以下のコードでは stream オブジェクトから MediaStreamTrack オブジェクトを取得し、getSettings メソッドを使って実際の Width と Height を取得しています。

MediaStreamTrack オブジェクトは stream オブジェクトから複数取得できる場合があるため readyStateプロパティで判定して アクティブなものだけを利用します。
readyStateプロパティは‘live’‘ended’のどちらかの値を持ちます。詳細は API仕様 を確認してください。
また MediaStreamTrack オブジェクト は使い終わったらstop()メソッドで終了させます。
今回は QRコードの読み取りが完了した時点でカメラは役目を全うしているので、読み取り結果のコールバック関数にて停止させています。

WebView から使う

ここからは Android WebView 上で動作させる上でのポイントを紹介します。

Permissions

getUserMedia メソッドを呼ぶとユーザーに使用許可を求めるダイアログが表示されますが、アプリ上で動かすのでアプリとしての設定と処理が必要です。

  • AndroidManifest.xml

  • Activity / Fragment
    • カメラのパーミッションレベルは Dangerous なので毎回パーミッションの有無の確認が必要です。

  • getUserMedia メソッドをトリガーにした使用許可のダイアログ表示に関するイベントはアプリ側で受け取って処理する必要があるので、下記のコードでその橋渡しをします。

WebView の設定

WebView で表示するページでの JavaScript の実行やその他インタラクティブな処理のためには明示的な設定が必要です。

  • JavaScript の有効化

  • おそらく videoタグに対してですがここではカメラの自動再生を指します

  • 開発中は特にキャッシュを無効化しておいた方が捗ります


WebView におけるオレオレ証明書対策についてはググると簡単に見つかると思いますのでそちらを参考にしてください。

JavaScript Interface

JavaScript から QRコードの読み取り結果を取得するために JavaScript Interface の仕組みを使います。

  • アプリ側(java)

  • JavaScript 側

JavaScript Interface の実装、特にアプリ側にはセキュリティ的な観点で注意が必要です。
このサンプルの場合、onSetScanResult コールバック関数で JavaScript から受け取った値をパースしていますが、 後述する デベロッパーツールによるリモートデバッグ機能を利用すると WebView の中であってもデバッグが可能です。 つまりブレイクポイントを貼って値を動的に書き換えることができてしまいます。
結果データの形式やサイズはある程度規定してサイズチェックやパース後の値のチェックなどバリデーションをかけておくほうが安全です。

画面のオリエンテーション

これは現状おそらく getUserMedia かブラウザの実装に依存した動きではないかと認識していることなのですが、少なくともAndroid (つまり Chrome)上では getUserMedia でアクセスするカメラの縦横は画面のオリエンテーションに関わらず横向きに固定されているようです。
分かりにくいので図を用意しました。

この図ではデバイスが縦向き横向きそれぞれの時の JavaScript で取得できる Width, Heightと getUserMedia API でアクセスするカメラの Width, Heightの意味を表しています。
デバイスが横向きの場合、JavaScript の Width, Height とカメラのそれらの意味は一致しています。 しかし縦向きの場合、JavaScript のそれらはオリエンテーションが考慮され軸が回転するのに対し、カメラのそれらは回転しません。
そのため縦向きの場合、getUserMediaメソッドへの入力や getSettingsメソッドからの取得値の Width, Height値は入れ替えて扱う必要があります。
今回は QRコードが読めればいいのでアプリとしてローテーションは固定しましたが、ローテーションを検知してカメラに動的に反映させるにはまた一手間かかりそうですね。

その他 Tips

WebView を用いたアプリ開発では以下の設定やツールを利用すると捗りやすいと思いますので紹介します。
JavaScript コンソールログを Logcat に出力する

WebView のリモートデバッグ
Google Chrome のデベロッパーツールには Android 端末をリモートデバッグする機能があります。
これを用いると端末にインストールされているアプリに含まれる WebView についてもデベロッパーツールでデバッグすることができます。
手順もデベロッパーツールからメニューを辿っていくだけで簡単に利用できます。
詳細は CHOME DEVTOOLS – Remote Debugging をご確認ください。

最後に

今回は QRコード読み取りというカメラのユースケースとしてはライトなものだったため getUserMedia について使い込んだとは決して言えませんが、 ある程度使い方とどのようなユースケースならば選択肢として候補に挙げられるかについて知見を得ることができました。
使い易さという点で、getUserMedia によるカメラ制御は Android のネイティブアプリのそれと比べてかなり簡略化されています。 もちろんそれは WebRTC というコミュニケーション用の API であることが前提にあるので、カメラその物を細かく制御する必要があるようなユースケースには向かないかもしれません。
また WebView から使う場合、カメラ制御以外の部分についての負担(パーミッションなど)はネイティブアプリと大して変わらないこと、 ネイティブ側と Web側それぞれの言語に関するスキルが要ること、デバッグ、テスト環境への影響、などなど考慮すると慣れていれば生産性的にもトントンという気がします。とはいえネイティブアプリでカメラを使う際の様々なクラスやメソッド、スレッドの用意から解放されることの恩恵は素晴らしいです。
まずは Web を前提としたシステムやアプリがあり、その中でカメラやマイクを用いた機能が欲しい場合には検討する対象となりうる、というところがこの API の立ち位置かなというのが正直な印象です。
(この API が無ければ選択肢すらないので十分にすごくありがたいものです!)
機会があればより踏み込んだ使い方を紹介できればと思います。
それでは失礼します。

関連記事

  1. ねぇClova、開発したい 【対話モデル作成編】

  2. 自宅Wi-Fiお知らせアプリ開発【第二回】 Beacon 探索機能編

  3. BLEAD-TSHで遊ぼう!【スピンアウト企画:サイコロトークのお題の…

  4. 何でこうなるの?現場で起きた開発回顧録【その2-JSFライフサイクルと…

  5. アジャイル ~壁を乗り越える~【第3回 壁を乗り越える】

  6. jQuery Stepsを導入してみよう