From 31364066e765b13c795d6e72e508242d666878fd Mon Sep 17 00:00:00 2001 From: Siddharth Date: Sat, 8 Nov 2025 14:22:47 -0700 Subject: [PATCH] change to pixi container --- src/components/video-editor/VideoPlayback.tsx | 229 ++++++++++++------ 1 file changed, 158 insertions(+), 71 deletions(-) diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index e4dc868..d05641d 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -1,4 +1,5 @@ -import { useEffect, useRef, useImperativeHandle, forwardRef } from "react"; +import { useEffect, useRef, useImperativeHandle, forwardRef, useState } from "react"; +import * as PIXI from 'pixi.js'; interface VideoPlaybackProps { videoPath: string; @@ -11,6 +12,9 @@ interface VideoPlaybackProps { export interface VideoPlaybackRef { video: HTMLVideoElement | null; + app: PIXI.Application | null; + videoSprite: PIXI.Sprite | null; + videoContainer: PIXI.Container | null; } const VideoPlayback = forwardRef(({ @@ -22,21 +26,135 @@ const VideoPlayback = forwardRef(({ wallpaper, }, ref) => { const videoRef = useRef(null); - const canvasRef = useRef(null); - const drawFrameRef = useRef<(() => void) | null>(null); + const containerRef = useRef(null); + const appRef = useRef(null); + const videoSpriteRef = useRef(null); + const videoContainerRef = useRef(null); const timeUpdateAnimationRef = useRef(null); + const [pixiReady, setPixiReady] = useState(false); + const [videoReady, setVideoReady] = useState(false); useImperativeHandle(ref, () => ({ video: videoRef.current, + app: appRef.current, + videoSprite: videoSpriteRef.current, + videoContainer: videoContainerRef.current, })); + // Initialize PixiJS application + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + let mounted = true; + let app: PIXI.Application | null = null; + + // Initialize the app + (async () => { + app = new PIXI.Application(); + + await app.init({ + width: container.clientWidth, + height: container.clientHeight, + backgroundAlpha: 0, + antialias: true, + resolution: window.devicePixelRatio || 1, + autoDensity: true, + }); + + if (!mounted) { + app.destroy(true, { children: true, texture: true, textureSource: true }); + return; + } + + appRef.current = app; + container.appendChild(app.canvas); + + // Create a container for the video (this will hold animations later) + const videoContainer = new PIXI.Container(); + videoContainerRef.current = videoContainer; + app.stage.addChild(videoContainer); + + setPixiReady(true); + })(); + + // Cleanup + return () => { + mounted = false; + setPixiReady(false); + if (app && app.renderer) { + app.destroy(true, { children: true, texture: true, textureSource: true }); + } + appRef.current = null; + videoContainerRef.current = null; + videoSpriteRef.current = null; + }; + }, []); + + // Ensure video starts paused whenever the source changes useEffect(() => { const video = videoRef.current; - const canvas = canvasRef.current; - if (!video || !canvas) return; + if (!video) return; + video.pause(); + video.currentTime = 0; + }, [videoPath]); - let animationId: number; + // Setup video sprite when both PixiJS and video are ready + useEffect(() => { + if (!pixiReady || !videoReady) return; + + const video = videoRef.current; + const app = appRef.current; + const videoContainer = videoContainerRef.current; + if (!video || !app || !videoContainer) return; + if (video.videoWidth === 0 || video.videoHeight === 0) return; + + // Create texture from video element + const source = PIXI.VideoSource.from(video); + const videoTexture = PIXI.Texture.from(source); + + // Create sprite with the video texture + const videoSprite = new PIXI.Sprite(videoTexture); + videoSpriteRef.current = videoSprite; + + // Create rounded rectangle mask + const maskGraphics = new PIXI.Graphics(); + + videoContainer.addChild(videoSprite); + videoContainer.addChild(maskGraphics); + videoSprite.mask = maskGraphics; + + // Position and scale video + const containerWidth = app.canvas.width / app.renderer.resolution; + const containerHeight = app.canvas.height / app.renderer.resolution; + const videoWidth = video.videoWidth; + const videoHeight = video.videoHeight; + + const scale = Math.min( + containerWidth / videoWidth, + containerHeight / videoHeight + ); + + videoSprite.width = videoWidth * scale; + videoSprite.height = videoHeight * scale; + videoSprite.x = (containerWidth - videoSprite.width) / 2; + videoSprite.y = (containerHeight - videoSprite.height) / 2; + + // Draw rounded mask + const radius = Math.min(videoSprite.width, videoSprite.height) * 0.02; + maskGraphics.roundRect( + videoSprite.x, + videoSprite.y, + videoSprite.width, + videoSprite.height, + radius + ); + maskGraphics.fill({ color: 0xffffff }); + + // Ensure Pixi does not trigger autoplay + video.pause(); + function updateTime() { if (!video) return; onTimeUpdate(video.currentTime); @@ -45,98 +163,65 @@ const VideoPlayback = forwardRef(({ } } - 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); - - // Apply rounded rectangle clipping path with consistent radius - const radius = Math.min(canvas.width, canvas.height) * 0.02; - ctx.save(); - ctx.beginPath(); - ctx.moveTo(radius, 0); - ctx.lineTo(canvas.width - radius, 0); - ctx.quadraticCurveTo(canvas.width, 0, canvas.width, radius); - ctx.lineTo(canvas.width, canvas.height - radius); - ctx.quadraticCurveTo(canvas.width, canvas.height, canvas.width - radius, canvas.height); - ctx.lineTo(radius, canvas.height); - ctx.quadraticCurveTo(0, canvas.height, 0, canvas.height - radius); - ctx.lineTo(0, radius); - ctx.quadraticCurveTo(0, 0, radius, 0); - ctx.closePath(); - ctx.clip(); - - ctx.drawImage(video, 0, 0, canvas.width, canvas.height); - ctx.restore(); - } - } - - // Store drawFrame in a ref so handleLoadedMetadata can use it - drawFrameRef.current = drawFrame; - function drawFrameLoop() { - if (!video || !canvas || video.paused || video.ended) return; - drawFrame(); - animationId = requestAnimationFrame(drawFrameLoop); - } const handlePlay = () => { - drawFrameLoop(); updateTime(); }; + const handlePause = () => { - cancelAnimationFrame(animationId); if (timeUpdateAnimationRef.current) { cancelAnimationFrame(timeUpdateAnimationRef.current); timeUpdateAnimationRef.current = null; } onTimeUpdate(video.currentTime); }; + const handleSeeked = () => { - drawFrame(); onTimeUpdate(video.currentTime); }; + const handleSeeking = () => { onTimeUpdate(video.currentTime); }; + video.addEventListener('play', handlePlay); video.addEventListener('pause', handlePause); video.addEventListener('ended', handlePause); video.addEventListener('seeked', handleSeeked); video.addEventListener('seeking', handleSeeking); + return () => { video.removeEventListener('play', handlePlay); video.removeEventListener('pause', handlePause); video.removeEventListener('ended', handlePause); video.removeEventListener('seeked', handleSeeked); video.removeEventListener('seeking', handleSeeking); - cancelAnimationFrame(animationId); + if (timeUpdateAnimationRef.current) { cancelAnimationFrame(timeUpdateAnimationRef.current); } - }; - }, [videoPath, onTimeUpdate]); - - // Draw first frame when metadata is loaded - const handleLoadedMetadata = (e: React.SyntheticEvent) => { - onDurationChange(e.currentTarget.duration); - // Draw first frame - const video = videoRef.current; - if (video) { - video.currentTime = 0; - const drawFirstFrame = () => { - // Use the shared drawFrame function from the ref - drawFrameRef.current?.(); - video.removeEventListener('seeked', drawFirstFrame); - }; - video.addEventListener('seeked', drawFirstFrame); - if (video.currentTime === 0 && video.readyState >= 2) { - drawFirstFrame(); + + // Clean up PixiJS resources + if (videoSprite) { + videoContainer.removeChild(videoSprite); + videoSprite.destroy(); } - } + if (maskGraphics) { + videoContainer.removeChild(maskGraphics); + maskGraphics.destroy(); + } + videoTexture.destroy(true); + + videoSpriteRef.current = null; + }; + }, [pixiReady, videoReady, onTimeUpdate]); + + // Handle video metadata loaded + const handleLoadedMetadata = (e: React.SyntheticEvent) => { + const video = e.currentTarget; + onDurationChange(video.duration); + video.currentTime = 0; + video.pause(); + setVideoReady(true); }; const isImageUrl = wallpaper?.startsWith('/wallpapers/') || wallpaper?.startsWith('http'); @@ -149,15 +234,17 @@ const VideoPlayback = forwardRef(({ className="aspect-video rounded-sm p-12 flex items-center justify-center overflow-hidden bg-cover bg-center" style={{ ...backgroundStyle, width: '90%' }} > -