Files
openscreen/src/components/video-editor/VideoPlayback.tsx
T
2025-10-17 21:24:37 -07:00

134 lines
4.2 KiB
TypeScript

import { useEffect, useRef, useImperativeHandle, forwardRef } from "react";
interface VideoPlaybackProps {
videoPath: string;
isSeeking: React.MutableRefObject<boolean>;
onDurationChange: (duration: number) => void;
onTimeUpdate: (time: number) => void;
onPlayStateChange: (playing: boolean) => void;
onError: (error: string) => void;
wallpaper?: string;
}
export interface VideoPlaybackRef {
video: HTMLVideoElement | null;
}
const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
videoPath,
isSeeking,
onDurationChange,
onTimeUpdate,
onPlayStateChange,
onError,
wallpaper,
}, ref) => {
const videoRef = useRef<HTMLVideoElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
useImperativeHandle(ref, () => ({
video: videoRef.current,
}));
useEffect(() => {
const video = videoRef.current;
const canvas = canvasRef.current;
if (!video || !canvas) return;
let animationId: number;
function drawFrame() {
if (!video || !canvas) return;
if (canvas.width !== video.videoWidth || canvas.height !== video.videoHeight) {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
}
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
}
}
function drawFrameLoop() {
if (!video || !canvas || video.paused || video.ended) return;
drawFrame();
animationId = requestAnimationFrame(drawFrameLoop);
}
const handlePlay = () => drawFrameLoop();
const handlePause = () => cancelAnimationFrame(animationId);
const handleSeeked = () => {
drawFrame();
};
video.addEventListener('play', handlePlay);
video.addEventListener('pause', handlePause);
video.addEventListener('ended', handlePause);
video.addEventListener('seeked', handleSeeked);
return () => {
video.removeEventListener('play', handlePlay);
video.removeEventListener('pause', handlePause);
video.removeEventListener('ended', handlePause);
video.removeEventListener('seeked', handleSeeked);
cancelAnimationFrame(animationId);
};
}, [videoPath]);
// Draw first frame when metadata is loaded
const handleLoadedMetadata = (e: React.SyntheticEvent<HTMLVideoElement, Event>) => {
onDurationChange(e.currentTarget.duration);
// Draw first frame
const video = videoRef.current;
const canvas = canvasRef.current;
if (video && canvas) {
video.currentTime = 0;
const drawFirstFrame = () => {
if (canvas.width !== video.videoWidth || canvas.height !== video.videoHeight) {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
}
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
}
video.removeEventListener('seeked', drawFirstFrame);
};
video.addEventListener('seeked', drawFirstFrame);
if (video.currentTime === 0 && video.readyState >= 2) {
drawFirstFrame();
}
}
};
return (
<div
className="w-full aspect-video rounded-xl p-8 flex items-center justify-center overflow-hidden bg-cover bg-center"
style={{ backgroundImage: `url(${wallpaper || '/wallpapers/wallpaper1.jpg'})` }}
>
<canvas
ref={canvasRef}
className="w-full h-full object-contain rounded-lg"
/>
<video
ref={videoRef}
src={videoPath}
className="hidden"
preload="metadata"
onLoadedMetadata={handleLoadedMetadata}
onDurationChange={e => {
onDurationChange(e.currentTarget.duration);
}}
onTimeUpdate={e => {
if (!isSeeking.current) onTimeUpdate(e.currentTarget.currentTime);
}}
onError={() => onError('Failed to load video')}
onPlay={() => onPlayStateChange(true)}
onPause={() => onPlayStateChange(false)}
onEnded={() => onPlayStateChange(false)}
/>
</div>
);
});
VideoPlayback.displayName = 'VideoPlayback';
export default VideoPlayback;