diff --git a/package-lock.json b/package-lock.json
index 556ce81..9028ef4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -52,6 +52,7 @@
"konva": "^9.3.20",
"laravel-vite-plugin": "^1.0",
"lucide-react": "^0.475.0",
+ "mitt": "^3.0.1",
"next-themes": "^0.4.6",
"react": "^19.0.0",
"react-day-picker": "^9.7.0",
@@ -6642,6 +6643,12 @@
"node": ">= 18"
}
},
+ "node_modules/mitt": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
+ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
+ "license": "MIT"
+ },
"node_modules/mkdirp": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
diff --git a/package.json b/package.json
index bb7c341..e12f6fe 100644
--- a/package.json
+++ b/package.json
@@ -70,6 +70,7 @@
"konva": "^9.3.20",
"laravel-vite-plugin": "^1.0",
"lucide-react": "^0.475.0",
+ "mitt": "^3.0.1",
"next-themes": "^0.4.6",
"react": "^19.0.0",
"react-day-picker": "^9.7.0",
diff --git a/resources/js/app.tsx b/resources/js/app.tsx
index 3ee0a88..f3e5e5c 100644
--- a/resources/js/app.tsx
+++ b/resources/js/app.tsx
@@ -5,6 +5,7 @@ import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import { createRoot } from 'react-dom/client';
import { initializeTheme } from './hooks/use-appearance';
import { AxiosProvider } from './plugins/AxiosContext';
+import { MittProvider } from './plugins/MittContext';
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
@@ -16,9 +17,11 @@ createInertiaApp({
const app = (
<>
-
-
-
+
+
+
+
+
>
);
diff --git a/resources/js/modules/editor/partials/canvas/video-editor.jsx b/resources/js/modules/editor/partials/canvas/video-editor.jsx
index 7a85011..c0811ec 100644
--- a/resources/js/modules/editor/partials/canvas/video-editor.jsx
+++ b/resources/js/modules/editor/partials/canvas/video-editor.jsx
@@ -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 (
{
+ // 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 (
+
isPlaying: {isPlaying ? 'true' : 'false'}
+
{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 (
{}, isEditActive = false }) => {
- return (
-
-
-
-
+ const { videoIsPlaying } = useVideoEditorStore();
+ const emitter = useMitt();
- {/*
{
+ emitter.emit('video-play');
+ };
+
+ const handlePause = () => {
+ emitter.emit('video-pause');
+ };
+
+ const togglePlayPause = () => {
+ if (videoIsPlaying) {
+ handlePause();
+ } else {
+ handlePlay();
+ }
+ };
+
+ return (
+
+
+ {videoIsPlaying ? : }
+
+
+ {/*
{}, isEditActive =
9:16
*/}
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
- )
-}
+
+
+
+
+
+
+
+ );
+};
export default EditorControls;
diff --git a/resources/js/plugins/MittContext.jsx b/resources/js/plugins/MittContext.jsx
new file mode 100644
index 0000000..fa71b62
--- /dev/null
+++ b/resources/js/plugins/MittContext.jsx
@@ -0,0 +1,13 @@
+// resources/js/Plugins/MittContext.jsx
+import mitt from 'mitt';
+import { createContext, useContext } from 'react';
+
+export const emitter = mitt();
+
+const MittContext = createContext(emitter);
+
+export const MittProvider = ({ children }) => {
+ return {children} ;
+};
+
+export const useMitt = () => useContext(MittContext);
diff --git a/resources/js/stores/VideoEditorStore.ts b/resources/js/stores/VideoEditorStore.ts
new file mode 100644
index 0000000..e9e2aab
--- /dev/null
+++ b/resources/js/stores/VideoEditorStore.ts
@@ -0,0 +1,27 @@
+import axiosInstance from '@/plugins/axios-plugin';
+import { mountStoreDevtool } from 'simple-zustand-devtools';
+import { toast } from 'sonner';
+import { route } from 'ziggy-js';
+import { create } from 'zustand';
+import { devtools } from 'zustand/middleware';
+
+const useVideoEditorStore = create(
+ devtools((set, get) => ({
+ videoIsPlaying: false,
+
+ setVideoIsPlaying: (isPlaying) => {
+ set({ videoIsPlaying: isPlaying });
+ },
+ })),
+
+ {
+ name: 'VideoEditorStore',
+ store: 'VideoEditorStore',
+ },
+);
+
+if (import.meta.env.APP_ENV === 'local') {
+ mountStoreDevtool('VideoEditorStore', useVideoEditorStore);
+}
+
+export default useVideoEditorStore;