import { BarcodeDetector as BarcodeDetectorPonyfill, prepareZXingModule } from "barcode-detector/dist/cjs/index.js"; import { createUUID } from "../../utils/uuid"; import { getConfig } from "../config"; import scanBeepAudio from "../../../res/scan_beep.ogg"; const scanWeb = { uuid: null, finish: true, stream: null, videoEl: null } const DEFAULT_SCAN_BEEP_AUDIO = scanBeepAudio; const ZXING_READER_WASM_URL = "./lib/reader.wasm"; let barcodeDetectorPreparePromise = null; let scanBeepAudioEl = null; let scanBeepAudioSrc = null; let scanBeepUnlocked = false; function removeEl(id, uuid) { try { let el = document.getElementById(id); if (uuid && el && el.uuid !== uuid) { return; } document.body.removeChild(el); } catch (error) { } } function createEl(tagName, id, style, appendChild) { let el = document.getElementById(id); if (!el) { el = document.createElement(tagName); el.id = id; el.style = style; appendChild && document.body.appendChild(el); } return el; } function stopMediaStream(stream) { try { const tracks = stream && stream.getTracks && stream.getTracks(); if (tracks && tracks.length) { for (let i = 0; i < tracks.length; i++) { tracks[i].stop(); } } } catch (e) { } } function stopActiveWebScan() { scanWeb.uuid = null; stopMediaStream(scanWeb.stream); try { if (scanWeb.videoEl) { scanWeb.videoEl.pause && scanWeb.videoEl.pause(); scanWeb.videoEl.srcObject = null; } } catch (e) { } scanWeb.stream = null; scanWeb.videoEl = null; } function transformPoint(point, width, height, mirrorHorizontal, mirrorVertical, cover) { let x = point.x; let y = point.y; if (cover) { x = cover.x + point.x * cover.scale; y = cover.y + point.y * cover.scale; } return { x: mirrorHorizontal ? width - x : x, y: mirrorVertical ? height - y : y }; } function canvasDrawLine(context, width, height, begin, end, color, mirrorHorizontal, mirrorVertical, cover) { const beginPoint = transformPoint(begin, width, height, mirrorHorizontal, mirrorVertical, cover); const endPoint = transformPoint(end, width, height, mirrorHorizontal, mirrorVertical, cover); context.beginPath(); context.moveTo(beginPoint.x, beginPoint.y); context.lineTo(endPoint.x, endPoint.y); context.lineWidth = 4; context.strokeStyle = color; context.stroke(); } function isMobile() { return typeof navigator !== 'undefined' && /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent || ""); } function getBarcodeFormats(scanType) { let formats = []; if (!scanType) { scanType = ["qrCode", "barCode"]; } if (scanType.includes('qrCode')) { formats.push('qr_code'); } if (scanType.includes('barCode')) { formats.push( 'ean_13', 'ean_8', 'code_128', 'code_39', 'codabar', 'upc_a', 'upc_e', 'itf', 'aztec', 'data_matrix', 'pdf417' ); } return formats; } function getBarcodeDetectorClass() { if (typeof BarcodeDetector !== 'undefined') { return BarcodeDetector; } return BarcodeDetectorPonyfill; } function prepareBarcodeDetector() { const BarcodeDetectorClass = getBarcodeDetectorClass(); if (typeof BarcodeDetector !== 'undefined' || !prepareZXingModule) { return Promise.resolve(BarcodeDetectorClass); } if (!barcodeDetectorPreparePromise) { prepareZXingModule({ overrides: { locateFile: path => { if (path && path.indexOf(".wasm") !== -1) { return ZXING_READER_WASM_URL; } return path; } } }); barcodeDetectorPreparePromise = Promise.resolve(BarcodeDetectorClass); } return barcodeDetectorPreparePromise; } function createBarcodeDetector(scanType) { return prepareBarcodeDetector().then(BarcodeDetectorClass => { if (!BarcodeDetectorClass) { throw new Error("BarcodeDetector is not supported"); } const formats = getBarcodeFormats(scanType); if (BarcodeDetectorClass.getSupportedFormats) { return BarcodeDetectorClass.getSupportedFormats().then(supportedFormats => { const supported = formats.filter(format => supportedFormats.indexOf(format) !== -1); if (!supported.length) { throw new Error("No supported barcode formats"); } return new BarcodeDetectorClass({ formats: supported }); }); } return new BarcodeDetectorClass({ formats }); }); } function shouldMirrorWebVideo(stream) { const webScanVideoMirror = getConfig("webScanVideoMirror"); if (typeof webScanVideoMirror === "boolean") { return webScanVideoMirror; } try { const track = stream && stream.getVideoTracks && stream.getVideoTracks()[0]; const settings = track && track.getSettings && track.getSettings(); if (settings && settings.facingMode === "environment") { return false; } if (settings && settings.facingMode === "user") { return true; } } catch (e) { } return !isMobile(); } function shouldMirrorWebVideoVertical() { return getConfig("webScanVideoMirrorVertical") === true; } function getCanvasDisplaySize(canvasEl, fallbackWidth, fallbackHeight) { const rect = canvasEl.getBoundingClientRect(); const width = rect.width || parseFloat(canvasEl.style.width) || fallbackWidth; const height = rect.height || parseFloat(canvasEl.style.height) || fallbackHeight; return { width: Math.max(1, Math.round(width)), height: Math.max(1, Math.round(height)) }; } function getCoverDrawOptions(sourceWidth, sourceHeight, targetWidth, targetHeight) { const scale = Math.max(targetWidth / sourceWidth, targetHeight / sourceHeight); const width = sourceWidth * scale; const height = sourceHeight * scale; return { scale, width, height, x: (targetWidth - width) / 2, y: (targetHeight - height) / 2 }; } function drawBarcode(context, width, height, barcode, mirrorHorizontal, mirrorVertical, cover) { const cornerPoints = barcode.cornerPoints; if (cornerPoints && cornerPoints.length) { for (let i = 0; i < cornerPoints.length; i++) { canvasDrawLine(context, width, height, cornerPoints[i], cornerPoints[(i + 1) % cornerPoints.length], "#FF3B58", mirrorHorizontal, mirrorVertical, cover); } return; } if (barcode.boundingBox) { const rect = barcode.boundingBox; const points = [ { x: rect.x, y: rect.y }, { x: rect.x + rect.width, y: rect.y }, { x: rect.x + rect.width, y: rect.y + rect.height }, { x: rect.x, y: rect.y + rect.height } ]; for (let i = 0; i < points.length; i++) { canvasDrawLine(context, width, height, points[i], points[(i + 1) % points.length], "#FF3B58", mirrorHorizontal, mirrorVertical, cover); } } } function playScanBeep() { if (getConfig("webScanBeepEnabled") === false) { return; } const audio = getScanBeepAudio(); if (!audio) { return; } try { audio.muted = false; audio.volume = 1; audio.currentTime = 0; const playPromise = audio.play(); playPromise && playPromise.catch && playPromise.catch(() => { // no thing }); } catch (e) { } } function getScanBeepAudio() { const audioSrc = getConfig("webScanBeepAudio") || DEFAULT_SCAN_BEEP_AUDIO; if (!audioSrc || typeof Audio === 'undefined') { return null; } if (!scanBeepAudioEl || scanBeepAudioSrc !== audioSrc) { scanBeepAudioSrc = audioSrc; scanBeepUnlocked = false; scanBeepAudioEl = new Audio(audioSrc); scanBeepAudioEl.preload = "auto"; try { scanBeepAudioEl.load && scanBeepAudioEl.load(); } catch (e) { } } return scanBeepAudioEl; } export function unlockScanBeep() { if (getConfig("webScanBeepEnabled") === false || scanBeepUnlocked) { return; } const audio = getScanBeepAudio(); if (!audio) { return; } try { // 这里只做预加载,不主动 play,避免部分浏览器露出提示音。 audio.load && audio.load(); scanBeepUnlocked = true; } catch (e) { } } export function isSupportWebScan() { return typeof navigator !== 'undefined' && navigator.mediaDevices && navigator.mediaDevices.getUserMedia && !!getBarcodeDetectorClass() && getConfig("webScanEnabled") !== false; } export function isSupportImageScan() { return typeof document !== 'undefined' && typeof URL !== 'undefined' && URL.createObjectURL; } export function stopScanForWeb() { return Promise.resolve().then(() => { stopActiveWebScan(); }) } function chooseImageFile() { return new Promise(resolve => { const input = document.createElement("input"); input.type = "file"; input.accept = "image/*"; input.style.display = "none"; let finished = false; let finish = file => { if (finished) { return; } finished = true; removeEl("__webscan_image_input__"); resolve(file); }; input.id = "__webscan_image_input__"; input.onchange = () => { finish(input.files && input.files[0]); }; input.oncancel = () => { finish(null); }; document.body.appendChild(input); input.click(); }); } function detectImageFile(detector, file) { if (!file) { return Promise.resolve(null); } if (typeof createImageBitmap !== 'undefined') { return createImageBitmap(file).then(image => { return detector.detect(image).then(barcodes => { image.close && image.close(); return barcodes && barcodes[0]; }).catch(err => { image.close && image.close(); throw err; }); }); } return new Promise((resolve, reject) => { const image = new Image(); const url = URL.createObjectURL(file); image.onload = () => { detector.detect(image).then(barcodes => { URL.revokeObjectURL(url); resolve(barcodes && barcodes[0]); }).catch(err => { URL.revokeObjectURL(url); reject(err); }); }; image.onerror = err => { URL.revokeObjectURL(url); reject(err); }; image.src = url; }); } export function startScanForImage() { return createBarcodeDetector(getConfig("webScanType")).then(detector => { return chooseImageFile().then(file => detectImageFile(detector, file)); }).then(code => { if (code && code.rawValue) { return { result: code.rawValue }; } return { success: false, error: "未识别到二维码或条形码" }; }); } export function startScanForWeb(canvasStyle, onResult) { let currentUuid = null; return new Promise((resolve, reject) => { try { stopActiveWebScan(); scanWeb.uuid = createUUID(); scanWeb.finish = false; let videoEl = createEl("video", "__webscan_video__", "display: none", false); let canvasEnabled = getConfig("webScanCanvasEnabled") !== false; let canvasDisplay = ""; let canvasBaseStyle = canvasStyle || "position: fixed; width: 300px; height: 300px; top: 0; left: 0; z-index: 9999;"; let canvasEl = createEl("canvas", "__webscan_canvas__", canvasBaseStyle + " display: none;", true); canvasDisplay = canvasEl.style.display; canvasEl.style.cssText = canvasBaseStyle; canvasDisplay = canvasEl.style.display; let canvasDisplaySize = getCanvasDisplaySize(canvasEl, 300, 240); canvasEl.style.display = "none"; let context = canvasEl.getContext("2d"); currentUuid = scanWeb.uuid; videoEl.width = 300; videoEl.height = 300; videoEl.uuid = scanWeb.uuid; canvasEl.uuid = scanWeb.uuid; createBarcodeDetector(getConfig("webScanType")).then(detector => { return navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" } }).then(stream => { return { detector, stream }; }); }).then(function (options) { const detector = options.detector; const stream = options.stream; if (scanWeb.uuid !== currentUuid) { stopMediaStream(stream); reject({ cancel: 1 }); return; } scanWeb.stream = stream; scanWeb.videoEl = videoEl; const mirrorVideo = shouldMirrorWebVideo(stream); const mirrorVideoVertical = shouldMirrorWebVideoVertical(); videoEl.srcObject = stream; videoEl.setAttribute("playsinline", true); // iOS使用 videoEl.play(); canvasEl.style.display = "none"; let detecting = false; let displayed = false; let closed = false; let close = () => { if (closed) { return; } closed = true; stopMediaStream(stream); if (scanWeb.uuid === currentUuid || scanWeb.stream === stream) { scanWeb.stream = null; scanWeb.videoEl = null; } try { videoEl.pause && videoEl.pause(); videoEl.srcObject = null; } catch (_e) { } }; let tick = () => { try { if (videoEl.readyState === videoEl.HAVE_ENOUGH_DATA && !detecting) { canvasEl.width = canvasDisplaySize.width; canvasEl.height = canvasDisplaySize.height; const cover = getCoverDrawOptions(videoEl.videoWidth, videoEl.videoHeight, canvasEl.width, canvasEl.height); context.setTransform( mirrorVideo ? -1 : 1, 0, 0, mirrorVideoVertical ? -1 : 1, mirrorVideo ? canvasEl.width : 0, mirrorVideoVertical ? canvasEl.height : 0 ); context.drawImage(videoEl, cover.x, cover.y, cover.width, cover.height); context.setTransform(1, 0, 0, 1, 0, 0); if (canvasEnabled && !displayed) { displayed = true; canvasEl.style.display = canvasDisplay || ""; } detecting = true; detector.detect(videoEl).then(barcodes => { const code = barcodes && barcodes[0]; if (code && code.rawValue && scanWeb.uuid == currentUuid) { if (!onResult || !onResult(code.rawValue)) { return; } drawBarcode(context, canvasEl.width, canvasEl.height, code, mirrorVideo, mirrorVideoVertical, cover); playScanBeep(); scanWeb.uuid = null; scanWeb.finish = true; close(); resolve({ result: code.rawValue }) } }).catch(() => { }).finally(() => { detecting = false; }); } } catch (e) { detecting = false; } if (scanWeb.uuid == currentUuid) { requestAnimationFrame(() => { tick() }); } else { if (!scanWeb.finish) { reject({ cancel: 1 }); scanWeb.finish = true; } close(); } if (scanWeb.finish) { close(); } }; requestAnimationFrame(() => { tick(); }); }).catch(err => { reject({ error: err }); }); } catch (e) { reject({ error: e }); } }).finally(() => { removeEl("__webscan_video__", currentUuid); removeEl("__webscan_canvas__", currentUuid); }) }