5年くらい前、Djangoでバーコードを出力する機能も備えたアプリを作りました。
ダンボールに入れた本を管理するDjangoアプリ「danborary」を作った - メモ的な思考的な
当時、バーコードを読むためには物理バーコードリーダーを用意していました。ただ、物理バーコードリーダーはPCと接続する必要があることから、運用の手間を感じるようになりました。
時は流れ、ふと「スマホでバーコードリーダーを作ればよいのでは」と思い立ちました。
まずはWeb標準でカメラを扱えないかを調べたところ、 MediaDevices APIがありました。
MediaDevices - Web API | MDN
サポート状況を確認したところ、スマホであれば扱えそうでした。
"mediaDevices" | Can I use... Support tables for HTML5, CSS3, etc
次に、バーコードを検知・解析するWeb標準がないかを調べたところ、BarcodeDetector APIがありました。
BarcodeDetector - Web API | MDN
サポート状況を確認したところ、AndroidのChromeであれば BarcodeDetector APIは使えそうでした。
- BarcodeDetector API | Can I use... Support tables for HTML5, CSS3, etc
- BarcodeDetector API: detect | Can I use... Support tables for HTML5, CSS3, etc
あとはPWA対応しておけば、Web標準だけでスマホのバーコードリーダーが作れそうでした。
ただ、今までPWA、MediaDevices、BarcodeDetector APIを使ったことがないこともありゼロから作るのは大変そうでした。
そこで、
という進め方にしたことから、その時のメモを残します。
目次
環境
- mac
- DevContainer
- Node.js 24.12.0
- Python 3.13.5
- Codex 0.77.0
- OpenSpec 0.17.2
- pnpm 10.26.2
- TypeScript 5.9.3
- Biome 2.3.10
- Tailscale CLIを使って、DevContainerのHTTPサーバーにあるPWAアプリをAndroidで使う
- Tailscaleまわりの設定は前回の記事を参照
PWA版のバーコードリーダーについて
今回のPWA版のバーコードリーダーは、DevContainer上の public ディレクトリに置いたHTML + JavaScriptファイルで構成されています。これをDevContainer上のPythonのHTTPサーバーを使って配信します。
カメラ利用のためには安全なコンテキスト(HTTPS通信)が必要なことから、前回の記事のようにTailscaleを介してHTTPS化します。
DevContainer上でHTMLを配信しているHTTPサーバにHTTPSでアクセスするため、localtunnelやTailscaleを試してみた - メモ的な思考的な
前回の記事の再掲となりますが、経路はこんな感じです。
[DevContainer] | | (port forward 5180) v [mac] | | (tailscale serve --https=5190) v [Tailscale tailnet] | | (WireGuard VPN) v [Android (Tailscaleログイン済み)]
あとはOpenSpecに依頼してバーコードリーダーを作ります。
最初は、使いたいAPIや構成を伝えつつ必要最低限のPWAアプリを作りました。
https://github.com/thinkAmi-sandbox/pwa_barcode_reader-example/tree/main/openspec/changes/archive/2025-12-24-add-pwa-barcode-scan
動作確認すると、カメラ画像のプレビューと実際にスキャンしたバーコードで位置がズレていたことから、その対応を行いました。
https://github.com/thinkAmi-sandbox/pwa_barcode_reader-example/tree/main/openspec/changes/archive/2025-12-24-update-scan-region
あとは細かな調整をした感じです。詳しくは後述のリポジトリにある openspec 以下のディレクトリの中身を確認してください。OpenSpecが作ったドキュメントが置いてあります。
実装の確認メモ
動くものができたところで実装を確認しました。
自分が詳しくない部分を中心にAIに聞いたり調べたところをメモしておきます。
バーコードリーダーの概要
- ブラウザ標準の BarcodeDetector API を使い、外部ライブラリなしでバーコード/QRを検出する軽量アプリ
- カメラ映像を video に流し、読み取り枠の範囲だけを canvas に切り出して検出精度と負荷を最適化
- TypeScript を最小構成でビルドし、静的ファイルとして配信する構成
スマホカメラの利用
src/app.ts のstartScan 関数にある以下の部分にて、利用するスマホの背面カメラからストリームを得られるようにしています。
stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { ideal: "environment" }, aspectRatio: { ideal: 4 / 3 }, }, audio: false, });
getUserMediaの引数は次の通りです。
- 音声は不要のため、
audio: falseとした - カメラ関係の設定
- 背面カメラを使うため、
facingModeにenvironmentを指定- MediaTrackConstraints: facingMode property - Web APIs | MDN
- ただし、見つからない場合にもエラーとしたくないため、
idealにて指定
- プレビュー枠を4:3の比率で用意したため、カメラ画像も4:3の比率になるよう
aspectRatio: { ideal: 4 / 3 }を指定- MediaTrackConstraints: aspectRatio property - Web APIs | MDN
- カメラのデフォルト比率は端末ごとに異なるため、読み取り範囲の不一致を防ぐことも想定
- ただし、端末が対応していなければ無視できるよう、
idealで指定
- 背面カメラを使うため、
得られたストリームは videoタグのメディアソースとして割り当てます。
HTMLMediaElement: srcObject プロパティ - Web API | MDN
video.srcObject = stream;
続いて、メタデータ(解像度、縦横比、再生時間)などを読み込んだときに発生するイベント loadedmetadata にて、カメラ映像の縦横比に合わせてプレビュー枠を更新する関数 updatePreviewAspectを呼んでいます。
HTMLMediaElement: loadedmetadata イベント - Web API | MDN
video.addEventListener("loadedmetadata", updatePreviewAspect, { once: true, });
準備ができたら、 play でカメラ画像を再生します。
HTMLMediaElement: play() メソッド - Web API | MDN
await video.play();
あとは BarcodeDetector オブジェクトを生成します。
BarcodeDetector() - Web API | MDN
今回対象とするのはQRコードとEAN-13なため、formatを指定して誤検知を防ぎます。
バーコード検出 API - Web API | MDN
バーコードの解析
scanLoop関数にて解析しています。
videoタグの画像全体を渡すと解析に負荷がかかるため、 drawScanFrame 関数で読み取り枠内の画像をcanvas化しています。
const frame = drawScanFrame();
そのcanvasを元に、BarcodeDetector.detectで解析をします。
BarcodeDetector.detect() - Web API | MDN
解析できた場合は、バーコードデータをデコードした文字列を画面に反映します。
const barcodes = await detector.detect(frame); if (barcodes.length > 0) { const value = barcodes[0].rawValue || ""; if (value && value !== lastValue) { lastValue = value; setResult(value); } }
PWA版バーコードリーダーのインストール
今回のPWA版バーコードリーダーはスマホへのインストールを可能とします。一方で、必要最低限の実装としたいため、Service Workerによるオフライン操作は不要とします。
そこで、Webアプリマニフェストとして public/manifest.webmanifest だけを用意しました。
- ウェブアプリマニフェスト | PWA をインストール可能にする - プログレッシブウェブアプリ (PWA) | MDN
- ウェブアプリマニフェスト - プログレッシブウェブアプリ (PWA) | MDN
{ "name": "PWA版バーコードスキャナー", "short_name": "PWA版バーコードスキャナー", "start_url": ".", "display": "standalone", "background_color": "#f3f0e8", "theme_color": "#0f1a1c", "icons": [ { "src": "icons/icon-192.svg", "sizes": "192x192", "type": "image/svg+xml" }, { "src": "icons/icon-512.svg", "sizes": "512x512", "type": "image/svg+xml" } ] }
動作確認
次の手順で動作準備をします。
- DevContainer上で
publicディレクトリをHTTPサーバーで公開する- DevContainer上のターミナルで
python -m http.server --directory public 5180
- DevContainer上のターミナルで
- Tailscaleの準備をする
- Tailscale CLIで、リバースプロキシを起動する
- macのターミナルで
tailscale serve --https=5190 5180
- macのターミナルで
Tailscale CLIの画面に表示されたURLをAndroidで開くと、バーコードリーダーが表示されました。読み取り枠にバーコードを入れたところ、値を正しく取得できました。
ソースコード
GitHubに上げました。
https://github.com/thinkAmi-sandbox/pwa_barcode_reader-example
