fix(producer): probe variable-bound media sources (#2444)

This commit is contained in:
Miguel Ángel
2026-07-14 15:42:00 -04:00
committed by GitHub
parent 4cba58f5a3
commit d2c8c2d808
2 changed files with 57 additions and 2 deletions
@@ -1,5 +1,9 @@
import { describe, expect, it, mock } from "bun:test";
import { hasAutoStartVideos, hasScriptedAudioVolumeAutomation } from "./probeStage.js";
import {
hasAutoStartVideos,
hasScriptedAudioVolumeAutomation,
hasVariableBoundMedia,
} from "./probeStage.js";
// ── Mocks for runProbeStage tests ────────────────────────────────────────────
// Capture the cfg passed to createCaptureSession so we can assert it carries
@@ -244,7 +248,36 @@ describe("hasAutoStartVideos", () => {
});
});
describe("hasVariableBoundMedia", () => {
it("requires a browser probe when an audio src is overridden by render variables", () => {
const html = `<audio id="voice" src="fallback.wav" data-var-src="voice_src"></audio>`;
expect(hasVariableBoundMedia(html, { voice_src: "row-02.wav" })).toBe(true);
});
it("does not probe unrelated overrides or image-only bindings", () => {
const audio = `<audio src="fallback.wav" data-var-src="voice_src"></audio>`;
const image = `<img src="fallback.png" data-var-src="hero_src" />`;
expect(hasVariableBoundMedia(audio, { title: "Row 02" })).toBe(false);
expect(hasVariableBoundMedia(image, { hero_src: "row-02.png" })).toBe(false);
});
});
describe("runProbeStage — forceScreenshot threading", () => {
it("launches a probe for a static-duration composition with variable-bound audio", async () => {
capturedCfgs.length = 0;
const { runProbeStage } = await import("./probeStage.js");
const input = makeProbeInput({});
input.composition.duration = 5;
input.compiled.html = `<audio id="voice" src="fallback.wav" data-var-src="voice_src"></audio>`;
input.job.config.variables = { voice_src: "row-02.wav" };
await runProbeStage(input);
expect(capturedCfgs.length).toBeGreaterThan(0);
});
it("passes forceScreenshot:true to createCaptureSession when stage input carries it but cfg does not (low-memory mode fix #1236)", async () => {
capturedCfgs.length = 0;
@@ -135,6 +135,25 @@ export function hasAutoStartVideos(html: string): boolean {
return document.querySelector("video[data-hf-auto-start]") !== null;
}
/**
* Variable-bound audio/video sources are resolved by the browser runtime, not
* the static compiler. Probe them whenever the current render overrides the
* referenced variable so media extraction follows the resolved row value.
*/
export function hasVariableBoundMedia(
html: string,
variables: Record<string, unknown> | undefined,
): boolean {
if (!variables || Object.keys(variables).length === 0) return false;
const { document } = parseHTML(html);
return Array.from(
document.querySelectorAll("audio[data-var-src], video[data-var-src], source[data-var-src]"),
).some((element) => {
const variableId = element.getAttribute("data-var-src")?.trim();
return Boolean(variableId && Object.hasOwn(variables, variableId));
});
}
export async function runProbeStage(input: ProbeStageInput): Promise<ProbeStageResult> {
const {
projectDir,
@@ -166,11 +185,13 @@ export async function runProbeStage(input: ProbeStageInput): Promise<ProbeStageR
compiled.html,
composition.audios.length,
);
const hasVariableMedia = hasVariableBoundMedia(compiled.html, job.config.variables);
const needsBrowser =
composition.duration <= 0 ||
compiled.unresolvedCompositions.length > 0 ||
hasAutoStart ||
hasScriptedAudio;
hasScriptedAudio ||
hasVariableMedia;
if (needsBrowser) {
const reasons = [];
@@ -179,6 +200,7 @@ export async function runProbeStage(input: ProbeStageInput): Promise<ProbeStageR
reasons.push(`${compiled.unresolvedCompositions.length} unresolved composition(s)`);
if (hasAutoStart) reasons.push("auto-start video(s)");
if (hasScriptedAudio) reasons.push("scripted audio volume");
if (hasVariableMedia) reasons.push("variable-bound media source(s)");
log.info("Launching browser for composition probe...", {
reasons,