@dcg-overseas/scanner 0.1.0 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +21 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +21 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -273,6 +273,25 @@ function useScanner() {
|
|
|
273
273
|
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
|
274
274
|
};
|
|
275
275
|
}, [scanState, startDetection]);
|
|
276
|
+
const stop = (0, import_react.useCallback)(() => {
|
|
277
|
+
if (rafRef.current) {
|
|
278
|
+
cancelAnimationFrame(rafRef.current);
|
|
279
|
+
rafRef.current = null;
|
|
280
|
+
}
|
|
281
|
+
streamRef.current?.getTracks().forEach((t) => t.stop());
|
|
282
|
+
streamRef.current = null;
|
|
283
|
+
validCountRef.current = 0;
|
|
284
|
+
if (capturedUrlRef.current) {
|
|
285
|
+
URL.revokeObjectURL(capturedUrlRef.current);
|
|
286
|
+
setCapturedUrl("");
|
|
287
|
+
}
|
|
288
|
+
setFoundCount(0);
|
|
289
|
+
setQrValue("");
|
|
290
|
+
qrValueRef.current = "";
|
|
291
|
+
setErrorMsg("");
|
|
292
|
+
setStatusMsg("Opening camera\u2026");
|
|
293
|
+
setScanState("init");
|
|
294
|
+
}, []);
|
|
276
295
|
const restart = (0, import_react.useCallback)(async () => {
|
|
277
296
|
if (rafRef.current) {
|
|
278
297
|
cancelAnimationFrame(rafRef.current);
|
|
@@ -311,7 +330,8 @@ function useScanner() {
|
|
|
311
330
|
statusMsg,
|
|
312
331
|
foundCount,
|
|
313
332
|
qrValue,
|
|
314
|
-
restart
|
|
333
|
+
restart,
|
|
334
|
+
stop
|
|
315
335
|
};
|
|
316
336
|
}
|
|
317
337
|
// Annotate the CommonJS export names for ESM import in node:
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/useScanner.ts"],"sourcesContent":["export { useScanner } from './useScanner';\nexport type { UseScannerReturn, ScanState } from './useScanner';\n","import { useCallback, useEffect, useRef, useState } from 'react';\nimport type { RefObject } from 'react';\nimport { initWorker, detectInImageData } from '@dcg-overseas/cv-worker';\nimport { estimateQRCropBox, detectQRInCrop } from '@dcg-overseas/core';\nimport type { DetectionResult } from '@dcg-overseas/types';\n\n// ── 检测常量 ──────────────────────────────────────────────────────────────────\nconst SCAN_DIM = 720; // 检测缩略图长边(px)\nconst VALID_FRAMES_REQUIRED = 2; // 连续有效帧数阈值\nconst QR_REALTIME_CROP_PX = 600; // QR 裁切图上限(px),只缩小不放大\n\n// ── Overlay 绘制常量 ──────────────────────────────────────────────────────────\nconst BRACKET_INSET = 12; // 取景框括号内缩距离(px)\nconst BRACKET_LEN_RATIO = 0.08; // 括号边长占短边比例\nconst STATUS_BAR_H = 40; // 底部状态栏高度(px)\n\nexport type ScanState = 'init' | 'scanning' | 'captured' | 'error';\n\nexport interface UseScannerReturn {\n // ── DOM refs(直接绑定到 JSX 元素)──────────────────────────────────────────\n videoRef: RefObject<HTMLVideoElement | null>;\n overlayRef: RefObject<HTMLCanvasElement | null>;\n captureCanvasRef: RefObject<HTMLCanvasElement | null>;\n // ── 状态 ─────────────────────────────────────────────────────────────────────\n scanState: ScanState;\n capturedUrl: string;\n errorMsg: string;\n statusMsg: string;\n foundCount: number;\n qrValue: string;\n // ── 动作 ─────────────────────────────────────────────────────────────────────\n restart: () => Promise<void>;\n}\n\nasync function openCamera(): Promise<MediaStream> {\n return navigator.mediaDevices.getUserMedia({\n video: { width: { ideal: 3840 }, height: { ideal: 2160 }, facingMode: { ideal: 'environment' } },\n audio: false,\n });\n}\n\nfunction countFound(result: DetectionResult): number {\n const aruco = [0, 1, 2].filter(id => result.arucoMarkers.some(m => m.id === id)).length;\n return aruco + (result.qrCode.found ? 1 : 0);\n}\n\nexport function useScanner(): UseScannerReturn {\n const videoRef = useRef<HTMLVideoElement>(null);\n const overlayRef = useRef<HTMLCanvasElement>(null);\n const captureCanvasRef = useRef<HTMLCanvasElement>(null);\n const streamRef = useRef<MediaStream | null>(null);\n const rafRef = useRef<number | null>(null);\n const detectingRef = useRef(false);\n const validCountRef = useRef(0);\n const overlayDimRef = useRef({ w: 0, h: 0 });\n const offscreenRef = useRef<OffscreenCanvas | null>(null);\n const offscreenDimRef = useRef({ w: 0, h: 0 });\n const qrCropCanvasRef = useRef<OffscreenCanvas | null>(null);\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const barcodeDetectorRef = useRef<any>(undefined); // undefined=未检测, null=不支持, 否则为实例\n // 检测循环中暂存 QR 文本,避免 setState 干扰帧率,捕获时一次性写入 state\n const qrValueRef = useRef('');\n\n const [scanState, setScanState] = useState<ScanState>('init');\n const [capturedUrl, setCapturedUrl] = useState('');\n const [errorMsg, setErrorMsg] = useState('');\n const [statusMsg, setStatusMsg] = useState('Loading OpenCV…');\n const [foundCount, setFoundCount] = useState(0);\n const [qrValue, setQrValue] = useState('');\n\n // ── unmount cleanup ───────────────────────────────────────────────────────\n useEffect(() => {\n return () => {\n streamRef.current?.getTracks().forEach(t => t.stop());\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n };\n }, []);\n\n const capturedUrlRef = useRef('');\n useEffect(() => { capturedUrlRef.current = capturedUrl; }, [capturedUrl]);\n useEffect(() => {\n return () => { if (capturedUrlRef.current) URL.revokeObjectURL(capturedUrlRef.current); };\n }, []);\n\n const attachStream = useCallback(async (stream: MediaStream) => {\n const video = videoRef.current!;\n video.srcObject = stream;\n await video.play();\n }, []);\n\n // ── initial boot ──────────────────────────────────────────────────────────\n useEffect(() => {\n let cancelled = false;\n (async () => {\n try {\n setStatusMsg('Loading OpenCV…');\n await initWorker();\n if (cancelled) return;\n setStatusMsg('Opening camera…');\n const stream = await openCamera();\n if (cancelled) { stream.getTracks().forEach(t => t.stop()); return; }\n streamRef.current = stream;\n await attachStream(stream);\n setScanState('scanning');\n } catch (e: unknown) {\n if (!cancelled) {\n setErrorMsg(e instanceof Error ? e.message : 'Camera or OpenCV error');\n setScanState('error');\n }\n }\n })();\n return () => { cancelled = true; };\n }, [attachStream]);\n\n // ── overlay drawing ───────────────────────────────────────────────────────\n const drawOverlay = useCallback((found: number, result?: DetectionResult) => {\n const canvas = overlayRef.current;\n const video = videoRef.current;\n if (!canvas || !video) return;\n const ctx = canvas.getContext('2d')!;\n const rect = video.getBoundingClientRect();\n\n if (rect.width !== overlayDimRef.current.w || rect.height !== overlayDimRef.current.h) {\n canvas.width = rect.width;\n canvas.height = rect.height;\n overlayDimRef.current = { w: rect.width, h: rect.height };\n }\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n\n const allFound = found === 4;\n const color = allFound ? '#00ff88' : '#ff4444';\n const label = allFound ? '✓ All markers found' : `${found} / 4 markers found`;\n const bw = canvas.width, bh = canvas.height;\n const len = Math.min(bw, bh) * BRACKET_LEN_RATIO;\n\n ctx.strokeStyle = color;\n ctx.lineWidth = 3;\n const drawBracket = (x: number, y: number, dx: number, dy: number) => {\n ctx.beginPath();\n ctx.moveTo(x + dx * len, y); ctx.lineTo(x, y); ctx.lineTo(x, y + dy * len);\n ctx.stroke();\n };\n drawBracket(BRACKET_INSET, BRACKET_INSET, 1, 1);\n drawBracket(bw - BRACKET_INSET, BRACKET_INSET, -1, 1);\n drawBracket(BRACKET_INSET, bh - BRACKET_INSET, 1, -1);\n drawBracket(bw - BRACKET_INSET, bh - BRACKET_INSET,-1, -1);\n\n if (result) {\n const sx = bw / result.imageWidth;\n const sy = bh / result.imageHeight;\n const markerColors: Record<number, string> = { 0: '#00e5ff', 1: '#ffe100', 2: '#ff4f00' };\n const markerLabels: Record<number, string> = { 0: 'TL', 1: 'BR', 2: 'BL' };\n\n for (const m of result.arucoMarkers) {\n const mc = markerColors[m.id] ?? '#ffffff';\n ctx.strokeStyle = mc; ctx.lineWidth = 2;\n ctx.beginPath();\n m.corners.forEach((p, i) => {\n if (i === 0) ctx.moveTo(p.x * sx, p.y * sy); else ctx.lineTo(p.x * sx, p.y * sy);\n });\n ctx.closePath(); ctx.stroke();\n ctx.fillStyle = mc; ctx.font = 'bold 12px sans-serif'; ctx.textAlign = 'left';\n ctx.fillText(markerLabels[m.id] ?? `A${m.id}`, m.center.x * sx + 4, m.center.y * sy - 4);\n }\n if (result.qrCode.found) {\n ctx.fillStyle = '#00ff88'; ctx.font = 'bold 12px sans-serif'; ctx.textAlign = 'left';\n ctx.fillText('QR ✓', 4, 16);\n }\n }\n\n ctx.fillStyle = 'rgba(0,0,0,0.55)';\n ctx.fillRect(0, bh - STATUS_BAR_H, bw, STATUS_BAR_H);\n ctx.fillStyle = color;\n ctx.font = `bold ${Math.round(bh * 0.022 + 11)}px sans-serif`;\n ctx.textAlign = 'center';\n ctx.fillText(label, bw / 2, bh - 12);\n }, []);\n\n // ── capture ───────────────────────────────────────────────────────────────\n // captureCanvasRef 在检测循环顶部已同步冻结当前帧,直接转 blob 即可\n const captureBestFrame = useCallback(() => {\n const canvas = captureCanvasRef.current!;\n streamRef.current?.getTracks().forEach(t => t.stop());\n streamRef.current = null;\n canvas.toBlob(blob => {\n if (!blob) return;\n if (capturedUrlRef.current) URL.revokeObjectURL(capturedUrlRef.current);\n setCapturedUrl(URL.createObjectURL(blob));\n }, 'image/png');\n setScanState('captured');\n setQrValue(qrValueRef.current);\n }, []);\n\n // ── detection loop(rAF)────────────────────────────────────────────────\n const startDetection = useCallback(() => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n detectingRef.current = false;\n validCountRef.current = 0;\n\n const loop = async () => {\n const video = videoRef.current;\n if (!video || video.readyState < 2 || detectingRef.current) {\n rafRef.current = requestAnimationFrame(loop);\n return;\n }\n detectingRef.current = true;\n try {\n const { videoWidth: vw, videoHeight: vh } = video;\n\n // 同步冻结当前帧到 captureCanvas(await 前)\n const captureCanvas = captureCanvasRef.current!;\n if (captureCanvas.width !== vw || captureCanvas.height !== vh) {\n captureCanvas.width = vw; captureCanvas.height = vh;\n }\n captureCanvas.getContext('2d')!.drawImage(video, 0, 0);\n\n const scale = Math.min(1, SCAN_DIM / Math.max(vw, vh));\n const w = Math.round(vw * scale), h = Math.round(vh * scale);\n if (!offscreenRef.current || offscreenDimRef.current.w !== w || offscreenDimRef.current.h !== h) {\n offscreenRef.current = new OffscreenCanvas(w, h);\n offscreenDimRef.current = { w, h };\n }\n const ctx = offscreenRef.current.getContext('2d', { willReadFrequently: true }) as OffscreenCanvasRenderingContext2D;\n ctx.drawImage(video, 0, 0, w, h);\n const result = await detectInImageData(ctx.getImageData(0, 0, w, h));\n\n // ── QR 检测 ───────────────────────────────────────────────────────\n if (barcodeDetectorRef.current === undefined) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const BD = (window as any).BarcodeDetector;\n barcodeDetectorRef.current = BD ? new BD({ formats: ['qr_code'] }) : null;\n }\n const hasAllAruco = [0, 1, 2].every(id => result.arucoMarkers.some(m => m.id === id));\n if (hasAllAruco) {\n if (qrValueRef.current) {\n result.qrCode = { found: true }; // 已解码,复用缓存\n } else {\n const qrCrop = estimateQRCropBox(result.arucoMarkers, w, h, vw, vh);\n if (qrCrop) {\n const cropScale = Math.min(1, QR_REALTIME_CROP_PX / Math.max(qrCrop.w, qrCrop.h));\n const qrW = Math.max(1, Math.round(qrCrop.w * cropScale));\n const qrH = Math.max(1, Math.round(qrCrop.h * cropScale));\n if (!qrCropCanvasRef.current ||\n qrCropCanvasRef.current.width !== qrW || qrCropCanvasRef.current.height !== qrH) {\n qrCropCanvasRef.current = new OffscreenCanvas(qrW, qrH);\n }\n const qrCtx = qrCropCanvasRef.current.getContext('2d', { willReadFrequently: true }) as OffscreenCanvasRenderingContext2D;\n qrCtx.drawImage(captureCanvas, qrCrop.x0, qrCrop.y0, qrCrop.w, qrCrop.h, 0, 0, qrW, qrH);\n const qrDet = await detectQRInCrop(qrCropCanvasRef.current!, { barcodeDetector: barcodeDetectorRef.current });\n if (qrDet.found) {\n result.qrCode = { found: true };\n if (qrDet.rawValue) qrValueRef.current = qrDet.rawValue;\n }\n }\n }\n }\n\n const found = countFound(result);\n setFoundCount(found);\n drawOverlay(found, result);\n\n if (found === 4) {\n validCountRef.current += 1;\n if (validCountRef.current >= VALID_FRAMES_REQUIRED) {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n captureBestFrame();\n return;\n }\n } else {\n validCountRef.current = 0;\n }\n } catch (e) {\n console.warn('[useScanner] detection error', e);\n validCountRef.current = 0;\n } finally {\n detectingRef.current = false;\n }\n rafRef.current = requestAnimationFrame(loop);\n };\n rafRef.current = requestAnimationFrame(loop);\n }, [drawOverlay, captureBestFrame]);\n\n useEffect(() => {\n if (scanState === 'scanning') startDetection();\n return () => { if (rafRef.current) cancelAnimationFrame(rafRef.current); };\n }, [scanState, startDetection]);\n\n // ── restart ───────────────────────────────────────────────────────────────\n const restart = useCallback(async () => {\n if (rafRef.current) { cancelAnimationFrame(rafRef.current); rafRef.current = null; }\n streamRef.current?.getTracks().forEach(t => t.stop());\n streamRef.current = null;\n validCountRef.current = 0;\n if (capturedUrlRef.current) { URL.revokeObjectURL(capturedUrlRef.current); setCapturedUrl(''); }\n setFoundCount(0); setQrValue(''); qrValueRef.current = '';\n setErrorMsg(''); setStatusMsg('Opening camera…'); setScanState('init');\n try {\n const stream = await openCamera();\n streamRef.current = stream;\n await attachStream(stream);\n setScanState('scanning');\n } catch (e) {\n setErrorMsg(e instanceof Error ? e.message : 'Camera error');\n setScanState('error');\n }\n }, [attachStream]);\n\n return {\n videoRef, overlayRef, captureCanvasRef,\n scanState, capturedUrl, errorMsg, statusMsg, foundCount, qrValue,\n restart,\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/useScanner.ts"],"sourcesContent":["export { useScanner } from './useScanner';\nexport type { UseScannerReturn, ScanState } from './useScanner';\n","import { useCallback, useEffect, useRef, useState } from 'react';\r\nimport type { RefObject } from 'react';\r\nimport { initWorker, detectInImageData } from '@dcg-overseas/cv-worker';\r\nimport { estimateQRCropBox, detectQRInCrop } from '@dcg-overseas/core';\r\nimport type { DetectionResult } from '@dcg-overseas/types';\r\n\r\n// ── 检测常量 ──────────────────────────────────────────────────────────────────\r\nconst SCAN_DIM = 720; // 检测缩略图长边(px)\r\nconst VALID_FRAMES_REQUIRED = 2; // 连续有效帧数阈值\r\nconst QR_REALTIME_CROP_PX = 600; // QR 裁切图上限(px),只缩小不放大\r\n\r\n// ── Overlay 绘制常量 ──────────────────────────────────────────────────────────\r\nconst BRACKET_INSET = 12; // 取景框括号内缩距离(px)\r\nconst BRACKET_LEN_RATIO = 0.08; // 括号边长占短边比例\r\nconst STATUS_BAR_H = 40; // 底部状态栏高度(px)\r\n\r\nexport type ScanState = 'init' | 'scanning' | 'captured' | 'error';\r\n\r\nexport interface UseScannerReturn {\r\n // ── DOM refs(直接绑定到 JSX 元素)──────────────────────────────────────────\r\n videoRef: RefObject<HTMLVideoElement | null>;\r\n overlayRef: RefObject<HTMLCanvasElement | null>;\r\n captureCanvasRef: RefObject<HTMLCanvasElement | null>;\r\n // ── 状态 ─────────────────────────────────────────────────────────────────────\r\n scanState: ScanState;\r\n capturedUrl: string;\r\n errorMsg: string;\r\n statusMsg: string;\r\n foundCount: number;\r\n qrValue: string;\r\n // ── 动作 ─────────────────────────────────────────────────────────────────────\r\n restart: () => Promise<void>;\r\n stop: () => void;\r\n}\r\n\r\nasync function openCamera(): Promise<MediaStream> {\r\n return navigator.mediaDevices.getUserMedia({\r\n video: { width: { ideal: 3840 }, height: { ideal: 2160 }, facingMode: { ideal: 'environment' } },\r\n audio: false,\r\n });\r\n}\r\n\r\nfunction countFound(result: DetectionResult): number {\r\n const aruco = [0, 1, 2].filter(id => result.arucoMarkers.some(m => m.id === id)).length;\r\n return aruco + (result.qrCode.found ? 1 : 0);\r\n}\r\n\r\nexport function useScanner(): UseScannerReturn {\r\n const videoRef = useRef<HTMLVideoElement>(null);\r\n const overlayRef = useRef<HTMLCanvasElement>(null);\r\n const captureCanvasRef = useRef<HTMLCanvasElement>(null);\r\n const streamRef = useRef<MediaStream | null>(null);\r\n const rafRef = useRef<number | null>(null);\r\n const detectingRef = useRef(false);\r\n const validCountRef = useRef(0);\r\n const overlayDimRef = useRef({ w: 0, h: 0 });\r\n const offscreenRef = useRef<OffscreenCanvas | null>(null);\r\n const offscreenDimRef = useRef({ w: 0, h: 0 });\r\n const qrCropCanvasRef = useRef<OffscreenCanvas | null>(null);\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n const barcodeDetectorRef = useRef<any>(undefined); // undefined=未检测, null=不支持, 否则为实例\r\n // 检测循环中暂存 QR 文本,避免 setState 干扰帧率,捕获时一次性写入 state\r\n const qrValueRef = useRef('');\r\n\r\n const [scanState, setScanState] = useState<ScanState>('init');\r\n const [capturedUrl, setCapturedUrl] = useState('');\r\n const [errorMsg, setErrorMsg] = useState('');\r\n const [statusMsg, setStatusMsg] = useState('Loading OpenCV…');\r\n const [foundCount, setFoundCount] = useState(0);\r\n const [qrValue, setQrValue] = useState('');\r\n\r\n // ── unmount cleanup ───────────────────────────────────────────────────────\r\n useEffect(() => {\r\n return () => {\r\n streamRef.current?.getTracks().forEach(t => t.stop());\r\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\r\n };\r\n }, []);\r\n\r\n const capturedUrlRef = useRef('');\r\n useEffect(() => { capturedUrlRef.current = capturedUrl; }, [capturedUrl]);\r\n useEffect(() => {\r\n return () => { if (capturedUrlRef.current) URL.revokeObjectURL(capturedUrlRef.current); };\r\n }, []);\r\n\r\n const attachStream = useCallback(async (stream: MediaStream) => {\r\n const video = videoRef.current!;\r\n video.srcObject = stream;\r\n await video.play();\r\n }, []);\r\n\r\n // ── initial boot ──────────────────────────────────────────────────────────\r\n useEffect(() => {\r\n let cancelled = false;\r\n (async () => {\r\n try {\r\n setStatusMsg('Loading OpenCV…');\r\n await initWorker();\r\n if (cancelled) return;\r\n setStatusMsg('Opening camera…');\r\n const stream = await openCamera();\r\n if (cancelled) { stream.getTracks().forEach(t => t.stop()); return; }\r\n streamRef.current = stream;\r\n await attachStream(stream);\r\n setScanState('scanning');\r\n } catch (e: unknown) {\r\n if (!cancelled) {\r\n setErrorMsg(e instanceof Error ? e.message : 'Camera or OpenCV error');\r\n setScanState('error');\r\n }\r\n }\r\n })();\r\n return () => { cancelled = true; };\r\n }, [attachStream]);\r\n\r\n // ── overlay drawing ───────────────────────────────────────────────────────\r\n const drawOverlay = useCallback((found: number, result?: DetectionResult) => {\r\n const canvas = overlayRef.current;\r\n const video = videoRef.current;\r\n if (!canvas || !video) return;\r\n const ctx = canvas.getContext('2d')!;\r\n const rect = video.getBoundingClientRect();\r\n\r\n if (rect.width !== overlayDimRef.current.w || rect.height !== overlayDimRef.current.h) {\r\n canvas.width = rect.width;\r\n canvas.height = rect.height;\r\n overlayDimRef.current = { w: rect.width, h: rect.height };\r\n }\r\n ctx.clearRect(0, 0, canvas.width, canvas.height);\r\n\r\n const allFound = found === 4;\r\n const color = allFound ? '#00ff88' : '#ff4444';\r\n const label = allFound ? '✓ All markers found' : `${found} / 4 markers found`;\r\n const bw = canvas.width, bh = canvas.height;\r\n const len = Math.min(bw, bh) * BRACKET_LEN_RATIO;\r\n\r\n ctx.strokeStyle = color;\r\n ctx.lineWidth = 3;\r\n const drawBracket = (x: number, y: number, dx: number, dy: number) => {\r\n ctx.beginPath();\r\n ctx.moveTo(x + dx * len, y); ctx.lineTo(x, y); ctx.lineTo(x, y + dy * len);\r\n ctx.stroke();\r\n };\r\n drawBracket(BRACKET_INSET, BRACKET_INSET, 1, 1);\r\n drawBracket(bw - BRACKET_INSET, BRACKET_INSET, -1, 1);\r\n drawBracket(BRACKET_INSET, bh - BRACKET_INSET, 1, -1);\r\n drawBracket(bw - BRACKET_INSET, bh - BRACKET_INSET,-1, -1);\r\n\r\n if (result) {\r\n const sx = bw / result.imageWidth;\r\n const sy = bh / result.imageHeight;\r\n const markerColors: Record<number, string> = { 0: '#00e5ff', 1: '#ffe100', 2: '#ff4f00' };\r\n const markerLabels: Record<number, string> = { 0: 'TL', 1: 'BR', 2: 'BL' };\r\n\r\n for (const m of result.arucoMarkers) {\r\n const mc = markerColors[m.id] ?? '#ffffff';\r\n ctx.strokeStyle = mc; ctx.lineWidth = 2;\r\n ctx.beginPath();\r\n m.corners.forEach((p, i) => {\r\n if (i === 0) ctx.moveTo(p.x * sx, p.y * sy); else ctx.lineTo(p.x * sx, p.y * sy);\r\n });\r\n ctx.closePath(); ctx.stroke();\r\n ctx.fillStyle = mc; ctx.font = 'bold 12px sans-serif'; ctx.textAlign = 'left';\r\n ctx.fillText(markerLabels[m.id] ?? `A${m.id}`, m.center.x * sx + 4, m.center.y * sy - 4);\r\n }\r\n if (result.qrCode.found) {\r\n ctx.fillStyle = '#00ff88'; ctx.font = 'bold 12px sans-serif'; ctx.textAlign = 'left';\r\n ctx.fillText('QR ✓', 4, 16);\r\n }\r\n }\r\n\r\n ctx.fillStyle = 'rgba(0,0,0,0.55)';\r\n ctx.fillRect(0, bh - STATUS_BAR_H, bw, STATUS_BAR_H);\r\n ctx.fillStyle = color;\r\n ctx.font = `bold ${Math.round(bh * 0.022 + 11)}px sans-serif`;\r\n ctx.textAlign = 'center';\r\n ctx.fillText(label, bw / 2, bh - 12);\r\n }, []);\r\n\r\n // ── capture ───────────────────────────────────────────────────────────────\r\n // captureCanvasRef 在检测循环顶部已同步冻结当前帧,直接转 blob 即可\r\n const captureBestFrame = useCallback(() => {\r\n const canvas = captureCanvasRef.current!;\r\n streamRef.current?.getTracks().forEach(t => t.stop());\r\n streamRef.current = null;\r\n canvas.toBlob(blob => {\r\n if (!blob) return;\r\n if (capturedUrlRef.current) URL.revokeObjectURL(capturedUrlRef.current);\r\n setCapturedUrl(URL.createObjectURL(blob));\r\n }, 'image/png');\r\n setScanState('captured');\r\n setQrValue(qrValueRef.current);\r\n }, []);\r\n\r\n // ── detection loop(rAF)────────────────────────────────────────────────\r\n const startDetection = useCallback(() => {\r\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\r\n detectingRef.current = false;\r\n validCountRef.current = 0;\r\n\r\n const loop = async () => {\r\n const video = videoRef.current;\r\n if (!video || video.readyState < 2 || detectingRef.current) {\r\n rafRef.current = requestAnimationFrame(loop);\r\n return;\r\n }\r\n detectingRef.current = true;\r\n try {\r\n const { videoWidth: vw, videoHeight: vh } = video;\r\n\r\n // 同步冻结当前帧到 captureCanvas(await 前)\r\n const captureCanvas = captureCanvasRef.current!;\r\n if (captureCanvas.width !== vw || captureCanvas.height !== vh) {\r\n captureCanvas.width = vw; captureCanvas.height = vh;\r\n }\r\n captureCanvas.getContext('2d')!.drawImage(video, 0, 0);\r\n\r\n const scale = Math.min(1, SCAN_DIM / Math.max(vw, vh));\r\n const w = Math.round(vw * scale), h = Math.round(vh * scale);\r\n if (!offscreenRef.current || offscreenDimRef.current.w !== w || offscreenDimRef.current.h !== h) {\r\n offscreenRef.current = new OffscreenCanvas(w, h);\r\n offscreenDimRef.current = { w, h };\r\n }\r\n const ctx = offscreenRef.current.getContext('2d', { willReadFrequently: true }) as OffscreenCanvasRenderingContext2D;\r\n ctx.drawImage(video, 0, 0, w, h);\r\n const result:DetectionResult = await detectInImageData(ctx.getImageData(0, 0, w, h));\r\n\r\n // ── QR 检测 ───────────────────────────────────────────────────────\r\n if (barcodeDetectorRef.current === undefined) {\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n const BD = (window as any).BarcodeDetector;\r\n barcodeDetectorRef.current = BD ? new BD({ formats: ['qr_code'] }) : null;\r\n }\r\n const hasAllAruco = [0, 1, 2].every(id => result.arucoMarkers.some(m => m.id === id));\r\n if (hasAllAruco) {\r\n if (qrValueRef.current) {\r\n result.qrCode = { found: true }; // 已解码,复用缓存\r\n } else {\r\n const qrCrop = estimateQRCropBox(result.arucoMarkers, w, h, vw, vh);\r\n if (qrCrop) {\r\n const cropScale = Math.min(1, QR_REALTIME_CROP_PX / Math.max(qrCrop.w, qrCrop.h));\r\n const qrW = Math.max(1, Math.round(qrCrop.w * cropScale));\r\n const qrH = Math.max(1, Math.round(qrCrop.h * cropScale));\r\n if (!qrCropCanvasRef.current ||\r\n qrCropCanvasRef.current.width !== qrW || qrCropCanvasRef.current.height !== qrH) {\r\n qrCropCanvasRef.current = new OffscreenCanvas(qrW, qrH);\r\n }\r\n const qrCtx = qrCropCanvasRef.current.getContext('2d', { willReadFrequently: true }) as OffscreenCanvasRenderingContext2D;\r\n qrCtx.drawImage(captureCanvas, qrCrop.x0, qrCrop.y0, qrCrop.w, qrCrop.h, 0, 0, qrW, qrH);\r\n const qrDet = await detectQRInCrop(qrCropCanvasRef.current!, { barcodeDetector: barcodeDetectorRef.current });\r\n if (qrDet.found) {\r\n result.qrCode = { found: true };\r\n if (qrDet.rawValue) qrValueRef.current = qrDet.rawValue;\r\n }\r\n }\r\n }\r\n }\r\n\r\n const found = countFound(result);\r\n setFoundCount(found);\r\n drawOverlay(found, result);\r\n\r\n if (found === 4) {\r\n validCountRef.current += 1;\r\n if (validCountRef.current >= VALID_FRAMES_REQUIRED) {\r\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\r\n rafRef.current = null;\r\n captureBestFrame();\r\n return;\r\n }\r\n } else {\r\n validCountRef.current = 0;\r\n }\r\n } catch (e) {\r\n console.warn('[useScanner] detection error', e);\r\n validCountRef.current = 0;\r\n } finally {\r\n detectingRef.current = false;\r\n }\r\n rafRef.current = requestAnimationFrame(loop);\r\n };\r\n rafRef.current = requestAnimationFrame(loop);\r\n }, [drawOverlay, captureBestFrame]);\r\n\r\n useEffect(() => {\r\n if (scanState === 'scanning') startDetection();\r\n return () => { if (rafRef.current) cancelAnimationFrame(rafRef.current); };\r\n }, [scanState, startDetection]);\r\n\r\n const stop = useCallback(()=>{\r\n if (rafRef.current) { cancelAnimationFrame(rafRef.current); rafRef.current = null; }\r\n streamRef.current?.getTracks().forEach(t => t.stop());\r\n streamRef.current = null;\r\n validCountRef.current = 0;\r\n if (capturedUrlRef.current) { URL.revokeObjectURL(capturedUrlRef.current); setCapturedUrl(''); }\r\n setFoundCount(0); setQrValue(''); qrValueRef.current = '';\r\n setErrorMsg(''); setStatusMsg('Opening camera…'); setScanState('init');\r\n },[])\r\n\r\n // ── restart ───────────────────────────────────────────────────────────────\r\n const restart = useCallback(async () => {\r\n if (rafRef.current) { cancelAnimationFrame(rafRef.current); rafRef.current = null; }\r\n streamRef.current?.getTracks().forEach(t => t.stop());\r\n streamRef.current = null;\r\n validCountRef.current = 0;\r\n if (capturedUrlRef.current) { URL.revokeObjectURL(capturedUrlRef.current); setCapturedUrl(''); }\r\n setFoundCount(0); setQrValue(''); qrValueRef.current = '';\r\n setErrorMsg(''); setStatusMsg('Opening camera…'); setScanState('init');\r\n try {\r\n const stream = await openCamera();\r\n streamRef.current = stream;\r\n await attachStream(stream);\r\n setScanState('scanning');\r\n } catch (e) {\r\n setErrorMsg(e instanceof Error ? e.message : 'Camera error');\r\n setScanState('error');\r\n }\r\n }, [attachStream]);\r\n\r\n return {\r\n videoRef, overlayRef, captureCanvasRef,\r\n scanState, capturedUrl, errorMsg, statusMsg, foundCount, qrValue,\r\n restart, stop\r\n };\r\n}\r\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -247,6 +247,25 @@ function useScanner() {
|
|
|
247
247
|
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
|
248
248
|
};
|
|
249
249
|
}, [scanState, startDetection]);
|
|
250
|
+
const stop = useCallback(() => {
|
|
251
|
+
if (rafRef.current) {
|
|
252
|
+
cancelAnimationFrame(rafRef.current);
|
|
253
|
+
rafRef.current = null;
|
|
254
|
+
}
|
|
255
|
+
streamRef.current?.getTracks().forEach((t) => t.stop());
|
|
256
|
+
streamRef.current = null;
|
|
257
|
+
validCountRef.current = 0;
|
|
258
|
+
if (capturedUrlRef.current) {
|
|
259
|
+
URL.revokeObjectURL(capturedUrlRef.current);
|
|
260
|
+
setCapturedUrl("");
|
|
261
|
+
}
|
|
262
|
+
setFoundCount(0);
|
|
263
|
+
setQrValue("");
|
|
264
|
+
qrValueRef.current = "";
|
|
265
|
+
setErrorMsg("");
|
|
266
|
+
setStatusMsg("Opening camera\u2026");
|
|
267
|
+
setScanState("init");
|
|
268
|
+
}, []);
|
|
250
269
|
const restart = useCallback(async () => {
|
|
251
270
|
if (rafRef.current) {
|
|
252
271
|
cancelAnimationFrame(rafRef.current);
|
|
@@ -285,7 +304,8 @@ function useScanner() {
|
|
|
285
304
|
statusMsg,
|
|
286
305
|
foundCount,
|
|
287
306
|
qrValue,
|
|
288
|
-
restart
|
|
307
|
+
restart,
|
|
308
|
+
stop
|
|
289
309
|
};
|
|
290
310
|
}
|
|
291
311
|
export {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/useScanner.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\nimport type { RefObject } from 'react';\nimport { initWorker, detectInImageData } from '@dcg-overseas/cv-worker';\nimport { estimateQRCropBox, detectQRInCrop } from '@dcg-overseas/core';\nimport type { DetectionResult } from '@dcg-overseas/types';\n\n// ── 检测常量 ──────────────────────────────────────────────────────────────────\nconst SCAN_DIM = 720; // 检测缩略图长边(px)\nconst VALID_FRAMES_REQUIRED = 2; // 连续有效帧数阈值\nconst QR_REALTIME_CROP_PX = 600; // QR 裁切图上限(px),只缩小不放大\n\n// ── Overlay 绘制常量 ──────────────────────────────────────────────────────────\nconst BRACKET_INSET = 12; // 取景框括号内缩距离(px)\nconst BRACKET_LEN_RATIO = 0.08; // 括号边长占短边比例\nconst STATUS_BAR_H = 40; // 底部状态栏高度(px)\n\nexport type ScanState = 'init' | 'scanning' | 'captured' | 'error';\n\nexport interface UseScannerReturn {\n // ── DOM refs(直接绑定到 JSX 元素)──────────────────────────────────────────\n videoRef: RefObject<HTMLVideoElement | null>;\n overlayRef: RefObject<HTMLCanvasElement | null>;\n captureCanvasRef: RefObject<HTMLCanvasElement | null>;\n // ── 状态 ─────────────────────────────────────────────────────────────────────\n scanState: ScanState;\n capturedUrl: string;\n errorMsg: string;\n statusMsg: string;\n foundCount: number;\n qrValue: string;\n // ── 动作 ─────────────────────────────────────────────────────────────────────\n restart: () => Promise<void>;\n}\n\nasync function openCamera(): Promise<MediaStream> {\n return navigator.mediaDevices.getUserMedia({\n video: { width: { ideal: 3840 }, height: { ideal: 2160 }, facingMode: { ideal: 'environment' } },\n audio: false,\n });\n}\n\nfunction countFound(result: DetectionResult): number {\n const aruco = [0, 1, 2].filter(id => result.arucoMarkers.some(m => m.id === id)).length;\n return aruco + (result.qrCode.found ? 1 : 0);\n}\n\nexport function useScanner(): UseScannerReturn {\n const videoRef = useRef<HTMLVideoElement>(null);\n const overlayRef = useRef<HTMLCanvasElement>(null);\n const captureCanvasRef = useRef<HTMLCanvasElement>(null);\n const streamRef = useRef<MediaStream | null>(null);\n const rafRef = useRef<number | null>(null);\n const detectingRef = useRef(false);\n const validCountRef = useRef(0);\n const overlayDimRef = useRef({ w: 0, h: 0 });\n const offscreenRef = useRef<OffscreenCanvas | null>(null);\n const offscreenDimRef = useRef({ w: 0, h: 0 });\n const qrCropCanvasRef = useRef<OffscreenCanvas | null>(null);\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const barcodeDetectorRef = useRef<any>(undefined); // undefined=未检测, null=不支持, 否则为实例\n // 检测循环中暂存 QR 文本,避免 setState 干扰帧率,捕获时一次性写入 state\n const qrValueRef = useRef('');\n\n const [scanState, setScanState] = useState<ScanState>('init');\n const [capturedUrl, setCapturedUrl] = useState('');\n const [errorMsg, setErrorMsg] = useState('');\n const [statusMsg, setStatusMsg] = useState('Loading OpenCV…');\n const [foundCount, setFoundCount] = useState(0);\n const [qrValue, setQrValue] = useState('');\n\n // ── unmount cleanup ───────────────────────────────────────────────────────\n useEffect(() => {\n return () => {\n streamRef.current?.getTracks().forEach(t => t.stop());\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n };\n }, []);\n\n const capturedUrlRef = useRef('');\n useEffect(() => { capturedUrlRef.current = capturedUrl; }, [capturedUrl]);\n useEffect(() => {\n return () => { if (capturedUrlRef.current) URL.revokeObjectURL(capturedUrlRef.current); };\n }, []);\n\n const attachStream = useCallback(async (stream: MediaStream) => {\n const video = videoRef.current!;\n video.srcObject = stream;\n await video.play();\n }, []);\n\n // ── initial boot ──────────────────────────────────────────────────────────\n useEffect(() => {\n let cancelled = false;\n (async () => {\n try {\n setStatusMsg('Loading OpenCV…');\n await initWorker();\n if (cancelled) return;\n setStatusMsg('Opening camera…');\n const stream = await openCamera();\n if (cancelled) { stream.getTracks().forEach(t => t.stop()); return; }\n streamRef.current = stream;\n await attachStream(stream);\n setScanState('scanning');\n } catch (e: unknown) {\n if (!cancelled) {\n setErrorMsg(e instanceof Error ? e.message : 'Camera or OpenCV error');\n setScanState('error');\n }\n }\n })();\n return () => { cancelled = true; };\n }, [attachStream]);\n\n // ── overlay drawing ───────────────────────────────────────────────────────\n const drawOverlay = useCallback((found: number, result?: DetectionResult) => {\n const canvas = overlayRef.current;\n const video = videoRef.current;\n if (!canvas || !video) return;\n const ctx = canvas.getContext('2d')!;\n const rect = video.getBoundingClientRect();\n\n if (rect.width !== overlayDimRef.current.w || rect.height !== overlayDimRef.current.h) {\n canvas.width = rect.width;\n canvas.height = rect.height;\n overlayDimRef.current = { w: rect.width, h: rect.height };\n }\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n\n const allFound = found === 4;\n const color = allFound ? '#00ff88' : '#ff4444';\n const label = allFound ? '✓ All markers found' : `${found} / 4 markers found`;\n const bw = canvas.width, bh = canvas.height;\n const len = Math.min(bw, bh) * BRACKET_LEN_RATIO;\n\n ctx.strokeStyle = color;\n ctx.lineWidth = 3;\n const drawBracket = (x: number, y: number, dx: number, dy: number) => {\n ctx.beginPath();\n ctx.moveTo(x + dx * len, y); ctx.lineTo(x, y); ctx.lineTo(x, y + dy * len);\n ctx.stroke();\n };\n drawBracket(BRACKET_INSET, BRACKET_INSET, 1, 1);\n drawBracket(bw - BRACKET_INSET, BRACKET_INSET, -1, 1);\n drawBracket(BRACKET_INSET, bh - BRACKET_INSET, 1, -1);\n drawBracket(bw - BRACKET_INSET, bh - BRACKET_INSET,-1, -1);\n\n if (result) {\n const sx = bw / result.imageWidth;\n const sy = bh / result.imageHeight;\n const markerColors: Record<number, string> = { 0: '#00e5ff', 1: '#ffe100', 2: '#ff4f00' };\n const markerLabels: Record<number, string> = { 0: 'TL', 1: 'BR', 2: 'BL' };\n\n for (const m of result.arucoMarkers) {\n const mc = markerColors[m.id] ?? '#ffffff';\n ctx.strokeStyle = mc; ctx.lineWidth = 2;\n ctx.beginPath();\n m.corners.forEach((p, i) => {\n if (i === 0) ctx.moveTo(p.x * sx, p.y * sy); else ctx.lineTo(p.x * sx, p.y * sy);\n });\n ctx.closePath(); ctx.stroke();\n ctx.fillStyle = mc; ctx.font = 'bold 12px sans-serif'; ctx.textAlign = 'left';\n ctx.fillText(markerLabels[m.id] ?? `A${m.id}`, m.center.x * sx + 4, m.center.y * sy - 4);\n }\n if (result.qrCode.found) {\n ctx.fillStyle = '#00ff88'; ctx.font = 'bold 12px sans-serif'; ctx.textAlign = 'left';\n ctx.fillText('QR ✓', 4, 16);\n }\n }\n\n ctx.fillStyle = 'rgba(0,0,0,0.55)';\n ctx.fillRect(0, bh - STATUS_BAR_H, bw, STATUS_BAR_H);\n ctx.fillStyle = color;\n ctx.font = `bold ${Math.round(bh * 0.022 + 11)}px sans-serif`;\n ctx.textAlign = 'center';\n ctx.fillText(label, bw / 2, bh - 12);\n }, []);\n\n // ── capture ───────────────────────────────────────────────────────────────\n // captureCanvasRef 在检测循环顶部已同步冻结当前帧,直接转 blob 即可\n const captureBestFrame = useCallback(() => {\n const canvas = captureCanvasRef.current!;\n streamRef.current?.getTracks().forEach(t => t.stop());\n streamRef.current = null;\n canvas.toBlob(blob => {\n if (!blob) return;\n if (capturedUrlRef.current) URL.revokeObjectURL(capturedUrlRef.current);\n setCapturedUrl(URL.createObjectURL(blob));\n }, 'image/png');\n setScanState('captured');\n setQrValue(qrValueRef.current);\n }, []);\n\n // ── detection loop(rAF)────────────────────────────────────────────────\n const startDetection = useCallback(() => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n detectingRef.current = false;\n validCountRef.current = 0;\n\n const loop = async () => {\n const video = videoRef.current;\n if (!video || video.readyState < 2 || detectingRef.current) {\n rafRef.current = requestAnimationFrame(loop);\n return;\n }\n detectingRef.current = true;\n try {\n const { videoWidth: vw, videoHeight: vh } = video;\n\n // 同步冻结当前帧到 captureCanvas(await 前)\n const captureCanvas = captureCanvasRef.current!;\n if (captureCanvas.width !== vw || captureCanvas.height !== vh) {\n captureCanvas.width = vw; captureCanvas.height = vh;\n }\n captureCanvas.getContext('2d')!.drawImage(video, 0, 0);\n\n const scale = Math.min(1, SCAN_DIM / Math.max(vw, vh));\n const w = Math.round(vw * scale), h = Math.round(vh * scale);\n if (!offscreenRef.current || offscreenDimRef.current.w !== w || offscreenDimRef.current.h !== h) {\n offscreenRef.current = new OffscreenCanvas(w, h);\n offscreenDimRef.current = { w, h };\n }\n const ctx = offscreenRef.current.getContext('2d', { willReadFrequently: true }) as OffscreenCanvasRenderingContext2D;\n ctx.drawImage(video, 0, 0, w, h);\n const result = await detectInImageData(ctx.getImageData(0, 0, w, h));\n\n // ── QR 检测 ───────────────────────────────────────────────────────\n if (barcodeDetectorRef.current === undefined) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const BD = (window as any).BarcodeDetector;\n barcodeDetectorRef.current = BD ? new BD({ formats: ['qr_code'] }) : null;\n }\n const hasAllAruco = [0, 1, 2].every(id => result.arucoMarkers.some(m => m.id === id));\n if (hasAllAruco) {\n if (qrValueRef.current) {\n result.qrCode = { found: true }; // 已解码,复用缓存\n } else {\n const qrCrop = estimateQRCropBox(result.arucoMarkers, w, h, vw, vh);\n if (qrCrop) {\n const cropScale = Math.min(1, QR_REALTIME_CROP_PX / Math.max(qrCrop.w, qrCrop.h));\n const qrW = Math.max(1, Math.round(qrCrop.w * cropScale));\n const qrH = Math.max(1, Math.round(qrCrop.h * cropScale));\n if (!qrCropCanvasRef.current ||\n qrCropCanvasRef.current.width !== qrW || qrCropCanvasRef.current.height !== qrH) {\n qrCropCanvasRef.current = new OffscreenCanvas(qrW, qrH);\n }\n const qrCtx = qrCropCanvasRef.current.getContext('2d', { willReadFrequently: true }) as OffscreenCanvasRenderingContext2D;\n qrCtx.drawImage(captureCanvas, qrCrop.x0, qrCrop.y0, qrCrop.w, qrCrop.h, 0, 0, qrW, qrH);\n const qrDet = await detectQRInCrop(qrCropCanvasRef.current!, { barcodeDetector: barcodeDetectorRef.current });\n if (qrDet.found) {\n result.qrCode = { found: true };\n if (qrDet.rawValue) qrValueRef.current = qrDet.rawValue;\n }\n }\n }\n }\n\n const found = countFound(result);\n setFoundCount(found);\n drawOverlay(found, result);\n\n if (found === 4) {\n validCountRef.current += 1;\n if (validCountRef.current >= VALID_FRAMES_REQUIRED) {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n captureBestFrame();\n return;\n }\n } else {\n validCountRef.current = 0;\n }\n } catch (e) {\n console.warn('[useScanner] detection error', e);\n validCountRef.current = 0;\n } finally {\n detectingRef.current = false;\n }\n rafRef.current = requestAnimationFrame(loop);\n };\n rafRef.current = requestAnimationFrame(loop);\n }, [drawOverlay, captureBestFrame]);\n\n useEffect(() => {\n if (scanState === 'scanning') startDetection();\n return () => { if (rafRef.current) cancelAnimationFrame(rafRef.current); };\n }, [scanState, startDetection]);\n\n // ── restart ───────────────────────────────────────────────────────────────\n const restart = useCallback(async () => {\n if (rafRef.current) { cancelAnimationFrame(rafRef.current); rafRef.current = null; }\n streamRef.current?.getTracks().forEach(t => t.stop());\n streamRef.current = null;\n validCountRef.current = 0;\n if (capturedUrlRef.current) { URL.revokeObjectURL(capturedUrlRef.current); setCapturedUrl(''); }\n setFoundCount(0); setQrValue(''); qrValueRef.current = '';\n setErrorMsg(''); setStatusMsg('Opening camera…'); setScanState('init');\n try {\n const stream = await openCamera();\n streamRef.current = stream;\n await attachStream(stream);\n setScanState('scanning');\n } catch (e) {\n setErrorMsg(e instanceof Error ? e.message : 'Camera error');\n setScanState('error');\n }\n }, [attachStream]);\n\n return {\n videoRef, overlayRef, captureCanvasRef,\n scanState, capturedUrl, errorMsg, statusMsg, foundCount, qrValue,\n restart,\n };\n}\n"],"mappings":";AAAA,SAAS,aAAa,WAAW,QAAQ,gBAAgB;AAEzD,SAAS,YAAY,yBAAyB;AAC9C,SAAS,mBAAmB,sBAAsB;AAIlD,IAAM,WAAuB;AAC7B,IAAM,wBAAwB;AAC9B,IAAM,sBAAuB;AAG7B,IAAM,gBAAmB;AACzB,IAAM,oBAAoB;AAC1B,IAAM,eAAmB;AAoBzB,eAAe,aAAmC;AAChD,SAAO,UAAU,aAAa,aAAa;AAAA,IACzC,OAAO,EAAE,OAAO,EAAE,OAAO,KAAK,GAAG,QAAQ,EAAE,OAAO,KAAK,GAAG,YAAY,EAAE,OAAO,cAAc,EAAE;AAAA,IAC/F,OAAO;AAAA,EACT,CAAC;AACH;AAEA,SAAS,WAAW,QAAiC;AACnD,QAAM,QAAQ,CAAC,GAAG,GAAG,CAAC,EAAE,OAAO,QAAM,OAAO,aAAa,KAAK,OAAK,EAAE,OAAO,EAAE,CAAC,EAAE;AACjF,SAAO,SAAS,OAAO,OAAO,QAAQ,IAAI;AAC5C;AAEO,SAAS,aAA+B;AAC7C,QAAM,WAAmB,OAAyB,IAAI;AACtD,QAAM,aAAmB,OAA0B,IAAI;AACvD,QAAM,mBAAmB,OAA0B,IAAI;AACvD,QAAM,YAAmB,OAA2B,IAAI;AACxD,QAAM,SAAmB,OAAsB,IAAI;AACnD,QAAM,eAAmB,OAAO,KAAK;AACrC,QAAM,gBAAmB,OAAO,CAAC;AACjC,QAAM,gBAAmB,OAAO,EAAE,GAAG,GAAG,GAAG,EAAE,CAAC;AAC9C,QAAM,eAAmB,OAA+B,IAAI;AAC5D,QAAM,kBAAmB,OAAO,EAAE,GAAG,GAAG,GAAG,EAAE,CAAC;AAC9C,QAAM,kBAAmB,OAA+B,IAAI;AAE5D,QAAM,qBAAqB,OAAY,MAAS;AAEhD,QAAM,aAAa,OAAO,EAAE;AAE5B,QAAM,CAAC,WAAa,YAAY,IAAM,SAAoB,MAAM;AAChE,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,EAAE;AACjD,QAAM,CAAC,UAAa,WAAW,IAAO,SAAS,EAAE;AACjD,QAAM,CAAC,WAAa,YAAY,IAAM,SAAS,sBAAiB;AAChE,QAAM,CAAC,YAAa,aAAa,IAAK,SAAS,CAAC;AAChD,QAAM,CAAC,SAAa,UAAU,IAAQ,SAAS,EAAE;AAGjD,YAAU,MAAM;AACd,WAAO,MAAM;AACX,gBAAU,SAAS,UAAU,EAAE,QAAQ,OAAK,EAAE,KAAK,CAAC;AACpD,UAAI,OAAO,QAAS,sBAAqB,OAAO,OAAO;AAAA,IACzD;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,QAAM,iBAAiB,OAAO,EAAE;AAChC,YAAU,MAAM;AAAE,mBAAe,UAAU;AAAA,EAAa,GAAG,CAAC,WAAW,CAAC;AACxE,YAAU,MAAM;AACd,WAAO,MAAM;AAAE,UAAI,eAAe,QAAS,KAAI,gBAAgB,eAAe,OAAO;AAAA,IAAG;AAAA,EAC1F,GAAG,CAAC,CAAC;AAEL,QAAM,eAAe,YAAY,OAAO,WAAwB;AAC9D,UAAM,QAAQ,SAAS;AACvB,UAAM,YAAY;AAClB,UAAM,MAAM,KAAK;AAAA,EACnB,GAAG,CAAC,CAAC;AAGL,YAAU,MAAM;AACd,QAAI,YAAY;AAChB,KAAC,YAAY;AACX,UAAI;AACF,qBAAa,sBAAiB;AAC9B,cAAM,WAAW;AACjB,YAAI,UAAW;AACf,qBAAa,sBAAiB;AAC9B,cAAM,SAAS,MAAM,WAAW;AAChC,YAAI,WAAW;AAAE,iBAAO,UAAU,EAAE,QAAQ,OAAK,EAAE,KAAK,CAAC;AAAG;AAAA,QAAQ;AACpE,kBAAU,UAAU;AACpB,cAAM,aAAa,MAAM;AACzB,qBAAa,UAAU;AAAA,MACzB,SAAS,GAAY;AACnB,YAAI,CAAC,WAAW;AACd,sBAAY,aAAa,QAAQ,EAAE,UAAU,wBAAwB;AACrE,uBAAa,OAAO;AAAA,QACtB;AAAA,MACF;AAAA,IACF,GAAG;AACH,WAAO,MAAM;AAAE,kBAAY;AAAA,IAAM;AAAA,EACnC,GAAG,CAAC,YAAY,CAAC;AAGjB,QAAM,cAAc,YAAY,CAAC,OAAe,WAA6B;AAC3E,UAAM,SAAS,WAAW;AAC1B,UAAM,QAAS,SAAS;AACxB,QAAI,CAAC,UAAU,CAAC,MAAO;AACvB,UAAM,MAAO,OAAO,WAAW,IAAI;AACnC,UAAM,OAAO,MAAM,sBAAsB;AAEzC,QAAI,KAAK,UAAU,cAAc,QAAQ,KAAK,KAAK,WAAW,cAAc,QAAQ,GAAG;AACrF,aAAO,QAAS,KAAK;AACrB,aAAO,SAAS,KAAK;AACrB,oBAAc,UAAU,EAAE,GAAG,KAAK,OAAO,GAAG,KAAK,OAAO;AAAA,IAC1D;AACA,QAAI,UAAU,GAAG,GAAG,OAAO,OAAO,OAAO,MAAM;AAE/C,UAAM,WAAW,UAAU;AAC3B,UAAM,QAAW,WAAW,YAAY;AACxC,UAAM,QAAW,WAAW,6BAAwB,GAAG,KAAK;AAC5D,UAAM,KAAK,OAAO,OAAO,KAAK,OAAO;AACrC,UAAM,MAAM,KAAK,IAAI,IAAI,EAAE,IAAI;AAE/B,QAAI,cAAc;AAClB,QAAI,YAAc;AAClB,UAAM,cAAc,CAAC,GAAW,GAAW,IAAY,OAAe;AACpE,UAAI,UAAU;AACd,UAAI,OAAO,IAAI,KAAK,KAAK,CAAC;AAAG,UAAI,OAAO,GAAG,CAAC;AAAG,UAAI,OAAO,GAAG,IAAI,KAAK,GAAG;AACzE,UAAI,OAAO;AAAA,IACb;AACA,gBAAY,eAAoB,eAAoB,GAAG,CAAC;AACxD,gBAAY,KAAK,eAAe,eAAmB,IAAI,CAAC;AACxD,gBAAY,eAAoB,KAAK,eAAe,GAAG,EAAE;AACzD,gBAAY,KAAK,eAAe,KAAK,eAAc,IAAI,EAAE;AAEzD,QAAI,QAAQ;AACV,YAAM,KAAK,KAAK,OAAO;AACvB,YAAM,KAAK,KAAK,OAAO;AACvB,YAAM,eAAuC,EAAE,GAAG,WAAW,GAAG,WAAW,GAAG,UAAU;AACxF,YAAM,eAAuC,EAAE,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK;AAEzE,iBAAW,KAAK,OAAO,cAAc;AACnC,cAAM,KAAK,aAAa,EAAE,EAAE,KAAK;AACjC,YAAI,cAAc;AAAI,YAAI,YAAY;AACtC,YAAI,UAAU;AACd,UAAE,QAAQ,QAAQ,CAAC,GAAG,MAAM;AAC1B,cAAI,MAAM,EAAG,KAAI,OAAO,EAAE,IAAI,IAAI,EAAE,IAAI,EAAE;AAAA,cAAQ,KAAI,OAAO,EAAE,IAAI,IAAI,EAAE,IAAI,EAAE;AAAA,QACjF,CAAC;AACD,YAAI,UAAU;AAAG,YAAI,OAAO;AAC5B,YAAI,YAAY;AAAI,YAAI,OAAO;AAAwB,YAAI,YAAY;AACvE,YAAI,SAAS,aAAa,EAAE,EAAE,KAAK,IAAI,EAAE,EAAE,IAAI,EAAE,OAAO,IAAI,KAAK,GAAG,EAAE,OAAO,IAAI,KAAK,CAAC;AAAA,MACzF;AACA,UAAI,OAAO,OAAO,OAAO;AACvB,YAAI,YAAY;AAAW,YAAI,OAAO;AAAwB,YAAI,YAAY;AAC9E,YAAI,SAAS,aAAQ,GAAG,EAAE;AAAA,MAC5B;AAAA,IACF;AAEA,QAAI,YAAY;AAChB,QAAI,SAAS,GAAG,KAAK,cAAc,IAAI,YAAY;AACnD,QAAI,YAAY;AAChB,QAAI,OAAO,QAAQ,KAAK,MAAM,KAAK,QAAQ,EAAE,CAAC;AAC9C,QAAI,YAAY;AAChB,QAAI,SAAS,OAAO,KAAK,GAAG,KAAK,EAAE;AAAA,EACrC,GAAG,CAAC,CAAC;AAIL,QAAM,mBAAmB,YAAY,MAAM;AACzC,UAAM,SAAS,iBAAiB;AAChC,cAAU,SAAS,UAAU,EAAE,QAAQ,OAAK,EAAE,KAAK,CAAC;AACpD,cAAU,UAAU;AACpB,WAAO,OAAO,UAAQ;AACpB,UAAI,CAAC,KAAM;AACX,UAAI,eAAe,QAAS,KAAI,gBAAgB,eAAe,OAAO;AACtE,qBAAe,IAAI,gBAAgB,IAAI,CAAC;AAAA,IAC1C,GAAG,WAAW;AACd,iBAAa,UAAU;AACvB,eAAW,WAAW,OAAO;AAAA,EAC/B,GAAG,CAAC,CAAC;AAGL,QAAM,iBAAiB,YAAY,MAAM;AACvC,QAAI,OAAO,QAAS,sBAAqB,OAAO,OAAO;AACvD,iBAAa,UAAU;AACvB,kBAAc,UAAU;AAExB,UAAM,OAAO,YAAY;AACvB,YAAM,QAAQ,SAAS;AACvB,UAAI,CAAC,SAAS,MAAM,aAAa,KAAK,aAAa,SAAS;AAC1D,eAAO,UAAU,sBAAsB,IAAI;AAC3C;AAAA,MACF;AACA,mBAAa,UAAU;AACvB,UAAI;AACF,cAAM,EAAE,YAAY,IAAI,aAAa,GAAG,IAAI;AAG5C,cAAM,gBAAgB,iBAAiB;AACvC,YAAI,cAAc,UAAU,MAAM,cAAc,WAAW,IAAI;AAC7D,wBAAc,QAAQ;AAAI,wBAAc,SAAS;AAAA,QACnD;AACA,sBAAc,WAAW,IAAI,EAAG,UAAU,OAAO,GAAG,CAAC;AAErD,cAAM,QAAQ,KAAK,IAAI,GAAG,WAAW,KAAK,IAAI,IAAI,EAAE,CAAC;AACrD,cAAM,IAAI,KAAK,MAAM,KAAK,KAAK,GAAG,IAAI,KAAK,MAAM,KAAK,KAAK;AAC3D,YAAI,CAAC,aAAa,WAAW,gBAAgB,QAAQ,MAAM,KAAK,gBAAgB,QAAQ,MAAM,GAAG;AAC/F,uBAAa,UAAW,IAAI,gBAAgB,GAAG,CAAC;AAChD,0BAAgB,UAAU,EAAE,GAAG,EAAE;AAAA,QACnC;AACA,cAAM,MAAM,aAAa,QAAQ,WAAW,MAAM,EAAE,oBAAoB,KAAK,CAAC;AAC9E,YAAI,UAAU,OAAO,GAAG,GAAG,GAAG,CAAC;AAC/B,cAAM,SAAS,MAAM,kBAAkB,IAAI,aAAa,GAAG,GAAG,GAAG,CAAC,CAAC;AAGnE,YAAI,mBAAmB,YAAY,QAAW;AAE5C,gBAAM,KAAM,OAAe;AAC3B,6BAAmB,UAAU,KAAK,IAAI,GAAG,EAAE,SAAS,CAAC,SAAS,EAAE,CAAC,IAAI;AAAA,QACvE;AACA,cAAM,cAAc,CAAC,GAAG,GAAG,CAAC,EAAE,MAAM,QAAM,OAAO,aAAa,KAAK,OAAK,EAAE,OAAO,EAAE,CAAC;AACpF,YAAI,aAAa;AACf,cAAI,WAAW,SAAS;AACtB,mBAAO,SAAS,EAAE,OAAO,KAAK;AAAA,UAChC,OAAO;AACL,kBAAM,SAAS,kBAAkB,OAAO,cAAc,GAAG,GAAG,IAAI,EAAE;AAClE,gBAAI,QAAQ;AACV,oBAAM,YAAY,KAAK,IAAI,GAAG,sBAAsB,KAAK,IAAI,OAAO,GAAG,OAAO,CAAC,CAAC;AAChF,oBAAM,MAAM,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,IAAI,SAAS,CAAC;AACxD,oBAAM,MAAM,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,IAAI,SAAS,CAAC;AACxD,kBAAI,CAAC,gBAAgB,WACjB,gBAAgB,QAAQ,UAAU,OAAO,gBAAgB,QAAQ,WAAW,KAAK;AACnF,gCAAgB,UAAU,IAAI,gBAAgB,KAAK,GAAG;AAAA,cACxD;AACA,oBAAM,QAAQ,gBAAgB,QAAQ,WAAW,MAAM,EAAE,oBAAoB,KAAK,CAAC;AACnF,oBAAM,UAAU,eAAe,OAAO,IAAI,OAAO,IAAI,OAAO,GAAG,OAAO,GAAG,GAAG,GAAG,KAAK,GAAG;AACvF,oBAAM,QAAQ,MAAM,eAAe,gBAAgB,SAAU,EAAE,iBAAiB,mBAAmB,QAAQ,CAAC;AAC5G,kBAAI,MAAM,OAAO;AACf,uBAAO,SAAS,EAAE,OAAO,KAAK;AAC9B,oBAAI,MAAM,SAAU,YAAW,UAAU,MAAM;AAAA,cACjD;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAEA,cAAM,QAAQ,WAAW,MAAM;AAC/B,sBAAc,KAAK;AACnB,oBAAY,OAAO,MAAM;AAEzB,YAAI,UAAU,GAAG;AACf,wBAAc,WAAW;AACzB,cAAI,cAAc,WAAW,uBAAuB;AAClD,gBAAI,OAAO,QAAS,sBAAqB,OAAO,OAAO;AACvD,mBAAO,UAAU;AACjB,6BAAiB;AACjB;AAAA,UACF;AAAA,QACF,OAAO;AACL,wBAAc,UAAU;AAAA,QAC1B;AAAA,MACF,SAAS,GAAG;AACV,gBAAQ,KAAK,gCAAgC,CAAC;AAC9C,sBAAc,UAAU;AAAA,MAC1B,UAAE;AACA,qBAAa,UAAU;AAAA,MACzB;AACA,aAAO,UAAU,sBAAsB,IAAI;AAAA,IAC7C;AACA,WAAO,UAAU,sBAAsB,IAAI;AAAA,EAC7C,GAAG,CAAC,aAAa,gBAAgB,CAAC;AAElC,YAAU,MAAM;AACd,QAAI,cAAc,WAAY,gBAAe;AAC7C,WAAO,MAAM;AAAE,UAAI,OAAO,QAAS,sBAAqB,OAAO,OAAO;AAAA,IAAG;AAAA,EAC3E,GAAG,CAAC,WAAW,cAAc,CAAC;AAG9B,QAAM,UAAU,YAAY,YAAY;AACtC,QAAI,OAAO,SAAS;AAAE,2BAAqB,OAAO,OAAO;AAAG,aAAO,UAAU;AAAA,IAAM;AACnF,cAAU,SAAS,UAAU,EAAE,QAAQ,OAAK,EAAE,KAAK,CAAC;AACpD,cAAU,UAAU;AACpB,kBAAc,UAAU;AACxB,QAAI,eAAe,SAAS;AAAE,UAAI,gBAAgB,eAAe,OAAO;AAAG,qBAAe,EAAE;AAAA,IAAG;AAC/F,kBAAc,CAAC;AAAG,eAAW,EAAE;AAAG,eAAW,UAAU;AACvD,gBAAY,EAAE;AAAG,iBAAa,sBAAiB;AAAG,iBAAa,MAAM;AACrE,QAAI;AACF,YAAM,SAAS,MAAM,WAAW;AAChC,gBAAU,UAAU;AACpB,YAAM,aAAa,MAAM;AACzB,mBAAa,UAAU;AAAA,IACzB,SAAS,GAAG;AACV,kBAAY,aAAa,QAAQ,EAAE,UAAU,cAAc;AAC3D,mBAAa,OAAO;AAAA,IACtB;AAAA,EACF,GAAG,CAAC,YAAY,CAAC;AAEjB,SAAO;AAAA,IACL;AAAA,IAAU;AAAA,IAAY;AAAA,IACtB;AAAA,IAAW;AAAA,IAAa;AAAA,IAAU;AAAA,IAAW;AAAA,IAAY;AAAA,IACzD;AAAA,EACF;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/useScanner.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\r\nimport type { RefObject } from 'react';\r\nimport { initWorker, detectInImageData } from '@dcg-overseas/cv-worker';\r\nimport { estimateQRCropBox, detectQRInCrop } from '@dcg-overseas/core';\r\nimport type { DetectionResult } from '@dcg-overseas/types';\r\n\r\n// ── 检测常量 ──────────────────────────────────────────────────────────────────\r\nconst SCAN_DIM = 720; // 检测缩略图长边(px)\r\nconst VALID_FRAMES_REQUIRED = 2; // 连续有效帧数阈值\r\nconst QR_REALTIME_CROP_PX = 600; // QR 裁切图上限(px),只缩小不放大\r\n\r\n// ── Overlay 绘制常量 ──────────────────────────────────────────────────────────\r\nconst BRACKET_INSET = 12; // 取景框括号内缩距离(px)\r\nconst BRACKET_LEN_RATIO = 0.08; // 括号边长占短边比例\r\nconst STATUS_BAR_H = 40; // 底部状态栏高度(px)\r\n\r\nexport type ScanState = 'init' | 'scanning' | 'captured' | 'error';\r\n\r\nexport interface UseScannerReturn {\r\n // ── DOM refs(直接绑定到 JSX 元素)──────────────────────────────────────────\r\n videoRef: RefObject<HTMLVideoElement | null>;\r\n overlayRef: RefObject<HTMLCanvasElement | null>;\r\n captureCanvasRef: RefObject<HTMLCanvasElement | null>;\r\n // ── 状态 ─────────────────────────────────────────────────────────────────────\r\n scanState: ScanState;\r\n capturedUrl: string;\r\n errorMsg: string;\r\n statusMsg: string;\r\n foundCount: number;\r\n qrValue: string;\r\n // ── 动作 ─────────────────────────────────────────────────────────────────────\r\n restart: () => Promise<void>;\r\n stop: () => void;\r\n}\r\n\r\nasync function openCamera(): Promise<MediaStream> {\r\n return navigator.mediaDevices.getUserMedia({\r\n video: { width: { ideal: 3840 }, height: { ideal: 2160 }, facingMode: { ideal: 'environment' } },\r\n audio: false,\r\n });\r\n}\r\n\r\nfunction countFound(result: DetectionResult): number {\r\n const aruco = [0, 1, 2].filter(id => result.arucoMarkers.some(m => m.id === id)).length;\r\n return aruco + (result.qrCode.found ? 1 : 0);\r\n}\r\n\r\nexport function useScanner(): UseScannerReturn {\r\n const videoRef = useRef<HTMLVideoElement>(null);\r\n const overlayRef = useRef<HTMLCanvasElement>(null);\r\n const captureCanvasRef = useRef<HTMLCanvasElement>(null);\r\n const streamRef = useRef<MediaStream | null>(null);\r\n const rafRef = useRef<number | null>(null);\r\n const detectingRef = useRef(false);\r\n const validCountRef = useRef(0);\r\n const overlayDimRef = useRef({ w: 0, h: 0 });\r\n const offscreenRef = useRef<OffscreenCanvas | null>(null);\r\n const offscreenDimRef = useRef({ w: 0, h: 0 });\r\n const qrCropCanvasRef = useRef<OffscreenCanvas | null>(null);\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n const barcodeDetectorRef = useRef<any>(undefined); // undefined=未检测, null=不支持, 否则为实例\r\n // 检测循环中暂存 QR 文本,避免 setState 干扰帧率,捕获时一次性写入 state\r\n const qrValueRef = useRef('');\r\n\r\n const [scanState, setScanState] = useState<ScanState>('init');\r\n const [capturedUrl, setCapturedUrl] = useState('');\r\n const [errorMsg, setErrorMsg] = useState('');\r\n const [statusMsg, setStatusMsg] = useState('Loading OpenCV…');\r\n const [foundCount, setFoundCount] = useState(0);\r\n const [qrValue, setQrValue] = useState('');\r\n\r\n // ── unmount cleanup ───────────────────────────────────────────────────────\r\n useEffect(() => {\r\n return () => {\r\n streamRef.current?.getTracks().forEach(t => t.stop());\r\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\r\n };\r\n }, []);\r\n\r\n const capturedUrlRef = useRef('');\r\n useEffect(() => { capturedUrlRef.current = capturedUrl; }, [capturedUrl]);\r\n useEffect(() => {\r\n return () => { if (capturedUrlRef.current) URL.revokeObjectURL(capturedUrlRef.current); };\r\n }, []);\r\n\r\n const attachStream = useCallback(async (stream: MediaStream) => {\r\n const video = videoRef.current!;\r\n video.srcObject = stream;\r\n await video.play();\r\n }, []);\r\n\r\n // ── initial boot ──────────────────────────────────────────────────────────\r\n useEffect(() => {\r\n let cancelled = false;\r\n (async () => {\r\n try {\r\n setStatusMsg('Loading OpenCV…');\r\n await initWorker();\r\n if (cancelled) return;\r\n setStatusMsg('Opening camera…');\r\n const stream = await openCamera();\r\n if (cancelled) { stream.getTracks().forEach(t => t.stop()); return; }\r\n streamRef.current = stream;\r\n await attachStream(stream);\r\n setScanState('scanning');\r\n } catch (e: unknown) {\r\n if (!cancelled) {\r\n setErrorMsg(e instanceof Error ? e.message : 'Camera or OpenCV error');\r\n setScanState('error');\r\n }\r\n }\r\n })();\r\n return () => { cancelled = true; };\r\n }, [attachStream]);\r\n\r\n // ── overlay drawing ───────────────────────────────────────────────────────\r\n const drawOverlay = useCallback((found: number, result?: DetectionResult) => {\r\n const canvas = overlayRef.current;\r\n const video = videoRef.current;\r\n if (!canvas || !video) return;\r\n const ctx = canvas.getContext('2d')!;\r\n const rect = video.getBoundingClientRect();\r\n\r\n if (rect.width !== overlayDimRef.current.w || rect.height !== overlayDimRef.current.h) {\r\n canvas.width = rect.width;\r\n canvas.height = rect.height;\r\n overlayDimRef.current = { w: rect.width, h: rect.height };\r\n }\r\n ctx.clearRect(0, 0, canvas.width, canvas.height);\r\n\r\n const allFound = found === 4;\r\n const color = allFound ? '#00ff88' : '#ff4444';\r\n const label = allFound ? '✓ All markers found' : `${found} / 4 markers found`;\r\n const bw = canvas.width, bh = canvas.height;\r\n const len = Math.min(bw, bh) * BRACKET_LEN_RATIO;\r\n\r\n ctx.strokeStyle = color;\r\n ctx.lineWidth = 3;\r\n const drawBracket = (x: number, y: number, dx: number, dy: number) => {\r\n ctx.beginPath();\r\n ctx.moveTo(x + dx * len, y); ctx.lineTo(x, y); ctx.lineTo(x, y + dy * len);\r\n ctx.stroke();\r\n };\r\n drawBracket(BRACKET_INSET, BRACKET_INSET, 1, 1);\r\n drawBracket(bw - BRACKET_INSET, BRACKET_INSET, -1, 1);\r\n drawBracket(BRACKET_INSET, bh - BRACKET_INSET, 1, -1);\r\n drawBracket(bw - BRACKET_INSET, bh - BRACKET_INSET,-1, -1);\r\n\r\n if (result) {\r\n const sx = bw / result.imageWidth;\r\n const sy = bh / result.imageHeight;\r\n const markerColors: Record<number, string> = { 0: '#00e5ff', 1: '#ffe100', 2: '#ff4f00' };\r\n const markerLabels: Record<number, string> = { 0: 'TL', 1: 'BR', 2: 'BL' };\r\n\r\n for (const m of result.arucoMarkers) {\r\n const mc = markerColors[m.id] ?? '#ffffff';\r\n ctx.strokeStyle = mc; ctx.lineWidth = 2;\r\n ctx.beginPath();\r\n m.corners.forEach((p, i) => {\r\n if (i === 0) ctx.moveTo(p.x * sx, p.y * sy); else ctx.lineTo(p.x * sx, p.y * sy);\r\n });\r\n ctx.closePath(); ctx.stroke();\r\n ctx.fillStyle = mc; ctx.font = 'bold 12px sans-serif'; ctx.textAlign = 'left';\r\n ctx.fillText(markerLabels[m.id] ?? `A${m.id}`, m.center.x * sx + 4, m.center.y * sy - 4);\r\n }\r\n if (result.qrCode.found) {\r\n ctx.fillStyle = '#00ff88'; ctx.font = 'bold 12px sans-serif'; ctx.textAlign = 'left';\r\n ctx.fillText('QR ✓', 4, 16);\r\n }\r\n }\r\n\r\n ctx.fillStyle = 'rgba(0,0,0,0.55)';\r\n ctx.fillRect(0, bh - STATUS_BAR_H, bw, STATUS_BAR_H);\r\n ctx.fillStyle = color;\r\n ctx.font = `bold ${Math.round(bh * 0.022 + 11)}px sans-serif`;\r\n ctx.textAlign = 'center';\r\n ctx.fillText(label, bw / 2, bh - 12);\r\n }, []);\r\n\r\n // ── capture ───────────────────────────────────────────────────────────────\r\n // captureCanvasRef 在检测循环顶部已同步冻结当前帧,直接转 blob 即可\r\n const captureBestFrame = useCallback(() => {\r\n const canvas = captureCanvasRef.current!;\r\n streamRef.current?.getTracks().forEach(t => t.stop());\r\n streamRef.current = null;\r\n canvas.toBlob(blob => {\r\n if (!blob) return;\r\n if (capturedUrlRef.current) URL.revokeObjectURL(capturedUrlRef.current);\r\n setCapturedUrl(URL.createObjectURL(blob));\r\n }, 'image/png');\r\n setScanState('captured');\r\n setQrValue(qrValueRef.current);\r\n }, []);\r\n\r\n // ── detection loop(rAF)────────────────────────────────────────────────\r\n const startDetection = useCallback(() => {\r\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\r\n detectingRef.current = false;\r\n validCountRef.current = 0;\r\n\r\n const loop = async () => {\r\n const video = videoRef.current;\r\n if (!video || video.readyState < 2 || detectingRef.current) {\r\n rafRef.current = requestAnimationFrame(loop);\r\n return;\r\n }\r\n detectingRef.current = true;\r\n try {\r\n const { videoWidth: vw, videoHeight: vh } = video;\r\n\r\n // 同步冻结当前帧到 captureCanvas(await 前)\r\n const captureCanvas = captureCanvasRef.current!;\r\n if (captureCanvas.width !== vw || captureCanvas.height !== vh) {\r\n captureCanvas.width = vw; captureCanvas.height = vh;\r\n }\r\n captureCanvas.getContext('2d')!.drawImage(video, 0, 0);\r\n\r\n const scale = Math.min(1, SCAN_DIM / Math.max(vw, vh));\r\n const w = Math.round(vw * scale), h = Math.round(vh * scale);\r\n if (!offscreenRef.current || offscreenDimRef.current.w !== w || offscreenDimRef.current.h !== h) {\r\n offscreenRef.current = new OffscreenCanvas(w, h);\r\n offscreenDimRef.current = { w, h };\r\n }\r\n const ctx = offscreenRef.current.getContext('2d', { willReadFrequently: true }) as OffscreenCanvasRenderingContext2D;\r\n ctx.drawImage(video, 0, 0, w, h);\r\n const result:DetectionResult = await detectInImageData(ctx.getImageData(0, 0, w, h));\r\n\r\n // ── QR 检测 ───────────────────────────────────────────────────────\r\n if (barcodeDetectorRef.current === undefined) {\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n const BD = (window as any).BarcodeDetector;\r\n barcodeDetectorRef.current = BD ? new BD({ formats: ['qr_code'] }) : null;\r\n }\r\n const hasAllAruco = [0, 1, 2].every(id => result.arucoMarkers.some(m => m.id === id));\r\n if (hasAllAruco) {\r\n if (qrValueRef.current) {\r\n result.qrCode = { found: true }; // 已解码,复用缓存\r\n } else {\r\n const qrCrop = estimateQRCropBox(result.arucoMarkers, w, h, vw, vh);\r\n if (qrCrop) {\r\n const cropScale = Math.min(1, QR_REALTIME_CROP_PX / Math.max(qrCrop.w, qrCrop.h));\r\n const qrW = Math.max(1, Math.round(qrCrop.w * cropScale));\r\n const qrH = Math.max(1, Math.round(qrCrop.h * cropScale));\r\n if (!qrCropCanvasRef.current ||\r\n qrCropCanvasRef.current.width !== qrW || qrCropCanvasRef.current.height !== qrH) {\r\n qrCropCanvasRef.current = new OffscreenCanvas(qrW, qrH);\r\n }\r\n const qrCtx = qrCropCanvasRef.current.getContext('2d', { willReadFrequently: true }) as OffscreenCanvasRenderingContext2D;\r\n qrCtx.drawImage(captureCanvas, qrCrop.x0, qrCrop.y0, qrCrop.w, qrCrop.h, 0, 0, qrW, qrH);\r\n const qrDet = await detectQRInCrop(qrCropCanvasRef.current!, { barcodeDetector: barcodeDetectorRef.current });\r\n if (qrDet.found) {\r\n result.qrCode = { found: true };\r\n if (qrDet.rawValue) qrValueRef.current = qrDet.rawValue;\r\n }\r\n }\r\n }\r\n }\r\n\r\n const found = countFound(result);\r\n setFoundCount(found);\r\n drawOverlay(found, result);\r\n\r\n if (found === 4) {\r\n validCountRef.current += 1;\r\n if (validCountRef.current >= VALID_FRAMES_REQUIRED) {\r\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\r\n rafRef.current = null;\r\n captureBestFrame();\r\n return;\r\n }\r\n } else {\r\n validCountRef.current = 0;\r\n }\r\n } catch (e) {\r\n console.warn('[useScanner] detection error', e);\r\n validCountRef.current = 0;\r\n } finally {\r\n detectingRef.current = false;\r\n }\r\n rafRef.current = requestAnimationFrame(loop);\r\n };\r\n rafRef.current = requestAnimationFrame(loop);\r\n }, [drawOverlay, captureBestFrame]);\r\n\r\n useEffect(() => {\r\n if (scanState === 'scanning') startDetection();\r\n return () => { if (rafRef.current) cancelAnimationFrame(rafRef.current); };\r\n }, [scanState, startDetection]);\r\n\r\n const stop = useCallback(()=>{\r\n if (rafRef.current) { cancelAnimationFrame(rafRef.current); rafRef.current = null; }\r\n streamRef.current?.getTracks().forEach(t => t.stop());\r\n streamRef.current = null;\r\n validCountRef.current = 0;\r\n if (capturedUrlRef.current) { URL.revokeObjectURL(capturedUrlRef.current); setCapturedUrl(''); }\r\n setFoundCount(0); setQrValue(''); qrValueRef.current = '';\r\n setErrorMsg(''); setStatusMsg('Opening camera…'); setScanState('init');\r\n },[])\r\n\r\n // ── restart ───────────────────────────────────────────────────────────────\r\n const restart = useCallback(async () => {\r\n if (rafRef.current) { cancelAnimationFrame(rafRef.current); rafRef.current = null; }\r\n streamRef.current?.getTracks().forEach(t => t.stop());\r\n streamRef.current = null;\r\n validCountRef.current = 0;\r\n if (capturedUrlRef.current) { URL.revokeObjectURL(capturedUrlRef.current); setCapturedUrl(''); }\r\n setFoundCount(0); setQrValue(''); qrValueRef.current = '';\r\n setErrorMsg(''); setStatusMsg('Opening camera…'); setScanState('init');\r\n try {\r\n const stream = await openCamera();\r\n streamRef.current = stream;\r\n await attachStream(stream);\r\n setScanState('scanning');\r\n } catch (e) {\r\n setErrorMsg(e instanceof Error ? e.message : 'Camera error');\r\n setScanState('error');\r\n }\r\n }, [attachStream]);\r\n\r\n return {\r\n videoRef, overlayRef, captureCanvasRef,\r\n scanState, capturedUrl, errorMsg, statusMsg, foundCount, qrValue,\r\n restart, stop\r\n };\r\n}\r\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dcg-overseas/scanner",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
7
|
-
"import": "./dist/index.
|
|
7
|
+
"import": "./dist/index.js",
|
|
8
8
|
"require": "./dist/index.cjs",
|
|
9
9
|
"types": "./dist/index.d.ts"
|
|
10
10
|
}
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
"dist"
|
|
14
14
|
],
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@dcg-overseas/core": "0.1.1",
|
|
17
16
|
"@dcg-overseas/types": "0.1.1",
|
|
18
|
-
"@dcg-overseas/cv-worker": "0.1.
|
|
17
|
+
"@dcg-overseas/cv-worker": "0.1.7",
|
|
18
|
+
"@dcg-overseas/core": "0.1.2"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@types/react": "^19.2.14",
|