[Work/Class/ES2015で動的Webアプリ/WebSocketを用いた動的Webアプリ]

ES2015でWebSocket - ビデオチャットのように端末側のカメラ画像をWebSocketで送る / 2017-11-02 (木)

WebRTC

JavaScriptからWebカメラを扱う

WebRTCとは,簡単に言えば,端末のメディアデバイス(カメラやマイク)をWebブラウザのJavaScriptから扱うための規格である.Safari&iOSは11から対応している.

Videoに関しては,ChromeとSafari,つまりAndroidとiOSで書法というか扱い方の詳細に違いがあるので,クライアントのWebブラウザをを検出して,分岐させて両方書いておく必要がある.

セキュアHTTP(https://での接続)

WebRTCを使うためには,https://で始まるセキュアHTTPで接続する必要がある.

コード例

どのブラウザでアクセスしているかの文字列UserAgentを最初に取得し,iOSデバイス,Androidデバイス,PCのChrome, MacのSafariをそれぞれ切り分けて分岐処理を行う.

iOSとAndroidはスマホ本体を縦にして撮影している,つまりカメラから取得できる画像が縦長(3:4)であると想定し,PCのChromeとSafariは横長(4:3)であると想定する.

なお,ボタンをスマホブラウザから押しやすくするために,skeletonという軽量CSSフレームワークを利用している.

<!DOCTYPE html>
<!-- WSCameraImage_drawToVideoArea.html -->
<html lang="ja">
<head>
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta charset="utf-8" />
  <title>スマホやPCのカメラをWebブラウザから使う</title>
  <script type="text/javascript" src="./WSCameraImage_drawToVideoArea.js">
  </script>
  <!-- skeltonフレームワークを使う -->
  <link rel="stylesheet" href="./style/normalize.css" />
  <link rel="stylesheet" href="./style/skeleton.css" />
</head>
<body>
  <div id="display_user_agent">
  </div>
  <div>
    <!-- skeltonフレームワークを使った押しやすいボタン.クラスを指定するだけ -->
    <button class="button button=primary" onclick="startVideo()">Start</button><br/>
    <!-- ビデオ表示エリア -->
    <video id="local_video" autoplay playsinline></video>
  </div>
</body>
</html>
// WSCameraImage_drawToCanvas.js
'use strict';
function startVideo() {
    if(navigator.getUserMedia || navigator.webkitGetUserMedia || 
       navigator.mozGetUserMedia || navigator.msGetUserMedia){
	if(window.thisBrowser == 'ios'){
	    // for iOS Devices
	    // スマホを立てて撮影を想定
	    const medias = {audio: false,
			    video: {width: 480,
				    height: 640,
				    facingMode : {exact: "environment"}}};
			    // facingModeというのは,背面カメラと顔カメラのどちらを使うか
			    // 'environment'を指定すると背面カメラ,
			    // 'user'を指定すると顔カメラになる
	    navigator.getUserMedia(medias,
				   (stream) => {
				       window.localVideo.srcObject = stream;
				   },
				   (error) => {
				       alert('navigator.getUserMedia() error:' + error);
				   });
	}
	else if(window.thisBrowser == 'android'){
	    // for Android Chrome (with both Front and Back Camera)
	    // iOSと同じくスマホを立てて撮影しているのを想定
	    const medias = {audio: false,
			    video: {width: 480,
				    wheight: 640,
				    facingMode:{exact: "environment"}}};
	    navigator.mediaDevices.getUserMedia(medias) // ChromeはPCもAndroidもPromiseで記述する
		.then(stream => {
		    window.localVideo.src = window.URL.createObjectURL(stream);
		})
		.catch(error => {
		    console.error('navigator.mediaDvice.getUserMedia() error:', error);
		    return;
		});
	}
	else if(window.thisBrowser == 'pcchrome'){   
	    // PC Chrome (with only 1 Camera for Face)
	    const medias = {audio: false,
			    video: {width: 640,
				    wheight: 480}};
	    navigator.mediaDevices.getUserMedia(medias)
		.then(stream => {
		    window.localVideo.src = window.URL.createObjectURL(stream);
		})
		.catch(error => {
		    console.error('navigator.mediaDvice.getUserMedia() error:', error);
		    return;
		});
	}
	else if(window.thisBrowser == 'pcsafari'){
	    // for Mac Safari (with only 1 Camera for Face)
	    const medias = {audio: false,
			    video: {width:640, 
				    height:480}};
	    navigator.getUserMedia(medias,
				   (stream) => {
				       window.localVideo.srcObject = stream;
				   },
				   (error) => {
				       console.error('navigator.getUserMedia() error:', error);
				   });
	}
    }
}

window.addEventListener('load', () => {
    // ブラウザ情報が入っているUserAgenetを取得し,全部小文字の文字列にする
    window.userAgentInLowerCase = window.navigator.userAgent.toLowerCase();
    // UserAgentを表示
    document.getElementById("display_user_agent").innerHTML = 
	window.userAgentInLowerCase;

    // 各ブラウザで切り分ける
    if(userAgentInLowerCase.indexOf('iphone') != -1 ||
      userAgentInLowerCase.indexOf('ipad') != -1)
	window.thisBrowser = 'ios';
    if(userAgentInLowerCase.indexOf('android') != -1)
	window.thisBrowser = 'android';
    else if(userAgentInLowerCase.indexOf('chrome') != -1)
	window.thisBrowser = 'pcchrome';
    else if(userAgentInLowerCase.indexOf('safari') != -1)
	window.thisBrowser = 'pcsafari';
    else
	alert('Cannot use this program for your browser');

    window.localVideo = document.getElementById('local_video');
}, false);

window.addEventListener('unload', () => {
    
}, false);

DataURL形式

WebSocketで送るために画像を文字列にする

DataURLとは,画像や音などのメディアを,WebSocketのメッセージとして送ることができるような形式,すなわちテキスト情報にエンコードしたものである.

WebRTCから得られたビデオストリームをそのままDataURLにはできない(単に対応していないという問題だけではなく,縦横サイズを統一するために整えたりする必要がある)ので,videoタグの内容をgetImageData()関数で,クロップ(切り抜き)&縮小しながら取得し,そこで得られたImageDataオブジェクトが持つtoDatURL()関数を実行してDataURL形式に変換して取り出す.

受け取り側で復号する

DataURL形式は文字列なので,サーバ側ではこれまでのWebSocketメッセージと同じように扱うが,それを再度受け取ったクライアント側ではデコードする必要がある.この時のヘッダをsplitする文字列に注意する.

セキュアWebSocket(wss://での接続)とTornadoサーバ側の準備

WebRTCを使う場合,必ずhttps://で接続する必要があると前述したが,その結果WebSocketもセキュアWebSocket(wss://)である必要がある.

セキュア接続に必要な証明書(.crtファイルと.keyファイル)はサーバ毎に用意されているので,そのコピーを持って来て読み込める場所(例えば同じフォルダなど)に設置しておき,Tornadoのサーバ生成を定義する段階,つまりtornado.web.Application()関数を実行してインスタンスをm作る時に,ssl_optionで読み込ませる.