import { useEffect, useRef } from 'react'; /** * FadingVideo Component * Custom requestAnimationFrame crossfade logic for looping background videos. * Opacity fades out at 0.55s before video ends and fades back in on loop start. */ export default function FadingVideo({ src, className, style }) { const videoRef = useRef(null); const rafIdRef = useRef(null); const fadingOutRef = useRef(false); const FADE_MS = 500; const FADE_OUT_LEAD = 0.55; // seconds before end to start fading out const fadeTo = (targetOpacity, duration) => { if (!videoRef.current) return; const video = videoRef.current; // Cancel any ongoing animation frame if (rafIdRef.current) { cancelAnimationFrame(rafIdRef.current); } const currentOpacity = parseFloat(video.style.opacity) || 0; const opacityDifference = targetOpacity - currentOpacity; if (opacityDifference === 0) return; const startTime = performance.now(); const animate = (currentTime) => { const elapsed = currentTime - startTime; const progress = Math.min(elapsed / duration, 1); // Calculate next opacity const nextOpacity = currentOpacity + opacityDifference * progress; if (videoRef.current) { videoRef.current.style.opacity = nextOpacity.toString(); } if (progress < 1) { rafIdRef.current = requestAnimationFrame(animate); } }; rafIdRef.current = requestAnimationFrame(animate); }; useEffect(() => { const video = videoRef.current; if (!video) return; // Initially transparent video.style.opacity = '0'; const handleLoadedData = () => { video.style.opacity = '0'; video.play().catch(() => {}); fadeTo(1, FADE_MS); }; const handleTimeUpdate = () => { if (!video.duration) return; const remainingTime = video.duration - video.currentTime; // Start fade out at 0.55 seconds before end if (!fadingOutRef.current && remainingTime <= FADE_OUT_LEAD && remainingTime > 0) { fadingOutRef.current = true; fadeTo(0, FADE_MS); } }; const handleEnded = () => { video.style.opacity = '0'; setTimeout(() => { if (!videoRef.current) return; videoRef.current.currentTime = 0; videoRef.current.play().catch(() => {}); fadingOutRef.current = false; fadeTo(1, FADE_MS); }, 100); }; // Listeners video.addEventListener('loadeddata', handleLoadedData); video.addEventListener('timeupdate', handleTimeUpdate); video.addEventListener('ended', handleEnded); // If already loaded/ready if (video.readyState >= 2) { handleLoadedData(); } return () => { if (rafIdRef.current) { cancelAnimationFrame(rafIdRef.current); } video.removeEventListener('loadeddata', handleLoadedData); video.removeEventListener('timeupdate', handleTimeUpdate); video.removeEventListener('ended', handleEnded); }; }, [src]); return (