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
@@ -1574,6 +1574,25 @@ describe("localizeRemoteMediaSources", () => {
expect(remoteMediaAssets.size).toBe(0);
});
it("localizes remote <source> children of a <video>", async () => {
const orig = globalThis.fetch;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = async () => validTestMediaResponse();
try {
const dl = mkdtempSync(join(tmpdir(), "hf-dl-src-"));
const html = `<video id="rec" data-start="0" data-end="5" muted>
<source src="https://src-ok.example.com/rec.mp4" type="video/mp4">
<source src="https://src-ok.example.com/rec.webm" type="video/webm">
</video>`;
const { html: result, remoteMediaAssets } = await localizeRemoteMediaSources(html, dl);
expect(result).not.toContain("https://src-ok.example.com/");
expect(remoteMediaAssets.size).toBe(2);
} finally {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = orig;
}
});
it("rewrites src in both double-quoted and single-quoted attributes", async () => {
const orig = globalThis.fetch;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
+13 -9
View File
@@ -1280,6 +1280,8 @@ const REMOTE_MEDIA_SUBDIR = "_remote_media";
// have `>` inside quoted attribute values (data-title etc.).
const REMOTE_MEDIA_TAG_RE =
/<(?:video|audio)\b[^>]*?\bsrc\s*=\s*["'](https?:\/\/[^"']+)["'][^>]*>/gi;
// <source src> on media elements (picture uses srcset, not src).
const REMOTE_SOURCE_TAG_RE = /<source\b[^>]*?\bsrc\s*=\s*["'](https?:\/\/[^"']+)["'][^>]*>/gi;
// Match <img> tags (including agent-pipeline-emitted variants where `src` is
// not the first attribute). Producer-side localisation is the primary fix for
// the remote-<img> flicker; frameCapture's `pollImagesReady`/`decodeAllImages`
@@ -1350,10 +1352,11 @@ async function downloadAndRewriteUrls(
}
/**
* Download any remote `src` URLs on `<video>` and `<audio>` elements into a
* local subdirectory of `downloadDir`, rewrite the HTML src attributes to
* relative paths, and return the updated HTML along with a map of
* `{ relativePath → absoluteLocalPath }` for callers to add to `externalAssets`.
* Download any remote `src` URLs on `<video>` / `<audio>` elements and their
* `<source>` children into a local subdirectory of `downloadDir`, rewrite the
* HTML src attributes to relative paths, and return the updated HTML along with
* a map of `{ relativePath → absoluteLocalPath }` for callers to add to
* `externalAssets`.
*
* Skips URLs that fail to download (warns and preserves the original URL so
* the browser can still attempt the remote fetch as a fallback).
@@ -1369,12 +1372,13 @@ export async function localizeRemoteMediaSources(
html: string,
downloadDir: string,
): Promise<{ html: string; remoteMediaAssets: Map<string, string> }> {
// Collect unique HTTP URLs from <video>/<audio> src attributes.
const urlSet = new Set<string>();
const re = new RegExp(REMOTE_MEDIA_TAG_RE.source, REMOTE_MEDIA_TAG_RE.flags);
let m: RegExpExecArray | null;
while ((m = re.exec(html)) !== null) {
if (m[1]) urlSet.add(m[1]);
for (const tagRe of [REMOTE_MEDIA_TAG_RE, REMOTE_SOURCE_TAG_RE]) {
const re = new RegExp(tagRe.source, tagRe.flags);
let m: RegExpExecArray | null;
while ((m = re.exec(html)) !== null) {
if (m[1]) urlSet.add(m[1]);
}
}
return downloadAndRewriteUrls(
urlSet,