mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat(lint): error on crossorigin on media (breaks preview) (#1793)
`crossorigin` on <video>/<audio> forces a CORS-checked fetch. The server-side renderer downloads media directly (no CORS) so renders always work, but Studio preview runs in the browser — a media host that omits Access-Control-Allow-Origin silently fails the load, so the media shows blank/black in preview while the render looks fine, hiding the bug. Plain displayed media never needs crossorigin; it's only required to read pixels/samples back (canvas/WebGL texture, WebAudio createMediaElementSource) and only when the host is known CORS-enabled. New rule media_crossorigin_breaks_preview flags it as an error with that guidance. Claude-Session: https://claude.ai/code/session_01NsmfF5FzhqXY6hZ8buXgUE Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
04700ea9c5
commit
1faeba4aa5
@@ -267,4 +267,32 @@ describe("media rules", () => {
|
|||||||
const finding = result.findings.find((f) => f.code === "media_in_subcomposition");
|
const finding = result.findings.find((f) => f.code === "media_in_subcomposition");
|
||||||
expect(finding).toBeUndefined();
|
expect(finding).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("reports error for media with crossorigin (breaks preview when host omits CORS)", async () => {
|
||||||
|
const html = `
|
||||||
|
<html><body>
|
||||||
|
<div id="root" data-composition-id="c1" data-width="1920" data-height="1080">
|
||||||
|
<video id="v1" crossorigin="anonymous" src="https://cdn.example.com/clip.mp4" data-start="0" data-duration="5" muted playsinline></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_crossorigin_breaks_preview");
|
||||||
|
expect(finding).toBeDefined();
|
||||||
|
expect(finding?.severity).toBe("error");
|
||||||
|
expect(finding?.elementId).toBe("v1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not flag media without crossorigin", async () => {
|
||||||
|
const html = `
|
||||||
|
<html><body>
|
||||||
|
<div id="root" data-composition-id="c1" data-width="1920" data-height="1080">
|
||||||
|
<video id="v1" src="https://cdn.example.com/clip.mp4" data-start="0" data-duration="5" muted playsinline></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_crossorigin_breaks_preview");
|
||||||
|
expect(finding).toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -483,6 +483,33 @@ export const mediaRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> =
|
|||||||
return findings;
|
return findings;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// media_crossorigin_breaks_preview — `crossorigin` on <video>/<audio> forces a
|
||||||
|
// CORS-checked fetch. The server-side renderer downloads media directly (no CORS),
|
||||||
|
// so it always works there; but Studio preview runs in the browser, where a media
|
||||||
|
// host that omits Access-Control-Allow-Origin silently fails the load — the media
|
||||||
|
// shows BLANK/black in preview while renders look fine, hiding the bug. Plain
|
||||||
|
// displayed media never needs crossorigin; it's only required to read pixels/samples
|
||||||
|
// back (canvas/WebGL texture, WebAudio createMediaElementSource) AND only when the
|
||||||
|
// host is known CORS-enabled.
|
||||||
|
({ tags }) => {
|
||||||
|
const findings: HyperframeLintFinding[] = [];
|
||||||
|
for (const tag of tags) {
|
||||||
|
if (tag.name !== "video" && tag.name !== "audio") continue;
|
||||||
|
if (!hasAttrName(tag.raw, "crossorigin")) continue;
|
||||||
|
const elementId = readAttr(tag.raw, "id") || undefined;
|
||||||
|
findings.push({
|
||||||
|
code: "media_crossorigin_breaks_preview",
|
||||||
|
severity: "error",
|
||||||
|
message: `<${tag.name}${elementId ? ` id="${elementId}"` : ""}> has crossorigin, which forces a CORS-checked fetch. If the media host omits Access-Control-Allow-Origin, the load silently fails in Studio preview (media shows BLANK/black) while server-side renders still work — hiding the bug.`,
|
||||||
|
elementId,
|
||||||
|
fixHint:
|
||||||
|
"Remove the crossorigin attribute unless you read the media back via canvas/WebGL/WebAudio AND the host is known to send CORS headers. Plain displayed media never needs it.",
|
||||||
|
snippet: truncateSnippet(tag.raw),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return findings;
|
||||||
|
},
|
||||||
|
|
||||||
// video_audio_double_source — catches audible <video> paired with a separate
|
// video_audio_double_source — catches audible <video> paired with a separate
|
||||||
// <audio> pointing to the same file, which causes double playback at runtime
|
// <audio> pointing to the same file, which causes double playback at runtime
|
||||||
({ tags }) => {
|
({ tags }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user