898 lines
29 KiB
JavaScript
898 lines
29 KiB
JavaScript
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);
|
||
})
|
||
} |