fix(render): discover runtime-inserted media before extraction (#2474)

* fix(render): discover runtime-inserted media before extraction

* fix(render): detect runtime Audio constructors

* test(render): cover runtime audio insertion paths
This commit is contained in:
Miguel Ángel
2026-07-15 08:13:17 -04:00
committed by GitHub
parent 017183ad66
commit 47e4b18f99
2 changed files with 106 additions and 1 deletions
@@ -267,6 +267,90 @@ describe("hasVariableBoundMedia", () => {
});
describe("runProbeStage — forceScreenshot threading", () => {
it("launches a probe when a static-duration composition inserts video at runtime", async () => {
capturedCfgs.length = 0;
const { runProbeStage } = await import("./probeStage.js");
const input = makeProbeInput({});
input.composition.duration = 5;
input.compiled.html = `<script>
const video = document.createElement("video");
video.id = "gameplay";
video.src = "gameplay.mp4";
video.dataset.start = "0";
video.dataset.duration = "5";
document.body.appendChild(video);
</script>`;
await runProbeStage(input);
expect(capturedCfgs.length).toBeGreaterThan(0);
});
it("launches a probe when a static-duration composition inserts audio at runtime", async () => {
capturedCfgs.length = 0;
const { runProbeStage } = await import("./probeStage.js");
const input = makeProbeInput({});
input.composition.duration = 5;
input.compiled.html = `<script>
const audio = document.createElement("audio");
audio.src = "music.mp3";
document.body.appendChild(audio);
</script>`;
await runProbeStage(input);
expect(capturedCfgs.length).toBeGreaterThan(0);
});
it("launches a probe when a static-duration composition uses the Audio constructor", async () => {
capturedCfgs.length = 0;
const { runProbeStage } = await import("./probeStage.js");
const input = makeProbeInput({});
input.composition.duration = 5;
input.compiled.html = `<script>
const audio = new Audio("music.mp3");
document.body.appendChild(audio);
</script>`;
await runProbeStage(input);
expect(capturedCfgs.length).toBeGreaterThan(0);
});
it("launches a probe when script-inserted markup contains timed video", async () => {
capturedCfgs.length = 0;
const { runProbeStage } = await import("./probeStage.js");
const input = makeProbeInput({});
input.composition.duration = 5;
input.compiled.html = `<script>
document.body.insertAdjacentHTML(
"beforeend",
'<video id="gameplay" src="gameplay.mp4" data-start="0" data-duration="5"></video>',
);
</script>`;
await runProbeStage(input);
expect(capturedCfgs.length).toBeGreaterThan(0);
});
it("launches a probe when script-inserted markup contains timed audio", async () => {
capturedCfgs.length = 0;
const { runProbeStage } = await import("./probeStage.js");
const input = makeProbeInput({});
input.composition.duration = 5;
input.compiled.html = `<script>
document.body.insertAdjacentHTML(
"beforeend",
'<audio id="music" src="music.mp3" data-start="0" data-duration="5"></audio>',
);
</script>`;
await runProbeStage(input);
expect(capturedCfgs.length).toBeGreaterThan(0);
});
it("launches a probe for a static-duration composition with variable-bound audio", async () => {
capturedCfgs.length = 0;
const { runProbeStage } = await import("./probeStage.js");
@@ -164,6 +164,24 @@ export function hasVariableBoundMedia(
});
}
/**
* Runtime-created media does not exist when the static compiler scans the HTML.
* Launch a browser probe so discoverMediaFromBrowser can reconcile it before
* extraction, even when the root duration is already known. External script
* sources have no inline text to inspect and remain a known heuristic gap.
*/
function hasRuntimeInsertedMedia(html: string): boolean {
const { document } = parseHTML(html);
const scriptBodies = [...document.querySelectorAll("script")]
.map((script) => script.textContent ?? "")
.join("\n");
return (
/\bcreateElement\s*\(\s*["'`](?:video|audio)["'`]\s*\)/i.test(scriptBodies) ||
/\bnew\s+(?:Audio|Video)\s*\(/i.test(scriptBodies) ||
/<(?:video|audio)\b[^>]*>/i.test(scriptBodies)
);
}
export async function runProbeStage(input: ProbeStageInput): Promise<ProbeStageResult> {
const {
projectDir,
@@ -196,12 +214,14 @@ export async function runProbeStage(input: ProbeStageInput): Promise<ProbeStageR
composition.audios.length,
);
const hasVariableMedia = hasVariableBoundMedia(compiled.html, job.config.variables);
const hasInsertedMedia = hasRuntimeInsertedMedia(compiled.html);
const needsBrowser =
composition.duration <= 0 ||
compiled.unresolvedCompositions.length > 0 ||
hasAutoStart ||
hasScriptedAudio ||
hasVariableMedia;
hasVariableMedia ||
hasInsertedMedia;
if (needsBrowser) {
const reasons = [];
@@ -210,6 +230,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 (hasInsertedMedia) reasons.push("runtime-inserted media");
if (hasVariableMedia) reasons.push("variable-bound media source(s)");
log.info("Launching browser for composition probe...", {