From e93babd4f7cdcf8a15126b17ba058267ff7e70aa Mon Sep 17 00:00:00 2001 From: wiiiii123 Date: Fri, 10 Jul 2026 18:52:54 +0700 Subject: [PATCH 1/3] fix(export): preserve cropped native aspect ratio --- src/components/video-editor/VideoEditor.tsx | 18 +++++++++--------- .../video-editor/exportDimensions.test.ts | 12 ++++++++++++ .../video-editor/exportDimensions.ts | 10 ++++++++-- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 0b23fb69..b13559c6 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -1485,15 +1485,15 @@ export default function VideoEditor() { [gifSizePreset], ); - const desiredMp4SourceDimensions = useMemo( - () => - calculateMp4SourceDimensions( - videoPlaybackRef.current?.video?.videoWidth || 1920, - videoPlaybackRef.current?.video?.videoHeight || 1080, - aspectRatio, - ), - [aspectRatio], - ); + const desiredMp4SourceDimensions = useMemo(() => { + const sourceVideo = isPreviewReady ? videoPlaybackRef.current?.video : null; + return calculateMp4SourceDimensions( + sourceVideo?.videoWidth || 1920, + sourceVideo?.videoHeight || 1080, + aspectRatio, + cropRegion, + ); + }, [aspectRatio, cropRegion, isPreviewReady]); const mp4OutputDimensions = useMemo(() => { const baseWidth = supportedMp4SourceDimensions.encoderPath diff --git a/src/components/video-editor/exportDimensions.test.ts b/src/components/video-editor/exportDimensions.test.ts index 7c79f4ed..d939cf6a 100644 --- a/src/components/video-editor/exportDimensions.test.ts +++ b/src/components/video-editor/exportDimensions.test.ts @@ -9,6 +9,18 @@ describe("calculateMp4SourceDimensions", () => { }); }); + it("uses the cropped source bounds for native exports", () => { + expect( + calculateMp4SourceDimensions(320, 180, "native", { + width: 1, + height: 0.8, + }), + ).toEqual({ + width: 320, + height: 144, + }); + }); + it("uses the rotated source bounds for 9:16 original exports", () => { expect(calculateMp4SourceDimensions(1920, 1080, "9:16")).toEqual({ width: 1080, diff --git a/src/components/video-editor/exportDimensions.ts b/src/components/video-editor/exportDimensions.ts index 8c36d895..45e0f54e 100644 --- a/src/components/video-editor/exportDimensions.ts +++ b/src/components/video-editor/exportDimensions.ts @@ -30,9 +30,15 @@ export function calculateMp4SourceDimensions( sourceWidth: number, sourceHeight: number, aspectRatio: AspectRatio, + cropRegion?: { width: number; height: number }, ): { width: number; height: number } { - const safeSourceWidth = normalizeEvenDimension(sourceWidth); - const safeSourceHeight = normalizeEvenDimension(sourceHeight); + const useCroppedBounds = aspectRatio === "native"; + const safeSourceWidth = normalizeEvenDimension( + sourceWidth * (useCroppedBounds ? (cropRegion?.width ?? 1) : 1), + ); + const safeSourceHeight = normalizeEvenDimension( + sourceHeight * (useCroppedBounds ? (cropRegion?.height ?? 1) : 1), + ); const sourceAspectRatio = safeSourceHeight > 0 ? safeSourceWidth / safeSourceHeight : 16 / 9; const aspectRatioValue = getAspectRatioValue(aspectRatio, sourceAspectRatio); From 855998ef63906e67d47ef1482d3b90c27145acd2 Mon Sep 17 00:00:00 2001 From: wiiiii123 Date: Sat, 11 Jul 2026 12:14:59 +0700 Subject: [PATCH 2/3] perf(export): debounce crop support probes --- src/components/video-editor/VideoEditor.tsx | 101 +++++++++++------- .../video-editor/exportDimensions.test.ts | 51 ++++++++- .../video-editor/exportDimensions.ts | 32 +++++- 3 files changed, 145 insertions(+), 39 deletions(-) diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index b13559c6..c72d5df0 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -87,7 +87,12 @@ import { } from "@/utils/aspectRatioUtils"; import { planClipSpeedChange } from "./clipSpeedChange"; import { ExtensionIcon } from "./ExtensionIcon"; -import { calculateMp4ExportDimensions, calculateMp4SourceDimensions } from "./exportDimensions"; +import { + calculateMp4ExportDimensions, + calculateMp4SourceDimensions, + type Mp4SupportProbeSnapshot, + shouldDebounceMp4SupportProbe, +} from "./exportDimensions"; import { resolveSavingExportProgress } from "./exportProgressState"; import { resolveExportStartSettings } from "./exportStartSettings"; import { resolveExportStatusModel } from "./exportStatusModel"; @@ -256,6 +261,7 @@ type CancelableExporter = { }; const EXPORT_BLOB_STREAM_CHUNK_BYTES = 16 * 1024 * 1024; +const MP4_CROP_PROBE_DEBOUNCE_MS = 200; async function streamExportBlobToTempFile(blob: Blob, extension: string): Promise { if ( @@ -701,6 +707,7 @@ export default function VideoEditor() { const pendingFreshRecordingAutoSuggestTelemetryCountRef = useRef(0); const cropSnapshotRef = useRef(null); const mp4SupportRequestRef = useRef(0); + const previousMp4SupportProbeRef = useRef(null); const smokeExportStartedRef = useRef(false); const projectAutosaveTimeoutRef = useRef(null); const pendingProjectSaveDialogRef = useRef(null); @@ -1485,15 +1492,22 @@ export default function VideoEditor() { [gifSizePreset], ); - const desiredMp4SourceDimensions = useMemo(() => { + const mp4SourceDimensions = useMemo(() => { const sourceVideo = isPreviewReady ? videoPlaybackRef.current?.video : null; + return { + width: sourceVideo?.videoWidth || 1920, + height: sourceVideo?.videoHeight || 1080, + }; + }, [isPreviewReady]); + + const desiredMp4SourceDimensions = useMemo(() => { return calculateMp4SourceDimensions( - sourceVideo?.videoWidth || 1920, - sourceVideo?.videoHeight || 1080, + mp4SourceDimensions.width, + mp4SourceDimensions.height, aspectRatio, cropRegion, ); - }, [aspectRatio, cropRegion, isPreviewReady]); + }, [aspectRatio, cropRegion, mp4SourceDimensions.height, mp4SourceDimensions.width]); const mp4OutputDimensions = useMemo(() => { const baseWidth = supportedMp4SourceDimensions.encoderPath @@ -1533,21 +1547,6 @@ export default function VideoEditor() { ); } - setSupportedMp4SourceDimensions((current) => { - if ( - current.width === result.width && - current.height === result.height && - current.capped === result.capped && - current.encoderPath?.codec === result.encoderPath?.codec && - current.encoderPath?.hardwareAcceleration === - result.encoderPath?.hardwareAcceleration - ) { - return current; - } - - return result; - }); - return result; }, [desiredMp4SourceDimensions.height, desiredMp4SourceDimensions.width], @@ -1557,6 +1556,19 @@ export default function VideoEditor() { let cancelled = false; const requestId = mp4SupportRequestRef.current + 1; mp4SupportRequestRef.current = requestId; + const probeSnapshot: Mp4SupportProbeSnapshot = { + sourceWidth: mp4SourceDimensions.width, + sourceHeight: mp4SourceDimensions.height, + targetWidth: desiredMp4SourceDimensions.width, + targetHeight: desiredMp4SourceDimensions.height, + aspectRatio, + frameRate: mp4FrameRate, + }; + const shouldDebounce = shouldDebounceMp4SupportProbe( + previousMp4SupportProbeRef.current, + probeSnapshot, + ); + previousMp4SupportProbeRef.current = probeSnapshot; setSupportedMp4SourceDimensions({ width: desiredMp4SourceDimensions.width, height: desiredMp4SourceDimensions.height, @@ -1564,33 +1576,48 @@ export default function VideoEditor() { encoderPath: null, }); - void ensureSupportedMp4SourceDimensions(mp4FrameRate) - .then((result) => { - if (cancelled || requestId !== mp4SupportRequestRef.current) { - return; - } - setSupportedMp4SourceDimensions(result); - }) - .catch(() => { - if (cancelled || requestId !== mp4SupportRequestRef.current) { - return; - } - setSupportedMp4SourceDimensions({ - width: desiredMp4SourceDimensions.width, - height: desiredMp4SourceDimensions.height, - capped: false, - encoderPath: null, + const runProbe = () => { + void ensureSupportedMp4SourceDimensions(mp4FrameRate) + .then((result) => { + if (cancelled || requestId !== mp4SupportRequestRef.current) { + return; + } + setSupportedMp4SourceDimensions(result); + }) + .catch(() => { + if (cancelled || requestId !== mp4SupportRequestRef.current) { + return; + } + setSupportedMp4SourceDimensions({ + width: desiredMp4SourceDimensions.width, + height: desiredMp4SourceDimensions.height, + capped: false, + encoderPath: null, + }); }); - }); + }; + + const timeoutId = shouldDebounce + ? window.setTimeout(runProbe, MP4_CROP_PROBE_DEBOUNCE_MS) + : null; + if (timeoutId === null) { + runProbe(); + } return () => { cancelled = true; + if (timeoutId !== null) { + window.clearTimeout(timeoutId); + } }; }, [ + aspectRatio, desiredMp4SourceDimensions.height, desiredMp4SourceDimensions.width, ensureSupportedMp4SourceDimensions, mp4FrameRate, + mp4SourceDimensions.height, + mp4SourceDimensions.width, ]); // Extension-contributed standalone section pages (no parentSection) diff --git a/src/components/video-editor/exportDimensions.test.ts b/src/components/video-editor/exportDimensions.test.ts index d939cf6a..2eae9c5b 100644 --- a/src/components/video-editor/exportDimensions.test.ts +++ b/src/components/video-editor/exportDimensions.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from "vitest"; -import { calculateMp4ExportDimensions, calculateMp4SourceDimensions } from "./exportDimensions"; +import { + calculateMp4ExportDimensions, + calculateMp4SourceDimensions, + shouldDebounceMp4SupportProbe, +} from "./exportDimensions"; describe("calculateMp4SourceDimensions", () => { it("keeps native exports at the source dimensions", () => { @@ -82,3 +86,48 @@ describe("calculateMp4ExportDimensions", () => { }); }); }); + +describe("shouldDebounceMp4SupportProbe", () => { + const baseSnapshot = { + sourceWidth: 1920, + sourceHeight: 1080, + targetWidth: 1920, + targetHeight: 1080, + aspectRatio: "native" as const, + frameRate: 30 as const, + }; + + it("debounces only native crop-driven target changes", () => { + expect( + shouldDebounceMp4SupportProbe(baseSnapshot, { + ...baseSnapshot, + targetHeight: 864, + }), + ).toBe(true); + }); + + it("keeps non-crop probe changes immediate", () => { + expect(shouldDebounceMp4SupportProbe(null, baseSnapshot)).toBe(false); + expect( + shouldDebounceMp4SupportProbe(baseSnapshot, { + ...baseSnapshot, + frameRate: 60, + }), + ).toBe(false); + expect( + shouldDebounceMp4SupportProbe(baseSnapshot, { + ...baseSnapshot, + sourceWidth: 1280, + sourceHeight: 720, + targetWidth: 1280, + targetHeight: 720, + }), + ).toBe(false); + expect( + shouldDebounceMp4SupportProbe(baseSnapshot, { + ...baseSnapshot, + aspectRatio: "16:9", + }), + ).toBe(false); + }); +}); diff --git a/src/components/video-editor/exportDimensions.ts b/src/components/video-editor/exportDimensions.ts index 45e0f54e..2f9dcdc2 100644 --- a/src/components/video-editor/exportDimensions.ts +++ b/src/components/video-editor/exportDimensions.ts @@ -1,6 +1,36 @@ -import type { ExportQuality } from "@/lib/exporter"; +import type { ExportMp4FrameRate, ExportQuality } from "@/lib/exporter"; import { type AspectRatio, getAspectRatioValue } from "@/utils/aspectRatioUtils"; +export type Mp4SupportProbeSnapshot = { + sourceWidth: number; + sourceHeight: number; + targetWidth: number; + targetHeight: number; + aspectRatio: AspectRatio; + frameRate: ExportMp4FrameRate; +}; + +export function shouldDebounceMp4SupportProbe( + previous: Mp4SupportProbeSnapshot | null, + current: Mp4SupportProbeSnapshot, +): boolean { + if ( + !previous || + current.aspectRatio !== "native" || + previous.aspectRatio !== current.aspectRatio || + previous.frameRate !== current.frameRate || + previous.sourceWidth !== current.sourceWidth || + previous.sourceHeight !== current.sourceHeight + ) { + return false; + } + + return ( + previous.targetWidth !== current.targetWidth || + previous.targetHeight !== current.targetHeight + ); +} + function normalizeEvenDimension(value: number): number { return Math.max(2, Math.floor(value / 2) * 2); } From 3d17f874e88f14f04ef73fd0e10922285924043e Mon Sep 17 00:00:00 2001 From: wiiiii123 Date: Sat, 11 Jul 2026 12:25:24 +0700 Subject: [PATCH 3/3] test(export): preserve fixed aspect crop boundary --- src/components/video-editor/exportDimensions.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/components/video-editor/exportDimensions.test.ts b/src/components/video-editor/exportDimensions.test.ts index 2eae9c5b..5797daae 100644 --- a/src/components/video-editor/exportDimensions.test.ts +++ b/src/components/video-editor/exportDimensions.test.ts @@ -32,6 +32,18 @@ describe("calculateMp4SourceDimensions", () => { }); }); + it("ignores crop bounds for fixed-aspect exports", () => { + expect( + calculateMp4SourceDimensions(1920, 1080, "9:16", { + width: 0.5, + height: 0.5, + }), + ).toEqual({ + width: 1080, + height: 1920, + }); + }); + it("uses the rotated source bounds for portrait social ratios", () => { expect(calculateMp4SourceDimensions(1920, 1080, "4:5")).toEqual({ width: 1080,