115 lines
3.3 KiB
TypeScript
115 lines
3.3 KiB
TypeScript
import type { RefObject } from "react";
|
|
import { useEffect } from "react";
|
|
|
|
interface UseBackgroundCanvasProps {
|
|
isVideo: boolean;
|
|
isPlaying: boolean;
|
|
videoRef: RefObject<HTMLVideoElement | null>;
|
|
backgroundCanvasRef: RefObject<HTMLCanvasElement | null>;
|
|
}
|
|
|
|
const CANVAS_DRAW_INTERVAL_MS = 100; // 10fps 对 1/10 分辨率 + blur(40px) 已完全平滑,大幅降低 GPU 负载
|
|
|
|
export function useBackgroundCanvas({
|
|
isVideo,
|
|
isPlaying,
|
|
videoRef,
|
|
backgroundCanvasRef,
|
|
}: UseBackgroundCanvasProps): void {
|
|
// 初始化尺寸与卸载清理
|
|
useEffect(() => {
|
|
const canvas = backgroundCanvasRef.current;
|
|
if (!canvas) return;
|
|
|
|
const ctx = canvas.getContext("2d");
|
|
if (!ctx || !isVideo) {
|
|
canvas.width = 0;
|
|
canvas.height = 0;
|
|
return;
|
|
}
|
|
|
|
const updateCanvasSize = (): void => {
|
|
canvas.width = Math.max(1, Math.floor(window.innerWidth / 10));
|
|
canvas.height = Math.max(1, Math.floor(window.innerHeight / 10));
|
|
};
|
|
updateCanvasSize();
|
|
|
|
let resizeTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
const debouncedResize = (): void => {
|
|
if (resizeTimeout !== null) clearTimeout(resizeTimeout);
|
|
resizeTimeout = setTimeout(updateCanvasSize, 300);
|
|
};
|
|
window.addEventListener("resize", debouncedResize);
|
|
|
|
return () => {
|
|
window.removeEventListener("resize", debouncedResize);
|
|
if (resizeTimeout !== null) clearTimeout(resizeTimeout);
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
canvas.width = 0;
|
|
canvas.height = 0;
|
|
};
|
|
}, [isVideo, backgroundCanvasRef]);
|
|
|
|
// 仅在视频实际播放时进行低频采样绘制,暂停时不占 CPU/GPU
|
|
useEffect(() => {
|
|
if (!isVideo || !isPlaying) return;
|
|
const canvas = backgroundCanvasRef.current;
|
|
if (!canvas) return;
|
|
const ctx = canvas.getContext("2d");
|
|
if (!ctx) return;
|
|
|
|
const drawMediaToCanvas = (): void => {
|
|
if (document.hidden) return;
|
|
const sourceElement = videoRef.current;
|
|
if (
|
|
!sourceElement ||
|
|
sourceElement.readyState < 2 ||
|
|
sourceElement.paused
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const canvasWidth = canvas.width;
|
|
const canvasHeight = canvas.height;
|
|
const sourceWidth = sourceElement.videoWidth;
|
|
const sourceHeight = sourceElement.videoHeight;
|
|
|
|
if (!canvasWidth || !canvasHeight || !sourceWidth || !sourceHeight) {
|
|
return;
|
|
}
|
|
|
|
const canvasRatio = canvasWidth / canvasHeight;
|
|
const sourceRatio = sourceWidth / sourceHeight;
|
|
|
|
let drawWidth: number;
|
|
let drawHeight: number;
|
|
let offsetX: number;
|
|
let offsetY: number;
|
|
|
|
if (canvasRatio > sourceRatio) {
|
|
drawWidth = canvasWidth;
|
|
drawHeight = canvasWidth / sourceRatio;
|
|
offsetX = 0;
|
|
offsetY = (canvasHeight - drawHeight) / 2;
|
|
} else {
|
|
drawHeight = canvasHeight;
|
|
drawWidth = canvasHeight * sourceRatio;
|
|
offsetX = (canvasWidth - drawWidth) / 2;
|
|
offsetY = 0;
|
|
}
|
|
|
|
ctx.drawImage(sourceElement, offsetX, offsetY, drawWidth, drawHeight);
|
|
};
|
|
|
|
drawMediaToCanvas();
|
|
const intervalId = window.setInterval(
|
|
drawMediaToCanvas,
|
|
CANVAS_DRAW_INTERVAL_MS,
|
|
);
|
|
|
|
return () => {
|
|
window.clearInterval(intervalId);
|
|
};
|
|
}, [isVideo, isPlaying, videoRef, backgroundCanvasRef]);
|
|
}
|