動機
WebRTCでP2Pに通信しようとすると,SDPとかCandidateとかの文字列情報を相手側に伝える必要がある.
一般的な方法だとnodejsとかで簡易サーバを立てて相手側と通信するんだけど,もうちょっと小洒落たことができないかなぁ,と思ってQRコードを使って通信してみた.
200kbpsぐらい出ればファイル転送も夢じゃないと思うけれど,ちょっと作ってみた感じだと200bps(キロじゃない)ぐらいしか出ないのでWebRTCのシグナリングに使うのがやっとかな,という感じ.
一応,前回の記事の続きなのでWebカメラの設定とかはそっちを参照してください.
文字列の圧縮
JSON文字列をそのままQRコードに変換すると非常に効率が悪い(QRコードサイズが大きくなる)ので前処理としてdeflate圧縮→base64エンコード.
deflateのライブラリはこちらのdeflate.jsを使用しました.
事前にscriptタグで読み込んでおいてください.
function deflate_obj(obj) {
var utf16 = JSON.stringify(obj);
var utf8 = encodeURIComponent(utf16);
var raw = zip_deflate(utf8);
var base64 = btoa(raw);
return base64;
}
function inflate_obj(base64) {
var raw = atob(base64);
var utf8 = zip_inflate(raw);
var utf16 = decodeURIComponent(utf8);
var obj = JSON.parse(utf16);
return obj;
}
deflateで圧縮,inflateで展開.
ご想像の通り,文字列が短いと逆に長くなるけど,JSONみたいに冗長な場合はそこそこ短くなる(はず).
QRコードの作成
QRコードの作成はjquery.qrcode.jsを使用.
JQueryを排除したかったけれど,JQueryを使わないライブラリがどうにも動かなかったので・・・.
本当は文字列を投げ込んだら画像が返ってくるようなライブラリがいいんだけど,しょうがない.
事前にjqueryと上記のjquery.qrcode.min.jsを読み込んでおいてください.
function createMultiQR(obj, qr_id, length) {
var base64 = deflate_obj(obj);
var data_list = [];
for (var i = 0; i < base64.length; i += length) {
data_list.push(base64.slice(i, i + length));
}
var idx = 0;
var task = function() {
$('#' + qr_id).empty();
if (idx < 0) {
return;
}
setTimeout(task, 100 + Math.random() * 400);
$('#' + qr_id).empty().qrcode({
render: 'image',
size: 400,
text: idx + "_" + data_list.length + ":" + data_list[idx]
});
++idx;
if (idx >= data_list.length) {
idx = 0;
}
}
task();
return function() {
idx = -1;
}
}
やっていることは
- objを圧縮
- lengthで指定した文字数ずつに分解
- 分解された文字数にヘッダ(何番目のデータか+'_'+全部で何個あるか+':')を付けてQRコード生成
- qr_idで指定されたElementで順番に表示
という感じ.
lengthで分解しているのは文字数が多くなるとQRコードの限界を越えてしまってエラーになることと,あまり文字数を多くするとQRコードが細かくなりすぎてカメラによっては認識率が低下するため.
複数のQRコードにすることで読み取り側も少しずつ読み取れるので結果的に全てのデータを受け取るまでの時間は短縮できる(はず).
そのため.データがどの位置のデータかを示すためにヘッダを付けて管理している.割と適当なのでもうちょっとちゃんとした方がいいかも.(QRコードの限界の大きさを考慮するとか)
また,同じ間隔で表示させると読み取り側と同期してしまって特定の画像がいつまでも読み取られない現象が起きるため,ランダムな待ち時間を追加して表示させている.
PCとかだと読み取りが早いのでそんなことはほぼ起きないけど,スマホとかだと結構遅くなるのでこういう工夫が必要になった.
終了するときは返り値の関数を呼び出すとQRコードも消える.
相手側が読み込み終わったらボタンなりなんなりをクリックして消すことができる.
QRコードの読み取り
読み取りぐらい楽勝でしょう,と思ってたら意外とここでハマってしまった.
結局,jsqrcodeを使ったけれど,どうにも使い方が気持ち悪い・・・.
function startReadQR(video, callback, error){
var store = {
all_length: 0,
length: 0
};
var state = "run";
qrcode.callback = function(res) {
if(res == 'error decoding QR Code'){
error();
} else {
console.log("read", res);
receiveQr(res, store, callback);
}
}
var videoRead = function() {
var w = video.videoWidth;
var h = video.videoHeight;
var canvas = document.createElement("canvas");
canvas.width = w;
canvas.height = h;
var ctx = canvas.getContext("2d");
var draw = function(){
if(state == "stop"){
return;
}
requestAnimationFrame(draw);
ctx.drawImage(video, 0, 0, w, h);
var img = canvas.toDataURL("image/png");
qrcode.decode(img);
};
draw();
}
if (video.readyState == 0) {
video.onloadedmetadata = videoRead;
} else {
videoRead();
}
return function(){
state = "stop";
};
}
Webカメラの取得は前回の記事の方法で取得して,videoタグのsrcに設定されているものとしている.
startReadQRメソッドにvideoのElement,読み取り成功したときのコールバック,エラーのコールバックを引数に指定することでQRコードを読み取りが開始される.
startReadQRメソッドではcanvasを内部的に作成してそこにvideoを描画,画像変換を行う.
videoの大きさに合わせたcanvasを作成するためにonloadedmetadataの中で処理を行っている.
これをやらないとvideoのサイズが0になって上手く動かない.
追記:metadataがロード出来ていた場合に上手く動かないため,readyStateによる判別を追加.
フレーム毎に画像を作成し,qrcode.decodeメソッドを呼び出す.
QRコードが見つかればqrcode.callbackが呼び出される.普通のQRコードリーダならここで終わりだけど,複数枚に分割しているので統合する処理を下記のメソッドに実装している.
function receiveQr(data, store, callback) {
var idx = data.slice(0, data.indexOf("_"))
var len = data.slice(data.indexOf("_") + 1, data.indexOf(":"));
if (store.all_length != len) {
for(var k in store){
delete store[k];
}
store.all_length = Number(len);
store.length=0;
}
if (!store[idx]) {
store[idx] = data.slice(data.indexOf(":") + 1);
store.length++;
}
console.log("receive " + idx + " / " + len, store);
if (store.length == store.all_length) {
var d = "";
for(var i=0; i<store.all_length; ++i){
d += store[i];
}
var obj = inflate_obj(d);
callback(obj);
}
}
QRコードから読み取った文字列のヘッダから何番目のデータか,全部でいくつあるかを取得し,連想配列のstoreに格納.
全てが揃ったところで結合してinflate,コールバックを呼び出している.
テストページ
とりあえずテストしてみた.
<!DOCTYPE html>
<html>
<head>
<script src="js/grid.js"></script>
<script src="js/version.js"></script>
<script src="js/detector.js"></script>
<script src="js/formatinf.js"></script>
<script src="js/errorlevel.js"></script>
<script src="js/bitmat.js"></script>
<script src="js/datablock.js"></script>
<script src="js/bmparser.js"></script>
<script src="js/datamask.js"></script>
<script src="js/rsdecoder.js"></script>
<script src="js/gf256poly.js"></script>
<script src="js/gf256.js"></script>
<script src="js/decoder.js"></script>
<script src="js/qrcode.js"></script>
<script src="js/findpat.js"></script>
<script src="js/alignpat.js"></script>
<script src="js/databr.js"></script>
<script src="js/deflate.js"></script>
<script src="js/inflate.js"></script>
<script src="js/qr_exchange.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
<script src="js/jquery.qrcode.min.js"></script>
</head>
<body>
<div id="qr_code_area"></div>
<video id="video"></video>
<form id="control"></form>
<span id="result"></span>
<script>
var obj = {
data0: "abcdefghijklmnopqrstuvwxyz",
data1: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
data2: "0123456789"
};
var stop = createMultiQR(obj, "qr_code_area", 100);
document.getElementById("qr_code_area").onclick = function() {
stop();
};
var video = document.getElementById("video");
var control = document.getElementById("control");
var result = document.getElementById("result");
getVideoSources(function(cam) {
console.log("cam", cam);
var b = document.createElement("input");
b.type = "button";
b.value = cam.name;
b.onclick = getMain(cam.id);
control.appendChild(b);
console.log('add button');
});
function getMain(cam_id) {
return function() {
main(cam_id);
};
}
function main(cam_id) {
navigator.getUserMedia({
audio: false,
video: {
optional: [{
width: 1280
}, {
sourceId: cam_id
}]
}
}, function(stream) { // success
console.log("Start Video", stream);
localStream = stream;
video.src = window.URL.createObjectURL(stream);
video.play();
video.volume = 0;
var stop = startReadQR(video, function(obj){
stop();
success(obj);
}, function() {} );
}, function(e) { // error
console.error("Error on start video: " + e.code);
});
};
function success(obj){
console.log("Success", obj);
result.innerText = JSON.stringify(obj);
}
</script>
</body>
</html>
適当なJSONデータを作って,QRコードオブジェクトを作成,カメラ作成を呼び出して読み取りの実行.
100文字+メタデータのQRコードを作成するので,この例だと2枚のQRコードが生成されて順番に表示される.
Webカメラで2枚とも読み取れたら結果を表示して終わり.
はい,無事に読み取り出来ました.
次はこれを使ってWebRTCのシグナリングをやってみます.