mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 23:03:09 +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", () => {
|
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 () => {
|
it("launches a probe for a static-duration composition with variable-bound audio", async () => {
|
||||||
capturedCfgs.length = 0;
|
capturedCfgs.length = 0;
|
||||||
const { runProbeStage } = await import("./probeStage.js");
|
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> {
|
export async function runProbeStage(input: ProbeStageInput): Promise<ProbeStageResult> {
|
||||||
const {
|
const {
|
||||||
projectDir,
|
projectDir,
|
||||||
@@ -196,12 +214,14 @@ export async function runProbeStage(input: ProbeStageInput): Promise<ProbeStageR
|
|||||||
composition.audios.length,
|
composition.audios.length,
|
||||||
);
|
);
|
||||||
const hasVariableMedia = hasVariableBoundMedia(compiled.html, job.config.variables);
|
const hasVariableMedia = hasVariableBoundMedia(compiled.html, job.config.variables);
|
||||||
|
const hasInsertedMedia = hasRuntimeInsertedMedia(compiled.html);
|
||||||
const needsBrowser =
|
const needsBrowser =
|
||||||
composition.duration <= 0 ||
|
composition.duration <= 0 ||
|
||||||
compiled.unresolvedCompositions.length > 0 ||
|
compiled.unresolvedCompositions.length > 0 ||
|
||||||
hasAutoStart ||
|
hasAutoStart ||
|
||||||
hasScriptedAudio ||
|
hasScriptedAudio ||
|
||||||
hasVariableMedia;
|
hasVariableMedia ||
|
||||||
|
hasInsertedMedia;
|
||||||
|
|
||||||
if (needsBrowser) {
|
if (needsBrowser) {
|
||||||
const reasons = [];
|
const reasons = [];
|
||||||
@@ -210,6 +230,7 @@ export async function runProbeStage(input: ProbeStageInput): Promise<ProbeStageR
|
|||||||
reasons.push(`${compiled.unresolvedCompositions.length} unresolved composition(s)`);
|
reasons.push(`${compiled.unresolvedCompositions.length} unresolved composition(s)`);
|
||||||
if (hasAutoStart) reasons.push("auto-start video(s)");
|
if (hasAutoStart) reasons.push("auto-start video(s)");
|
||||||
if (hasScriptedAudio) reasons.push("scripted audio volume");
|
if (hasScriptedAudio) reasons.push("scripted audio volume");
|
||||||
|
if (hasInsertedMedia) reasons.push("runtime-inserted media");
|
||||||
if (hasVariableMedia) reasons.push("variable-bound media source(s)");
|
if (hasVariableMedia) reasons.push("variable-bound media source(s)");
|
||||||
|
|
||||||
log.info("Launching browser for composition probe...", {
|
log.info("Launching browser for composition probe...", {
|
||||||
|
|||||||
Reference in New Issue
Block a user