From d4e03363b320179df2003e8e70333159567eec02 Mon Sep 17 00:00:00 2001 From: jayesh durge <179298187+jayesh-durge@users.noreply.github.com> Date: Wed, 3 Jun 2026 14:49:58 +0530 Subject: [PATCH 1/8] implemented : rotation preview and aspect ratio preview --- src/components/VideoPreview.tsx | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/components/VideoPreview.tsx b/src/components/VideoPreview.tsx index 7456ac89..b96c45c8 100644 --- a/src/components/VideoPreview.tsx +++ b/src/components/VideoPreview.tsx @@ -144,43 +144,35 @@ export default function VideoPreview({ return () => window.removeEventListener("resize", updateDimensions); }, []); - const overlay = (() => { - if (!recipe || !showOverlay) return null; + const preset = recipe?.preset === "custom" + ? { width: recipe.customWidth, height: recipe.customHeight } + : recipe ? getPresetById(recipe.preset) : null; - const preset = recipe.preset === "custom" - ? { width: recipe.customWidth, height: recipe.customHeight } - : getPresetById(recipe.preset); + const targetRatio = preset ? preset.width / preset.height : 16 / 9; - if (!preset) return null; + const overlay = (() => { + if (!recipe || !showOverlay || !preset) return null; - // Preview container is 16:9 - const containerW = 16; - const containerH = 9; - const containerRatio = containerW / containerH; // 1.777… + // Use the dynamic target ratio instead of hardcoded 16:9 + const containerRatio = targetRatio; const outputRatio = preset.width / preset.height; if (recipe.framing === "fit") { - // Letterbox: the output video fits entirely inside 16:9, padded with bars. if (outputRatio > containerRatio) { - // Wider output → pillarbox bars on top & bottom const contentH = (containerRatio / outputRatio) * 100; const barH = (100 - contentH) / 2; return { mode: "fit", barTop: `${barH}%`, barBottom: `${barH}%`, barLeft: "0", barRight: "0" }; } else { - // Taller output → letterbox bars on left & right const contentW = (outputRatio / containerRatio) * 100; const barW = (100 - contentW) / 2; return { mode: "fit", barTop: "0", barBottom: "0", barLeft: `${barW}%`, barRight: `${barW}%` }; } } else { - // Fill / crop: the output fills the entire 16:9 preview — show a box representing what survives the crop. if (outputRatio < containerRatio) { - // Output is taller → crops top & bottom const visibleH = (outputRatio / containerRatio) * 100; const cropH = (100 - visibleH) / 2; return { mode: "fill", barTop: `${cropH}%`, barBottom: `${cropH}%`, barLeft: "0", barRight: "0" }; } else { - // Output is wider → crops left & right const visibleW = (containerRatio / outputRatio) * 100; const cropW = (100 - visibleW) / 2; return { mode: "fill", barTop: "0", barBottom: "0", barLeft: `${cropW}%`, barRight: `${cropW}%` }; @@ -218,7 +210,8 @@ export default function VideoPreview({
setIsLoading(false)} playsInline muted={!recipe?.keepAudio} From d0a565fc6a06ad263e6a8fa9e7c569bccc4d8952 Mon Sep 17 00:00:00 2001 From: jayesh durge <179298187+jayesh-durge@users.noreply.github.com> Date: Wed, 3 Jun 2026 14:59:31 +0530 Subject: [PATCH 2/8] implemented : trim preview --- src/components/VideoPreview.tsx | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/components/VideoPreview.tsx b/src/components/VideoPreview.tsx index b96c45c8..ff25ed12 100644 --- a/src/components/VideoPreview.tsx +++ b/src/components/VideoPreview.tsx @@ -144,6 +144,29 @@ export default function VideoPreview({ return () => window.removeEventListener("resize", updateDimensions); }, []); + useEffect(() => { + const video = videoRef.current; + if (!video || !recipe) return; + + const handleTimeUpdate = () => { + // If duration is not yet available, fallback to a safe large number to prevent immediate stops + const start = recipe.trimStart || 0; + const end = recipe.trimEnd !== null ? recipe.trimEnd : (video.duration || Infinity); + + if (video.currentTime < start) { + video.currentTime = start; + } else if (video.currentTime >= end && end > 0) { + video.pause(); + video.currentTime = start; + } + }; + + video.addEventListener("timeupdate", handleTimeUpdate); + return () => { + video.removeEventListener("timeupdate", handleTimeUpdate); + }; + }, [recipe, videoRef]); + const preset = recipe?.preset === "custom" ? { width: recipe.customWidth, height: recipe.customHeight } : recipe ? getPresetById(recipe.preset) : null; From fe4b788d846f382fc68808b765de5fae7ca061c9 Mon Sep 17 00:00:00 2001 From: jayesh durge <179298187+jayesh-durge@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:28:58 +0530 Subject: [PATCH 3/8] rotation implimentational bug fixed --- src/components/VideoPreview.tsx | 150 +++++++++++++++++++++++++++----- src/hooks/useVideoEditor.ts | 55 +++++++++--- src/lib/ffmpeg.worker.ts | 3 + 3 files changed, 171 insertions(+), 37 deletions(-) diff --git a/src/components/VideoPreview.tsx b/src/components/VideoPreview.tsx index ff25ed12..cd156ce3 100644 --- a/src/components/VideoPreview.tsx +++ b/src/components/VideoPreview.tsx @@ -5,7 +5,7 @@ import { useEffect, useRef, useState, useCallback, RefObject } from "react"; import { EditRecipe, TextOverlay } from "@/lib/types"; import { getPresetById } from "@/lib/presets"; import { cn } from "@/lib/utils"; -import { Camera } from "lucide-react"; +import { Camera, Play, Pause, Volume2, VolumeX } from "lucide-react"; import ComparisonPreview from "./ComparisonPreview"; import DraggableTextOverlays from "./DraggableTextOverlays"; @@ -32,6 +32,30 @@ export default function VideoPreview({ const [showOverlay, setShowOverlay] = useState(false); const [showComparison, setShowComparison] = useState(false); const [showGridOverlay, setShowGridOverlay] = useState(false); + const [isPlaying, setIsPlaying] = useState(false); + const [currentTime, setCurrentTime] = useState(0); + const [isMuted, setIsMuted] = useState(!recipe?.keepAudio); + + useEffect(() => { + setIsMuted(!recipe?.keepAudio); + }, [recipe?.keepAudio]); + + useEffect(() => { + const video = videoRef.current; + if (!video) return; + + const handlePlay = () => setIsPlaying(true); + const handlePause = () => setIsPlaying(false); + + video.addEventListener("play", handlePlay); + video.addEventListener("pause", handlePause); + + return () => { + video.removeEventListener("play", handlePlay); + video.removeEventListener("pause", handlePause); + }; + }, [videoRef]); + const [containerDimensions, setContainerDimensions] = useState({ width: 0, height: 0, @@ -159,6 +183,8 @@ export default function VideoPreview({ video.pause(); video.currentTime = start; } + + setCurrentTime(video.currentTime); }; video.addEventListener("timeupdate", handleTimeUpdate); @@ -171,14 +197,20 @@ export default function VideoPreview({ ? { width: recipe.customWidth, height: recipe.customHeight } : recipe ? getPresetById(recipe.preset) : null; - const targetRatio = preset ? preset.width / preset.height : 16 / 9; + const isRotated = recipe?.rotate === 90 || recipe?.rotate === 270; + + const targetW = preset ? preset.width : 16; + const targetH = preset ? preset.height : 9; + + const targetRatio = targetW / targetH; const overlay = (() => { if (!recipe || !showOverlay || !preset) return null; - // Use the dynamic target ratio instead of hardcoded 16:9 - const containerRatio = targetRatio; - const outputRatio = preset.width / preset.height; + // Both containerRatio and outputRatio must use the same rotation-aware + // dimensions so the bar math stays consistent regardless of rotation. + const containerRatio = targetRatio; // targetW / targetH, already swapped for 90°/270° + const outputRatio = targetW / targetH; // identical to containerRatio — bars are zero when output fits exactly if (recipe.framing === "fit") { if (outputRatio > containerRatio) { @@ -233,7 +265,7 @@ export default function VideoPreview({
{isLoading && (
)} - {/* eslint-disable-next-line jsx-a11y/media-has-caption */} -