feat: release v0.0.1
This commit is contained in:
25
dist/react/video-player/components/qr-scanner/index.d.ts
vendored
Normal file
25
dist/react/video-player/components/qr-scanner/index.d.ts
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from "react";
|
||||
import Player from "video.js/dist/types/player";
|
||||
interface VideoQRScannerPluginProps {
|
||||
player: Player | null;
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* Интервал между запусками сканирования в миллисекундах.
|
||||
* По умолчанию 200 мс.
|
||||
*/
|
||||
scanInterval?: number;
|
||||
/**
|
||||
* Масштаб для сканирования (0 < scanningScale <= 1).
|
||||
* Значение меньше 1 уменьшает разрешение для ускорения обработки.
|
||||
* По умолчанию 1.
|
||||
*/
|
||||
scanningScale?: number;
|
||||
/**
|
||||
* Количество неудачных попыток обнаружения QR-кода до снятия маски.
|
||||
* По умолчанию 2.a
|
||||
*/
|
||||
maxFailedAttempts?: number;
|
||||
}
|
||||
declare const VideoQRScannerPlugin: React.FC<VideoQRScannerPluginProps>;
|
||||
export default VideoQRScannerPlugin;
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
1
dist/react/video-player/components/qr-scanner/index.d.ts.map
vendored
Normal file
1
dist/react/video-player/components/qr-scanner/index.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/react/video-player/components/qr-scanner/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAKxE,OAAO,MAAM,MAAM,4BAA4B,CAAC;AAIhD,UAAU,yBAAyB;IAClC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,QAAA,MAAM,oBAAoB,EAAE,KAAK,CAAC,EAAE,CAAC,yBAAyB,CAqR7D,CAAC;AAEF,eAAe,oBAAoB,CAAC"}
|
||||
209
dist/react/video-player/components/qr-scanner/index.js
vendored
Normal file
209
dist/react/video-player/components/qr-scanner/index.js
vendored
Normal file
@@ -0,0 +1,209 @@
|
||||
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
|
||||
1
dist/react/video-player/components/qr-scanner/index.js.map
vendored
Normal file
1
dist/react/video-player/components/qr-scanner/index.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user