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