diff --git a/src/components/VideoEditor.tsx b/src/components/VideoEditor.tsx index db5c3793..dd16b41f 100644 --- a/src/components/VideoEditor.tsx +++ b/src/components/VideoEditor.tsx @@ -282,7 +282,35 @@ export default function VideoEditor() { />
} title="Rotate" delay={100}> - + + + {/* Reverse video toggle */} +
+ + +
diff --git a/src/hooks/useVideoEditor.ts b/src/hooks/useVideoEditor.ts index d894ed07..67758543 100644 --- a/src/hooks/useVideoEditor.ts +++ b/src/hooks/useVideoEditor.ts @@ -397,6 +397,13 @@ export function useVideoEditor() { setStatus("error"); return; } + const REVERSE_SIZE_LIMIT = 100 * 1024 * 1024; // 100MB + if (recipe.reverse && file.size > REVERSE_SIZE_LIMIT) { + const confirmed = window.confirm( + "⚠️ Warning: Reversing files larger than 100MB loads the entire video into memory and may be slow or crash your browser. Continue anyway?" + ); + if (!confirmed) return; + } const abortController = new AbortController(); exportAbortControllerRef.current = abortController; diff --git a/src/lib/constants.ts b/src/lib/constants.ts index fddfe9f2..7221d202 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -22,4 +22,5 @@ export const DEFAULT_RECIPE: EditRecipe = { soundOnCompletion: false, normalizeAudio: false, version: RECIPE_VERSION, + reverse: false, }; \ No newline at end of file diff --git a/src/lib/ffmpeg.ts b/src/lib/ffmpeg.ts index 4f215075..89971bf0 100644 --- a/src/lib/ffmpeg.ts +++ b/src/lib/ffmpeg.ts @@ -128,11 +128,17 @@ function buildVideoFilter(recipe: EditRecipe, targetW: number, targetH: number): filters.push( `eq=brightness=${recipe.brightness}:contrast=${recipe.contrast}:saturation=${recipe.saturation}` ); + // ADD THIS before the return: + if (recipe.reverse) { + filters.push("reverse"); + } + return filters.join(","); } - export function buildAudioFilter(speed: number, normalizeAudio: boolean): string { + export function buildAudioFilter(speed: number, normalizeAudio: boolean, reverse = false): string { const filters: string[] = []; + if (reverse) filters.push("areverse"); let remaining = speed; while (remaining < 0.5) { @@ -177,7 +183,7 @@ function buildArguments( ): string[] { const vf = buildVideoFilter(recipe, targetW, targetH); const audioTrim = hasOriginalAudio ? buildAudioTrimFilter(recipe) : ""; -const audioSpeed = hasOriginalAudio ? buildAudioFilter(recipe.speed, recipe.normalizeAudio ?? false) : ""; +const audioSpeed = hasOriginalAudio ? buildAudioFilter(recipe.speed, recipe.normalizeAudio ?? false, recipe.reverse) : ""; const afParts = [audioTrim, audioSpeed].filter(Boolean); const af = afParts.join(","); @@ -334,8 +340,7 @@ export async function exportVideo( const vf = buildVideoFilter(recipe, targetW, targetH); const audioTrim = buildAudioTrimFilter(recipe); - const audioSpeed = buildAudioFilter(recipe.speed, recipe.normalizeAudio ?? false); - + const audioSpeed = buildAudioFilter(recipe.speed, recipe.normalizeAudio ?? false, recipe.reverse); const afParts = [audioTrim, audioSpeed].filter(Boolean); const af = afParts.join(","); const hasMusicTrack = !!(musicOptions?.file && recipe.keepAudio); diff --git a/src/lib/types.ts b/src/lib/types.ts index 91040259..c912e4ee 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -18,6 +18,7 @@ export interface EditRecipe { contrast: number; saturation: number; soundOnCompletion: boolean; + reverse: boolean; version: number; } @@ -81,6 +82,7 @@ export const DEFAULT_RECIPE: EditRecipe = { quality: 23, format: "mp4", stabilization: false, + reverse: false, brightness: 0, contrast: 0, saturation: 0, @@ -116,6 +118,7 @@ export function isValidRecipe(value: unknown): value is EditRecipe { if (typeof v.contrast !== "number" || !isFinite(v.contrast)) return false; if (typeof v.saturation !== "number" || !isFinite(v.saturation)) return false; if (typeof v.soundOnCompletion !== "boolean") return false; + if (typeof v.reverse !== "boolean") return false; return true; } \ No newline at end of file