Files
_hublib-web/dist/react/video-player/components/qr-scanner/index.js
2026-02-27 09:50:13 +03:00

209 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { 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