|||||||||||||||||||||

なんぶ電子

- 更新: 

QuaggaJSを使ってブラウザでバーコードスキャン

ブラウザでバーコードスキャン

canvas要素からよくわかっていない筆者が、QuaggaJSを使って実際にバーコードスキャンのできるページを実装してみようと思います。

今回参考にさせていただいたページは次の通りです。

JavaScriptライブラリ

ライブラリは、github:「QuaggaJS」(公式)ページよりダウロードできます。

githubのdistディレクトリ内にquagga.jsとquagga.min.jsファイルがありますので、どちらかをダウンロードして使います。

このライブラリは画像からもバーコードスキャンできますが、今回はライブストリームから取得します。その際の注意点としてlocalhost以外のサイトではhttpsでないと利用できません。これはmadeiaDevices.getUserMedia APIの仕様です。

またQuaggaJSのページではブラウザによりその実装に違いがあるためWebRTC adapterを合わせて利用することを勧めています。

このライブラリは2017年以降メンテナンスされていないようです。それを気にした方が本家のメンテナンスが再開されるまでという条件でforkしているようです。そちらのサイトもGitHubになり、アドレスはhttps://github.com/ericblade/quagga2となります。

こちらの場合自サイトに配置する.jsファイルがありませんが、https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.jsにCDNがありますので、そちらか拝借すれば配置できます。min.jsはこちらのアドレスです。

これらのファイルをCDNからのダウンロードする際は、CDNのJSファイルダウンローダをよかったら使ってみてください。

MediaDevices.getUserMedia()

そもそも、MediaDevices.getUserMediaがなんなのかよくわかっていないので、MDN:「MediaDevices.getUserMedia()」のページで調べたところ次のような説明がされていました。

「要求された種類のメディアを含むトラックを持つ MediaStream を生成するメディア入力を使用する許可をユーザーに求めます。」

つまりカメラの使用許可を求め、許可を得られればそこから出力されるストリームを得られるということになります。

これがWeb APIとして用意されているということです。

結果はPromiseで返されます。ユーザーが拒否した場合にNotAllowedErrorが返され、メディア(装置)見つからない場合はNotFoundErrorが返されます。他にもエラーはあります。

引数はオブジェクトで渡します。主なものは次の通りです。

  • audio

    音声の設定です。falseを指定すると音声を取得しません。

  • video

    映像の設定です。falseを指定すると映像を取得しません。

    trueの代わりに解像度を指定することもできます。これはまたオブジェクトとして渡します。たとえば、幅640、高さ480にする場合なら「video: { width: 640, height:480}」という風にします。

    ただし、必ず指定した解像度になるわけではありません。がさらに、width:とhightに子孫オブジェクトを設定することで最小(min)、最大(max)を指定することもできます。「width: { max: 1280 }」

    min、maxの他に、値指定(exact)、理想(ideal)の値も利用できます。

    さらに次のようにフレームレートを指定することもできます。「video:{frameRate:{ ideal: 10, max: 15 }}」 フレームレートや動画に関する基本的な知識は過去の記事でffmpegを取り上げながら紹介しています。よろしかったら読んでみてください。

  • facingMode

    カメラの表裏を指定します。"user"を指定すると自撮り側のカメラ、"environment"を指定すると外向きのカメラを利用します。

    この設定も希望として受け取られますので自撮りカメラを設定しても存在しない場合は外向きのカメラを利用することになります。

    外向きカメラでなければいけないという指定は次のようにします。「facingMode: { exact: "environment" }」

  • deviceId

    diviceIdを指定して特定のカメラを指定する事ができます。これも希望となり、厳密に指定するにはexactを指定します。

条件を指定してストリーミングを受け取り、videoタグのソースとして割り当て再生するサンプルが先のページにありました。

var constraints = { audio: true, video: { width: 1280, height: 720 } };

navigator.mediaDevices.getUserMedia(constraints)
.then(function(mediaStream) {
  var video = document.querySelector('video');
  video.srcObject = mediaStream;
  video.onloadedmetadata = function(e) {
    video.play();
  };
})
.catch(function(err) { console.log(err.name + ": " + err.message); });

mediaDevicesはwindow.navigatorのプロパティとして存在しています。

navigatorはユーザーエージェントや身元情報を提供するインターフェースです。過去の記事でAngularのRxJSについてに触れた際には、チュートリアルスクリプトで「navigator.geolocation」という位置情報を取得するプロパティを参照していました。

canvas

canvas属性は、Canvas APIWebGL APIを使って線画をすることのできる要素です。

MDNのページでは次のようなCanvas APIのサンプルコードが紹介されています。

canvas要素を取得し、そこに含まれる2Dコンテキストを取得します。

コンテキストの正式な意味は筆者にはよくわかりませんが、2D線画の為の機能を提供するインターフェースだととらえています。

ちなみにそれを受けている変数名のctxはcontextの略語のようです。

コンテキストで、塗りつぶし色をgreenに設定したあと、塗りつぶしの四角形をx=10、y=10の位置から幅=150、高=100で線画しています。

canvasでは属性値としてwidthとheightがあります。こちらでサイズを設定した際は入力画像に従ったサイズが設定されますが、CSSで設定すると入力画像が変形して表示されます。

<canvas id="canvas"></canvas>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

ctx.fillStyle = 'green';
ctx.fillRect(10, 10, 150, 100);
</script>

QuaggaJS

QuaggaJSの最もシンプルな使い方を、公式ページより抜粋すると次のようになります。

<!-- ライブラリ読み込み -->
<script src="./js/quagga.min.js"></script>
<!-- 表示領域 -->
<div id="barcode-scanner"></div>

<script>
  // バーコードリーダーイニシャル
  Quagga.init({
  locate: true,
  inputStream:{
    name:"Live",
    type:"LiveStream",
    constraints: {
     width: 640,
     height: 480,
    },
    target: document.querySelector('#barcode-scanner'),
  },
  decoder: {
    readers : ["ean_reader","ean_8_reader"],
    multiple: false
  },
  locator: {
    halfSample: false,
    patchSize: "medium"
  }
}, function(err) {
  if (err) {
    console.log(err);
    return;
  }
  
  //バーコードをスキャンできた際のイベント
  Quagga.onDetected((data)=> {alert(data.codeResult.code)});

  Quagga.start();
});
</script>

Quaggaのinit(初期化)メソッドに、設定用のオブジェクトと、設定後実行するコールバック関数を渡しています。コールバック関数はエラーを引数に受け取ります。ここではエラーが起きていればコンソールに出力して終了、そうでなければバーコード検出時のイベントを設定してスキャンを開始しています。

Quagga.initの設定用オブジェクトに渡せる値は次の通りです。

  • numOfWorkers

    Web Workerのスレッド数を数値で指定します。デフォルト値は4です。対象となる装置のコアの数に応じて設定します。

    node.jsで利用する場合は0を指定する必要があるそうです。

  • locate

    画像中のバーコードを見つける機能を設定するか否かをboolean値(true,false)で指定します。負担が大きいためデフォルトではoffとなっています。

  • inputstream

    先の例でも出ているように入力に関する設定をします。

    constraintsの中で、先のgetUserMediaで設定したconstraintsを記述できるようです。

  • requency

    バーコードスキャンの頻度を数値で指定します。オプション項目です。

  • decoder

    バーコードの種類を文字列の配列で指定します。

    上記の例はEAN(JAN)と8桁のEANを指定しています。日本の小売店で商品についているバーコードはJANと8桁JANです。アメリカやカナダ輸入品にはUPC-AやUPC-Eがついていることがあります。UPC-AはEANの元となった規格なのでEANの設定で読むことができます。

    有効値は次の通りです。

    • code_128_reader
    • ean_reader
    • ean_8_reader
    • code_39_reader
    • code_39_vin_reader
    • codabar_reader
    • upc_reader
    • upc_e_reader
    • i2of5_reader
    • 2of5_reader
    • code_93_reader
  • multiple

    前述のバーコードの種類の検出を一度に行うかどうかの設定をboolean値で設定します。trueにすると結果は配列で戻ります。

  • locator

    先のlocateフラグがtureの時に有効な設定です。設定す値はオブジェクトですが、そのうち本番環境で利用するのはhalfSampleとpatchSizeのふたつです。

    halfSampleは実際の入力をスケールダウンするか否かをboolean値で設定します。よほど小さなバーコードでないかぎりtrueにしておた方がスムーズな処理ができます。

    patchSizeには「x-small, small, medium, large, x-large」のいずれかの値を設定します。スキャンの密度を設定するもので大きなバーコードの場合はlarge、小さなバーコードの場合はsmallを指定すると認知率が上がります。

コード中に登場した、Quagga.startやQuagga.onDetectedの他に、Quagga.endや、バーコードを検知した際にのイベントを設定するQuagga.onProcessed(callback(data))、単発処理を行うQuagga.decodeSingle(config, callback)などがあります。

実装

先に提示した参考サイトでは、Quagga.onProcessedを使って検知した範囲を表示するコードや、Quagga.decodeSingleを使ってcanvasに表示した画像からバーコードを読み込むコードが紹介されています。

自動でバーコードを認識させるとエラーも多かったため、筆者も後者に倣ってcanvasのデータをQuagga.decodeSingleで取得することにしました。

コードサンプル

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>バーコードリーダーテスト</title>
    
    <script src="./js/quagga.min.js"></script>
</head>
<body>
<div clas=""><input type="tel" id="barcode-input" maxlength="13" /><button onClick="toggleScan()">スキャン開始/停止</button></div>
<div id="barcode-wrapper" style="visibility: hidden; border: 2px solid #099;">
    <canvas id="barcode-view"></canvas>
    <p id="message">カメラ初期化中です</p>
</div>

<script>

//スキャナの表示非表示切り替え
const warpper = document.getElementById("barcode-wrapper");
let blnCameraInit = false;//カメラ初期化が正常終了したかどうか

//生の映像 ページには表示しない
const video = document.createElement("video");
video.muted=true;
video.playsInline=true;

//videoのサイズ【カメラの画素数に合わせて入力】
const videoSize = {
    w: 640,
    h: 480
};

//表示領域のサイズ【任意の値を入力】
const viewSize = {
    w: 300,
    h: 200
};

//表示領域用のパラメータ 
const viewParam = {
    init: false,
    sx: 0,
    sy: 0,
    sw: 0,
    sh: 0,
    dx: 0,
    dy: 0,
    dw: 0,
    dh: 0
};

//バーコードスキャン部分のサイズとガイドの太さ【任意の値を入力】
const targetSize = {
    w: 200,
    h: 100,
    border: 2
};

//バーコードスキャン部分のパラメータ
const targetParam = {
    sx: 0,
    sy: 0,
    sw: 0,
    sh: 0,
    dx: 0,
    dy: 0,
    dw: 0,
    dh: 0
};

//バーコードガイドのパラメータ
const sqParam = {
    valid : false,
    x: 0,
    y: 0,
    w: 0,
    h: 0
};

// Quagga用のパラメータ
const qConfig={
  decoder: { 
    readers: ["ean_reader","ean_8_reader"],
    multiple: false, //同時に複数のバーコードを解析しない
  },
  src:''//後から指定
};

//線画領域
const barView = document.getElementById("barcode-view");
// 線画領域のコンテキスト取得
const barViewCtx = barView.getContext("2d");

// 内部処理用のバーコード領域のコンテキスト取得
const barcodeArea = document.createElement("canvas");
const barcodeAreaCtx=barcodeArea.getContext("2d");

const message = document.getElementById("message");

// バーコード表示要素
const barInput = document.getElementById("barcode-input");

//スキャンのインターバル(ミリ秒)
const INTERVAL = 100;

//コードのエラーチェック回数
const VALIDATION = 3;
let validationCnt = 0;
let validationCode = "";

// スキャンされた回数 -1の時スキャンしない
let scanningCnt = -1; 
let reserveEnd=null;

function initBarcodeScaner() {
    blnCameraInit = false;
    
    //カメラ使用の許可ダイアログが表示される
    navigator.mediaDevices.getUserMedia(
    //マイクはオフ, カメラの設定   背面カメラを希望する 640×480を希望する
    {"audio":false,"video":{facingMode:"environment","width":{"ideal": videoSize.w},"height":{"ideal": videoSize.h}}}
    ).then(
    //カメラと連携が取れた場合
    function(stream){
        video.srcObject = stream;

        //Quaggaのスキャンイベント
        Quagga.onDetected(function (result) {
            //スキャンを止める
            if (scanningCnt < 0) {
                //遅延してスキャンデータが来た場合は無視
                return;
            }

            if (VALIDATION <= 1) {
                scanEnd();
                //コードをセット
                barInput.value=result.codeResult.code;
            } else {
                if (validationCode==result.codeResult.code) {
                    validationCnt++;
                    if (VALIDATION <= validationCnt) {
                        scanEnd();
                        //コードをセット
                        barInput.value=result.codeResult.code;
                    }
                } else {
                    validationCode=result.codeResult.code;
                    validationCnt = 1;
                }
            }
        });

        blnCameraInit = true;
        message.innerHTML="スキャンしてください";
    }
    ).catch(
    //エラー時
    function(err){
        switch(err.message) {
        case "Requested device not found":
            message.innerHTML="カメラ取得に失敗しました";
            break;
        default:
            message.innerHTML=err.message;
        }

     }
    );
}

function initParam() {

    //すでに初期化されていた場合は処理しない
    if (viewParam.init) {
        return;
    }

    //実際取得したサイズは要求したサイズと違う際は上書きされる。
    //videoが開始されていないと0になる
    videoSize.w=video.videoWidth
    videoSize.h=video.videoHeight;
    
    //線画領域のサイズセット
    //barView.style.width = viewSize.w;
    //barView.style.height = viewSize.h;

    //canvasは属性値でサイズを指定する必要がある
    barView.setAttribute("width",viewSize.w);
    barView.setAttribute("height",viewSize.h);
    
    
    //表示領域の計算
    if (videoSize.w <= viewSize.w) {
        //元のサイズの方が小さかったらそのまま
        viewParam.sx = 0;
        viewParam.sw = videoSize.w;
        
        viewParam.dx = 0;
        viewParam.dw = videoSize.w;
    } else {
        //中央部を取得
        let wk = videoSize.w - viewSize.w;
        if (wk < 0) {
            message.innerHTML="サイズ設定不備(view-X)";
            blnCameraInit = false;
            return;
        }
        wk = wk /2; //中央寄せするので÷2

        viewParam.sx = wk;
        viewParam.sw = viewSize.w;
        
        viewParam.dx = 0;
        viewParam.dw = viewSize.w;
    }
    if (videoSize.h <= viewSize.h) {
        //元のサイズの方が小さかったらそのまま
        viewParam.sy = 0;
        viewParam.sh = videoSize.h;
        
        viewParam.dy = 0;
        viewParam.dh = videoSize.h;
    } else {
        //中央部を取得
        let wk = videoSize.h - viewSize.h;
        if (wk < 0) {
            message.innerHTML="サイズ設定不備(view-Y)";
            blnCameraInit = false;
            return;
        }
        wk = wk /2; //中央寄せするので÷2

        viewParam.sy = wk;
        viewParam.sh = viewSize.h;
        
        viewParam.dy = 0;
        viewParam.dh = viewSize.h;
    }

    //バーコードスキャン部分の計算
    if (videoSize.w <= targetSize.w) {
        //元のサイズの方が小さかったらそのまま
        targetParam.sx = 0;
        targetParam.sw = videoSize.w;
        
        targetParam.dx = 0;
        targetParam.dw = videoSize.w;
    } else {
        //中央部を取得
        let wk = videoSize.w - targetSize.w;
        if (wk < 0) {
            message.innerHTML="サイズ設定不備(target-X)";
            blnCameraInit = false;
            return;
        }
        wk = wk /2; //中央寄せするので÷2

        targetParam.sx = wk;
        targetParam.sw = targetSize.w;
        
        targetParam.dx = 0;
        targetParam.dw = targetSize.w;
    }
    if (videoSize.h <= targetSize.h) {
        //元のサイズの方が小さかったらそのまま
        targetParam.sy = 0;
        targetParam.sh = videoSize.h;
        
        targetParam.dy = 0;
        targetParam.dh = videoSize.h;
    } else {
        //中央部を取得
        let wk = videoSize.h - targetSize.h;
        if (wk < 0) {
            message.innerHTML="サイズ設定不備(target-Y)";
            blnCameraInit = false;
            return;
        }
        wk = wk /2; //中央寄せするので÷2

        targetParam.sy = wk;
        targetParam.sh = targetSize.h;
        
        targetParam.dy = 0;
        targetParam.dh = targetSize.h;
    }

    //バーコードガイドの設定
    sqParam.valid = true;
    sqParam.w = targetSize.w;
    sqParam.h = targetSize.h;
    sqParam.x = (viewSize.w - targetSize.w) / 2;
    if (sqParam.x < 0) {
        sqParam.valid = false;
    }
    sqParam.y = (viewSize.h - targetSize.h) / 2;
    if (sqParam.y < 0) {
        sqParam.valid = false;
    }
    
    viewParam.init = true;
}

function toggleScan() {
    if(warpper.style.visibility=="visible") {
        scanEnd();
    } else {
        scanStart();
    }
}

function scanStart() {
    video.play();
    //setIntervalだと処理の遅延のかかわらず実行してしまうので都度再帰する。
    scanningCnt = 0;

    warpper.style.visibility="visible";

    if (blnCameraInit==false) {
        reserveEnd = setTimeout(() => {
            warpper.style.visibility="hidden";
        }, 3000);
    } else {
        setTimeout(scanning,0);
    }
}

function scanning() {
    //スキャン本体
    if (scanningCnt < 0) {
        return;
    }

    //パラメータ初期化()
    initParam();

    scanningCnt++;

    //バーコードエリアに線画
    barcodeAreaCtx.drawImage(video,targetParam.sx,targetParam.sy,targetParam.sw,targetParam.sh,targetParam.dx,targetParam.dy,targetParam.dw,targetParam.dh);

    //線画からバーコード解析
    barcodeArea.toBlob(function(blob){
      let reader = new FileReader();
      reader.onload=function(){
        qConfig.src=reader.result;
        Quagga.decodeSingle(qConfig,function(){});
      }
      reader.readAsDataURL(blob);
    });
    
    //プレビューエリアに線画
    //処理が遅くなるような場合は、scanningCntを使ってプレビューの線画を間引く
    //if (scanningCnt % 2 == 0) {}
    barViewCtx.drawImage(video,viewParam.sx,viewParam.sy,viewParam.sw,viewParam.sh,viewParam.dx,viewParam.dy,viewParam.dw,viewParam.dh);

    //バーコードガイドの線画
    if(sqParam.valid) {
        barViewCtx.beginPath();
        barViewCtx.strokeStyle="rgb(255,0,0)";
        barViewCtx.lineWidth=targetSize.border;
        barViewCtx.rect(sqParam.x,sqParam.y,sqParam.w,sqParam.h);
        barViewCtx.stroke();
    }

    //再帰
    setTimeout(scanning,INTERVAL);
}

function scanEnd() {
    if (reserveEnd != null) {
        clearTimeout(reserveEnd);
    }
    scanningCnt=-1;
    video.pause();
    warpper.style.visibility="hidden";
    validationCnt = 0;
    validationCode="";
}

//エントリーポイント
initBarcodeScaner();

</script>
</body>
</html>

扱いやすいようにクラス版も作ってみました。コンストラクタにターゲットとなるエレメントと内部で使うパラメータを渡します。

コードサンプル(クラス版)

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>バーコードリーダーテスト(クラス版)</title>
    
    <script src="./js/quagga.min.js"></script>
</head>
<body>
    <div id="barcode-scanner"></div>
<script>
class BarcodeReader {

    //競合する場合は修正する
    wrapperId="barcode-wrapper";
    viewId="barcode-view";
    messageId="message";
    inputId="barcode-input";
    //----

    videoSize = {w:640, h:480};
    viewSize = {w:300, h:200};
    targetSize = {w:200, h:100, border: 2};
    wrapperElement=null;//この領域を表示非表示にする
    viewElement = null;//表示される画像洋装
    viewContext = null;//表示される画像要素のコンテキスト
    targetElement = null;//バーコード部の要素
    targetContext=null;//バーコード部のコンテキスト
    inputElement = null;//結果が戻る要素 value値にせっとする
    messageElement=null;//エラー時メッセージがinnerHTMLとしてセットされる
    blnCameraInit = false;//カメラ初期化が正常終了したかどうか
    video = null;
    scanInterval = 100;//スキャンインターバル
    validationCnt = 3; //スキャンチェック回数
    validatainWkCnt = 0;//スキャンチェックワーク(回数)
    validationCode = "";//スキャンチェックワーク(コード)
    
    blnScaning = false;//スキャン中かどうか
    reserveEnd=null;//スキャン終了予約

    //表示領域用のパラメータ
    viewParam = {
        init: false,
        sx: 0,
        sy: 0,
        sw: 0,
        sh: 0,
        dx: 0,
        dy: 0,
        dw: 0,
        dh: 0
    };

    //バーコードスキャン部分のパラメータ
    targetParam = {
        sx: 0,
        sy: 0,
        sw: 0,
        sh: 0,
        dx: 0,
        dy: 0,
        dw: 0,
        dh: 0
    };

    //バーコードガイドのパラメータ
    sqParam = {
        valid : false,
        x: 0,
        y: 0,
        w: 0,
        h: 0
    };

    // Quagga用のパラメータ
    qConfig={
        locate: true,
        decoder: { 
            readers: ["ean_reader","ean_8_reader"],
            multiple: false, //同時に複数のバーコードを解析しない
        },
        locator: {
            halfSample: false,
            patchSize: "large"
        },
        src:''//後から指定
    };

    constructor(element,
        videoSizeW, videoSizeH, 
        viewSizeW, viewSizeH,
        targetSizeW, targetSizeH,border,
        scanInterval, validationCnt) {

        let strInnerHtml= '<input type="tel" id="'+this.inputId+'" maxlength="13" />';
            strInnerHtml+='<button onClick="toggleScan()">スキャン開始/停止</button>';
            strInnerHtml+='</div><div id="'+this.wrapperId+'" style="visibility: hidden; border: 2px solid #099;">';
            strInnerHtml+='<canvas id="'+this.viewId+'"></canvas>';
            strInnerHtml+='<p id="'+this.messageId+'">カメラ初期化中です</p>';
        
        //上記のコードを出力する都合上、togglescanという名前で呼び出したいのでwindowに関数をthisをバインドしてセット
        window.toggleScan = this.toggleScan.bind(this);

        element.innerHTML = strInnerHtml;
        
        this.videoSize.w = videoSizeW;
        this.videoSize.h  = videoSizeH;
        this.viewSize.w = viewSizeW;
        this.viewSize.h  = viewSizeH;
        this.targetSize.w = targetSizeW;
        this.targetSize.h  = targetSizeH;
        this.targetSize.border = border;

        this.wrapperElement=document.getElementById(this.wrapperId);
        this.viewElement=document.getElementById(this.viewId);
        this.viewContext = this.viewElement.getContext("2d");;
        this.messageElement=document.getElementById(this.messageId);
        this.inputElement = document.getElementById(this.inputId);
        //バーコード部
        this.targetElement = document.createElement("canvas");
        this.targetContext = this.targetElement.getContext("2d");

        this.validationCnt = validationCnt;
        this.scanInterval = scanInterval;

        //カメラ映像領域作成(非表示)
        this.video = document.createElement("video");
        this.video.muted=true;
        this.video.playsInline=true;

        //カメラ、Quagga初期化
        this.initBarcodeScaner();
   
    }

    //バーコードスキャン初期化
    initBarcodeScaner() {
    
        //カメラ使用の許可ダイアログが表示される
        navigator.mediaDevices.getUserMedia(
            //マイクはオフ, カメラの設定   背面カメラを希望する 640×480を希望する
            {"audio":false,"video":{facingMode:"environment","width":{"ideal": this.videoSize.w},"height":{"ideal": this.videoSize.h}}}
        ).then(
        //カメラと連携が取れた場合
        (stream)=>{
            this.video.srcObject = stream;

            //Quaggaのスキャンイベント
            Quagga.onDetected((result)=> {
                //スキャンを止める
                console.log(result.codeResult.code);

                if (this.blnScaning == false) {
                    //遅延してスキャンデータが来た場合は無視
                    return;
                }

                if (this.validationCnt <= 1) {
                    this.scanEnd();
                    //コードをセット
                    this.inputElement.value=result.codeResult.code;
                } else {
                    if (this.validationCode==result.codeResult.code) {
                        this.validatainWkCnt++;
                        if (this.validationCnt <= this.validatainWkCnt) {
                            this.scanEnd();
                            //コードをセット
                            this.inputElement.value=result.codeResult.code;
                        }
                    } else {
                        this.validationCode=result.codeResult.code;
                        this.validatainWkCnt = 1;
                    }
                }
            });

            this.blnCameraInit = true;
            this.messageElement.innerHTML="スキャンしてください";
        }
        ).catch(
        //エラー時
        (err)=>{
            console.log(err);
            switch(err.message) {
            case "Requested device not found":
                this.messageElement.innerHTML="カメラ取得に失敗しました";
                break;
            default:
                this.messageElement.innerHTML=err.message;
            }

        }
        );
    }

    //パラメータ初期化
    initParam() {

        //すでに初期化されていた場合は処理しない
        if (this.viewParam.init) {
            return;
        }

        //実際取得したサイズは要求したサイズと違う際は上書きされる。
        //videoが開始されていないと0になる
        this.videoSize.w=this.video.videoWidth
        this.videoSize.h=this.video.videoHeight;

      
        //canvasは属性値でサイズを指定する必要がある
        this.viewElement.setAttribute("width",this.viewSize.w);
        this.viewElement.setAttribute("height",this.viewSize.h);


        //表示領域の計算
        if (this.videoSize.w <= this.viewSize.w) {
            //元のサイズの方が小さかったらそのまま
            this.viewParam.sx = 0;
            this.viewParam.sw = this.videoSize.w;
            
            this.viewParam.dx = 0;
            this.viewParam.dw = this.videoSize.w;
        } else {
            //中央部を取得
            let wk = this.videoSize.w - this.viewSize.w;
            if (wk < 0) {
                this.messageElement.innerHTML="サイズ設定不備(view-X)";
                this.blnCamerainit = false;
                return;
            }
            wk = wk /2; //中央寄せするので÷2

            this.viewParam.sx = wk;
            this.viewParam.sw = this.viewSize.w;
            
            this.viewParam.dx = 0;
            this.viewParam.dw = this.viewSize.w;
        }
        if (this.videoSize.h <= this.viewSize.h) {
            //元のサイズの方が小さかったらそのまま
            this.viewParam.sy = 0;
            this.viewParam.sh = this.videoSize.h;
            
            this.viewParam.dy = 0;
            this.viewParam.dh = this.videoSize.h;
        } else {
            //中央部を取得
            let wk = this.videoSize.h - this.viewSize.h;
            if (wk < 0) {
                this.messageElement.innerHTML="サイズ設定不備(view-Y)";
                this.blnCameraInit = false;
                return;
            }
            wk = wk /2; //中央寄せするので÷2

            this.viewParam.sy = wk;
            this.viewParam.sh = this.viewSize.h;
            
            this.viewParam.dy = 0;
            this.viewParam.dh = this.viewSize.h;
        }

        //バーコードスキャン部分の計算
        if (this.videoSize.w <= this.targetSize.w) {
            //元のサイズの方が小さかったらそのまま
            this.targetParam.sx = 0;
            this.targetParam.sw = this.videoSize.w;
            
            this.targetParam.dx = 0;
            this.targetParam.dw = this.videoSize.w;
        } else {
            //中央部を取得
            let wk = this.videoSize.w - this.targetSize.w;
            if (wk < 0) {
                this.messageElement.innerHTML="サイズ設定不備(target-X)";
                this.blnCamerainit = false;
                return;
            }
            wk = wk /2; //中央寄せするので÷2

            this.targetParam.sx = wk;
            this.targetParam.sw = this.targetSize.w;
            
            this.targetParam.dx = 0;
            this.targetParam.dw = this.targetSize.w;
        }
        if (this.videoSize.h <= this.targetSize.h) {
            //元のサイズの方が小さかったらそのまま
            this.targetParam.sy = 0;
            this.targetParam.sh = this.videoSize.h;
            
            this.targetParam.dy = 0;
            this.targetParam.dh = this.videoSize.h;
        } else {
            //中央部を取得
            let wk = this.videoSize.h - this.targetSize.h;
            if (wk < 0) {
                this.messageElement.innerHTML="サイズ設定不備(target-Y)";
                this.blnCamerainit = false;
                return;
            }
            wk = wk /2; //中央寄せするので÷2

            this.targetParam.sy = wk;
            this.targetParam.sh = this.targetSize.h;
            
            this.targetParam.dy = 0;
            this.targetParam.dh = this.targetSize.h;
        }

        //バーコードガイドの設定
        this.sqParam.valid = true;
        this.sqParam.w = this.targetSize.w;
        this.sqParam.h = this.targetSize.h;
        this.sqParam.x = (this.viewSize.w - this.targetSize.w) / 2;
        if (this.sqParam.x < 0) {
            this.sqParam.valid = false;
        }
        this.sqParam.y = (this.viewSize.h - this.targetSize.h) / 2;
        if (this.sqParam.y < 0) {
            this.sqParam.valid = false;
        }

        this.viewParam.init = true;
    }

    toggleScan() {
        if(this.wrapperElement.style.visibility=="visible") {
            this.scanEnd();
        } else {
            this.scanStart();
        }
    }

    scanStart() {
        this.video.play();
        //setIntervalだと処理の遅延のかかわらず実行してしまうので都度再帰する。
        
        this.wrapperElement.style.visibility="visible";


        if (this.blnCameraInit==false) {
            this.reserveEnd = setTimeout(() => {
                this.wrapperElement.style.visibility="hidden";
            }, 3000);
        } else {
            this.blnScaning = true;   
            setTimeout(this.scanning.bind(this),0);
        }
    }
    
    scanning() {
        //スキャン本体
        if (this.blnScaning == false) {
            return;
        }

        //パラメータ初期化()
        this.initParam();

        this.blnScaning=true;

        //バーコードエリアに線画
        this.targetContext.drawImage(this.video,this.targetParam.sx,this.targetParam.sy,this.targetParam.sw,this.targetParam.sh,this.targetParam.dx,this.targetParam.dy,this.targetParam.dw,this.targetParam.dh);

        //線画からバーコード解析
        this.targetElement.toBlob((blob)=>{
                let reader = new FileReader();
                reader.onload=()=>{
                this.qConfig.src=reader.result;
                Quagga.decodeSingle(this.qConfig,function(){});
            }
            reader.readAsDataURL(blob);
        });
        
        //プレビューエリアに線画
        //処理が遅くなるような場合は、scanningCntを使ってプレビューの線画を間引く
        //if (scanningCnt % 2 == 0) {}
        this.viewContext.drawImage(this.video,this.viewParam.sx,this.viewParam.sy,this.viewParam.sw,this.viewParam.sh,this.viewParam.dx,this.viewParam.dy,this.viewParam.dw,this.viewParam.dh);

        //バーコードガイドの線画
        if(this.sqParam.valid) {
            this.viewContext.beginPath();
            this.viewContext.strokeStyle="rgb(255,0,0)";
            this.viewContext.lineWidth=this.targetSize.border;
            this.viewContext.rect(this.sqParam.x,this.sqParam.y,this.sqParam.w,this.sqParam.h);
            this.viewContext.stroke();
        }

        //再帰
        setTimeout(this.scanning.bind(this),this.scanInterval);
    }
    
    scanEnd() {
        if (this.reserveEnd != null) {
            clearTimeout(this.reserveEnd);
        }
        this.blnScaning = false;
        this.video.pause();
        this.wrapperElement.style.visibility="hidden";
        this.validatainWkCnt = 0;
        this.validationCode="";
    }
}

//エントリーポイント
new BarcodeReader(document.getElementById("barcode-scanner"),640,480,300,200,200,100,2,100,3);
<script>
</body>
</html>

稼働サンプル

クラス版を実際に動かしています。カメラのある端末で試してみてください。

カメラを止める

上記のコードだと一度カメラを起動させると、スキャンをやめても止まりません。そこでMediaStreamTrack.stop()を参考にカメラを解放します。

ただ手持ちの古いAndroid(v6)の機体だと、カメラ停止後再度起動させることができませんでした。他Safariでもエラーになるという報告を受けました。

Android11やWindows10のPCなら再起動することができました。

scanStartメソッドでカメラを初期化するようにし、scanEndメソッドでvideoのsrcObjectであるMediaStreamからすべてのトラックを取り除き、念のためトラックすべてを停止しています。

カメラは参照しているオブジェクトがすべてなくなると自動的に停止となるようです。停止するとブラウザやOSのカメラ使用中のアラートが消えます。

let mediastream = this.video.srcObject;
let tracks = mediastream.getTracks();

tracks.forEach(function(track) {
  //mediastream.removeTrack(track);//不要・環境によっては有効かもしれません。
  track.stop();
});
this.video.srcObject=null;
this.blnCameraInit==false

カメラ停止のスクリプトを組み込んだコードのサンプルはGit Hubで公開しています。

筆者紹介


自分の写真
がーふぁ、とか、ふぃんてっく、とか世の中すっかりハイテクになってしまいました。プログラムのコーディングに触れることもある筆者ですが、自分の作業は硯と筆で文字をかいているみたいな古臭いものだと思っています。 今やこんな風にブログを書くことすらAIにとって代わられそうなほど技術は進んでいます。 生活やビジネスでPCを活用しようとするとき、そんな第一線の技術と比べてしまうとやる気が失せてしまいがちですが、おいしいお惣菜をネットで注文できる時代でも、手作りの味はすたれていません。 提示されたもの(アプリ)に自分を合わせるのでなく、自分の活動にあったアプリを作る。それがPC活用の基本なんじゃなかと思います。 そんな意見に同調していただける方向けにLinuxのDebianOSをはじめとした基本無料のアプリの使い方を紹介できたらなと考えています。

広告