Files
scan-code-jssdk/src/services/web/index.js
2026-05-25 20:48:54 +08:00

898 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { BarcodeDetector as BarcodeDetectorPonyfill, prepareZXingModule } from "barcode-detector/dist/cjs/ponyfill.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_PATH = "lib/reader.wasm";
const WEBSCAN_CLOSE_BUTTON_ID = "__webscan_close_button__";
const WEBSCAN_PICK_IMAGE_BUTTON_ID = "__webscan_pick_image_button__";
const WEBSCAN_CLOSE_ICON_SVG = "<svg viewBox=\"0 0 1024 1024\" xmlns=\"http://www.w3.org/2000/svg\" width=\"22\" height=\"22\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M566.97558594 521.09667969L856.8828125 231.18945312c14.63378906-14.63378906 14.63378906-38.75976563 0-53.39355468l-1.58203125-1.58203125c-14.63378906-14.63378906-38.75976563-14.63378906-53.39355469 0L512 466.51660156 222.09277344 176.21386719c-14.63378906-14.63378906-38.75976563-14.63378906-53.39355469 0l-1.58203125 1.58203125c-15.02929688 14.63378906-15.02929688 38.75976563 0 53.39355469l289.90722656 289.90722656L167.1171875 811.00390625c-14.63378906 14.63378906-14.63378906 38.75976563 0 53.39355469l1.58203125 1.58203125c14.63378906 14.63378906 38.75976563 14.63378906 53.39355469 0L512 576.07226563 801.90722656 865.97949219c14.63378906 14.63378906 38.75976563 14.63378906 53.39355469 0l1.58203125-1.58203125c14.63378906-14.63378906 14.63378906-38.75976563 0-53.39355469L566.97558594 521.09667969z\" fill=\"currentColor\"/></svg>";
const WEBSCAN_PICK_IMAGE_ICON_SVG = "<svg viewBox=\"0 0 1024 1024\" xmlns=\"http://www.w3.org/2000/svg\" width=\"22\" height=\"22\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M896 160H128v704h768zm-704 64h640v423.68L706.08 496l-157.12 157.12L352 358.24l-160 240zm0 576v-86.24l160-240 187.04 280.48L701.92 592 832 747.52V800z\" fill=\"currentColor\"/><path d=\"M672 448a96 96 0 1 0-96-96 96 96 0 0 0 96 96zm0-128a32 32 0 1 1-32 32 32 32 0 0 1 32-32z\" fill=\"currentColor\"/></svg>";
const WEBSCAN_OVERLAY_BUTTON_SIZE = 40;
const WEBSCAN_OVERLAY_BUTTON_EDGE = 8;
const WEBSCAN_OVERLAY_BUTTON_GAP = 10;
function getDefaultWebScanOverlayButtonStyle() {
return "width: " + WEBSCAN_OVERLAY_BUTTON_SIZE + "px; height: " + WEBSCAN_OVERLAY_BUTTON_SIZE + "px; padding: 0; border: 0; border-radius: 50%; background: rgba(0, 0, 0, 0.55); color: #fff; display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 10000;";
}
const currentScriptSrc = typeof document !== "undefined"
&& document.currentScript
&& document.currentScript.src;
let barcodeDetectorPreparePromise = null;
let scanBeepAudioEl = null;
let scanBeepAudioSrc = null;
let scanBeepUnlockAudioEl = null;
let scanBeepUnlocked = false;
let scanBeepUnlocking = false;
let scanBeepGestureUnlockInstalled = false;
/** 极短静音 WAV仅用于在用户手势内解锁自动播放不播放真实提示音文件 */
const SILENT_BEEP_UNLOCK_AUDIO = "data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA=";
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 getZXingReaderWasmUrl() {
if (currentScriptSrc) {
return new URL(ZXING_READER_WASM_PATH, currentScriptSrc).href;
}
return "./" + ZXING_READER_WASM_PATH;
}
function prepareBarcodeDetector() {
const BarcodeDetectorClass = getBarcodeDetectorClass();
if (typeof BarcodeDetector !== 'undefined' || !prepareZXingModule) {
return Promise.resolve(BarcodeDetectorClass);
}
if (!barcodeDetectorPreparePromise) {
barcodeDetectorPreparePromise = prepareZXingModule({
fireImmediately: true,
overrides: {
locateFile: path => {
if (path && path.indexOf(".wasm") !== -1) {
return getZXingReaderWasmUrl();
}
return path;
}
}
}).then(() => 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 getDefaultWebScanCanvasSizePx() {
if (typeof window === "undefined") {
return { width: 300, height: 300 };
}
if (isMobile()) {
const side = Math.min(window.innerWidth || 300, window.innerHeight || 300);
return {
width: Math.max(1, Math.round(side)),
height: Math.max(1, Math.round(side))
};
}
return { width: 300, height: 300 };
}
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 getWebScanCloseButtonPositionStyle(canvasEl) {
const rect = canvasEl.getBoundingClientRect();
const top = Math.max(0, Math.round(rect.top + WEBSCAN_OVERLAY_BUTTON_EDGE));
const left = Math.max(0, Math.round(rect.right - (WEBSCAN_OVERLAY_BUTTON_SIZE + WEBSCAN_OVERLAY_BUTTON_EDGE)));
return "position: fixed; top: " + top + "px; left: " + left + "px;";
}
function getWebScanPickImageButtonPositionStyle(canvasEl) {
const rect = canvasEl.getBoundingClientRect();
const top = Math.max(0, Math.round(rect.top + WEBSCAN_OVERLAY_BUTTON_EDGE + WEBSCAN_OVERLAY_BUTTON_SIZE + WEBSCAN_OVERLAY_BUTTON_GAP));
const left = Math.max(0, Math.round(rect.right - (WEBSCAN_OVERLAY_BUTTON_SIZE + WEBSCAN_OVERLAY_BUTTON_EDGE)));
return "position: fixed; top: " + top + "px; left: " + left + "px;";
}
function updateWebScanCloseButtonStyle(closeButtonEl, canvasEl) {
if (!closeButtonEl) {
return;
}
const customStyle = getConfig("webScanCloseButtonStyle") || "";
closeButtonEl.style.cssText = getDefaultWebScanOverlayButtonStyle()
+ getWebScanCloseButtonPositionStyle(canvasEl)
+ customStyle;
}
function updateWebScanPickImageButtonStyle(pickButtonEl, canvasEl) {
if (!pickButtonEl) {
return;
}
const customStyle = getConfig("webScanPickImageButtonStyle") || "";
pickButtonEl.style.cssText = getDefaultWebScanOverlayButtonStyle()
+ getWebScanPickImageButtonPositionStyle(canvasEl)
+ customStyle;
}
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 isScanBeepEnabled() {
return getConfig("scanBeepEnabled") !== false;
}
function getScanBeepAudioSrc() {
return getConfig("scanBeepAudio") || DEFAULT_SCAN_BEEP_AUDIO;
}
function resetScanBeepPlayback(audio) {
try {
audio.pause();
audio.currentTime = 0;
} catch (e) {
}
}
function installScanBeepGestureUnlock() {
if (scanBeepGestureUnlockInstalled || typeof document === "undefined" || !isScanBeepEnabled()) {
return;
}
scanBeepGestureUnlockInstalled = true;
const onGesture = () => {
unlockScanBeep();
};
document.addEventListener("click", onGesture, true);
document.addEventListener("touchstart", onGesture, true);
document.addEventListener("keydown", onGesture, true);
}
export function playScanBeep() {
if (!isScanBeepEnabled()) {
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(err => {
if (err && err.name === "NotAllowedError") {
scanBeepUnlocked = false;
}
});
} catch (e) {
}
}
function getScanBeepUnlockAudio() {
if (typeof Audio === "undefined") {
return null;
}
if (!scanBeepUnlockAudioEl) {
scanBeepUnlockAudioEl = new Audio(SILENT_BEEP_UNLOCK_AUDIO);
scanBeepUnlockAudioEl.preload = "auto";
scanBeepUnlockAudioEl.muted = true;
scanBeepUnlockAudioEl.volume = 0;
scanBeepUnlockAudioEl.setAttribute("playsinline", "");
try {
scanBeepUnlockAudioEl.load && scanBeepUnlockAudioEl.load();
} catch (e) {
}
}
return scanBeepUnlockAudioEl;
}
function getScanBeepAudio() {
const audioSrc = getScanBeepAudioSrc();
if (!audioSrc || typeof Audio === 'undefined') {
return null;
}
if (!scanBeepAudioEl || scanBeepAudioSrc !== audioSrc) {
scanBeepAudioSrc = audioSrc;
scanBeepUnlocked = false;
scanBeepAudioEl = new Audio(audioSrc);
scanBeepAudioEl.preload = "auto";
scanBeepAudioEl.setAttribute("playsinline", "");
try {
scanBeepAudioEl.load && scanBeepAudioEl.load();
} catch (e) {
}
}
installScanBeepGestureUnlock();
return scanBeepAudioEl;
}
export function unlockScanBeep() {
if (!isScanBeepEnabled() || scanBeepUnlocked || scanBeepUnlocking) {
return;
}
getScanBeepAudio();
const audio = getScanBeepUnlockAudio();
if (!audio) {
return;
}
scanBeepUnlocking = true;
try {
audio.muted = true;
audio.volume = 0;
audio.currentTime = 0;
const playPromise = audio.play();
const finishUnlock = () => {
resetScanBeepPlayback(audio);
scanBeepUnlocking = false;
scanBeepUnlocked = true;
};
if (!playPromise || !playPromise.then) {
finishUnlock();
return;
}
playPromise.then(finishUnlock).catch(() => {
resetScanBeepPlayback(audio);
scanBeepUnlocking = false;
});
} catch (e) {
scanBeepUnlocking = false;
}
}
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 isWebScanImageFallbackEnabled() {
return isSupportImageScan() && getConfig("webScanImageFallbackOnVideoError") !== false;
}
function getWebScanVideoAccessTimeout() {
const timeout = getConfig("webScanVideoAccessTimeout");
return typeof timeout === "number" && timeout > 0 ? timeout : 10000;
}
function getWebScanVideoReadyTimeout() {
const timeout = getConfig("webScanVideoReadyTimeout");
return typeof timeout === "number" && timeout > 0 ? timeout : 8000;
}
function getUserMediaWithTimeout(constraints, timeoutMs) {
return new Promise((resolve, reject) => {
let settled = false;
const timer = setTimeout(() => {
if (settled) {
return;
}
settled = true;
reject(new Error("getUserMedia timeout"));
}, timeoutMs);
navigator.mediaDevices.getUserMedia(constraints).then(stream => {
if (settled) {
stopMediaStream(stream);
return;
}
settled = true;
clearTimeout(timer);
resolve(stream);
}).catch(err => {
if (settled) {
return;
}
settled = true;
clearTimeout(timer);
reject(err);
});
});
}
function runWebScanImageFallback(onResult, resolve, reject) {
stopActiveWebScan();
unlockScanBeep();
return createBarcodeDetector(getConfig("webScanType")).then(detector => {
return chooseImageFile({ preferCapture: isMobile() }).then(file => {
if (!file) {
return Promise.reject({ cancel: 1 });
}
return detectImageFile(detector, file);
}).then(code => {
if (code && code.rawValue) {
if (!onResult || !onResult(code.rawValue)) {
return Promise.reject({ cancel: 1 });
}
scanWeb.finish = true;
resolve({
result: code.rawValue
});
return;
}
return Promise.reject({
success: false,
error: "未识别到二维码或条形码",
imageFallbackUsed: true
});
});
}).catch(err => {
if (err && err.cancel) {
reject({ cancel: 1, imageFallbackUsed: true });
return;
}
if (err && err.result) {
reject(Object.assign({ imageFallbackUsed: true }, err));
return;
}
reject({
imageFallbackUsed: true,
error: err && err.error ? err.error : err
});
});
}
function tryWebScanImageFallback(onResult, resolve, reject, state) {
if (!isWebScanImageFallbackEnabled()) {
return false;
}
if (state && state.imageFallbackStarted) {
return false;
}
if (state) {
state.imageFallbackStarted = true;
}
runWebScanImageFallback(onResult, resolve, reject);
return true;
}
export function stopScanForWeb() {
return Promise.resolve().then(() => {
stopActiveWebScan();
})
}
function chooseImageFile(options) {
options = options || {};
return new Promise(resolve => {
const input = document.createElement("input");
input.type = "file";
input.accept = "image/*";
if (options.preferCapture) {
input.setAttribute("capture", options.captureMode || "environment");
}
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 = () => {
unlockScanBeep();
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 chooseImageFile().then(file => {
if (!file) {
return null;
}
return createBarcodeDetector(getConfig("webScanType")).then(detector => {
return detectImageFile(detector, file);
});
}).then(code => {
if (code && code.rawValue) {
return {
result: code.rawValue
};
}
return {
success: false,
error: "未识别到二维码或条形码"
};
});
}
export function startScanForWeb(onResult) {
let currentUuid = null;
const fallbackState = { imageFallbackStarted: false };
return new Promise((resolve, reject) => {
try {
stopActiveWebScan();
scanWeb.uuid = createUUID();
scanWeb.finish = false;
currentUuid = scanWeb.uuid;
let canvasStyle = getConfig("webScanCanvasStyle");
let canvasClass = getConfig("webScanCanvasClass");
let videoEl = createEl("video",
"__webscan_video__",
"display: none", false);
let canvasEnabled = getConfig("webScanCanvasEnabled") !== false;
let canvasDisplay = "";
let useDefaultCanvasLayout = !canvasStyle && !canvasClass;
let defaultCanvasPx = useDefaultCanvasLayout ? getDefaultWebScanCanvasSizePx() : { width: 300, height: 300 };
let canvasFallbackWidth = defaultCanvasPx.width;
let canvasFallbackHeight = defaultCanvasPx.height;
let canvasBaseStyle = canvasStyle || (!!canvasClass ? "" : "position: fixed; width: " + defaultCanvasPx.width + "px; height: " + defaultCanvasPx.height + "px; 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;
canvasEl.className = canvasClass || "";
canvasDisplay = canvasEl.style.display;
let canvasDisplaySize = getCanvasDisplaySize(canvasEl, canvasFallbackWidth, canvasFallbackHeight);
canvasEl.style.display = "none";
let closeButtonEl = null;
let pickImageButtonEl = null;
if (canvasEnabled) {
closeButtonEl = createEl("button", WEBSCAN_CLOSE_BUTTON_ID, "display: none;", true);
closeButtonEl.type = "button";
closeButtonEl.className = getConfig("webScanCloseButtonClass") || "";
closeButtonEl.innerHTML = WEBSCAN_CLOSE_ICON_SVG;
closeButtonEl.setAttribute("aria-label", "close");
closeButtonEl.onclick = event => {
event.preventDefault && event.preventDefault();
event.stopPropagation && event.stopPropagation();
if (scanWeb.uuid === currentUuid) {
scanWeb.uuid = null;
}
};
closeButtonEl.uuid = scanWeb.uuid;
if (isSupportImageScan()) {
pickImageButtonEl = createEl("button", WEBSCAN_PICK_IMAGE_BUTTON_ID, "display: none;", true);
pickImageButtonEl.type = "button";
pickImageButtonEl.className = getConfig("webScanPickImageButtonClass") || "";
pickImageButtonEl.innerHTML = WEBSCAN_PICK_IMAGE_ICON_SVG;
pickImageButtonEl.setAttribute("aria-label", "pick image");
pickImageButtonEl.uuid = scanWeb.uuid;
}
}
let context = canvasEl.getContext("2d");
videoEl.width = 300;
videoEl.height = 300;
videoEl.uuid = scanWeb.uuid;
canvasEl.uuid = scanWeb.uuid;
createBarcodeDetector(getConfig("webScanType")).then(detector => {
return getUserMediaWithTimeout({
video: {
facingMode: "environment"
}
}, getWebScanVideoAccessTimeout()).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使用
canvasEl.style.display = "none";
let detecting = false;
let displayed = false;
let closed = false;
let videoReadyTimer = null;
const clearVideoReadyTimer = () => {
if (videoReadyTimer) {
clearTimeout(videoReadyTimer);
videoReadyTimer = null;
}
};
const scheduleVideoReadyTimeout = () => {
clearVideoReadyTimer();
videoReadyTimer = setTimeout(() => {
if (scanWeb.uuid !== currentUuid || closed || scanWeb.finish) {
return;
}
if (videoEl.readyState >= videoEl.HAVE_ENOUGH_DATA) {
return;
}
closed = true;
close();
tryWebScanImageFallback(onResult, resolve, reject, fallbackState);
}, getWebScanVideoReadyTimeout());
};
scheduleVideoReadyTimeout();
let close = () => {
if (closed) {
return;
}
closed = true;
clearVideoReadyTimer();
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) { }
if (closeButtonEl) {
closeButtonEl.style.display = "none";
}
if (pickImageButtonEl) {
pickImageButtonEl.style.display = "none";
}
};
const videoPlayPromise = videoEl.play && videoEl.play();
if (videoPlayPromise && videoPlayPromise.catch) {
videoPlayPromise.catch(() => {
if (scanWeb.uuid !== currentUuid || closed || scanWeb.finish) {
return;
}
close();
tryWebScanImageFallback(onResult, resolve, reject, fallbackState);
});
}
if (pickImageButtonEl) {
pickImageButtonEl.onclick = event => {
event.preventDefault && event.preventDefault();
event.stopPropagation && event.stopPropagation();
if (scanWeb.uuid !== currentUuid || closed) {
return;
}
unlockScanBeep();
chooseImageFile().then(file => {
if (!file || scanWeb.uuid !== currentUuid || closed) {
return;
}
detectImageFile(detector, file).then(code => {
if (!code || !code.rawValue || scanWeb.uuid !== currentUuid || closed) {
return;
}
if (!onResult || !onResult(code.rawValue)) {
return;
}
scanWeb.uuid = null;
scanWeb.finish = true;
close();
resolve({
result: code.rawValue
});
}).catch(() => {
});
});
};
}
let tick = () => {
try {
if (videoEl.readyState === videoEl.HAVE_ENOUGH_DATA && !detecting) {
clearVideoReadyTimer();
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 || "";
canvasDisplaySize = getCanvasDisplaySize(canvasEl, canvasFallbackWidth, canvasFallbackHeight);
updateWebScanCloseButtonStyle(closeButtonEl, canvasEl);
closeButtonEl && (closeButtonEl.style.display = "flex");
if (pickImageButtonEl) {
updateWebScanPickImageButtonStyle(pickImageButtonEl, canvasEl);
pickImageButtonEl.style.display = "flex";
}
}
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);
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 => {
if (!tryWebScanImageFallback(onResult, resolve, reject, fallbackState)) {
reject({ error: err });
}
});
} catch (e) {
reject({ error: e });
}
}).finally(() => {
removeEl("__webscan_video__", currentUuid);
removeEl("__webscan_canvas__", currentUuid);
removeEl(WEBSCAN_CLOSE_BUTTON_ID, currentUuid);
removeEl(WEBSCAN_PICK_IMAGE_BUTTON_ID, currentUuid);
})
}