refactor(editor): extract smoke export progress sampler

This commit is contained in:
wiiiii123
2026-05-22 15:31:35 +07:00
parent 7b3eaaa6c2
commit f4a68ca126
3 changed files with 198 additions and 35 deletions
+7 -35
View File
@@ -147,6 +147,7 @@ import {
} from "./projectPersistence";
import { SettingsPanel } from "./SettingsPanel";
import { getDevOpenRecordingConfig, getSmokeExportConfig } from "./smokeExportConfig";
import { createSmokeExportProgressSampler } from "./smokeExportProgress";
import {
APP_HEADER_ICON_BUTTON_CLASS,
DiscordLinkButton,
@@ -3984,41 +3985,12 @@ export default function VideoEditor() {
smokeExportConfig.enabled && smokeExportConfig.shadowIntensity !== undefined
? smokeExportConfig.shadowIntensity
: shadowIntensity;
const smokeProgressSamples: Array<Record<string, unknown>> = [];
let lastSmokeProgressSampleAt = 0;
let lastSmokeProgressPhase: ExportProgress["phase"] | undefined;
const recordSmokeProgress = (progress: ExportProgress) => {
if (!smokeExportConfig.enabled || smokeExportStartedAt === null) {
return;
}
const now = performance.now();
const phase = progress.phase ?? "extracting";
const shouldSample =
smokeProgressSamples.length === 0 ||
phase !== lastSmokeProgressPhase ||
now - lastSmokeProgressSampleAt >= 1000 ||
progress.currentFrame >= progress.totalFrames;
if (!shouldSample) {
return;
}
smokeProgressSamples.push({
elapsedMs: Math.round(now - smokeExportStartedAt),
phase,
currentFrame: progress.currentFrame,
totalFrames: progress.totalFrames,
percentage: progress.percentage,
estimatedTimeRemaining: progress.estimatedTimeRemaining,
renderFps: progress.renderFps,
renderBackend: progress.renderBackend,
encodeBackend: progress.encodeBackend,
encoderName: progress.encoderName,
});
lastSmokeProgressSampleAt = now;
lastSmokeProgressPhase = phase;
};
const smokeProgressSampler = createSmokeExportProgressSampler({
enabled: smokeExportConfig.enabled,
startedAtMs: smokeExportStartedAt,
});
const smokeProgressSamples = smokeProgressSampler.samples;
const recordSmokeProgress = smokeProgressSampler.record;
if (settings.format === "gif" && settings.gifConfig) {
// GIF Export
@@ -0,0 +1,119 @@
import { describe, expect, it } from "vitest";
import type { ExportProgress } from "@/lib/exporter/types";
import { createSmokeExportProgressSampler } from "./smokeExportProgress";
function progress(overrides: Partial<ExportProgress> = {}): ExportProgress {
return {
currentFrame: 1,
totalFrames: 100,
percentage: 1,
estimatedTimeRemaining: 10,
...overrides,
};
}
describe("createSmokeExportProgressSampler", () => {
it("does not sample when smoke export is disabled", () => {
const sampler = createSmokeExportProgressSampler({
enabled: false,
startedAtMs: 100,
now: () => 200,
});
sampler.record(progress());
expect(sampler.samples).toEqual([]);
});
it("does not sample before the smoke export start time is known", () => {
const sampler = createSmokeExportProgressSampler({
enabled: true,
startedAtMs: null,
now: () => 200,
});
sampler.record(progress());
expect(sampler.samples).toEqual([]);
});
it("records the first progress update with the default phase", () => {
const sampler = createSmokeExportProgressSampler({
enabled: true,
startedAtMs: 100,
now: () => 245.4,
});
sampler.record(
progress({
currentFrame: 4,
totalFrames: 200,
percentage: 2,
estimatedTimeRemaining: 12,
renderFps: 58,
renderBackend: "webgpu",
encodeBackend: "webcodecs",
encoderName: "VideoEncoder",
}),
);
expect(sampler.samples).toEqual([
{
elapsedMs: 145,
phase: "extracting",
currentFrame: 4,
totalFrames: 200,
percentage: 2,
estimatedTimeRemaining: 12,
renderFps: 58,
renderBackend: "webgpu",
encodeBackend: "webcodecs",
encoderName: "VideoEncoder",
},
]);
});
it("skips same-phase updates inside the sampling interval", () => {
const timestamps = [100, 500];
const sampler = createSmokeExportProgressSampler({
enabled: true,
startedAtMs: 0,
now: () => timestamps.shift() ?? 500,
});
sampler.record(progress({ currentFrame: 1, percentage: 1 }));
sampler.record(progress({ currentFrame: 2, percentage: 2 }));
expect(sampler.samples).toHaveLength(1);
expect(sampler.samples[0]?.currentFrame).toBe(1);
});
it("samples phase changes and interval updates", () => {
const timestamps = [100, 200, 1200];
const sampler = createSmokeExportProgressSampler({
enabled: true,
startedAtMs: 0,
now: () => timestamps.shift() ?? 1200,
});
sampler.record(progress({ currentFrame: 1, phase: "extracting" }));
sampler.record(progress({ currentFrame: 2, phase: "finalizing" }));
sampler.record(progress({ currentFrame: 3, phase: "finalizing" }));
expect(sampler.samples.map((sample) => sample.currentFrame)).toEqual([1, 2, 3]);
});
it("samples final progress even inside the sampling interval", () => {
const timestamps = [100, 250];
const sampler = createSmokeExportProgressSampler({
enabled: true,
startedAtMs: 0,
now: () => timestamps.shift() ?? 250,
});
sampler.record(progress({ currentFrame: 1, totalFrames: 10 }));
sampler.record(progress({ currentFrame: 10, totalFrames: 10 }));
expect(sampler.samples.map((sample) => sample.currentFrame)).toEqual([1, 10]);
});
});
@@ -0,0 +1,72 @@
import type { ExportProgress } from "@/lib/exporter/types";
const DEFAULT_SAMPLE_INTERVAL_MS = 1000;
export type SmokeExportProgressSample = {
elapsedMs: number;
phase: NonNullable<ExportProgress["phase"]> | "extracting";
currentFrame: number;
totalFrames: number;
percentage: number;
estimatedTimeRemaining: number;
renderFps?: number;
renderBackend?: ExportProgress["renderBackend"];
encodeBackend?: ExportProgress["encodeBackend"];
encoderName?: string;
};
type SmokeExportProgressSamplerOptions = {
enabled: boolean;
startedAtMs: number | null;
now?: () => number;
sampleIntervalMs?: number;
};
export function createSmokeExportProgressSampler({
enabled,
startedAtMs,
now = () => performance.now(),
sampleIntervalMs = DEFAULT_SAMPLE_INTERVAL_MS,
}: SmokeExportProgressSamplerOptions) {
const samples: SmokeExportProgressSample[] = [];
let lastSampleAt = 0;
let lastSamplePhase: ExportProgress["phase"] | "extracting" | undefined;
const record = (progress: ExportProgress) => {
if (!enabled || startedAtMs === null) {
return;
}
const timestamp = now();
const phase = progress.phase ?? "extracting";
const shouldSample =
samples.length === 0 ||
phase !== lastSamplePhase ||
timestamp - lastSampleAt >= sampleIntervalMs ||
progress.currentFrame >= progress.totalFrames;
if (!shouldSample) {
return;
}
samples.push({
elapsedMs: Math.round(timestamp - startedAtMs),
phase,
currentFrame: progress.currentFrame,
totalFrames: progress.totalFrames,
percentage: progress.percentage,
estimatedTimeRemaining: progress.estimatedTimeRemaining,
renderFps: progress.renderFps,
renderBackend: progress.renderBackend,
encodeBackend: progress.encodeBackend,
encoderName: progress.encoderName,
});
lastSampleAt = timestamp;
lastSamplePhase = phase;
};
return {
record,
samples,
};
}