From 465c9e764138b94faa48badbee468eb42bd1a39d Mon Sep 17 00:00:00 2001 From: James Russo Date: Tue, 21 Jul 2026 19:49:39 -0400 Subject: [PATCH] fix(producer): preserve runtime audio variables in distributed plans (#2725) * fix(producer): preserve runtime audio variables in distributed plans * fix(producer): prefer runtime duration for volume sampling --- .../src/services/distributed/plan.test.ts | 19 +++++++++++++++++++ .../producer/src/services/distributed/plan.ts | 1 + .../src/services/distributed/shared.ts | 3 +++ .../src/services/htmlCompiler.test.ts | 4 ++-- .../producer/src/services/htmlCompiler.ts | 8 ++++---- .../src/services/render/shared.test.ts | 17 +++++++++++++++++ .../producer/src/services/render/shared.ts | 13 +++++++++++++ .../services/render/stages/probeStage.test.ts | 2 ++ .../src/services/render/stages/probeStage.ts | 9 +++++---- 9 files changed, 66 insertions(+), 10 deletions(-) create mode 100644 packages/producer/src/services/render/shared.test.ts diff --git a/packages/producer/src/services/distributed/plan.test.ts b/packages/producer/src/services/distributed/plan.test.ts index cf7152128..d005e23e0 100644 --- a/packages/producer/src/services/distributed/plan.test.ts +++ b/packages/producer/src/services/distributed/plan.test.ts @@ -81,6 +81,25 @@ describe("distributed warning policy", () => { }); }); +describe("distributed synthetic render job", () => { + it("threads render variables into the plan browser probe job", () => { + const variables = { + voiceoverSrc: "assets/voiceover.wav", + narrationDurationSeconds: 56.738, + }; + const job = buildSyntheticRenderJob({ + fps: { num: 30, den: 1 }, + format: "mp4", + quality: "high", + hdrMode: "force-sdr", + entryFile: "index.html", + variables, + }); + + expect(job.config.variables).toEqual(variables); + }); +}); + describe("resolveChunkPlan", () => { it("returns 1 chunk when totalFrames fits in configChunkSize", () => { const result = resolveChunkPlan(60, 240, 16); diff --git a/packages/producer/src/services/distributed/plan.ts b/packages/producer/src/services/distributed/plan.ts index c84966c03..084cc44fe 100644 --- a/packages/producer/src/services/distributed/plan.ts +++ b/packages/producer/src/services/distributed/plan.ts @@ -782,6 +782,7 @@ export async function plan( entryFile: config.entryFile ?? "index.html", logger: config.logger, producerConfig: cfg, + variables: config.variables, }); const entryFile = config.entryFile ?? "index.html"; const htmlPath = join(projectDir, entryFile); diff --git a/packages/producer/src/services/distributed/shared.ts b/packages/producer/src/services/distributed/shared.ts index cbd84dd77..4137a3f88 100644 --- a/packages/producer/src/services/distributed/shared.ts +++ b/packages/producer/src/services/distributed/shared.ts @@ -106,6 +106,8 @@ export interface SyntheticRenderJobInput { entryFile: string; logger?: ProducerLogger; producerConfig?: RenderConfig["producerConfig"]; + /** Render-time overrides consumed by the plan browser probe. */ + variables?: RenderConfig["variables"]; } /** @@ -133,6 +135,7 @@ export function buildSyntheticRenderJob(input: SyntheticRenderJobInput): RenderJ hdrMode: input.hdrMode, strictness: input.strictness, producerConfig: input.producerConfig, + variables: input.variables, }; return createRenderJob(renderConfig); } diff --git a/packages/producer/src/services/htmlCompiler.test.ts b/packages/producer/src/services/htmlCompiler.test.ts index fef35fc86..a4d0a8427 100644 --- a/packages/producer/src/services/htmlCompiler.test.ts +++ b/packages/producer/src/services/htmlCompiler.test.ts @@ -1763,11 +1763,11 @@ h1 { font-size: 2rem; }`; }); describe("discoverAudioVolumeAutomationFromTimeline", () => { - it("samples video-derived audio volume without firing GSAP callbacks", async () => { + it("prefers runtime duration over stale data-end while sampling video-derived audio", async () => { class TestAudioElement {} class TestVideoElement { id = "bg-video"; - dataset = { start: "0", duration: "1", volume: "0" }; + dataset = { start: "0", end: "0.25", duration: "1", volume: "0" }; volume = 0; } diff --git a/packages/producer/src/services/htmlCompiler.ts b/packages/producer/src/services/htmlCompiler.ts index 4c1ea6e73..436f1ab8e 100644 --- a/packages/producer/src/services/htmlCompiler.ts +++ b/packages/producer/src/services/htmlCompiler.ts @@ -2134,10 +2134,10 @@ export async function discoverAudioVolumeAutomationFromTimeline( const endAttr = Number.parseFloat(el.dataset.end ?? ""); const durationAttr = Number.parseFloat(el.dataset.duration ?? ""); const end = - Number.isFinite(endAttr) && endAttr > start - ? endAttr - : Number.isFinite(durationAttr) && durationAttr > 0 - ? start + durationAttr + Number.isFinite(durationAttr) && durationAttr > 0 + ? start + durationAttr + : Number.isFinite(endAttr) && endAttr > start + ? endAttr : duration; const sampleStart = Math.max(0, start); const sampleEnd = Math.min(duration, end); diff --git a/packages/producer/src/services/render/shared.test.ts b/packages/producer/src/services/render/shared.test.ts new file mode 100644 index 000000000..04e47bc90 --- /dev/null +++ b/packages/producer/src/services/render/shared.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from "bun:test"; +import { resolveBrowserMediaEnd } from "./shared.js"; + +describe("resolveBrowserMediaEnd", () => { + it("prefers a runtime duration over a stale compiler-clamped end", () => { + expect(resolveBrowserMediaEnd(0, 5.04, 56.738)).toBe(56.738); + }); + + it("projects a runtime duration from the browser-local start", () => { + expect(resolveBrowserMediaEnd(2, 7.04, 56.738)).toBe(58.738); + }); + + it("falls back to data-end when runtime duration is unavailable", () => { + expect(resolveBrowserMediaEnd(0, 5.04, Number.NaN)).toBe(5.04); + expect(resolveBrowserMediaEnd(0, 5.04, 0)).toBe(5.04); + }); +}); diff --git a/packages/producer/src/services/render/shared.ts b/packages/producer/src/services/render/shared.ts index 8c26224d1..e48f3f518 100644 --- a/packages/producer/src/services/render/shared.ts +++ b/packages/producer/src/services/render/shared.ts @@ -53,6 +53,19 @@ export interface CompositionMetadata { */ export const BROWSER_MEDIA_EPSILON = 0.0001; +/** + * Resolve the browser/runtime end for a media element. + * + * `data-end` is compiler-generated metadata, while `data-duration` is the + * authored/runtime value. A render variable can replace a placeholder source + * and update `data-duration` after compilation, leaving the compiler-clamped + * `data-end` stale. Prefer the live duration when present so the audio/video + * extraction window follows the runtime media slot. + */ +export function resolveBrowserMediaEnd(start: number, end: number, duration: number): number { + return Number.isFinite(duration) && duration > 0 ? start + duration : end; +} + export function writeFileExclusiveSync(path: string, data: NodeJS.ArrayBufferView | string): void { try { writeFileSync(path, data, { flag: "wx", mode: 0o600 }); diff --git a/packages/producer/src/services/render/stages/probeStage.test.ts b/packages/producer/src/services/render/stages/probeStage.test.ts index 4c3726a89..a1b78c190 100644 --- a/packages/producer/src/services/render/stages/probeStage.test.ts +++ b/packages/producer/src/services/render/stages/probeStage.test.ts @@ -103,6 +103,8 @@ mock.module("../../htmlCompiler.js", () => ({ mock.module("../shared.js", () => ({ BROWSER_MEDIA_EPSILON: 0.0001, projectBrowserEndToCompositionTimeline: () => 0, + resolveBrowserMediaEnd: (_start: number, end: number, duration: number) => + Number.isFinite(duration) && duration > 0 ? _start + duration : end, writeCompiledArtifacts: () => {}, })); diff --git a/packages/producer/src/services/render/stages/probeStage.ts b/packages/producer/src/services/render/stages/probeStage.ts index 8e155f152..011ccd308 100644 --- a/packages/producer/src/services/render/stages/probeStage.ts +++ b/packages/producer/src/services/render/stages/probeStage.ts @@ -55,6 +55,7 @@ import type { ProducerLogger } from "../../../logger.js"; import { BROWSER_MEDIA_EPSILON, projectBrowserEndToCompositionTimeline, + resolveBrowserMediaEnd, writeCompiledArtifacts, type CompositionMetadata, } from "../shared.js"; @@ -454,7 +455,7 @@ export async function runProbeStage(input: ProbeStageInput): Promise 0 && @@ -482,7 +483,7 @@ export async function runProbeStage(input: ProbeStageInput): Promise 0 && @@ -527,7 +528,7 @@ export async function runProbeStage(input: ProbeStageInput): Promise