Files
_hublib-web/dist/react/video-player/components/qr-scanner/index.js

209 lines
11 KiB
JavaScript
Raw Normal View History

2026-02-27 09:50:13 +03:00
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { useCallback, useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { Popover } from "antd";
import QrScanner from "qr-scanner";
import { CloseOutlined } from "@ant-design/icons";
const VideoQRScannerPlugin = ({ player, enabled = true, scanInterval = 200, scanningScale = 1, maxFailedAttempts = 2, }) => {
const canvasRef = useRef(null);
const [detectedQr, setDetectedQr] = useState(null);
const animationFrameRef = useRef(null);
const ignoredQRCodes = useRef(new Set());
const [playerEl, setPlayerEl] = useState(null);
const lastScanTimeRef = useRef(0);
const isScanningRef = useRef(false);
// Счётчик неудачных попыток обнаружения QR, если код уже отображается
const failedAttemptsRef = useRef(0);
// Для получения актуального значения detectedQr внутри асинхронного колбэка
const detectedQrRef = useRef(null);
useEffect(() => {
detectedQrRef.current = detectedQr;
}, [detectedQr]);
// Изначально устанавливаем контейнер для портала как элемент плеера
useEffect(() => {
if (player) {
setPlayerEl(player.el());
}
}, [player]);
// Обновление контейнера при переходе в полноэкранный режим
useEffect(() => {
const handleFullscreenChange = () => {
if (document.fullscreenElement) {
setPlayerEl(document.fullscreenElement);
}
else if (player) {
setPlayerEl(player.el());
}
};
document.addEventListener("fullscreenchange", handleFullscreenChange);
return () => {
document.removeEventListener("fullscreenchange", handleFullscreenChange);
};
}, [player]);
const scanFrame = useCallback(async () => {
if (!player || player.paused() || !canvasRef.current || !enabled)
return;
const now = performance.now();
if (now - lastScanTimeRef.current < scanInterval) {
animationFrameRef.current = requestAnimationFrame(scanFrame);
return;
}
lastScanTimeRef.current = now;
// Предотвращаем параллельное выполнение сканирования
if (isScanningRef.current) {
animationFrameRef.current = requestAnimationFrame(scanFrame);
return;
}
isScanningRef.current = true;
const videoEl = player.tech(true).el();
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
if (!ctx) {
isScanningRef.current = false;
return;
}
// Получаем оригинальные размеры видео
const originalVideoWidth = videoEl.videoWidth;
const originalVideoHeight = videoEl.videoHeight;
// Устанавливаем canvas с пониженным разрешением для сканирования
canvas.width = originalVideoWidth * scanningScale;
canvas.height = originalVideoHeight * scanningScale;
// Рисуем видео в canvas с пониженным разрешением
ctx.drawImage(videoEl, 0, 0, canvas.width, canvas.height);
try {
const result = await QrScanner.scanImage(canvas, {
returnDetailedScanResult: true,
});
if (result &&
result.data &&
result.cornerPoints?.length === 4 &&
!ignoredQRCodes.current.has(result.data)) {
// Сброс неудачных попыток при успешном обнаружении
failedAttemptsRef.current = 0;
// Преобразуем координаты из масштабированного canvas в координаты оригинального видео
const points = result.cornerPoints.map(p => ({
x: p.x / scanningScale,
y: p.y / scanningScale,
}));
const minX = Math.min(...points.map(p => p.x));
const minY = Math.min(...points.map(p => p.y));
const maxX = Math.max(...points.map(p => p.x));
const maxY = Math.max(...points.map(p => p.y));
// Получаем размеры отображаемого видео
const rect = videoEl.getBoundingClientRect();
const displayScale = Math.min(rect.width / originalVideoWidth, rect.height / originalVideoHeight);
const offsetX = (rect.width - originalVideoWidth * displayScale) / 2;
const offsetY = (rect.height - originalVideoHeight * displayScale) / 2;
const qrWidth = (maxX - minX) * displayScale;
const qrHeight = (maxY - minY) * displayScale;
const padding = 8;
const x = minX * displayScale + offsetX - padding;
const y = minY * displayScale + offsetY - padding;
setDetectedQr({
data: result.data,
position: new DOMRect(x, y, qrWidth + padding * 2, qrHeight + padding * 2),
});
}
else {
// Если код не найден, и он уже отображался, даем maxFailedAttempts попыток
if (detectedQrRef.current) {
failedAttemptsRef.current += 1;
if (failedAttemptsRef.current >= maxFailedAttempts) {
setDetectedQr(null);
failedAttemptsRef.current = 0;
}
}
}
}
catch (error) {
// В случае ошибки аналогичная логика: если код уже отображался — даем maxFailedAttempts попыток
if (detectedQrRef.current) {
failedAttemptsRef.current += 1;
if (failedAttemptsRef.current >= maxFailedAttempts) {
setDetectedQr(null);
failedAttemptsRef.current = 0;
}
}
}
finally {
isScanningRef.current = false;
}
animationFrameRef.current = requestAnimationFrame(scanFrame);
}, [player, enabled, scanInterval, scanningScale, maxFailedAttempts]);
useEffect(() => {
if (!player || !enabled)
return;
const startScanning = () => {
if (animationFrameRef.current)
cancelAnimationFrame(animationFrameRef.current);
animationFrameRef.current = requestAnimationFrame(scanFrame);
};
const stopScanning = () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
animationFrameRef.current = null;
}
setDetectedQr(null);
};
player.on("play", startScanning);
player.on("pause", stopScanning);
player.on("ended", stopScanning);
window.addEventListener("resize", startScanning);
return () => {
stopScanning();
player.off("play", startScanning);
player.off("pause", stopScanning);
player.off("ended", stopScanning);
window.removeEventListener("resize", startScanning);
};
}, [player, enabled, scanFrame]);
const ignoreQRCode = () => {
if (detectedQr) {
ignoredQRCodes.current.add(detectedQr.data);
setDetectedQr(null);
}
};
return (_jsxs(_Fragment, { children: [_jsx("canvas", { ref: canvasRef, style: { display: "none" } }), detectedQr &&
playerEl &&
createPortal(_jsx(Popover, { getPopupContainer: () => playerEl, content: _jsx("a", { href: detectedQr.data, target: "_blank", rel: "noreferrer", referrerPolicy: "no-referrer", style: { color: "#1677ff" }, children: detectedQr.data }), placement: "top", children: _jsxs("div", { style: {
position: "absolute",
left: detectedQr.position.x,
top: detectedQr.position.y,
width: detectedQr.position.width,
height: detectedQr.position.height,
pointerEvents: "auto",
cursor: "pointer",
zIndex: 10,
}, children: [["top-left", "top-right", "bottom-left", "bottom-right"].map(corner => (_jsx("div", { style: {
position: "absolute",
width: 20,
height: 20,
border: "3px solid var(--Accent-Primary)",
borderRadius: 4,
...(corner.includes("top") ? { top: 0 } : { bottom: 0 }),
...(corner.includes("left") ? { left: 0 } : { right: 0 }),
borderTop: corner.includes("bottom")
? "none"
: "3px solid var(--Accent-Primary)",
borderBottom: corner.includes("top")
? "none"
: "3px solid var(--Accent-Primary)",
borderLeft: corner.includes("right")
? "none"
: "3px solid var(--Accent-Primary)",
borderRight: corner.includes("left")
? "none"
: "3px solid var(--Accent-Primary)",
} }, corner))), _jsx(CloseOutlined, { style: {
color: "#ff4d4f",
position: "absolute",
top: "-8px",
right: "-8px",
background: "white",
borderRadius: "50%",
padding: "2px",
cursor: "pointer",
}, onClick: ignoreQRCode })] }) }), playerEl)] }));
};
export default VideoQRScannerPlugin;
//# sourceMappingURL=index.js.map