3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-06-23

概要

今回調べもので canvas の表示を画像として kintone の添付ファイルに保管できないか調べる機会がありました。試しに kintone アプリを作る際に折角なら canvas に Web カメラ画像を取り込んで保管するまでを実装を思い立ち、今回試してみました。
WebCam11.png

Web カメラ画像を canvas に表示する

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

<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カメラの画像がブラウザに表示されます。

	// 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;
	});

結果
WebCam02.png
PC内蔵のカメラ動画を表示しました。
WebCam03a.png

Webカメラを表示を canvas に複写する

video の webカメラ表示を canvas に複写表示します。
こちらも今回は JavaScript から canvas を追加していますが、HTML に直接タグを記述しても問題ありません。
キャンバスの drawImage() メソッドで、video からフレーム画像取得を繰り返さないと canvas に同じ動画を表示できないのですが、requestAnimationFrame() メソッドを使うと、ブラウザが次の再描画を行う前に canvasUpdate() を実行するようで、canvas に video と同じ動画が表示できました。ブラウザの 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カメラの動画が表示されています。
WebCam03b.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カメラ写真保存」というアプリを追加しました。
WebCam01.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 の追加については、先に説明しましたので省略します。

		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」にセットしています。

	// 撮影用画像を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で再度更新を行うことで、やっと添付ファイルが紐づけられます。

	// 画像を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 に大変有難い例がありましたので、参考にさせていただきました。(感謝!)

	// 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() の同期処理で実装しています。

		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カメラ表示
WebCam05.png

・追加画面で「撮影」
WebCam06.png

・追加画面で「保存」した後の明細画面
WebCam07.png

・変更画面で「撮影」した場合
WebCam08.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

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?