diff --git a/TESTING.md b/TESTING.md index c4ed58c..fc3c0b4 100644 --- a/TESTING.md +++ b/TESTING.md @@ -1297,9 +1297,10 @@ mapped differences"): video previews in the same `#local-preview` slot as `video.local-file-preview` with native `controls`, `playsInline`, `preload="metadata"` and `muted`, never autoplays inline, and is paused, has its `src` removed and is removed from the page on replacement - or dispose, before its URL is revoked; its `load()` reset runs in the next task, because - WebKit (GStreamer) blocked the page when a still-starting video was reset in the same - task (`replacing a video that just started loading never blocks the page`). + or dispose, before its URL is revoked. A settled video (metadata read, or failed) is then + reset with `load()`; one still starting is not, because WebKit (GStreamer) blocked the + page when such a pipeline was reset (`replacing a video that just started loading never + blocks the page`). - **D63** (status line): one `p.small.spaced-16` in `#local-preview`, for states the concept never shows: "Drop one file at a time.", "Folders cannot be inspected. Choose one image or video file.", "This file type cannot be previewed or inspected here. diff --git a/tests/browser/workspace-inspect.spec.mjs b/tests/browser/workspace-inspect.spec.mjs index f429946..c9844de 100644 --- a/tests/browser/workspace-inspect.spec.mjs +++ b/tests/browser/workspace-inspect.spec.mjs @@ -355,6 +355,8 @@ test('URLs and media end with their owning session', async ({ page }) => { }); await choose(page, localFixture('core-tags.webm', 'video/webm')); await expect(preview(page).locator('video')).toBeVisible(); + // A settled video (metadata read, or failed) is reset with load() when it is released. + await expect.poll(() => preview(page).locator('video').evaluate(node => node.readyState >= 1 || !!node.error)).toBe(true); // Replacement. await choose(page, parityPng('f02.png')); await expect(preview(page).locator('img.local-file-preview')).toBeVisible(); @@ -393,9 +395,9 @@ test('URLs and media end with their owning session', async ({ page }) => { test('replacing a video that just started loading never blocks the page', async ({ context, browserName }) => { test.setTimeout(120000); - // WebKit (GStreamer) blocked the page when a video's pipeline was reset with load() in the same task that - // removed its src, while it was still starting: the next chooser selection never completed. Each fresh page - // replaces its first video as soon as it shows. + // WebKit (GStreamer) blocked the page when a video whose pipeline was still starting was reset with load(): the + // next chooser selection, or the steps after it, never completed. Each fresh page replaces its first video as + // soon as it shows. for (let attempt = 0; attempt < 15; attempt++) { const page = await context.newPage(); try { diff --git a/web/js/workspace/inspect.mjs b/web/js/workspace/inspect.mjs index 29bad24..cb2dd53 100644 --- a/web/js/workspace/inspect.mjs +++ b/web/js/workspace/inspect.mjs @@ -39,13 +39,16 @@ function element(tag, className, text) { /** * Stops a preview from holding its blob: URL: media is paused and its src dropped before the session revokes the URL. - * The media resets with load() in the next task: WebKit (GStreamer) blocked the page when a starting video was reset in - * the same task that removed its src. + * A settled video (metadata read, or failed) is then reset with load() to free its decoder at once. A video still + * starting is not: WebKit (GStreamer) blocked the page when such a pipeline was reset, so it is left to be collected. */ function releaseMedia(node) { if (!node) return; - if (node instanceof HTMLMediaElement) { node.pause(); node.removeAttribute('src'); setTimeout(() => node.load(), 0); } - else node.removeAttribute('src'); + if (node instanceof HTMLMediaElement) { + const settled = node.readyState >= HTMLMediaElement.HAVE_METADATA || node.error !== null; + node.pause(); node.removeAttribute('src'); + if (settled) node.load(); + } else node.removeAttribute('src'); node.remove(); }