Update
This commit is contained in:
@@ -0,0 +1,574 @@
|
||||
// TODO: I moved the sample timeline data to a dedicated file, and delayed the loading to 1 sec with useEffect. as such, alot of the ogics are broken. I need to make sure the delayed timeline should work like normal
|
||||
|
||||
/**
|
||||
* ========================================
|
||||
* VIDEO EDITOR SYSTEM ARCHITECTURE OVERVIEW
|
||||
* ========================================
|
||||
*
|
||||
* This VideoEditor component is the core orchestrator of a multi-video timeline editing system.
|
||||
* It manages synchronized playback of multiple video elements with precise timing control.
|
||||
*
|
||||
* SYSTEM FLOW:
|
||||
*
|
||||
* 1. INITIALIZATION PHASE:
|
||||
* - Loads timeline data (delayed by 1s to simulate async loading)
|
||||
* - Creates HTML video elements for each timeline video with multiple source formats
|
||||
* - Loads poster images and calculates optimal scaling/positioning for canvas
|
||||
* - Sets up cross-component event communication via MittContext
|
||||
*
|
||||
* 2. VIDEO SETUP PHASE:
|
||||
* - For each video: creates <video> element with WebM + MOV sources for browser compatibility
|
||||
* - Loads poster images to determine video dimensions and calculate centered positioning
|
||||
* - Attaches event listeners for metadata loading and error handling
|
||||
* - Updates timeline elements with positioning data and video element references
|
||||
*
|
||||
* 3. PLAYBACK CONTROL SYSTEM:
|
||||
* - Animation loop runs at ~60fps using requestAnimationFrame
|
||||
* - Updates throttled to ~20fps (0.05s intervals) for performance optimization
|
||||
* - Calculates elapsed time from playback start + paused time offset
|
||||
* - Synchronizes all video elements to match timeline position
|
||||
* - Handles video trimming via inPoint offsets for precise start times
|
||||
*
|
||||
* 4. VIDEO SYNCHRONIZATION:
|
||||
* - Determines which videos should be active at current timeline position
|
||||
* - Plays/pauses individual videos based on their timeline segments
|
||||
* - Manages audio muting to prevent overlapping audio tracks
|
||||
* - Updates video currentTime with threshold-based seeking to avoid excessive operations
|
||||
*
|
||||
* 5. STATE MANAGEMENT:
|
||||
* - Local state for timeline position, playback status, video elements
|
||||
* - Global state sync via VideoEditorStore for cross-component coordination
|
||||
* - Event-driven communication for play/pause/seek operations from UI controls
|
||||
* - Loading progress tracking for user feedback during video initialization
|
||||
*
|
||||
* 6. PERFORMANCE OPTIMIZATIONS:
|
||||
* - useCallback for stable function references to prevent unnecessary re-renders
|
||||
* - Ref-based timing to avoid state dependency loops in animation
|
||||
* - Throttled updates to balance smooth playback with performance
|
||||
* - Cleanup functions to prevent memory leaks from video elements and event listeners
|
||||
*
|
||||
* 7. EXPORT INTEGRATION:
|
||||
* - Integrates with useVideoExport hook for FFmpeg-based video rendering
|
||||
* - Passes timeline data, dimensions, and duration for export processing
|
||||
* - Provides export progress and status feedback to user interface
|
||||
*
|
||||
* KEY ARCHITECTURAL DECISIONS:
|
||||
* - Separation of concerns: VideoEditor (logic) + VideoPreview (presentation)
|
||||
* - Event-driven communication for loose coupling between components
|
||||
* - HTML video elements for browser-native video handling and performance
|
||||
* - Multiple source formats (WebM/MOV) for maximum browser compatibility
|
||||
* - Poster-based sizing to ensure consistent layout before video metadata loads
|
||||
* - Ref-based animation timing to avoid React state update performance issues
|
||||
*
|
||||
* TIMELINE ELEMENT STRUCTURE:
|
||||
* Each timeline element contains: id, type, startTime, duration, inPoint, source files,
|
||||
* positioning data (x, y, width, height), and runtime references (videoElement, posterImage)
|
||||
*/
|
||||
|
||||
// Import dependencies for event communication, state management, React hooks, and child components
|
||||
import { useMitt } from '@/plugins/MittContext'; // Event emitter for cross-component communication
|
||||
import useVideoEditorStore from '@/stores/VideoEditorStore'; // Global state store for video editor
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'; // React hooks for state and lifecycle management
|
||||
import sampleTimelineElements from './sample-timeline-data'; // Sample timeline data for testing/demo
|
||||
import useVideoExport from './video-export'; // Custom hook for video export functionality
|
||||
import VideoPreview from './video-preview'; // Child component that renders the actual video preview
|
||||
|
||||
/**
|
||||
* VideoEditor - Main component that orchestrates video editing functionality
|
||||
* Manages timeline elements, video playback, synchronization, and export
|
||||
* @param {number} width - Canvas width for video rendering
|
||||
* @param {number} height - Canvas height for video rendering
|
||||
*/
|
||||
const VideoEditor = ({ width, height }) => {
|
||||
// Canvas dimensions - immutable once set, used for video scaling and positioning
|
||||
const [dimensions] = useState({
|
||||
width: width,
|
||||
height: height,
|
||||
});
|
||||
|
||||
// Timeline elements array - contains all video/audio/text elements with timing and positioning data
|
||||
// Each element has: id, type, startTime, duration, source files, positioning, etc.
|
||||
const [timelineElements, setTimelineElements] = useState([]);
|
||||
|
||||
// Performance optimization - tracks last animation frame update time to throttle rendering
|
||||
const lastUpdateRef = useRef(0);
|
||||
// Event emitter instance for cross-component communication (play, pause, seek events)
|
||||
const emitter = useMitt();
|
||||
|
||||
// Current playback time in seconds - drives the entire timeline synchronization
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
// Playback state - controls animation loop and video element play/pause
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
// Dictionary of HTML video elements keyed by timeline element ID
|
||||
const [videoElements, setVideoElements] = useState({});
|
||||
// Set tracking which video elements have finished loading (for UI feedback)
|
||||
const [loadedVideos, setLoadedVideos] = useState(new Set());
|
||||
// Status message for user feedback during loading/playback
|
||||
const [status, setStatus] = useState('Loading videos...');
|
||||
// Tracks which video elements should be playing at current time (optimization)
|
||||
const [videoStates, setVideoStates] = useState({});
|
||||
|
||||
// Reference to the animation loop controller for cleanup
|
||||
const animationRef = useRef(null);
|
||||
// Reference to Konva layer for manual redraw triggering
|
||||
const layerRef = useRef(null);
|
||||
// Timestamp when playback started (for calculating elapsed time)
|
||||
const startTimeRef = useRef(0);
|
||||
// Time when playback was paused (for resuming from correct position)
|
||||
const pausedTimeRef = useRef(0);
|
||||
|
||||
// Global state setter to sync playing state across the application
|
||||
const { setVideoIsPlaying } = useVideoEditorStore();
|
||||
|
||||
// INITIALIZATION EFFECT: Load sample timeline data with 1 second delay
|
||||
// This simulates async data loading and tests the component's ability to handle delayed timeline setup
|
||||
useEffect(() => {
|
||||
setTimeout(() => setTimelineElements(sampleTimelineElements), 1000);
|
||||
}, []);
|
||||
|
||||
// GLOBAL STATE SYNC EFFECT: Keep global video playing state in sync with local state
|
||||
// This allows other components to know when video is playing for UI updates
|
||||
useEffect(() => {
|
||||
setVideoIsPlaying(isPlaying);
|
||||
}, [isPlaying, setVideoIsPlaying]);
|
||||
|
||||
// Calculate total timeline duration by finding the element that ends latest
|
||||
// Used for progress bars, seek limits, and auto-stop functionality
|
||||
const totalDuration = Math.max(...timelineElements.map((el) => el.startTime + el.duration));
|
||||
|
||||
// VIDEO EXPORT INTEGRATION: Custom hook that handles FFmpeg-based video export
|
||||
// Provides export state, progress tracking, and export functions
|
||||
const { isExporting, exportProgress, exportStatus, ffmpegCommand, copyFFmpegCommand, exportVideo } = useVideoExport({
|
||||
timelineElements, // Timeline data for export processing
|
||||
dimensions, // Canvas dimensions for output video
|
||||
totalDuration, // Total duration for export progress calculation
|
||||
});
|
||||
|
||||
// VIDEO ELEMENTS SETUP EFFECT: Creates and configures HTML video elements for each timeline video
|
||||
// This is a complex effect that handles video loading, poster image scaling, and element positioning
|
||||
useEffect(() => {
|
||||
const videoEls = {}; // Local dictionary to store video elements before setting state
|
||||
// Filter timeline to get only video elements (excludes text, audio, etc.)
|
||||
const videoElementsData = timelineElements.filter((el) => el.type === 'video');
|
||||
|
||||
videoElementsData.forEach((element) => {
|
||||
// Create HTML video element with optimal settings for timeline playback
|
||||
const video = document.createElement('video');
|
||||
video.crossOrigin = 'anonymous'; // Allow cross-origin video loading
|
||||
video.muted = true; // Start muted to avoid autoplay restrictions
|
||||
video.preload = 'metadata'; // Load metadata only to save bandwidth
|
||||
video.playsInline = true; // Prevent fullscreen on mobile
|
||||
video.controls = false; // Hide native video controls
|
||||
|
||||
// Add multiple video sources for browser compatibility
|
||||
// WebM source with VP9 codec (modern browsers, better compression)
|
||||
const sourceWebM = document.createElement('source');
|
||||
sourceWebM.src = element.source_webm;
|
||||
sourceWebM.type = 'video/webm; codecs=vp09.00.41.08';
|
||||
|
||||
// MOV source with HEVC codec (Safari, high quality)
|
||||
const sourceMov = document.createElement('source');
|
||||
sourceMov.src = element.source_mov;
|
||||
sourceMov.type = 'video/quicktime; codecs=hvc1.1.6.H120.b0';
|
||||
|
||||
// Add sources to video element (browser will choose best supported format)
|
||||
video.appendChild(sourceMov);
|
||||
video.appendChild(sourceWebM);
|
||||
|
||||
// Load poster image for thumbnail display and size calculation
|
||||
const posterImg = new Image();
|
||||
posterImg.crossOrigin = 'anonymous'; // Allow cross-origin image loading
|
||||
posterImg.src = element.poster;
|
||||
|
||||
// POSTER LOAD HANDLER: Calculate video positioning and scaling when poster loads
|
||||
posterImg.onload = () => {
|
||||
// Get canvas dimensions for scaling calculations
|
||||
const maxWidth = dimensions.width;
|
||||
const maxHeight = dimensions.height;
|
||||
const posterWidth = posterImg.naturalWidth;
|
||||
const posterHeight = posterImg.naturalHeight;
|
||||
|
||||
// Initialize with original dimensions
|
||||
let scaledWidth = posterWidth;
|
||||
let scaledHeight = posterHeight;
|
||||
|
||||
// Scale down if video is larger than canvas (maintain aspect ratio)
|
||||
if (posterWidth > maxWidth || posterHeight > maxHeight) {
|
||||
const scaleX = maxWidth / posterWidth; // Horizontal scale factor
|
||||
const scaleY = maxHeight / posterHeight; // Vertical scale factor
|
||||
const scale = Math.min(scaleX, scaleY); // Use smaller scale to fit both dimensions
|
||||
|
||||
scaledWidth = posterWidth * scale;
|
||||
scaledHeight = posterHeight * scale;
|
||||
}
|
||||
|
||||
// Center the video within the canvas
|
||||
const centeredX = (maxWidth - scaledWidth) / 2;
|
||||
const centeredY = (maxHeight - scaledHeight) / 2;
|
||||
|
||||
// Update timeline element with calculated positioning and poster data
|
||||
setTimelineElements((prev) =>
|
||||
prev.map((el) => {
|
||||
if (el.id === element.id && el.type === 'video') {
|
||||
return {
|
||||
...el,
|
||||
x: centeredX, // Horizontal position
|
||||
y: centeredY, // Vertical position
|
||||
width: scaledWidth, // Scaled width
|
||||
height: scaledHeight, // Scaled height
|
||||
posterImage: posterImg, // Poster image reference
|
||||
isVideoPoster: true, // Flag indicating poster is loaded
|
||||
};
|
||||
}
|
||||
return el;
|
||||
}),
|
||||
);
|
||||
|
||||
// Mark this video as loaded for progress tracking
|
||||
setLoadedVideos((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.add(element.id);
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
// VIDEO METADATA LOAD HANDLER: Attach video element when metadata is ready
|
||||
video.addEventListener('loadedmetadata', () => {
|
||||
setTimelineElements((prev) =>
|
||||
prev.map((el) => {
|
||||
if (el.id === element.id && el.type === 'video') {
|
||||
return {
|
||||
...el,
|
||||
videoElement: video, // Attach HTML video element
|
||||
isVideoReady: true, // Flag indicating video is ready for playback
|
||||
};
|
||||
}
|
||||
return el;
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
// ERROR HANDLERS: Log errors for debugging
|
||||
video.addEventListener('error', (e) => {
|
||||
console.error(`Error loading video ${element.id}:`, e);
|
||||
});
|
||||
|
||||
posterImg.onerror = (e) => {
|
||||
console.error(`Error loading poster ${element.id}:`, e);
|
||||
};
|
||||
|
||||
// Store video element in local dictionary
|
||||
videoEls[element.id] = video;
|
||||
});
|
||||
|
||||
// Update state with all created video elements
|
||||
setVideoElements(videoEls);
|
||||
|
||||
// CLEANUP FUNCTION: Properly dispose of video elements to prevent memory leaks
|
||||
return () => {
|
||||
Object.values(videoEls).forEach((video) => {
|
||||
video.src = ''; // Clear source to stop loading
|
||||
video.load(); // Reset video element
|
||||
});
|
||||
};
|
||||
}, []); // Empty dependency array - only run once on mount
|
||||
|
||||
// LOADING STATUS EFFECT: Update status message based on video loading progress
|
||||
// Provides user feedback during the video loading process
|
||||
useEffect(() => {
|
||||
const videoCount = timelineElements.filter((el) => el.type === 'video').length;
|
||||
if (loadedVideos.size === videoCount && videoCount > 0) {
|
||||
setStatus('Ready to play'); // All videos loaded
|
||||
} else if (videoCount > 0) {
|
||||
setStatus(`Loading videos... (${loadedVideos.size}/${videoCount})`); // Show progress
|
||||
} else {
|
||||
setStatus('Ready to play'); // No videos to load
|
||||
}
|
||||
}, [loadedVideos, timelineElements]);
|
||||
|
||||
// PAUSE HANDLER: Stops playback and pauses all video elements
|
||||
// FIXED: Removed currentTime dependency to prevent excessive recreation
|
||||
const handlePause = useCallback(() => {
|
||||
if (isPlaying) {
|
||||
setIsPlaying(false); // Update playback state
|
||||
pausedTimeRef.current = currentTime; // Store current time for resume
|
||||
|
||||
// Pause all video elements and mute them
|
||||
Object.values(videoElements).forEach((video) => {
|
||||
if (!video.paused) {
|
||||
video.pause(); // Pause video playback
|
||||
}
|
||||
video.muted = true; // Mute to prevent audio overlap
|
||||
});
|
||||
|
||||
setVideoStates({}); // Clear video states
|
||||
|
||||
// Stop animation loop
|
||||
if (animationRef.current) {
|
||||
animationRef.current.stop();
|
||||
animationRef.current = null;
|
||||
}
|
||||
}
|
||||
}, [isPlaying, videoElements]);
|
||||
|
||||
// ACTIVE ELEMENTS CALCULATOR: Determines which timeline elements should be visible at given time
|
||||
// Used for rendering optimization - only process elements that are currently active
|
||||
const getActiveElements = useCallback(
|
||||
(time) => {
|
||||
return timelineElements.filter((element) => {
|
||||
const elementEndTime = element.startTime + element.duration;
|
||||
// Element is active if current time is within its start and end time
|
||||
return time >= element.startTime && time < elementEndTime;
|
||||
});
|
||||
},
|
||||
[timelineElements],
|
||||
);
|
||||
|
||||
// VIDEO STATES CALCULATOR: Determines which videos should be playing at given time
|
||||
// Returns object mapping video IDs to boolean play states for synchronization
|
||||
const getDesiredVideoStates = useCallback(
|
||||
(time) => {
|
||||
const states = {};
|
||||
timelineElements.forEach((element) => {
|
||||
if (element.type === 'video') {
|
||||
const elementEndTime = element.startTime + element.duration;
|
||||
// Video should play if current time is within its timeline segment
|
||||
states[element.id] = time >= element.startTime && time < elementEndTime;
|
||||
}
|
||||
});
|
||||
return states;
|
||||
},
|
||||
[timelineElements],
|
||||
);
|
||||
|
||||
// VIDEO TIME SYNCHRONIZER: Updates individual video currentTime to match timeline position
|
||||
// Handles video trimming (inPoint) and prevents excessive seeking with threshold
|
||||
const updateVideoTimes = useCallback(
|
||||
(time) => {
|
||||
timelineElements.forEach((element) => {
|
||||
if (element.type === 'video' && videoElements[element.id]) {
|
||||
const video = videoElements[element.id];
|
||||
const elementEndTime = element.startTime + element.duration;
|
||||
|
||||
// Only update videos that are currently active on timeline
|
||||
if (time >= element.startTime && time < elementEndTime) {
|
||||
// Calculate relative time within the element's duration
|
||||
const relativeTime = time - element.startTime;
|
||||
// Add inPoint offset for trimmed videos (start from specific time in source)
|
||||
const videoTime = element.inPoint + relativeTime;
|
||||
|
||||
// Only seek if difference is significant (>0.5s) to avoid excessive seeking
|
||||
if (Math.abs(video.currentTime - videoTime) > 0.5) {
|
||||
video.currentTime = videoTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
[timelineElements, videoElements],
|
||||
);
|
||||
|
||||
// VIDEO PLAYBACK SYNCHRONIZATION EFFECT: Manages play/pause state of individual videos
|
||||
// Ensures only videos that should be active at current time are playing
|
||||
useEffect(() => {
|
||||
if (!isPlaying) return; // Skip if timeline is paused
|
||||
|
||||
// Get which videos should be playing at current time
|
||||
const desiredStates = getDesiredVideoStates(currentTime);
|
||||
|
||||
// Synchronize each video's play state with desired state
|
||||
Object.entries(desiredStates).forEach(([videoId, shouldPlay]) => {
|
||||
const video = videoElements[videoId];
|
||||
const isCurrentlyPlaying = !video?.paused;
|
||||
|
||||
if (video) {
|
||||
if (shouldPlay && !isCurrentlyPlaying) {
|
||||
// Start playing: unmute and play
|
||||
video.muted = false;
|
||||
video.play().catch((e) => console.warn('Video play failed:', e));
|
||||
} else if (!shouldPlay && isCurrentlyPlaying) {
|
||||
// Stop playing: pause and mute
|
||||
video.pause();
|
||||
video.muted = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update video states for UI feedback
|
||||
setVideoStates(desiredStates);
|
||||
}, [currentTime, isPlaying, videoElements, getDesiredVideoStates]);
|
||||
|
||||
// ANIMATION LOOP EFFECT: Main playback engine that drives timeline progression
|
||||
// FIXED: Properly stop animation when not playing
|
||||
useEffect(() => {
|
||||
// Stop any existing animation when not playing
|
||||
if (!isPlaying) {
|
||||
if (animationRef.current) {
|
||||
animationRef.current.stop();
|
||||
animationRef.current = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let animationId; // RequestAnimationFrame ID for cleanup
|
||||
let isRunning = true; // Flag to control animation loop
|
||||
|
||||
// ANIMATION FRAME FUNCTION: Called ~60fps to update timeline
|
||||
const animateFrame = () => {
|
||||
if (!isRunning) return; // Exit if animation was stopped
|
||||
|
||||
// Calculate elapsed time since playback started
|
||||
const now = Date.now() / 1000;
|
||||
const newTime = pausedTimeRef.current + (now - startTimeRef.current);
|
||||
|
||||
// Auto-stop and reset when reaching end of timeline
|
||||
if (newTime >= totalDuration) {
|
||||
handlePause();
|
||||
handleSeek(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Throttle updates to ~20fps (0.05s intervals) for performance
|
||||
if (newTime - lastUpdateRef.current >= 0.05) {
|
||||
lastUpdateRef.current = newTime;
|
||||
setCurrentTime(newTime); // Update timeline position
|
||||
updateVideoTimes(newTime); // Sync video elements
|
||||
|
||||
// Trigger Konva layer redraw for visual updates
|
||||
if (layerRef.current) {
|
||||
layerRef.current.batchDraw();
|
||||
}
|
||||
}
|
||||
|
||||
// Schedule next frame if still running
|
||||
if (isRunning) {
|
||||
animationId = requestAnimationFrame(animateFrame);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize animation timing and start loop
|
||||
startTimeRef.current = Date.now() / 1000;
|
||||
animationId = requestAnimationFrame(animateFrame);
|
||||
|
||||
// Create animation controller for external stop capability
|
||||
animationRef.current = {
|
||||
stop: () => {
|
||||
isRunning = false;
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// CLEANUP: Stop animation when effect unmounts or dependencies change
|
||||
return () => {
|
||||
isRunning = false;
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId);
|
||||
}
|
||||
};
|
||||
}, [isPlaying, totalDuration, handlePause, updateVideoTimes]);
|
||||
|
||||
// PLAY HANDLER: Starts timeline playback from current position
|
||||
// FIXED: Stabilized handlers to prevent unnecessary re-renders
|
||||
const handlePlay = useCallback(() => {
|
||||
if (!isPlaying) {
|
||||
setIsPlaying(true); // Trigger animation loop
|
||||
startTimeRef.current = Date.now() / 1000; // Record start time for elapsed calculation
|
||||
lastUpdateRef.current = 0; // Reset update throttling
|
||||
setStatus(''); // Clear status message during playback
|
||||
}
|
||||
}, [isPlaying]);
|
||||
|
||||
// SEEK HANDLER: Jumps to specific time position on timeline
|
||||
const handleSeek = useCallback(
|
||||
(time) => {
|
||||
// Clamp time to valid range [0, totalDuration]
|
||||
const clampedTime = Math.max(0, Math.min(time, totalDuration));
|
||||
setCurrentTime(clampedTime); // Update timeline position
|
||||
pausedTimeRef.current = clampedTime; // Update pause reference for resume
|
||||
updateVideoTimes(clampedTime); // Sync all video elements to new time
|
||||
|
||||
setVideoStates({}); // Clear video states to force recalculation
|
||||
|
||||
// Trigger immediate visual update for seek feedback
|
||||
if (layerRef.current) {
|
||||
layerRef.current.draw();
|
||||
}
|
||||
},
|
||||
[totalDuration, updateVideoTimes],
|
||||
);
|
||||
|
||||
// RESET HANDLER: Stops playback and returns to beginning of timeline
|
||||
const handleReset = useCallback(() => {
|
||||
handlePause(); // Stop playback
|
||||
handleSeek(0); // Jump to start of timeline
|
||||
lastUpdateRef.current = 0; // Reset animation throttling
|
||||
|
||||
// Ensure all videos are muted after reset
|
||||
Object.values(videoElements).forEach((video) => {
|
||||
video.muted = true;
|
||||
});
|
||||
}, [handlePause, handleSeek, videoElements]);
|
||||
|
||||
// Get elements that should be visible/active at current timeline position
|
||||
// Used by VideoPreview component for rendering decisions
|
||||
const activeElements = getActiveElements(currentTime);
|
||||
|
||||
// EVENT LISTENERS EFFECT: Subscribe to cross-component video control events
|
||||
// FIXED: Added missing dependencies to event listeners
|
||||
useEffect(() => {
|
||||
// Subscribe to events from other components (timeline controls, keyboard shortcuts, etc.)
|
||||
emitter.on('video-play', handlePlay); // Play button clicked
|
||||
emitter.on('video-reset', handleReset); // Reset button clicked
|
||||
emitter.on('video-seek', handleSeek); // Timeline scrubber moved
|
||||
|
||||
// CLEANUP: Unsubscribe from events to prevent memory leaks
|
||||
return () => {
|
||||
emitter.off('video-play', handlePlay);
|
||||
emitter.off('video-reset', handleReset);
|
||||
emitter.off('video-seek', handleSeek);
|
||||
};
|
||||
}, [emitter, handlePlay, handleReset, handleSeek]);
|
||||
|
||||
// COMPONENT RENDER: Return the video editor UI wrapped in a container
|
||||
return (
|
||||
<div style={{ width: dimensions.width, height: dimensions.height }} className="rounded-3xl">
|
||||
{/*
|
||||
VideoPreview Component - Handles the actual video rendering and UI
|
||||
Receives all state, handlers, and data needed for video preview functionality
|
||||
This separation allows VideoEditor to focus on logic while VideoPreview handles presentation
|
||||
*/}
|
||||
<VideoPreview
|
||||
// Canvas and timing props
|
||||
dimensions={dimensions} // Canvas width/height for rendering
|
||||
currentTime={currentTime} // Current playback position
|
||||
totalDuration={totalDuration} // Total timeline duration
|
||||
isPlaying={isPlaying} // Playback state
|
||||
status={status} // Loading/status message
|
||||
// Export-related props
|
||||
isExporting={isExporting} // Export in progress flag
|
||||
exportProgress={exportProgress} // Export progress percentage
|
||||
exportStatus={exportStatus} // Export status message
|
||||
ffmpegCommand={ffmpegCommand} // Generated FFmpeg command
|
||||
copyFFmpegCommand={copyFFmpegCommand} // Function to copy command
|
||||
exportVideo={exportVideo} // Function to start export
|
||||
// Timeline and video data
|
||||
timelineElements={timelineElements} // All timeline elements
|
||||
activeElements={activeElements} // Currently visible elements
|
||||
videoElements={videoElements} // HTML video element references
|
||||
loadedVideos={loadedVideos} // Set of loaded video IDs
|
||||
videoStates={videoStates} // Current video play states
|
||||
// Control handlers
|
||||
handlePlay={handlePlay} // Start playback
|
||||
handlePause={handlePause} // Pause playback
|
||||
handleReset={handleReset} // Reset to beginning
|
||||
handleSeek={handleSeek} // Seek to specific time
|
||||
// Rendering reference
|
||||
layerRef={layerRef} // Konva layer reference for manual redraws
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VideoEditor;
|
||||
Reference in New Issue
Block a user