209 lines
11 KiB
JavaScript
209 lines
11 KiB
JavaScript
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
|