import React from 'react'; import { AbsoluteFill, Audio, OffthreadVideo, Img, staticFile, useVideoConfig, useCurrentFrame, interpolate, Easing, Loop, } from 'remotion'; interface Scene { sceneNumber: number; duration: number; visualDescription: string; voiceoverText: string; searchKeywords: string[]; visual?: { type: 'image' | 'video'; url: string; width: number; height: number; localPath?: string; videoDuration?: number; videoTrimAfterFrames?: number; } | null; audioPath?: string; } // Props interface for SingleSceneVideo component export interface SingleSceneProps { scene: Scene; isFirstScene: boolean; isLastScene: boolean; showText?: boolean; [key: string]: unknown; // Allow additional props for Remotion compatibility } // Transition duration in frames const FADE_DURATION = 12; // 0.4 seconds at 30fps const SAFE_VIDEO_END_BUFFER_FRAMES = 3; const resolveStaticMediaPath = (mediaPath: string): string => { const normalized = mediaPath.replace(/\\/g, '/'); if (/^[a-zA-Z]:\//.test(normalized) || normalized.startsWith('/')) { const basename = normalized.split('/').pop() || normalized; return `audio/${basename}`; } return normalized; }; const getUsableVideoTrimAfterFrames = ( scene: Scene, fps: number, durationInFrames: number ): number => { const explicitTrimFrames = scene.visual?.videoTrimAfterFrames; if (typeof explicitTrimFrames === 'number' && Number.isFinite(explicitTrimFrames) && explicitTrimFrames > 0) { return Math.max(1, Math.min(Math.floor(explicitTrimFrames), durationInFrames)); } const durationBasedFrames = scene.visual?.videoDuration ? Math.floor(scene.visual.videoDuration * fps) : durationInFrames; const safeFrames = Math.max(1, durationBasedFrames - SAFE_VIDEO_END_BUFFER_FRAMES); return Math.max(1, Math.min(safeFrames, durationInFrames)); }; /** * SingleSceneVideo - Renders a single scene for segmented rendering * Used when rendering videos scene-by-scene for memory efficiency */ export const SingleSceneVideo: React.FC = ({ scene, isFirstScene, isLastScene, showText = true, }) => { const { fps, durationInFrames } = useVideoConfig(); const frame = useCurrentFrame(); const hasLocalVideo = scene.visual?.localPath; const hasRemoteImage = scene.visual?.type === 'image' && !hasLocalVideo; const videoTrimAfterFrames = getUsableVideoTrimAfterFrames(scene, fps, durationInFrames); const shouldLoopVideo = videoTrimAfterFrames < durationInFrames; // Fade in only for first scene, fade out only for last scene // Middle scenes get no fade (seamless concatenation) const fadeIn = isFirstScene ? interpolate( frame, [0, FADE_DURATION], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.quad), } ) : 1; const fadeOut = isLastScene ? interpolate( frame, [durationInFrames - FADE_DURATION, durationInFrames], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.in(Easing.quad), } ) : 1; const opacity = Math.min(fadeIn, fadeOut); // Text animation const textOpacity = interpolate( frame, [FADE_DURATION * 0.5, FADE_DURATION * 1.2], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.quad), } ); const textFadeOut = interpolate( frame, [durationInFrames - FADE_DURATION * 1.2, durationInFrames - FADE_DURATION * 0.5], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.in(Easing.quad), } ); const combinedTextOpacity = Math.min(textOpacity, textFadeOut); const textSlide = interpolate(textOpacity, [0, 1], [15, 0]); return ( {/* Background Video/Image */} {scene.visual && hasLocalVideo ? (
{scene.visual.type === 'video' ? ( shouldLoopVideo ? ( ) : ( ) ) : ( )} {/* Dark overlay */}
) : hasRemoteImage ? (
{/* Dark overlay */}
) : ( // Fallback gradient background
)} {/* Text Overlay */} {showText && (

{scene.voiceoverText}

)} {/* Audio */} {scene.audioPath && scene.audioPath.endsWith('.mp3') && (