Files
scan-code-jssdk/src/services/web/index.js
iqudoo 46bd158b93 fix
2026-04-30 18:12:39 +08:00

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