diff --git a/packages/lint/src/rules/media.ts b/packages/lint/src/rules/media.ts
index d9640478df7..b5044ef1113 100644
--- a/packages/lint/src/rules/media.ts
+++ b/packages/lint/src/rules/media.ts
@@ -391,6 +391,7 @@ function findRuntimeMediaSrcMutationFindings(ctx: LintContext): HyperframeLintFi
const findings: HyperframeLintFinding[] = [];
for (const script of ctx.scripts) {
for (const mutation of extractMediaSrcMutations(script.content)) {
+ if (!mutation.selector) continue;
let targets: Element[];
try {
const id = /^#[A-Za-z_][\w-]*$/.test(mutation.selector) ? mutation.selector.slice(1) : null;
diff --git a/packages/parsers/src/mediaSrcMutation.ts b/packages/parsers/src/mediaSrcMutation.ts
index 942ecfd4370..52a274f8cab 100644
--- a/packages/parsers/src/mediaSrcMutation.ts
+++ b/packages/parsers/src/mediaSrcMutation.ts
@@ -2,7 +2,8 @@ import * as acorn from "acorn";
import * as acornWalk from "acorn-walk";
export interface MediaSrcMutation {
- selector: string;
+ /** Null when the write's target is not a statically resolvable DOM lookup. */
+ selector: string | null;
operation: "src_assignment" | "set_attribute";
raw: string;
}
@@ -206,7 +207,7 @@ function selectorFromDomLookup(
return method === "getElementById" ? `#${value}` : value;
}
-/** Find literal source writes whose target is a statically resolvable DOM lookup. */
+/** Find source writes, with the target's selector when it is a statically resolvable DOM lookup. */
export function extractMediaSrcMutations(script: string): MediaSrcMutation[] {
try {
const ast = parseProgram(script);
@@ -220,10 +221,8 @@ export function extractMediaSrcMutations(script: string): MediaSrcMutation[] {
memberName(node.left) !== "src"
)
return;
- const selector = lookupSelector(node.left.object, scopeAt(ancestors));
- if (!selector) return;
mutations.push({
- selector,
+ selector: lookupSelector(node.left.object, scopeAt(ancestors)) ?? null,
operation: "src_assignment",
raw: script.slice(node.start, node.end),
});
@@ -232,10 +231,8 @@ export function extractMediaSrcMutations(script: string): MediaSrcMutation[] {
if (node.callee.type !== "MemberExpression" || memberName(node.callee) !== "setAttribute")
return;
if (literalString(node.arguments[0])?.toLowerCase() !== "src") return;
- const selector = lookupSelector(node.callee.object, scopeAt(ancestors));
- if (!selector) return;
mutations.push({
- selector,
+ selector: lookupSelector(node.callee.object, scopeAt(ancestors)) ?? null,
operation: "set_attribute",
raw: script.slice(node.start, node.end),
});
diff --git a/packages/producer/src/services/render/stages/probeStage.test.ts b/packages/producer/src/services/render/stages/probeStage.test.ts
index fe927ed223f..420ed418ab6 100644
--- a/packages/producer/src/services/render/stages/probeStage.test.ts
+++ b/packages/producer/src/services/render/stages/probeStage.test.ts
@@ -1113,6 +1113,43 @@ describe("runProbeStage — forceScreenshot threading", () => {
expect(capturedCfgs).toHaveLength(0);
});
+ it("probes a src write whose target is resolved at runtime when the document has media", async () => {
+ resetRetryMocks();
+ capturedCfgs.length = 0;
+ const { runProbeStage } = await import("./probeStage.js");
+ const input = makeProbeInput({});
+ input.composition.duration = 5;
+ input.compiled.html = `
+ `;
+
+ await runProbeStage(input);
+
+ expect(capturedCfgs.length).toBeGreaterThan(0);
+ });
+
+ it("does not probe a src write resolved at runtime when the document has no media", async () => {
+ resetRetryMocks();
+ capturedCfgs.length = 0;
+ const { runProbeStage } = await import("./probeStage.js");
+ const input = makeProbeInput({});
+ input.composition.duration = 5;
+ input.compiled.html = `
+ `;
+
+ await runProbeStage(input);
+
+ expect(capturedCfgs).toHaveLength(0);
+ });
+
it("probes when a source child of existing media is mutated", async () => {
resetRetryMocks();
capturedCfgs.length = 0;
diff --git a/packages/producer/src/services/render/stages/probeStage.ts b/packages/producer/src/services/render/stages/probeStage.ts
index 83865f499aa..a8ee2300609 100644
--- a/packages/producer/src/services/render/stages/probeStage.ts
+++ b/packages/producer/src/services/render/stages/probeStage.ts
@@ -289,8 +289,15 @@ function hasRuntimeMediaChanges(html: string): boolean {
if (name === "video" || name === "audio") return true;
return name === "source" && element.closest("video, audio") !== null;
};
+ const hasManagedMedia = document.querySelector("video, audio") !== null;
for (const script of scriptBodies) {
for (const mutation of extractMediaSrcMutations(script)) {
+ // A write whose target can't be resolved statically, such as one in a loop
+ // over querySelectorAll, may land on any media in the document.
+ if (!mutation.selector) {
+ if (hasManagedMedia) return true;
+ continue;
+ }
try {
const id = /^#[A-Za-z_][\w-]*$/.test(mutation.selector) ? mutation.selector.slice(1) : null;
const idTarget = id ? document.getElementById(id) : null;