audio streaming app plyr.fm
38
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: /record duration fallback so upper bound is never 0 in preview (#1256)

the previous fix covered duration=Infinity (Firefox behavior) but not
duration=0 (what Chrome reports for MediaRecorder webm blobs until
playback actually reaches EOF). so the user still saw "00:07 / 00:00"
during playback, with the upper bound only updating to the real value
at the end of the track.

two changes:

1. broaden the seek-to-EOF workaround to fire for any non-positive
or non-finite duration (0, NaN, Infinity), not just Infinity. new
isUsableDuration helper so the logic reads the same in both the
early-return and the durationchange listener.

2. add a capturedDuration fallback: in finalizeRecording we snapshot
the live-tick elapsedSeconds counter, and a new effectiveDuration
derived prefers the audio element's real duration once it becomes
usable, else falls back to the captured seconds. this means the
display shows a correct (1s-resolution) upper bound immediately on
entering preview, even during the brief window before the browser
has finished scanning the blob. the seek workaround still fires in
parallel so the audio element eventually reports its own
sub-second-accurate duration and we use that.

effectiveDuration also feeds playbackProgress and handleSeek, so the
waveform playhead sweeps correctly and clicks seek to the right
position even during the fallback window. capturedDuration is reset
to 0 in reRecord along with the other playback state.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

authored by

nate nowack
Claude Opus 4.6 (1M context)
and committed by
GitHub
e8f542ce 6a14ccd4

+32 -11
+31 -10
frontend/src/routes/record/+page.svelte
··· 25 25 let currentTime = $state(0); 26 26 let duration = $state(0); 27 27 let isPlaying = $state(false); 28 + // captured at recording stop-time from the live-tick counter. gives us a 29 + // correct (1s-resolution) display duration immediately, before the browser 30 + // has finished scanning the blob for its real length. we prefer the audio 31 + // element's duration once it becomes a positive finite number, but fall 32 + // back to this so the UI never shows "0:00" for a valid recording. 33 + let capturedDuration = $state(0); 34 + const effectiveDuration = $derived( 35 + Number.isFinite(duration) && duration > 0 ? duration : capturedDuration 36 + ); 28 37 const playbackProgress = $derived( 29 - Number.isFinite(duration) && duration > 0 ? currentTime / duration : 0 38 + effectiveDuration > 0 ? currentTime / effectiveDuration : 0 39 + ); 40 + const timeDisplay = $derived( 41 + `${formatTime(currentTime)} / ${formatTime(effectiveDuration)}` 30 42 ); 31 - const timeDisplay = $derived(`${formatTime(currentTime)} / ${formatTime(duration)}`); 32 43 33 44 function handleSeek(ratio: number) { 34 - if (audioEl && Number.isFinite(duration) && duration > 0) { 35 - audioEl.currentTime = ratio * duration; 45 + if (audioEl && effectiveDuration > 0) { 46 + audioEl.currentTime = ratio * effectiveDuration; 36 47 } 37 48 } 38 49 ··· 42 53 else audioEl.play().catch((e) => console.error('playback failed:', e)); 43 54 } 44 55 56 + function isUsableDuration(d: number): boolean { 57 + return Number.isFinite(d) && d > 0; 58 + } 59 + 45 60 // MediaRecorder-produced webm/ogg blobs have no duration written into the 46 61 // container header — the recorder streams output without knowing the final 47 - // length. the browser reports `audioEl.duration === Infinity` until you 48 - // force a full seek to EOF, at which point it scans the file and emits a 49 - // `durationchange` with the real value. this is the MDN-documented fix. 62 + // length. different browsers surface this as Infinity (Firefox), 0 (some 63 + // Chrome versions), or NaN until the file is scanned to EOF. the 64 + // MDN-documented fix is to seek to a huge time value; the browser clamps 65 + // to real EOF, scans the file, and emits a `durationchange` with the real 66 + // duration, at which point we reset currentTime to 0. 50 67 function handleLoadedMetadata() { 51 68 if (!audioEl) return; 52 - if (Number.isFinite(audioEl.duration)) return; 69 + if (isUsableDuration(audioEl.duration)) return; 53 70 54 71 const el = audioEl; 55 72 const onDurationChange = () => { 56 - if (Number.isFinite(el.duration)) { 73 + if (isUsableDuration(el.duration)) { 57 74 el.currentTime = 0; 58 75 el.removeEventListener('durationchange', onDurationChange); 59 76 } 60 77 }; 61 78 el.addEventListener('durationchange', onDurationChange); 62 - // jump past any plausible duration — the browser will clamp to real EOF 79 + // jump past any plausible duration — the browser clamps to real EOF 63 80 el.currentTime = 1e101; 64 81 } 65 82 ··· 171 188 previewBlob = blob; 172 189 if (previewUrl) URL.revokeObjectURL(previewUrl); 173 190 previewUrl = URL.createObjectURL(blob); 191 + // capture the elapsed tick count as a fallback duration — shown until 192 + // the audio element reports a real positive finite duration 193 + capturedDuration = elapsedSeconds; 174 194 const now = new Date(); 175 195 const pad = (n: number) => String(n).padStart(2, '0'); 176 196 title = `recording ${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`; ··· 190 210 elapsedSeconds = 0; 191 211 currentTime = 0; 192 212 duration = 0; 213 + capturedDuration = 0; 193 214 isPlaying = false; 194 215 uiState = 'idle'; 195 216 }
+1 -1
loq.toml
··· 256 256 257 257 [[rules]] 258 258 path = "frontend/src/routes/record/+page.svelte" 259 - max_lines = 680 259 + max_lines = 701