diff --git a/src/components/canvas/players/html5-player.ts b/src/components/canvas/players/html5-player.ts index 85585556..86097065 100644 --- a/src/components/canvas/players/html5-player.ts +++ b/src/components/canvas/players/html5-player.ts @@ -119,7 +119,7 @@ export class Html5Player extends Player { private decodedFrames: Map = new Map(); private decodeInFlight: number | null = null; private captureInFlight: Promise | null = null; - private captureFps: number = 30; + private captureFps: number = 25; private mode: Html5Mode = "editing"; private contentHash: string | null = null; private capturedHash: string | null = null; @@ -293,7 +293,7 @@ export class Html5Player extends Player { const height = this.clipConfiguration.height || this.edit.size.height; this.renderedWidth = width; this.renderedHeight = height; - this.captureFps = this.edit.getOutputFps() || 30; + this.captureFps = this.edit.getOutputFps(); this.contentHash = await this.hashAsset(); const iframe = createOffscreenIframe(width, height); diff --git a/src/core/export/export-coordinator.ts b/src/core/export/export-coordinator.ts index ca46cee2..523589d6 100644 --- a/src/core/export/export-coordinator.ts +++ b/src/core/export/export-coordinator.ts @@ -72,7 +72,7 @@ export class ExportCoordinator { } async exportBlob(options: ExportBlobOptions = {}): Promise { - return this.encode(this.edit.getEdit().output?.fps ?? 30, options); + return this.encode(this.edit.getOutputFps(), options); } async export(filename = "shotstack-export.mp4", fps?: number): Promise { @@ -80,7 +80,7 @@ export class ExportCoordinator { this.progressUI.create(); try { - const blob = await this.encode(fps ?? this.edit.getEdit().output?.fps ?? 30, { + const blob = await this.encode(fps ?? this.edit.getOutputFps(), { onProgress: (fraction, stage) => this.progressUI.update(fraction * 100, 100, stage) }); @@ -282,7 +282,7 @@ export class ExportCoordinator { if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return; if (e.code === "KeyE" && (e.metaKey || e.ctrlKey) && !exporting.has(this.edit)) { e.preventDefault(); - this.export("shotstack-export.mp4", this.edit.getEdit().output?.fps || 30).catch(err => console.error("Export failed:", err)); + this.export("shotstack-export.mp4", this.edit.getOutputFps()).catch(err => console.error("Export failed:", err)); } }; diff --git a/src/core/output-settings-manager.ts b/src/core/output-settings-manager.ts index 1d99ceee..9bb32059 100644 --- a/src/core/output-settings-manager.ts +++ b/src/core/output-settings-manager.ts @@ -130,7 +130,9 @@ export class OutputSettingsManager { } getFps(): number { - return this.edit.getResolvedEdit()?.output?.fps ?? 30; + const output = this.edit.getResolvedEdit()?.output; + // The render's defaults, so the preview and the render step through the same frames. + return output?.fps ?? (output?.resolution === "preview" ? 12 : 25); } // ─── Format ─────────────────────────────────────────────────────────────── diff --git a/tests/export-coordinator.test.ts b/tests/export-coordinator.test.ts index 371c4a2e..c2e03206 100644 --- a/tests/export-coordinator.test.ts +++ b/tests/export-coordinator.test.ts @@ -80,6 +80,7 @@ function createHarness() { tracks: [] as Array>, getEdit: jest.fn(() => ({ output: { fps: 2 } as { fps: number; mute?: boolean } })), getOutputFormat: jest.fn(() => "mp4"), + getOutputFps: jest.fn(() => 2), getResolvedEdit: () => ({ timeline: { tracks: [] }, output: { format: "mp4" } }), getFontUrlByFamilyAndWeight: () => null, whenSettled: jest.fn(() => Promise.resolve()), diff --git a/tests/html5-player.test.ts b/tests/html5-player.test.ts index b30c3816..522587b9 100644 --- a/tests/html5-player.test.ts +++ b/tests/html5-player.test.ts @@ -585,7 +585,7 @@ const frameMarkup = (blob: Blob) => function createStillPlayer() { const created = createPlayer(); Object.assign(created.edit, { getCanvas: () => null }); - Object.assign(created.player, { getLength: () => 2 }); + Object.assign(created.player, { getLength: () => 2, captureFps: 30 }); jest.spyOn(HTMLIFrameElement.prototype, "srcdoc", "set").mockImplementation(function navigate(this: HTMLIFrameElement) { setTimeout(() => { const doc = this.contentDocument!; diff --git a/tests/output-resolution.test.ts b/tests/output-resolution.test.ts index 0774c818..0d575e50 100644 --- a/tests/output-resolution.test.ts +++ b/tests/output-resolution.test.ts @@ -122,6 +122,22 @@ describe("Output Resolution and AspectRatio", () => { let edit: Edit; let emitSpy: jest.SpyInstance; + describe("output fps", () => { + it("defaults to the render's 25 fps", () => { + expect(edit.getOutputFps()).toBe(25); + }); + + it("defaults to the render's 12 fps at preview resolution", async () => { + await edit.setOutputResolution("preview"); + expect(edit.getOutputFps()).toBe(12); + }); + + it("uses the edit's fps when set", async () => { + await edit.setOutputFps(30); + expect(edit.getOutputFps()).toBe(30); + }); + }); + beforeEach(async () => { edit = new Edit(createMinimalEdit()); await edit.load();