kintoneでWebカメラの映像を表示して添付ファイルに保管

## 概要
今回調べもので canvas の表示を画像として kintone の添付ファイルに保管できないか調べる機会がありました。試しに kintone アプリを作る際に折角なら canvas に Web カメラ画像を取り込んで保管するまでを実装を思い立ち、今回試してみました。
![WebCaｍ11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/762c0df4-5a04-52e2-f0c6-8d28fa6d2764.png)

## Web カメラ画像を canvas に表示する
先ず kintone に実装する前に、html ファイルでWebカメラの表示試験を行います。
id が contents の div 内に、video と canvas を追加します。 

```html
<body>
<h1>Webカメラの映像をvideoとcanvasに表示</h1>
<div id="contents"></div>
<div><a id="download" href="#" download="canvas.png" onClick="downloadImage()">画像ダウンロード</a></div>
</body>
```

### webカメラの表示
Webカメラを video に表示します。
今回は JavaScript から video を追加していますが、HTML に直接タグを記述しても問題ありません。
navigator.mediaDevices.getUserMedia() メソッドで、Webカメラを使用する許可をユーザーに求め、許可を得た場合に動画をストリームで受信できるようになり、then で取得できた際に video.srcObject に代入しています。これだけで、Webカメラの画像がブラウザに表示されます。

```javascript
	// video にWebカメラの映像を表示
	let videoPreview    = document.createElement('div');

	let video      = document.createElement('video');
	video.id       = 'video';
	video.width    = cameraSize.w;
	video.height   = cameraSize.h;
	video.autoplay = true;
	videoPreview.appendChild(video);

	document.getElementById('contents').appendChild(videoPreview);

	// Webカメラの映像を video にセット
	let media = navigator.mediaDevices.getUserMedia({
		audio: false,
		video: {
			width:  { ideal: resolution.w },
			height: { ideal: resolution.h }
		}
	}).then(function(stream) {
		video.srcObject = stream;
	});
```

__結果__
![WebCaｍ02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/3c5f1106-4ee7-022d-39ed-de8239b40b60.png)
PC内蔵のカメラ動画を表示しました。
![WebCaｍ03a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/aa3352f2-6ae4-4a34-38ce-bcdc992afecf.png)

### Webカメラを表示を canvas に複写する
video の webカメラ表示を canvas に複写表示します。 
こちらも今回は JavaScript から canvas を追加していますが、HTML に直接タグを記述しても問題ありません。
キャンバスの drawImage() メソッドで、video からフレーム画像取得を繰り返さないと canvas に同じ動画を表示できないのですが、requestAnimationFrame() メソッドを使うと、ブラウザが次の再描画を行う前に canvasUpdate() を実行するようで、canvas に video と同じ動画が表示できました。ブラウザの JavaScript の進化には驚きますね！

```javascript
	// canvas にvideoの映像を表示
	let canvasPreview    = document.createElement('div');

	let canvas    = document.createElement('canvas');
	canvas.id     = 'canvas';
	canvas.width  = canvasSize.w;
	canvas.height = canvasSize.h;
	canvasPreview.appendChild(canvas);

	document.getElementById('contents').appendChild(canvasPreview);

	// video の映像を canvas にセット
	let canvasCtx = canvas.getContext('2d');
	canvasUpdate();

	function canvasUpdate() {
	  canvasCtx.drawImage(video, 0, 0, canvas.width, canvas.height);
	  requestAnimationFrame(canvasUpdate);
	};
```

__結果__
canvas に video と同じWebカメラの動画が表示されています。
![WebCaｍ03b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/414f5628-8c86-9f2d-3102-c726a700f12d.png)

以下が全てのコードです。

```webcam.html
<html>
<head><title>Webカメラの映像をvideoとcanvasに表示</title></head>

<body>
<h1>Webカメラの映像をvideoとcanvasに表示</h1>
<div id="contents"></div>
<div><a id="download" href="#" download="canvas.png" onClick="downloadImage()">画像ダウンロード</a></div>
</body>

<script type="text/javascript">

	const cameraSize = { w: 640,  h: 400 };
	const canvasSize = { w: 640,  h: 400 };
	const resolution = { w: 1280, h: 720 };

	// video にWebカメラの映像を表示
	let videoPreview    = document.createElement('div');
	let videoText       = document.createElement('p');
	videoText.innerHTML = 'videoにWebカメラ表示';
	videoPreview.appendChild(videoText);

	let video      = document.createElement('video');
	video.id       = 'video';
	video.width    = cameraSize.w;
	video.height   = cameraSize.h;
	video.autoplay = true;
	videoPreview.appendChild(video);

	document.getElementById('contents').appendChild(videoPreview);

	// Webカメラの映像を video にセット
	let media = navigator.mediaDevices.getUserMedia({
		audio: false,
		video: {
			width:  { ideal: resolution.w },
			height: { ideal: resolution.h }
		}
	}).then(function(stream) {
		video.srcObject = stream;
	});

	// canvas にvideoの映像を表示
	let canvasPreview    = document.createElement('div');
	let canvasText       = document.createElement('p');
	canvasText.innerHTML = 'canvasにvideoのWebカメラ画像を表示';
	canvasPreview.appendChild(canvasText);

	let canvas    = document.createElement('canvas');
	canvas.id     = 'canvas';
	canvas.width  = canvasSize.w;
	canvas.height = canvasSize.h;
	canvasPreview.appendChild(canvas);

	document.getElementById('contents').appendChild(canvasPreview);

	// video の映像を canvas にセット
	let canvasCtx = canvas.getContext('2d');
	canvasUpdate();

	function canvasUpdate() {
	  canvasCtx.drawImage(video, 0, 0, canvas.width, canvas.height);
	  requestAnimationFrame(canvasUpdate);
	};

	// canvasの画像をダウンロード
	function downloadImage() {
	  var base64 = canvas.toDataURL();
	  document.getElementById("download").href = base64;
	}
</script>
</html>
```

## kintone でWebカメラを表示し保管するアプリを作る
前座が長くなりましたが、いよいよ kintone に Webカメラの画像を保管する方法を試します。
今回「Webカメラ写真保存」というアプリを追加しました。
![WebCaｍ01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/b6e854be-cd68-4301-c1da-baac54bdc8e0.png)

### フィールドの設定
アプリのフィールドは以下のように設定しています。（レコード番号などはお好みで。）

| フィールド名 | タイプ | フィードコート・要素ID | その他 |
|---------------|--------|----------------|----------------|
| 添付ファイル | 添付ファイル | 添付ファイル | サムネイルの大きさは 250x250 |
| Webカメラ画像 | グループ | Webカメラ画像 | 明細画面ではWebカメラフィールドを非表示に |
| 　　　　　　　 | スペース | videoPreview | Webカメラ画像グループ内に配置 |
| 　　　　　　　 | スペース | canvasPreview | Webカメラ画像グループ内に配置 |
| ファイルID | 文字列（1行） | ファイルID | Webカメラ画像グループ内に配置 |

### JavaScript プログラムの作成
アプリに以下のJavaScriptのプログラムファイルを追加します。
JavaScript でWebカメラをcanvasに表示する方法についてはすでに述べたので、kintone に実装する際のポイントのみを説明します。

__・レコード追加画面でWebカメラ画像を表示する__
先ず kintone に video や canvas を表示するには、kintone のスペースを利用します。
kintone のレコード追加画面のイベント処理 kintone.events.on(eventsCreateShow, function(event) {})　内ではそれぞれ showWebCamera() で video にWebカメラの表示、makeCanvas() で canvas の準備を行っています。スペースの要素IDを利用してgetSpaceElement() で Elementを取得し、必要な DOM を追加します。DOM の追加については、先に説明しましたので省略します。

```javascript
		let video    = showWebCamera();
		let canvas   = makeCanvas(isMobile, video);
		
		if(!isMobile) {
			kintone.app.record.setFieldShown('添付ファイル', false);
			kintone.app.record.getSpaceElement('videoPreview').appendChild(video);
			kintone.app.record.getSpaceElement('canvasPreview').appendChild(canvas);
```

__・「撮影」ボタン押下で canvas に画像を表示、kintone にファイルをアップロード__
makeCanvas() のメソッドでは、kintone のアプリでは video のWebカメラ動画から「撮影」ボタンを押したタイミングで canvas に表示すると同時に、画像をkintoneにアップロードし、その返信の fileKey をフィールドの「ファイルID」にセットしています。

```javascript
	// 撮影用画像をcanvasに表示
	function makeCanvas(isMobile, video) {
		
		if(video == null) return;
		let canvasPreview = document.createElement('div');
		
		let takeButton       = document.createElement('button');
（中略）
		// 撮影ボタンが押されたら canvas に画像を転送
		takeButton.onclick   = function() {
			let canvasCtx = canvas.getContext('2d');
			canvasCtx.drawImage(video, 0, 0, canvas.width, canvas.height);
			let blob = convertBlobImage(canvas);
			saveImage(isMobile, blob);
		};
		canvasPreview.appendChild(takeButton);
（中略）
		return canvasPreview;
	}
```

「撮影」ボタンを押したタイミングでアップロードするのは、kintone の添付ファイルフィールドを直接 JavaScript で操作できないため、JavaScript APIをコールして事前にファイルをアップロードし、全ての入力を終えて submit で kintone のレコードが追加された後（無論添付ファイルは空の状態）に、先のアップロードで返ってきた fileKey をセットしAPIで再度更新を行うことで、やっと添付ファイルが紐づけられます。

```javascript
	// 画像をkintoneに保存
	function saveImage(isMobile, blob){
		
		var record;
		if (isMobile) {
			record = kintone.mobile.app.record.get();
（中略）	
		// ファイルアップロード
		var key = "";
		var formData = new FormData();
		formData.append('__REQUEST_TOKEN__', kintone.getRequestToken());
		formData.append('file', blob, 'image.png');
		
		var xmlHttp = new XMLHttpRequest();
		xmlHttp.open('POST', kintone.api.url('/k/v1/file', true), false);
		xmlHttp.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
		xmlHttp.send(formData);
		if (xmlHttp.status === 200) {
			key = JSON.parse(xmlHttp.responseText).fileKey;
		}
		record.record["ファイルID"].value = key;
		
		if (isMobile) {
			kintone.mobile.app.record.set(record);
（中略）	
	}
```

さらに面倒なのは、canvas のデータは Base64 には簡単に変換できるのですが、ファイルとしてアップロードするために blob データでなければならない点です。以下は canvas の画像を blob データに変換する処理です。最後の参照先でも紹介していますが、qiita に大変有難い例がありましたので、参考にさせていただきました。（感謝！）

```javascript
	// canvas画像をblobデータに変換
	function convertBlobImage(canvas) {
		
		if(canvas == null) return;
		
		var base64 = canvas.toDataURL("image/jpeg");
		// Base64からバイナリへ変換
		var bin = atob(base64.replace(/^.*,/, ''));
		var buffer = new Uint8Array(bin.length);
		for (var i = 0; i < bin.length; i++) {
			buffer[i] = bin.charCodeAt(i);
		}
		// Blobを作成
		var blob = new Blob([buffer.buffer], {
			type: 'image/png'
		});
		
		return blob;
	}
```

__・レコードが追加された後に、先にアップロードしたファイルと紐づけの更新を行う__
先にも説明したとおり、添付ファイルフィールドは javaScript から直接操作できないため、事前にアップロードした際の fileKey を、レコード追加後に行う必要があります。kintone.events.on(eventsEditSuccess, function(event) {}) 内で、以下のようにAPI経由で更新します。本来は promise で同期させた方が良いのですが、今回は XMLHttpRequest() の同期処理で実装しています。

```javascript
		var json = {
			app: kintone.app.getId(),
			id: event.record["$id"].value,
			record: {
				"添付ファイル": {
					value: [{ fileKey: event.record["ファイルID"].value }]
				}
			},
			"__REQUEST_TOKEN__": kintone.getRequestToken()
		};
		
		var xmlHttp = new XMLHttpRequest();
		xmlHttp.open('PUT', kintone.api.url('/k/v1/record', true), false);
		xmlHttp.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
		xmlHttp.setRequestHeader('Content-Type', 'application/json');
		xmlHttp.send(JSON.stringify(json));
```

### 実行結果

__・追加画面でWebカメラ表示__
![WebCaｍ05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/3ffb3979-c176-c714-d2a5-716fccc688d8.png)

__・追加画面で「撮影」__
![WebCaｍ06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/9d532443-ad10-648c-45d6-977c3ffe8c09.png)

__・追加画面で「保存」した後の明細画面__
![WebCaｍ07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/88e5f763-500f-3ffe-50e5-3929d0fa245a.png)

__・変更画面で「撮影」した場合__
![WebCaｍ08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/f84231a7-a4b1-bdaf-80d2-acf503b351c1.png)

以上、予定していた実装が一通りできました。
以下が全てのコードです。

```webcam.js
(function() {
	"use strict";
	
	const cameraSize = { w: 320,  h: 240 };
	const canvasSize = { w: 320,  h: 240 };
	const resolution = { w: 1080, h: 720 };
	
	// レコード追加時表示イベント
	var eventsCreateShow = [
		'app.record.create.show',
		'mobile.app.record.create.show'];
	kintone.events.on(eventsCreateShow, function(event) {
		
		let isMobile = false;
		if(event.type === 'mobile.app.record.create.show'){
			isMobile = true;
		}
		
		let video    = showWebCamera();
		let canvas   = makeCanvas(isMobile, video);
		
		if(!isMobile) {
			kintone.app.record.setFieldShown('添付ファイル', false);
			kintone.app.record.getSpaceElement('videoPreview').appendChild(video);
			kintone.app.record.getSpaceElement('canvasPreview').appendChild(canvas);
		}else{
			kintone.mobile.app.record.setFieldShown('添付ファイル', false);
			kintone.mobile.app.record.getSpaceElement('videoPreview').appendChild(video);
			kintone.app.record.getSpaceElement('canvasPreview').appendChild(canvas);
		}
		event.record["ファイルID"].disabled = true;
		
		return event;
	});

	// レコード編集時表示イベント
	var eventsEditShow = [
		'app.record.edit.show',
		'mobile.app.record.edit.show'];
	kintone.events.on(eventsEditShow, function(event) {
		
		let isMobile = false;
		if(event.type === 'mobile.app.record.edit.show'){
			isMobile = true;
		}
		let video    = showWebCamera();
		let canvas   = makeCanvas(isMobile, video);
		
		if(!isMobile) {
			kintone.app.record.getSpaceElement('videoPreview').appendChild(video);
			kintone.app.record.getSpaceElement('canvasPreview').appendChild(canvas);
		}else{
			kintone.mobile.app.record.getSpaceElement('videoPreview').appendChild(video);
			kintone.app.record.getSpaceElement('canvasPreview').appendChild(canvas);
		}
		event.record["添付ファイル"].disabled = true;
		event.record["ファイルID"].disabled = true;
		
		return event;
	});
	
	// レコード追加・編集後イベント
	var eventsEditSuccess = [
		'app.record.create.submit.success',
		'app.record.edit.submit.success', 
		'mobile.app.record.create.submit.success',
		'mobile.app.record.edit.submit.success'];
	kintone.events.on(eventsEditSuccess, function(event) {
		
		var json = {
			app: kintone.app.getId(),
			id: event.record["$id"].value,
			record: {
				"添付ファイル": {
					value: [{ fileKey: event.record["ファイルID"].value }]
				}
			},
			"__REQUEST_TOKEN__": kintone.getRequestToken()
		};
		
		var xmlHttp = new XMLHttpRequest();
		xmlHttp.open('PUT', kintone.api.url('/k/v1/record', true), false);
		xmlHttp.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
		xmlHttp.setRequestHeader('Content-Type', 'application/json');
		xmlHttp.send(JSON.stringify(json));
		
	});
	
	// レコード詳細表示時イベント
	var eventsDetailShow = [
		'app.record.detail.show',
		'mobile.app.record.detail.show'];
	kintone.events.on(eventsDetailShow, function(event) {
	
		let isMobile = false;
		if(event.type === 'mobile.app.record.edit.show'){
			isMobile = true;
		}
		if(!isMobile) {
			kintone.app.record.setFieldShown('Webカメラ画像', false);
		}else{
			kintone.mobile.app.record.setFieldShown('Webカメラ画像', false);
		}
		
	});
	
	// Webカメラ画像をvideoに表示
	function showWebCamera() {
		let video      = document.createElement('video');
		video.id       = 'video';
		video.width    = cameraSize.w;
		video.height   = cameraSize.h;
		video.autoplay = true;
		
		let media = navigator.mediaDevices.getUserMedia({
			audio: false,
			video: {
				width:  { ideal: resolution.w },
				height: { ideal: resolution.h }
			}
		}).then(function(stream) {
			video.srcObject = stream;
		});
		
		return video;
	}
	
	// 撮影用画像をcanvasに表示
	function makeCanvas(isMobile, video) {
		
		if(video == null) return;
		let canvasPreview = document.createElement('div');
		
		let takeButton       = document.createElement('button');
		takeButton.id        = 'takeButton';
		takeButton.innerText = ' 撮 影 ';
		takeButton.className = "gaia-ui-actionmenu-save";
		takeButton.onclick   = function() {
			let canvasCtx = canvas.getContext('2d');
			canvasCtx.drawImage(video, 0, 0, canvas.width, canvas.height);
			let blob = convertBlobImage(canvas);
			saveImage(isMobile, blob);
		};
		canvasPreview.appendChild(takeButton);
		canvasPreview.appendChild(document.createElement('br'));
		
		let canvas    = document.createElement('canvas');
		canvas.id     = 'canvas';
		canvas.width  = canvasSize.w;
		canvas.height = canvasSize.h;
		canvasPreview.appendChild(canvas);
		
		return canvasPreview;
	}
	
	// 画像をkintoneに保存
	function saveImage(isMobile, blob){
		
		var record;
		if (isMobile) {
			record = kintone.mobile.app.record.get();
		}else{
			record = kintone.app.record.get();
		}
		
		// ファイルアップロード
		var key = "";
		var formData = new FormData();
		formData.append('__REQUEST_TOKEN__', kintone.getRequestToken());
		formData.append('file', blob, 'image.png');
		
		var xmlHttp = new XMLHttpRequest();
		xmlHttp.open('POST', kintone.api.url('/k/v1/file', true), false);
		xmlHttp.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
		xmlHttp.send(formData);
		if (xmlHttp.status === 200) {
			key = JSON.parse(xmlHttp.responseText).fileKey;
		}
		record.record["ファイルID"].value = key;
		
		if (isMobile) {
			kintone.mobile.app.record.set(record);
		}else{
			kintone.app.record.set(record);
		}
	}
	
	// canvas画像をblobデータに変換
	function convertBlobImage(canvas) {
		
		if(canvas == null) return;
		
		var base64 = canvas.toDataURL("image/jpeg");
		// Base64からバイナリへ変換
		var bin = atob(base64.replace(/^.*,/, ''));
		var buffer = new Uint8Array(bin.length);
		for (var i = 0; i < bin.length; i++) {
			buffer[i] = bin.charCodeAt(i);
		}
		// Blobを作成
		var blob = new Blob([buffer.buffer], {
			type: 'image/png'
		});
		
		return blob;
	}
	
})();
```

## まとめ

今回は kintone でWebカメラの映像を表示、canvas に「撮影」した画像を表示して添付ファイルとして保管できることを確認しました。Web カメラ以外でも canvas に表示したグラフや解析画像などもこの手順を応用して、kintone の添付ファイルとして保管できます。
添付ファイルで保存できれば、毎回グラフィックなどの表示にリソースを取られなくて済みますし、一覧で便利な添付ファイルの表示機能なども利用できるようになるので、今後はこの手法を活用するつもりです。

## 参照先
__Webカメラの映像をcanvasに表示させる__
https://qiita.com/chelcat3/items/02c77b55d080d770530a
__canvasの画像をBlobに変換__
https://qiita.com/0829/items/a8c98c8f53b2e821ac94
__ファイルアップロードで必須となる3つの手順__
https://developer.cybozu.io/hc/ja/articles/200724665
__MediaDevices.getUserMedia()__
https://developer.mozilla.org/ja/docs/Web/API/MediaDevices/getUserMedia
__Window.requestAnimationFrame()__
https://developer.mozilla.org/ja/docs/Web/API/Window/requestAnimationFrame
__HTMLCanvasElement.toDataURL（）__
https://developer.mozilla.org/ja/docs/Web/API/HTMLCanvasElement/toDataURL
__FormData オブジェクトの利用__
https://developer.mozilla.org/ja/docs/Web/Guide/Using_FormData_Objects