优化声音和安卓端适配
This commit is contained in:
@@ -89,6 +89,9 @@ interface ScanConfigOptions {
|
||||
webScanType?: ("qrCode" | "barCode")[];
|
||||
webScanVideoMirror?: boolean;
|
||||
webScanVideoMirrorVertical?: boolean;
|
||||
webScanImageFallbackOnVideoError?: boolean;
|
||||
webScanVideoAccessTimeout?: number;
|
||||
webScanVideoReadyTimeout?: number;
|
||||
scanBeepAudio?: string;
|
||||
scanBeepEnabled?: boolean;
|
||||
initWechatJssdk?: {
|
||||
@@ -120,6 +123,9 @@ interface ScanConfigOptions {
|
||||
| `webScanType` | WebScan 扫码类型 | `["qrCode", "barCode"]` |
|
||||
| `webScanVideoMirror` | WebScan 视频是否水平镜像;不配置时自动判断:前置/PC 镜像,后置不镜像 | 自动 |
|
||||
| `webScanVideoMirrorVertical` | WebScan 视频是否垂直镜像 | `false` |
|
||||
| `webScanImageFallbackOnVideoError` | 摄像头不可用或打开失败时,是否自动弹出拍照/选图(适用于部分安卓内置浏览器) | `true` |
|
||||
| `webScanVideoAccessTimeout` | 打开摄像头超时(毫秒),超时后走图片回退 | `10000` |
|
||||
| `webScanVideoReadyTimeout` | 摄像头已开但无画面超时(毫秒),超时后走图片回退 | `8000` |
|
||||
| `scanBeepAudio` | 扫码成功提示音地址(任意模式匹配成功时播放) | 内置提示音 |
|
||||
| `scanBeepEnabled` | 扫码成功是否播放提示音 | `true` |
|
||||
| `initWechatJssdk` | 微信 JSSDK 初始化配置,仅微信环境生效 | 无 |
|
||||
|
||||
12
dist/index.d.ts
vendored
12
dist/index.d.ts
vendored
@@ -89,6 +89,18 @@ interface ScanConfigOptions {
|
||||
* 网页扫码视频是否垂直镜像,默认不镜像
|
||||
*/
|
||||
webScanVideoMirrorVertical?: boolean,
|
||||
/**
|
||||
* 摄像头不可用(如部分安卓内置浏览器禁用 getUserMedia)时是否自动回退为拍照/选图识别,默认启用
|
||||
*/
|
||||
webScanImageFallbackOnVideoError?: boolean,
|
||||
/**
|
||||
* 打开摄像头超时时间(毫秒),超时后触发图片回退,默认 10000
|
||||
*/
|
||||
webScanVideoAccessTimeout?: number,
|
||||
/**
|
||||
* 摄像头已打开但长时间无画面时触发图片回退(毫秒),默认 8000
|
||||
*/
|
||||
webScanVideoReadyTimeout?: number,
|
||||
/**
|
||||
* 扫码成功提示音地址,默认使用内置提示音;任意识别模式匹配成功时播放
|
||||
*/
|
||||
|
||||
2
dist/index.js
vendored
2
dist/index.js
vendored
File diff suppressed because one or more lines are too long
6
dist/index.md
vendored
6
dist/index.md
vendored
@@ -89,6 +89,9 @@ interface ScanConfigOptions {
|
||||
webScanType?: ("qrCode" | "barCode")[];
|
||||
webScanVideoMirror?: boolean;
|
||||
webScanVideoMirrorVertical?: boolean;
|
||||
webScanImageFallbackOnVideoError?: boolean;
|
||||
webScanVideoAccessTimeout?: number;
|
||||
webScanVideoReadyTimeout?: number;
|
||||
scanBeepAudio?: string;
|
||||
scanBeepEnabled?: boolean;
|
||||
initWechatJssdk?: {
|
||||
@@ -120,6 +123,9 @@ interface ScanConfigOptions {
|
||||
| `webScanType` | WebScan 扫码类型 | `["qrCode", "barCode"]` |
|
||||
| `webScanVideoMirror` | WebScan 视频是否水平镜像;不配置时自动判断:前置/PC 镜像,后置不镜像 | 自动 |
|
||||
| `webScanVideoMirrorVertical` | WebScan 视频是否垂直镜像 | `false` |
|
||||
| `webScanImageFallbackOnVideoError` | 摄像头不可用或打开失败时,是否自动弹出拍照/选图(适用于部分安卓内置浏览器) | `true` |
|
||||
| `webScanVideoAccessTimeout` | 打开摄像头超时(毫秒),超时后走图片回退 | `10000` |
|
||||
| `webScanVideoReadyTimeout` | 摄像头已开但无画面超时(毫秒),超时后走图片回退 | `8000` |
|
||||
| `scanBeepAudio` | 扫码成功提示音地址(任意模式匹配成功时播放) | 内置提示音 |
|
||||
| `scanBeepEnabled` | 扫码成功是否播放提示音 | `true` |
|
||||
| `initWechatJssdk` | 微信 JSSDK 初始化配置,仅微信环境生效 | 无 |
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { inRuntime, bridgeAsync } from "../bridge";
|
||||
import { isSupportWebScan, startScanForWeb, stopScanForWeb, isSupportImageScan, startScanForImage, unlockScanBeep, playScanBeep } from "../web";
|
||||
import {
|
||||
isSupportWebScan,
|
||||
startScanForWeb,
|
||||
stopScanForWeb,
|
||||
isSupportImageScan,
|
||||
startScanForImage,
|
||||
unlockScanBeep,
|
||||
playScanBeep,
|
||||
isWebScanImageFallbackEnabled
|
||||
} from "../web";
|
||||
import { isSupportWxScan, startScanForWx } from "../wx";
|
||||
import { startScanner, stopScanner } from "../scanner";
|
||||
import { getConfig } from "../config";
|
||||
@@ -273,6 +282,35 @@ function __startWxScan() {
|
||||
});
|
||||
}
|
||||
|
||||
function __startWebScan() {
|
||||
return startScanForWeb(__result).then(resp => {
|
||||
if (!isScanning()) {
|
||||
return resp;
|
||||
}
|
||||
if (!resp || !resp.result) {
|
||||
return resp;
|
||||
}
|
||||
if (__result(resp.result)) {
|
||||
return resp;
|
||||
}
|
||||
if (isScanning()) {
|
||||
return __startWebScan();
|
||||
}
|
||||
return resp;
|
||||
}).catch(err => {
|
||||
if (!isScanning()) {
|
||||
return err;
|
||||
}
|
||||
if (err && err.cancel || err && err.imageFallbackUsed) {
|
||||
return err;
|
||||
}
|
||||
if (isWebScanImageFallbackEnabled()) {
|
||||
return __startImageScan();
|
||||
}
|
||||
return err;
|
||||
});
|
||||
}
|
||||
|
||||
function __startImageScan() {
|
||||
return startScanForImage().then(resp => {
|
||||
if (!isScanning()) {
|
||||
@@ -414,7 +452,7 @@ export function startScan() {
|
||||
} else if (isSupportWxScan()) {
|
||||
scanPromise = __startWxScan();
|
||||
} else if (isSupportWebScan()) {
|
||||
scanPromise = startScanForWeb(__result);
|
||||
scanPromise = __startWebScan();
|
||||
} else if (isSupportImageScan()) {
|
||||
scanPromise = __startImageScan();
|
||||
} else {
|
||||
|
||||
@@ -30,8 +30,12 @@ const currentScriptSrc = typeof document !== "undefined"
|
||||
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 {
|
||||
@@ -357,6 +361,24 @@ export function playScanBeep() {
|
||||
}
|
||||
}
|
||||
|
||||
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') {
|
||||
@@ -367,6 +389,7 @@ function getScanBeepAudio() {
|
||||
scanBeepUnlocked = false;
|
||||
scanBeepAudioEl = new Audio(audioSrc);
|
||||
scanBeepAudioEl.preload = "auto";
|
||||
scanBeepAudioEl.setAttribute("playsinline", "");
|
||||
try {
|
||||
scanBeepAudioEl.load && scanBeepAudioEl.load();
|
||||
} catch (e) {
|
||||
@@ -377,43 +400,35 @@ function getScanBeepAudio() {
|
||||
}
|
||||
|
||||
export function unlockScanBeep() {
|
||||
if (!isScanBeepEnabled() || scanBeepUnlocked) {
|
||||
if (!isScanBeepEnabled() || scanBeepUnlocked || scanBeepUnlocking) {
|
||||
return;
|
||||
}
|
||||
const audio = getScanBeepAudio();
|
||||
getScanBeepAudio();
|
||||
const audio = getScanBeepUnlockAudio();
|
||||
if (!audio) {
|
||||
return;
|
||||
}
|
||||
const prevMuted = audio.muted;
|
||||
const prevVolume = audio.volume;
|
||||
scanBeepUnlocking = true;
|
||||
try {
|
||||
audio.muted = true;
|
||||
audio.volume = 0;
|
||||
audio.currentTime = 0;
|
||||
const playPromise = audio.play();
|
||||
if (!playPromise || !playPromise.then) {
|
||||
const finishUnlock = () => {
|
||||
resetScanBeepPlayback(audio);
|
||||
audio.muted = prevMuted;
|
||||
audio.volume = prevVolume;
|
||||
scanBeepUnlocking = false;
|
||||
scanBeepUnlocked = true;
|
||||
};
|
||||
if (!playPromise || !playPromise.then) {
|
||||
finishUnlock();
|
||||
return;
|
||||
}
|
||||
playPromise.then(() => {
|
||||
playPromise.then(finishUnlock).catch(() => {
|
||||
resetScanBeepPlayback(audio);
|
||||
audio.muted = prevMuted;
|
||||
audio.volume = prevVolume;
|
||||
scanBeepUnlocked = true;
|
||||
}).catch(() => {
|
||||
resetScanBeepPlayback(audio);
|
||||
audio.muted = prevMuted;
|
||||
audio.volume = prevVolume;
|
||||
scanBeepUnlocking = false;
|
||||
});
|
||||
} catch (e) {
|
||||
try {
|
||||
audio.muted = prevMuted;
|
||||
audio.volume = prevVolume;
|
||||
} catch (_e) {
|
||||
}
|
||||
scanBeepUnlocking = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,17 +446,120 @@ export function isSupportImageScan() {
|
||||
&& !!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() {
|
||||
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 => {
|
||||
@@ -523,6 +641,7 @@ export function startScanForImage() {
|
||||
|
||||
export function startScanForWeb(onResult) {
|
||||
let currentUuid = null;
|
||||
const fallbackState = { imageFallbackStarted: false };
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
stopActiveWebScan();
|
||||
@@ -581,11 +700,11 @@ export function startScanForWeb(onResult) {
|
||||
videoEl.uuid = scanWeb.uuid;
|
||||
canvasEl.uuid = scanWeb.uuid;
|
||||
createBarcodeDetector(getConfig("webScanType")).then(detector => {
|
||||
return navigator.mediaDevices.getUserMedia({
|
||||
return getUserMediaWithTimeout({
|
||||
video: {
|
||||
facingMode: "environment"
|
||||
}
|
||||
}).then(stream => {
|
||||
}, getWebScanVideoAccessTimeout()).then(stream => {
|
||||
return {
|
||||
detector,
|
||||
stream
|
||||
@@ -605,16 +724,38 @@ export function startScanForWeb(onResult) {
|
||||
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 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;
|
||||
@@ -631,6 +772,16 @@ export function startScanForWeb(onResult) {
|
||||
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();
|
||||
@@ -664,6 +815,7 @@ export function startScanForWeb(onResult) {
|
||||
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);
|
||||
@@ -730,7 +882,9 @@ export function startScanForWeb(onResult) {
|
||||
tick();
|
||||
});
|
||||
}).catch(err => {
|
||||
reject({ error: err });
|
||||
if (!tryWebScanImageFallback(onResult, resolve, reject, fallbackState)) {
|
||||
reject({ error: err });
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
reject({ error: e });
|
||||
|
||||
12
types/index.d.ts
vendored
12
types/index.d.ts
vendored
@@ -89,6 +89,18 @@ interface ScanConfigOptions {
|
||||
* 网页扫码视频是否垂直镜像,默认不镜像
|
||||
*/
|
||||
webScanVideoMirrorVertical?: boolean,
|
||||
/**
|
||||
* 摄像头不可用(如部分安卓内置浏览器禁用 getUserMedia)时是否自动回退为拍照/选图识别,默认启用
|
||||
*/
|
||||
webScanImageFallbackOnVideoError?: boolean,
|
||||
/**
|
||||
* 打开摄像头超时时间(毫秒),超时后触发图片回退,默认 10000
|
||||
*/
|
||||
webScanVideoAccessTimeout?: number,
|
||||
/**
|
||||
* 摄像头已打开但长时间无画面时触发图片回退(毫秒),默认 8000
|
||||
*/
|
||||
webScanVideoReadyTimeout?: number,
|
||||
/**
|
||||
* 扫码成功提示音地址,默认使用内置提示音;任意识别模式匹配成功时播放
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user