535 lines
15 KiB
JavaScript
535 lines
15 KiB
JavaScript
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);
|
||
})
|
||
} |