change to pixi container

This commit is contained in:
Siddharth
2025-11-08 14:22:47 -07:00
parent a597ea619d
commit 31364066e7
+158 -71
View File
@@ -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<VideoPlaybackRef, VideoPlaybackProps>(({
@@ -22,21 +26,135 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
wallpaper,
}, ref) => {
const videoRef = useRef<HTMLVideoElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const drawFrameRef = useRef<(() => void) | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null);
const appRef = useRef<PIXI.Application | null>(null);
const videoSpriteRef = useRef<PIXI.Sprite | null>(null);
const videoContainerRef = useRef<PIXI.Container | null>(null);
const timeUpdateAnimationRef = useRef<number | null>(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<VideoPlaybackRef, VideoPlaybackProps>(({
}
}
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<HTMLVideoElement, Event>) => {
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<HTMLVideoElement, Event>) => {
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<VideoPlaybackRef, VideoPlaybackProps>(({
className="aspect-video rounded-sm p-12 flex items-center justify-center overflow-hidden bg-cover bg-center"
style={{ ...backgroundStyle, width: '90%' }}
>
<canvas
ref={canvasRef}
className="w-full h-full object-contain rounded-lg"
<div
ref={containerRef}
className="w-full h-full"
style={{ position: 'relative' }}
/>
<video
ref={videoRef}
src={videoPath}
className="hidden rounded"
className="hidden"
preload="metadata"
playsInline
onLoadedMetadata={handleLoadedMetadata}
onDurationChange={e => {
onDurationChange(e.currentTarget.duration);