perf(export): debounce crop support probes

This commit is contained in:
wiiiii123
2026-07-11 12:14:59 +07:00
parent 0d7596f680
commit 855998ef63
3 changed files with 145 additions and 39 deletions
+64 -37
View File
@@ -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<string | null> {
if (
@@ -701,6 +707,7 @@ export default function VideoEditor() {
const pendingFreshRecordingAutoSuggestTelemetryCountRef = useRef(0);
const cropSnapshotRef = useRef<CropRegion | null>(null);
const mp4SupportRequestRef = useRef(0);
const previousMp4SupportProbeRef = useRef<Mp4SupportProbeSnapshot | null>(null);
const smokeExportStartedRef = useRef(false);
const projectAutosaveTimeoutRef = useRef<number | null>(null);
const pendingProjectSaveDialogRef = useRef<PendingProjectSaveDialog | null>(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)
@@ -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);
});
});
@@ -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);
}