Сканирование QR в браузере: jsQR + Web Worker

Быстрые ссылки
- Получение камеры
- Захват видеопотока в
- Создание canvas
- Добавление jsQR в воркер
- Обновляющий цикл и отправка кадров
- Формат результата jsQR
- Критерии приёмки и тесты
- Альтернативные подходы и ограничения
- Безопасность и конфиденциальность
Введение
QR-коды широко используются для передачи ссылок, идентификаторов и коротких данных. Веб‑реализация сканера позволяет пользователю считывать коды без установки приложения. В этой инструкции показано, как совместить jsQR и Web Worker, чтобы: 1) избежать тормозов интерфейса, 2) обрабатывать по одному кадру за раз, 3) обеспечить стабильность на мобильных устройствах.
Определение: jsQR — библиотека на JavaScript для детекции QR-кодов в изображениях. Web Worker — фоновый веб‑поток, изолированная среда исполнения JavaScript.
1. Получение камеры
Основная задача — получить MediaStream через navigator.mediaDevices. Ниже — аккуратный пример с обработкой ошибок и выбором камеры.
const getCamera = async () => {
if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
throw new Error("mediaDevices API недоступен.");
}
const devices = await navigator.mediaDevices.enumerateDevices();
const cameras = devices.filter(d => d.kind === "videoinput");
if (cameras.length === 0) {
throw new Error("Камера не найдена.");
}
// Можно добавить логику выбора конкретной камеры по deviceId
return cameras[0];
};Если пользователь блокировал доступ к камере, браузер выбросит ошибку при запросе прав. Предусмотрите UI‑подсказку с инструкциями по открытию настроек браузера.
2. Захват видеопотока в
Получите поток и прикрепите его к
const startVideo = async () => {
const selectedCamera = await getCamera();
const stream = await navigator.mediaDevices.getUserMedia({
video: { deviceId: selectedCamera.deviceId }
});
const video = document.getElementById("video");
video.srcObject = stream;
await video.play();
return stream;
};Пояснение: playsinline нужен на iOS для воспроизведения встраиваемого видео без перехода в полноэкранный режим.
3. Создание canvas и извлечение пикселей
Canvas служит как буфер для текущего кадра видео. Его размер должен совпадать с разрешением видеопотока.
const setupCanvasFromStream = (stream) => {
const tracks = stream.getVideoTracks();
const settings = tracks[0].getSettings();
const canvas = document.createElement("canvas");
canvas.width = settings.width || 640; // fallback
canvas.height = settings.height || 480;
const ctx = canvas.getContext("2d");
return { canvas, ctx };
};Замечание: на некоторых устройствах getSettings() может вернуть undefined для width/height — используйте резервные значения или измеряйте video.videoWidth/video.videoHeight после начала воспроизведения.
4. Добавление jsQR в Web Worker
Запуск jsQR в главном потоке может замедлить интерфейс, особенно на старых телефонах. Вынесите вызов jsQR в воркер.
Главный поток создаёт воркер и слушает сообщения:
const qrWorker = new Worker("/qr-worker.js");
qrWorker.addEventListener("message", ({ data }) => {
if (data) {
// Данные QR-кода обнаружены
// Например: обработать data.data (строка), показать UI, выполнить навигацию
console.log('QR:', data.data);
// здесь можно остановить цикл или предпринять дальнейшие действия
} else {
// Нет кода в кадре — подать следующий кадр
tick();
}
});Код воркера (/qr-worker.js):
importScripts("jsQR.js");
self.addEventListener("message", e => {
const { data, width, height } = e.data; // data: ImageData или Uint8ClampedArray
const qrData = jsQR(data.data || data, width, height);
self.postMessage(qrData || null);
});Пояснение: importScripts загружает библиотеку jsQR в контекст воркера. В сообщении мы ожидаем либо ImageData, либо объект с полями data,width,height.
5. Обновляющий цикл: передаём кадры во воркер
Создайте цикл, который рисует текущий кадр video на canvas, извлекает imageData и отправляет его в воркер. Важно: отправляйте кадры последовательно — ждите ответа воркера, прежде чем отправлять следующий, чтобы избежать накопления задач.
let running = true;
const updateJsQr = () => {
// Нарисовать текущий кадр на canvas
canvasCtx.drawImage(video, 0, 0, canvas.width, canvas.height);
const imageData = canvasCtx.getImageData(0, 0, canvas.width, canvas.height);
// Передать только необходимые данные — ImageData включает .data (Uint8ClampedArray)
qrWorker.postMessage({ data: imageData, width: canvas.width, height: canvas.height });
};
const tick = () => {
if (!running) return;
requestAnimationFrame(updateJsQr);
};
// Запуск после инициализации видео/канвы
tick();Замечание: при отправке больших объектов между потоками можно использовать Transferable объекты (ArrayBuffer) для повышения производительности. ImageData.data.buffer можно передавать как transferable, но нужно аккуратно работать с типами и совместимостью браузеров.
6. Формат результата jsQR
При успешном распознавании jsQR возвращает объект со свойствами:
- data — строка, извлечённая из QR-кода.
- binaryData — Uint8ClampedArray с сырыми байтами (если требуется двоичная обработка).
- version — версия QR-кода.
- location — объект с координатами углов обнаруженного кода (topLeft, topRight, bottomLeft, bottomRight), полезен для оверлея и проверки положения.
Если код не найден, возвращается null/undefined.
Пример обработки результата в главном потоке:
qrWorker.addEventListener('message', ({ data }) => {
if (data) {
// Отобразить рамку по data.location
drawOverlay(data.location);
// Обработать полезную нагрузку
handleQrPayload(data.data);
running = false; // остановить цикл, если нужно одноразовое сканирование
} else {
tick();
}
});7. UX и устойчивость: лучшие практики
- Попросите пользователя обеспечить достаточное освещение и держать камеру устойчиво.
- Покажите подсказку, когда камера не имеет разрешения или отсутствует.
- Дайте пользователю выбор камеры (фронт/тыл) при наличии нескольких.
- Включите визуальный оверлей (крупная рамка) и анимацию при успешном считывании.
- Ограничьте размер canvas, если видеопоток очень большой, для уменьшения нагрузки.
Important: не отправляйте кадры чаще, чем воркер успевает обрабатывать. Ожидайте ответа, прежде чем посылать следующий.
8. Критерии приёмки
- Скринтесты: при хорошем освещении код распознаётся за <3 попыток (с поправкой на устройство).
- UI не блокируется во время сканирования (главный поток остаётся отзывчивым).
- При запрете камеры показывается понятное сообщение с инструкциями по включению прав.
- Обработка результата вызывает ожидаемую навигацию/вызов API без дублирования.
- Воркеры корректно закрываются при уходе со страницы.
9. Тестовые случаи / Приёмочные тесты
- Позитивные тесты:
- QR с URL => приложение открывает URL.
- QR с коротким текстом => текст показывается в модальном окне.
- Негативные тесты:
- Плохое освещение — приложение остаётся отзывчивым и не падает.
- Блокировка камеры — выводится ошибка без крушения.
- Дублирующиеся отправки — single‑shot детектирование не срабатывает дважды.
- Граничные случаи:
- Высокое разрешение камеры — проверка производительности.
- Несколько камер — выбор фронт/тыл.
10. Чек‑лист по ролям
Разработчик:
- Реализовать корректную инициализацию камеры.
- Создать canvas с корректными размерами.
- Вырезать и отправлять ImageData в воркер.
- Обработать результат и закрыть воркер.
Тестировщик:
- Проверить сценарии освещения и углов.
- Выполнить нагрузочные тесты на слабых устройствах.
Продуктовый менеджер:
- Утвердить требуемое поведение после распознавания (навигация/показ данных).
11. Альтернативные подходы и ограничения
Альтернативы:
- Использовать нативные API (например, Barcode Detection API): проще, но поддержка браузеров ограничена.
- Применять WebAssembly‑библиотеки (ZXing в виде WASM): потенциально быстрее, но сложнее в интеграции.
- Обрабатывать кадры на сервере (отправлять изображение на бэкенд): повышает задержку и создаёт проблемы конфиденциальности.
Когда это не работает:
- Очень плохое освещение, повреждённый/искажённый QR-код.
- Камера с низким разрешением или сильным шумом.
- Браузеры без поддержки Web Worker или без getUserMedia (очень старые).
Рекомендация: если ваше приложение должно работать в большом количестве устаревших браузеров, реализуйте graceful fallback с ручным вводом кода.
12. Производительность и оптимизации
- Используйте transferable objects: передавайте imageData.data.buffer во воркер для избежания копирования.
- Масштабируйте canvas до меньшего разрешения при обнаружении, что обработка занимает слишком много времени.
- Профилируйте на целевых устройствах: частотность кадров (FPS) и время выполнения jsQR — ключевые метрики.
Фактическая метрика: время выполнения jsQR зависит от размера изображения и мощности CPU. На слабых устройствах уменьшение размера кадра в 2× часто даёт заметное ускорение.
13. Безопасность и конфиденциальность
- Камера: не храните необработанные кадры на сервере без явного согласия пользователя.
- Передача: если отправляете кадры/изображения на сервер для распознавания, используйте HTTPS и удаляйте снимки после обработки.
- Правила GDPR: при обработке персональных данных (видео лиц, метаданные) обеспечьте юридическую основу (согласие) и минимизацию данных.
Privacy note: сканирование QR-кода обычно извлекает только текст/URL — опасность появляется, если вы сохраняете или пересылаете полные кадры камеры.
14. Совместимость и миграция
Поддерживаемые платформы:
- Современные десктопные браузеры и мобильные браузеры с поддержкой getUserMedia и Web Workers.
Проверки:
- iOS Safari: требует playsinline и пользовательского взаимодействия для запуска некоторых потоков.
- Android Chrome: работает из коробки в большинстве версий.
Миграция: если вы используете сборщик (webpack/Parcel), можно подключать jsQR как npm‑пакет и инлайнить воркер через worker-loader или создавая отдельный бандл.
15. Примеры кода: полный сценарий
Ниже — упрощённый поток инициализации, объединяющий шаги вместе.
(async () => {
const camera = await getCamera();
const stream = await navigator.mediaDevices.getUserMedia({ video: { deviceId: camera.deviceId } });
const video = document.getElementById('video');
video.srcObject = stream;
await video.play();
const { canvas, ctx } = setupCanvasFromStream(stream);
document.body.appendChild(canvas); // опционально для отладки
const qrWorker = new Worker('/qr-worker.js');
let processing = false;
let stopped = false;
qrWorker.addEventListener('message', ({ data }) => {
processing = false;
if (data) {
console.log('QR payload:', data.data);
// здесь можно остановить или продолжать в зависимости от UX
stopped = true; // пример: остановить после первого обнаружения
}
});
const loop = () => {
if (stopped) return;
if (!processing) {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// Можно передать Transferable: imageData.data.buffer
qrWorker.postMessage({ data: imageData, width: canvas.width, height: canvas.height });
processing = true;
}
requestAnimationFrame(loop);
};
loop();
})();16. Проблемы и отладка
- Если воркер не загружаются: проверьте путь /qr-worker.js и права CORS.
- Если jsQR возвращает null всегда: проверьте корректность передаваемого imageData (ширина/высота), убедитесь, что код виден целиком и освещён.
- Если интерфейс тормозит: ограничьте разрешение canvas или уменьшите частоту отправки кадров.
17. Короткая методология внедрения (SOP)
- Подключите jsQR (локально или npm).
- Сделайте компонент камеры и проверку разрешений.
- Создайте canvas с размерами потока.
- Напишите воркер с importScripts(‘jsQR.js’).
- Реализуйте цикл отправки кадров и ожидания ответа.
- Обработайте результат и закройте воркер/поток.
- Проведите тесты на целевых устройствах и добавьте fallback.
18. Краткий словарь (1‑строчники)
- jsQR: библиотека для распознавания QR на JavaScript.
- Web Worker: фоновый поток выполнения JavaScript.
- MediaStream: поток медиа (видео/аудио) от устройства.
- canvas: HTML элемент для рисования и извлечения пикселей.
Заключение
Комбинация jsQR и Web Worker — надёжный подход для реализации веб‑сканера QR: она сохраняет отзывчивость интерфейса и позволяет масштабировать решение под разные устройства. При внедрении обратите внимание на обработку ошибок, безопасность и тесты на целевых платформах.
Ключевые вещи: держите обработку кадров последовательной, не отправляйте следующий кадр, пока воркер не ответил; минимизируйте объём данных и уважайте приватность пользователя.
Похожие материалы
Обнаруживаемо другими в iOS: что это и как отключить
Как бесплатно разместить fan-gate Facebook на Heroku
Twitch PiP: как включить и смотреть
CSGO и высокая загрузка CPU — что делать
CS:GO не подключается к серверам — как исправить