This commit is contained in:
ct
2025-06-16 16:05:27 +08:00
parent d1fdeb6409
commit 9a81d90bfb
3 changed files with 508 additions and 36 deletions

View File

@@ -1,5 +1,3 @@
// 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
import { useMitt } from '@/plugins/MittContext';
import useVideoEditorStore from '@/stores/VideoEditorStore';
import { useCallback, useEffect, useRef, useState } from 'react';
@@ -16,8 +14,6 @@ const VideoEditor = ({ width, height }) => {
});
const [timelineElements, setTimelineElements] = useState([]);
// 🔧 FIX: Add ref to solve closure issue
const timelineElementsRef = useRef([]);
const lastUpdateRef = useRef(0);
@@ -39,12 +35,12 @@ const VideoEditor = ({ width, height }) => {
const FPS_INTERVAL = 1000 / 30; // 30 FPS
// 🔧 FIX: Keep ref synced with state
// Keep ref synced with state
useEffect(() => {
timelineElementsRef.current = timelineElements;
}, [timelineElements]);
// ✅ FIX 1: Use useEffect to automatically setup videos when timeline loads
// Initialize timeline
useEffect(() => {
initTimeline();
}, []);
@@ -58,7 +54,6 @@ const VideoEditor = ({ width, height }) => {
});
}, []);
// Add this useEffect to resolve the promise when timeline updates
useEffect(() => {
if (timelineUpdateResolverRef.current && timelineElements.length > 0) {
timelineUpdateResolverRef.current();
@@ -70,13 +65,35 @@ const VideoEditor = ({ width, height }) => {
cleanupVideos(videoElements);
setTimelineElementsAsync(sampleTimelineElements).then(() => {
showConsoleLogs && console.log('Loaded sample timeline');
setupVideos();
setupImages(); // Add image setup
setupImages();
});
};
// NEW: Setup function for image elements
// NEW: Handle element transformations (position, scale, rotation)
const handleElementUpdate = useCallback(
(elementId, updates) => {
setTimelineElements((prev) =>
prev.map((element) => {
if (element.id === elementId) {
return {
...element,
...updates,
};
}
return element;
}),
);
// Force redraw if not playing
if (!isPlaying && layerRef.current) {
layerRef.current.batchDraw();
}
},
[isPlaying],
);
// Setup function for image elements
const setupImages = () => {
showConsoleLogs && console.log('setupImages');
@@ -108,7 +125,6 @@ const VideoEditor = ({ width, height }) => {
let scaledWidth = imgWidth;
let scaledHeight = imgHeight;
// Scale down if image is larger than canvas
if (imgWidth > maxWidth || imgHeight > maxHeight) {
const scaleX = maxWidth / imgWidth;
const scaleY = maxHeight / imgHeight;
@@ -118,7 +134,6 @@ const VideoEditor = ({ width, height }) => {
scaledHeight = imgHeight * scale;
}
// Use provided position or center the image
const centeredX = element.x || (maxWidth - scaledWidth) / 2;
const centeredY = element.y || (maxHeight - scaledHeight) / 2;
@@ -131,6 +146,7 @@ const VideoEditor = ({ width, height }) => {
y: centeredY,
width: element.width || scaledWidth,
height: element.height || scaledHeight,
rotation: element.rotation || 0,
imageElement: img,
isImageReady: true,
};
@@ -153,7 +169,6 @@ const VideoEditor = ({ width, height }) => {
});
};
// ✅ FIX 3: Auto-update status when videos load
useEffect(() => {
setupVideoStatus();
}, [timelineElements, loadedVideos]);
@@ -164,7 +179,6 @@ const VideoEditor = ({ width, height }) => {
const totalDuration = Math.max(...timelineElements.map((el) => el.startTime + el.duration));
// Use the FFmpeg hook
const { isExporting, exportProgress, exportStatus, ffmpegCommand, copyFFmpegCommand, exportVideo } = useVideoExport({
timelineElements,
dimensions,
@@ -174,7 +188,6 @@ const VideoEditor = ({ width, height }) => {
const setupVideos = () => {
showConsoleLogs && console.log('setupVideos');
// 🔧 FIX: Read from ref instead of state to get latest data
const elements = timelineElementsRef.current;
if (elements.length === 0) {
@@ -246,6 +259,7 @@ const VideoEditor = ({ width, height }) => {
y: centeredY,
width: scaledWidth,
height: scaledHeight,
rotation: element.rotation || 0,
posterImage: posterImg,
isVideoPoster: true,
};
@@ -311,7 +325,6 @@ const VideoEditor = ({ width, height }) => {
};
const setupVideoStatus = () => {
// Update to count both videos and images
const mediaCount = timelineElements.filter((el) => el.type === 'video' || el.type === 'image').length;
if (loadedVideos.size === mediaCount && mediaCount > 0) {
setStatus('Ready to play');
@@ -322,7 +335,6 @@ const VideoEditor = ({ width, height }) => {
}
};
// FIXED: Removed currentTime dependency to prevent excessive recreation
const handlePause = useCallback(() => {
if (isPlaying) {
setIsPlaying(false);
@@ -412,7 +424,6 @@ const VideoEditor = ({ width, height }) => {
setVideoStates(desiredStates);
}, [currentTime, isPlaying, videoElements, getDesiredVideoStates]);
// FIXED: Properly stop animation when not playing
useEffect(() => {
if (!isPlaying) {
if (animationRef.current) {
@@ -466,7 +477,6 @@ const VideoEditor = ({ width, height }) => {
};
}, [isPlaying, totalDuration, handlePause, updateVideoTimes]);
// FIXED: Stabilized handlers
const handlePlay = useCallback(() => {
if (!isPlaying) {
setIsPlaying(true);
@@ -504,7 +514,6 @@ const VideoEditor = ({ width, height }) => {
const activeElements = getActiveElements(currentTime);
// FIXED: Added missing dependencies to event listeners
useEffect(() => {
emitter.on('video-play', handlePlay);
emitter.on('video-reset', handleReset);
@@ -540,6 +549,7 @@ const VideoEditor = ({ width, height }) => {
handleSeek={handleSeek}
copyFFmpegCommand={copyFFmpegCommand}
exportVideo={exportVideo}
onElementUpdate={handleElementUpdate} // NEW: Pass the update handler
layerRef={layerRef}
/>
</div>