fix(inspect): open a video's viewer only once its preview settles (#448)

Replacing a video while both the inline preview and the auto-opened viewer's
video were still starting deadlocked WebKit (D62). The viewer now opens for a
video once its inline preview reports metadata or an error (or after 15 s).
This commit is contained in:
Vito Sansevero
2026-10-28 19:12:53 -07:00
parent fc236291a2
commit 1c7e222a5f
2 changed files with 18 additions and 3 deletions
+5 -1
View File
@@ -2497,7 +2497,11 @@ mapped differences"):
only while that selection is current and no other dialog is open. A new selection
that starts while the viewer shows the previous file closes it first, so no element
holds the old `blob:` URL when the session revokes it (`URLs and media end with their
owning session`); the new file's viewer opens once it is ready. It opens for a selection with no status and for one
owning session`); the new file's viewer opens once it is ready. A video opens the
viewer only once its inline preview has settled (metadata, an error, or D62's 15 s):
replacing a video while both the inline and the viewer's video were still starting
deadlocked WebKit (`a video replaced while still starting is reset once it settles`
hung 2 of 2 runs before, passes 5 of 5 after). It opens for a selection with no status and for one
without a preview URL ("The preview could not be displayed. Metadata and the original
remain available in the viewer."); an unreadable file stays on the page with its
alert, and AVI/WMV keeps its note and the toast (the viewer could not play it and
+13 -2
View File
@@ -217,8 +217,10 @@ export function mountInspect(main, { parentScope, api, sender }) {
if (state.phase === 'ready' && entry && announced !== entry.key) {
announced = entry.key;
// The session selects the record right after it publishes `ready`, so the viewer opens after that, and only
// while this selection is still the current one and no other dialog is open (the viewer is replaced).
if (AUTO_OPEN.has(state.message)) queueMicrotask(() => {
// while this selection is still the current one and no other dialog is open (the viewer is replaced). D62:
// a video opens it once the inline preview has settled, so a replacement while it starts never tears down a
// second starting video in the viewer (that deadlocked WebKit).
if (AUTO_OPEN.has(state.message)) whenSettled(media, () => {
if (scope.signal.aborted || session.entry !== entry || session.state.phase !== 'ready' || document.querySelector('dialog[open]:not(#viewer-dialog)')) return;
openViewer(input);
});
@@ -275,6 +277,15 @@ export function mountInspect(main, { parentScope, api, sender }) {
});
// A drop that misses the area never navigates away from the page while it is mounted.
for (const type of ['dragover', 'drop']) scope.listen(window, type, event => event.preventDefault());
/** Runs `run` after this task, or once `preview`'s video stops starting (metadata, an error, or the D62 limit). */
function whenSettled(preview, run) {
if (!preview || !isStarting(preview.node)) { queueMicrotask(run); return; }
const settled = new AbortController();
const done = () => { settled.abort(); clearTimeout(timer); if (!scope.signal.aborted) run(); };
const timer = setTimeout(done, STARTING_VIDEO_MS);
for (const type of ['loadedmetadata', 'error']) preview.node.addEventListener(type, done, { once: true, signal: settled.signal });
scope.signal.addEventListener('abort', () => { settled.abort(); clearTimeout(timer); }, { once: true, signal: settled.signal });
}
/** The viewer on the current selection, details showing (D155 amends D97); focus returns to `trigger` on close. */
function openViewer(trigger) {
const entry = session.entry;