Update
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { useMitt } from '@/plugins/MittContext';
|
||||
import useVideoEditorStore from '@/stores/VideoEditorStore';
|
||||
import { FFmpeg } from '@ffmpeg/ffmpeg';
|
||||
import { fetchFile, toBlobURL } from '@ffmpeg/util';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
@@ -15,7 +17,7 @@ const VideoEditor = ({ width, height }) => {
|
||||
type: 'video',
|
||||
source_webm: 'https://cdn.memeaigen.com/g1/webm/they-not-like-us-oiia-cat-version.webm',
|
||||
source_mov: 'https://cdn.memeaigen.com/g1/mov/they-not-like-us-oiia-cat-version.mov',
|
||||
poster: 'https://cdn.memeaigen.com/g1/webm/they-not-like-us-oiia-cat-version.webp',
|
||||
poster: 'https://cdn.memeaigen.com/g1/webp/they-not-like-us-oiia-cat-version.webp',
|
||||
name: 'They not like us cat',
|
||||
startTime: 0,
|
||||
layer: 0,
|
||||
@@ -29,7 +31,9 @@ const VideoEditor = ({ width, height }) => {
|
||||
{
|
||||
id: '2',
|
||||
type: 'video',
|
||||
source: 'https://cdn.memeaigen.com/g1/webm/sad-cat.webm',
|
||||
source_webm: 'https://cdn.memeaigen.com/g1/webm/sad-cat.webm',
|
||||
source_mov: 'https://cdn.memeaigen.com/g1/mov/sad-cat.mov',
|
||||
poster: 'https://cdn.memeaigen.com/g1/webp/sad-cat.webp',
|
||||
name: 'Sad cat meme',
|
||||
startTime: 6,
|
||||
layer: 0,
|
||||
@@ -43,7 +47,9 @@ const VideoEditor = ({ width, height }) => {
|
||||
{
|
||||
id: '3',
|
||||
type: 'video',
|
||||
source: 'https://cdn.memeaigen.com/g1/webm/este-cat-dance.webm',
|
||||
source_webm: 'https://cdn.memeaigen.com/g1/webm/este-cat-dance.webm',
|
||||
source_mov: 'https://cdn.memeaigen.com/g1/mov/este-cat-dance.mov',
|
||||
poster: 'https://cdn.memeaigen.com/g1/webp/este-cat-dance.webp',
|
||||
name: 'Este cat dance',
|
||||
startTime: 2,
|
||||
layer: 1,
|
||||
@@ -89,6 +95,8 @@ const VideoEditor = ({ width, height }) => {
|
||||
// FFmpeg WASM states
|
||||
const ffmpegRef = useRef(new FFmpeg());
|
||||
|
||||
const emitter = useMitt();
|
||||
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
const [exportProgress, setExportProgress] = useState(0);
|
||||
const [exportStatus, setExportStatus] = useState('');
|
||||
@@ -107,6 +115,12 @@ const VideoEditor = ({ width, height }) => {
|
||||
const startTimeRef = useRef(0);
|
||||
const pausedTimeRef = useRef(0);
|
||||
|
||||
const { setVideoIsPlaying } = useVideoEditorStore();
|
||||
|
||||
useEffect(() => {
|
||||
setVideoIsPlaying(isPlaying);
|
||||
}, [isPlaying]);
|
||||
|
||||
// Calculate total timeline duration
|
||||
const totalDuration = Math.max(...timelineElements.map((el) => el.startTime + el.duration));
|
||||
|
||||
@@ -244,18 +258,18 @@ const VideoEditor = ({ width, height }) => {
|
||||
}, [ffmpegCommand]);
|
||||
|
||||
// Create video elements
|
||||
// Replace your existing useEffect with this:
|
||||
useEffect(() => {
|
||||
const videoEls = {};
|
||||
const videoElementsData = timelineElements.filter((el) => el.type === 'video');
|
||||
|
||||
videoElementsData.forEach((element) => {
|
||||
const video = document.createElement('video');
|
||||
video.poster = element.poster;
|
||||
video.crossOrigin = 'anonymous';
|
||||
video.muted = true; // Start muted, unmute on play
|
||||
video.preload = 'auto'; // Preload entire video content
|
||||
video.playsInline = true; // Better mobile performance
|
||||
video.controls = false; // Remove native controls
|
||||
video.muted = true;
|
||||
video.preload = 'metadata';
|
||||
video.playsInline = true;
|
||||
video.controls = false;
|
||||
|
||||
const sourceWebM = document.createElement('source');
|
||||
sourceWebM.src = element.source_webm;
|
||||
@@ -268,31 +282,34 @@ const VideoEditor = ({ width, height }) => {
|
||||
video.appendChild(sourceMov);
|
||||
video.appendChild(sourceWebM);
|
||||
|
||||
video.addEventListener('loadedmetadata', () => {
|
||||
// Calculate scaling to fit within canvas while maintaining aspect ratio
|
||||
// Load poster separately
|
||||
const posterImg = new Image();
|
||||
posterImg.crossOrigin = 'anonymous';
|
||||
posterImg.src = element.poster;
|
||||
|
||||
posterImg.onload = () => {
|
||||
// Calculate scaling for poster
|
||||
const maxWidth = dimensions.width;
|
||||
const maxHeight = dimensions.height;
|
||||
const videoWidth = video.videoWidth;
|
||||
const videoHeight = video.videoHeight;
|
||||
const posterWidth = posterImg.naturalWidth;
|
||||
const posterHeight = posterImg.naturalHeight;
|
||||
|
||||
let scaledWidth = videoWidth;
|
||||
let scaledHeight = videoHeight;
|
||||
let scaledWidth = posterWidth;
|
||||
let scaledHeight = posterHeight;
|
||||
|
||||
// Only scale down if video is larger than canvas
|
||||
if (videoWidth > maxWidth || videoHeight > maxHeight) {
|
||||
const scaleX = maxWidth / videoWidth;
|
||||
const scaleY = maxHeight / videoHeight;
|
||||
const scale = Math.min(scaleX, scaleY); // Use smaller scale to fit both dimensions
|
||||
if (posterWidth > maxWidth || posterHeight > maxHeight) {
|
||||
const scaleX = maxWidth / posterWidth;
|
||||
const scaleY = maxHeight / posterHeight;
|
||||
const scale = Math.min(scaleX, scaleY);
|
||||
|
||||
scaledWidth = videoWidth * scale;
|
||||
scaledHeight = videoHeight * scale;
|
||||
scaledWidth = posterWidth * scale;
|
||||
scaledHeight = posterHeight * scale;
|
||||
}
|
||||
|
||||
// Center the video in the canvas
|
||||
const centeredX = (maxWidth - scaledWidth) / 2;
|
||||
const centeredY = (maxHeight - scaledHeight) / 2;
|
||||
|
||||
// Update timeline element with scaled and centered video
|
||||
// Update timeline element with poster
|
||||
setTimelineElements((prev) =>
|
||||
prev.map((el) => {
|
||||
if (el.id === element.id && el.type === 'video') {
|
||||
@@ -302,6 +319,8 @@ const VideoEditor = ({ width, height }) => {
|
||||
y: centeredY,
|
||||
width: scaledWidth,
|
||||
height: scaledHeight,
|
||||
posterImage: posterImg, // Store poster reference
|
||||
isVideoPoster: true, // Flag to indicate poster is loaded
|
||||
};
|
||||
}
|
||||
return el;
|
||||
@@ -313,12 +332,32 @@ const VideoEditor = ({ width, height }) => {
|
||||
newSet.add(element.id);
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
video.addEventListener('loadedmetadata', () => {
|
||||
// Video metadata loaded - store video reference
|
||||
setTimelineElements((prev) =>
|
||||
prev.map((el) => {
|
||||
if (el.id === element.id && el.type === 'video') {
|
||||
return {
|
||||
...el,
|
||||
videoElement: video, // Store video reference
|
||||
isVideoReady: true, // Flag to indicate video is ready
|
||||
};
|
||||
}
|
||||
return el;
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
video.addEventListener('error', (e) => {
|
||||
console.error(`Error loading video ${element.id}:`, e);
|
||||
});
|
||||
|
||||
posterImg.onerror = (e) => {
|
||||
console.error(`Error loading poster ${element.id}:`, e);
|
||||
};
|
||||
|
||||
videoEls[element.id] = video;
|
||||
});
|
||||
|
||||
@@ -330,7 +369,7 @@ const VideoEditor = ({ width, height }) => {
|
||||
video.load();
|
||||
});
|
||||
};
|
||||
}, []); // Only run once on mount
|
||||
}, [dimensions.width, dimensions.height]); // Add dimensions as dependency
|
||||
|
||||
// Update status when videos load
|
||||
useEffect(() => {
|
||||
@@ -538,7 +577,7 @@ const VideoEditor = ({ width, height }) => {
|
||||
}, [currentTime, isPlaying, videoElements, getDesiredVideoStates]);
|
||||
|
||||
const animate = useCallback(() => {
|
||||
if (!isPlaying) return;
|
||||
if (!animationRef.current || !isPlaying) return;
|
||||
|
||||
const now = Date.now() / 1000;
|
||||
const newTime = pausedTimeRef.current + (now - startTimeRef.current);
|
||||
@@ -617,6 +656,28 @@ const VideoEditor = ({ width, height }) => {
|
||||
|
||||
const activeElements = getActiveElements(currentTime);
|
||||
|
||||
useEffect(() => {
|
||||
emitter.on('video-play', () => {
|
||||
console.log('video-play');
|
||||
handlePlay();
|
||||
});
|
||||
|
||||
emitter.on('video-pause', () => {
|
||||
console.log('video-pause');
|
||||
handlePause();
|
||||
});
|
||||
|
||||
emitter.on('video-seek', (time) => {
|
||||
handleSeek(time);
|
||||
});
|
||||
|
||||
return () => {
|
||||
emitter.off('video-play');
|
||||
emitter.off('video-pause');
|
||||
emitter.off('video-seek');
|
||||
};
|
||||
}, [emitter]);
|
||||
|
||||
return (
|
||||
<div style={{ width: dimensions.width, height: dimensions.height }} className="rounded-3xl">
|
||||
<VideoPreview
|
||||
|
||||
@@ -38,16 +38,40 @@ const VideoPreview = ({
|
||||
// Refs
|
||||
layerRef,
|
||||
}) => {
|
||||
// Function to determine which image source to use for videos
|
||||
const getImageSource = (element) => {
|
||||
// Check if this video should be playing right now
|
||||
const isVideoActive = videoStates[element.id] && isPlaying;
|
||||
|
||||
// Use video if it's ready and currently active, otherwise use poster
|
||||
if (isVideoActive && element.videoElement && element.isVideoReady) {
|
||||
return element.videoElement;
|
||||
} else if (element.posterImage && element.isVideoPoster) {
|
||||
return element.posterImage;
|
||||
}
|
||||
|
||||
// Fallback - no image ready yet
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>isPlaying: {isPlaying ? 'true' : 'false'}</div>
|
||||
|
||||
<Stage width={dimensions.width} height={dimensions.height} className="">
|
||||
<Layer ref={layerRef}>
|
||||
{activeElements.map((element) => {
|
||||
if (element.type === 'video' && videoElements[element.id]) {
|
||||
if (element.type === 'video') {
|
||||
const imageSource = getImageSource(element);
|
||||
|
||||
if (!imageSource) {
|
||||
return null; // Don't render if no source is ready
|
||||
}
|
||||
|
||||
return (
|
||||
<Image
|
||||
key={element.id}
|
||||
image={videoElements[element.id]}
|
||||
image={imageSource}
|
||||
x={element.x}
|
||||
y={element.y}
|
||||
width={element.width}
|
||||
|
||||
Reference in New Issue
Block a user