change to pixi container
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user