fix(engine,producer,lint): resolve <source> children for media extract and localize (#3238)

Parent src-only scans skipped multi-format <video>/<audio> markup, so those
elements were never extracted, downloaded, or mixed and rendered blank/silent.
Lint now accepts a child <source src> as a resolvable media src.
This commit is contained in:
Val
2026-08-26 20:17:12 +00:00
committed by GitHub
parent c9f43ebcfb
commit 97991bbd35
8 changed files with 155 additions and 24 deletions
+31
View File
@@ -217,6 +217,37 @@ describe("media rules", () => {
expect(finding?.severity).toBe("error");
});
it("accepts <source src> children in place of parent src", async () => {
const html = `
<html><body>
<div id="root" data-composition-id="c1" data-width="1920" data-height="1080">
<video id="rec" data-start="0" data-duration="4" muted playsinline>
<source src="clip.mp4" type="video/mp4">
<source src="clip.webm" type="video/webm">
</video>
</div>
<script>window.__timelines = window.__timelines || {}; window.__timelines["c1"] = gsap.timeline({ paused: true });</script>
</body></html>`;
const result = await lintHyperframeHtml(html);
expect(result.findings.some((f) => f.code === "media_missing_src")).toBe(false);
});
it("reports error for <source>-only media with no data-start", async () => {
const html = `
<html><body>
<div id="root" data-composition-id="c1" data-width="1920" data-height="1080">
<video id="rec" muted playsinline>
<source src="clip.mp4" type="video/mp4">
</video>
</div>
<script>window.__timelines = window.__timelines || {}; window.__timelines["c1"] = gsap.timeline({ paused: true });</script>
</body></html>`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "media_missing_data_start");
expect(finding).toBeDefined();
expect(finding?.elementId).toBe("rec");
});
it("reports error for media with src but no data-start", async () => {
const html = `
<html><body>
+18 -4
View File
@@ -1,4 +1,4 @@
import type { LintContext, HyperframeLintFinding } from "../context";
import type { LintContext, HyperframeLintFinding, OpenTag } from "../context";
import { readAttr, readDecodedAttr, stripJsComments, truncateSnippet, isMediaTag } from "../utils";
import { validateColorGradingContract } from "@hyperframes/parsers/color-grading-contract";
@@ -41,6 +41,20 @@ function hasAttrName(tagSource: string, attr: string): boolean {
return new RegExp(`(?:^|\\s)${escaped}(?:\\s*=|\\s|/?>)`, "i").test(attrs);
}
/** Parent `src`, else a descendant `<source src>` (matches engine resolveMediaElementSrc). */
function mediaHasResolvableSrc(tag: OpenTag, tags: readonly OpenTag[]): boolean {
if (readAttr(tag.raw, "src")) return true;
const end = tag.closeIndex ?? tag.endIndex;
if (end == null) return false;
return tags.some(
(child) =>
child.name === "source" &&
child.index > tag.index &&
child.index < end &&
Boolean(readAttr(child.raw, "src")),
);
}
function classNamesFromAttr(classAttr: string | null): string[] {
if (!classAttr) return [];
return classAttr.split(/\s+/).filter(Boolean);
@@ -499,7 +513,7 @@ export const mediaRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> =
if (tag.name !== "video" && tag.name !== "audio") continue;
const hasDataStart = readAttr(tag.raw, "data-start");
const hasId = readAttr(tag.raw, "id");
const hasSrc = readAttr(tag.raw, "src");
const hasSrc = mediaHasResolvableSrc(tag, tags);
if (hasSrc && !hasDataStart) {
findings.push({
code: "media_missing_data_start",
@@ -538,9 +552,9 @@ export const mediaRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> =
findings.push({
code: "media_missing_src",
severity: "error",
message: `<${tag.name} id="${hasId}"> has data-start but no src attribute. The renderer cannot load this media.`,
message: `<${tag.name} id="${hasId}"> has data-start but no src (on the element or a <source> child). The renderer cannot load this media.`,
elementId: hasId,
fixHint: `Add a src attribute to the <${tag.name}> element directly. If using <source> children, the renderer still requires src on the parent element.`,
fixHint: `Add src on the <${tag.name}> element, or a <source src="..."> child.`,
snippet: truncateSnippet(tag.raw),
});
}