mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-24 14:55:37 +00:00
refactor(editor): extract smoke export progress sampler
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user