mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
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:
@@ -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...", {
|
||||
|
||||
Reference in New Issue
Block a user