mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 16:39:42 +02:00
fix(file-viewer): make previewed video seekable and stop it on close
Two bugs in the File Viewer's media player, both reproduced in a real browser against an 18MB mp4 before and after the fix. 1. Closing the preview left the video playing. closeFilePreview() only dropped the overlay's `visible` class, which is display:none and nothing else, so the audio kept going with no visible player to pause. Detaching the element is not a fix either: a detached HTMLMediaElement plays on until it is garbage collected. _stopFilePreviewMedia() now pauses, drops src and load()s every media element (also on re-open, where overwriting innerHTML had the same effect), which additionally aborts the in-flight download. 2. The scrub bar was inert. file-raw read the whole file and answered 200 with no Accept-Ranges, so Chrome reported video.seekable as [0, 0] and silently reverted `currentTime = x`; Safari refuses to start such media at all. Raw bodies are now streamed and range-aware: Accept-Ranges: bytes on every response, 206 + Content-Range for a Range request, 416 for one past EOF, and a malformed spec ignored (200) per RFC 9110. Parsing is pure in src/web/http-range.ts. Measured on tmp/codeman-crt-v5-66s.mp4 (18MB, 66.6s): before seekable [0, 0] seek to 56.6s reverted to 3.9s close: still playing after seekable [0, 66.56] seek to 56.6s landed at 60.2s close: paused, NETWORK_EMPTY Range slices are byte-identical to `dd`, the full-file path is byte-identical to the file, and the SVG octet-stream/attachment hardening and the 50MB cap are unchanged (the cap is still checked before the range). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3246,6 +3246,9 @@ Object.assign(CodemanApp.prototype, {
|
||||
|
||||
// Edit mode: reset any prior editor state whenever a preview (re)loads.
|
||||
this._resetFilePreviewEdit();
|
||||
// Stop whatever the previous preview was playing. Overwriting innerHTML
|
||||
// only DETACHES a <video>/<audio>; a detached media element keeps playing.
|
||||
this._stopFilePreviewMedia();
|
||||
|
||||
// Show overlay with loading state
|
||||
overlay.classList.add('visible');
|
||||
@@ -3330,10 +3333,13 @@ Object.assign(CodemanApp.prototype, {
|
||||
bodyEl.innerHTML = `<img src="${data.url}" alt="${escapeHtml(filePath)}">`;
|
||||
footerEl.textContent = `${this.formatFileSize(data.size)} \u2022 ${data.extension}`;
|
||||
} else if (data.type === 'video') {
|
||||
bodyEl.innerHTML = `<video src="${data.url}" controls autoplay></video>`;
|
||||
// playsinline: iOS otherwise hijacks playback into its fullscreen
|
||||
// player, which leaves the overlay behind it and its own close button
|
||||
// as the only way back.
|
||||
bodyEl.innerHTML = `<video src="${escapeHtml(data.url)}" controls autoplay playsinline preload="metadata"></video>`;
|
||||
footerEl.textContent = `${this.formatFileSize(data.size)} \u2022 ${data.extension}`;
|
||||
} else if (data.type === 'audio') {
|
||||
bodyEl.innerHTML = `<audio src="${data.url}" controls autoplay></audio>`;
|
||||
bodyEl.innerHTML = `<audio src="${escapeHtml(data.url)}" controls autoplay preload="metadata"></audio>`;
|
||||
footerEl.textContent = `${this.formatFileSize(data.size)} \u2022 ${data.extension}`;
|
||||
} else if (data.type === 'binary') {
|
||||
const downloadHref = `/api/sessions/${sessionId}/file-raw?path=${encodeURIComponent(filePath)}&download=true`;
|
||||
@@ -3366,9 +3372,36 @@ Object.assign(CodemanApp.prototype, {
|
||||
if (overlay) {
|
||||
overlay.classList.remove('visible');
|
||||
}
|
||||
// The overlay is hidden with display:none, which stops it being PAINTED and
|
||||
// nothing else: a <video>/<audio> inside it keeps playing, keeps its audio
|
||||
// audible and keeps streaming from the server. Closing has to stop it.
|
||||
this._stopFilePreviewMedia();
|
||||
this.filePreviewContent = '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Pause and unload every media element in the preview body, then empty it.
|
||||
*
|
||||
* Removing the element from the DOM is NOT enough — a detached HTMLMediaElement
|
||||
* plays on until it is garbage collected, which is why the X button used to
|
||||
* leave a video audible. pause() stops playback, dropping src + load() aborts
|
||||
* the in-flight network fetch and puts the element back in NETWORK_EMPTY.
|
||||
*/
|
||||
_stopFilePreviewMedia() {
|
||||
const bodyEl = this.$('filePreviewBody');
|
||||
if (!bodyEl) return;
|
||||
for (const media of bodyEl.querySelectorAll('video, audio')) {
|
||||
try {
|
||||
media.pause();
|
||||
media.removeAttribute('src');
|
||||
media.load();
|
||||
} catch (err) {
|
||||
console.warn('Failed to stop preview media:', err);
|
||||
}
|
||||
}
|
||||
bodyEl.innerHTML = '';
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// File Viewer edit mode (issue #212 — docs/file-viewer-edit-plan.md)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user