From 2a0ecd9b1b087fafb40a6529d6e2020c326bd6f9 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Thu, 22 Oct 2026 13:08:23 -0700 Subject: [PATCH] fix(inspect): never reset a preview video that is still starting Deferring load() to the next task was not enough: CI on b7db4c3 hung WebKit one step later in the fresh-page regression. Reset with load() only a settled video (metadata read, or failed); a starting one is paused, loses its src before the URL is revoked and is removed, then left to be collected. The ownership workflow waits for the video to settle before asserting the reset. --- TESTING.md | 7 ++++--- tests/browser/workspace-inspect.spec.mjs | 8 +++++--- web/js/workspace/inspect.mjs | 11 +++++++---- 3 files changed, 16 insertions(+), 10 deletions(-) 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(); }