audio streaming app plyr.fm
38
fork

Configure Feed

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

fix: /record duration shows Infinity/NaN until first seek (#1255)

root cause is a well-known MediaRecorder quirk: blobs it produces have
no duration written into the webm/ogg container header, so
<audio>.duration reads as Infinity until the browser scans the full
file. the fix is the MDN-documented workaround — on loadedmetadata,
if duration is non-finite, seek to a huge time value (1e101), wait for
the durationchange event where duration becomes real, then reset
currentTime to 0.

changes in frontend/src/routes/record/+page.svelte:

- new handleLoadedMetadata wired to <audio onloadedmetadata={...}>
that triggers the seek-to-EOF workaround when duration is not finite
- playbackProgress guards against non-finite duration (was silently
returning 0 which made the waveform playhead never advance)
- handleSeek guards against non-finite duration so clicks on the
waveform during the brief pre-durationchange window are no-ops
- formatTime treats Infinity / NaN / negative as 0 so the display
never shows "NaN:NaN" or "Infinity:00" in any transient state
- reRecord resets currentTime, duration, and isPlaying so a second
take doesn't inherit playback state from the first

separately fixed at the same time (not in this PR): the prod backend
was missing TRANSCODER_AUTH_TOKEN which caused the transcoder to
reject webm uploads with 401. secret copied from staging and both
relay-api machines rolled successfully — a latent pre-existing bug
unrelated to the frontend fix here.

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
6a14ccd4 91b1496b

+37 -5
+36 -4
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 - const playbackProgress = $derived(duration > 0 ? currentTime / duration : 0); 28 + const playbackProgress = $derived( 29 + Number.isFinite(duration) && duration > 0 ? currentTime / duration : 0 30 + ); 29 31 const timeDisplay = $derived(`${formatTime(currentTime)} / ${formatTime(duration)}`); 30 32 31 33 function handleSeek(ratio: number) { 32 - if (audioEl && duration > 0) { 34 + if (audioEl && Number.isFinite(duration) && duration > 0) { 33 35 audioEl.currentTime = ratio * duration; 34 36 } 35 37 } ··· 40 42 else audioEl.play().catch((e) => console.error('playback failed:', e)); 41 43 } 42 44 45 + // MediaRecorder-produced webm/ogg blobs have no duration written into the 46 + // 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. 50 + function handleLoadedMetadata() { 51 + if (!audioEl) return; 52 + if (Number.isFinite(audioEl.duration)) return; 53 + 54 + const el = audioEl; 55 + const onDurationChange = () => { 56 + if (Number.isFinite(el.duration)) { 57 + el.currentTime = 0; 58 + el.removeEventListener('durationchange', onDurationChange); 59 + } 60 + }; 61 + el.addEventListener('durationchange', onDurationChange); 62 + // jump past any plausible duration — the browser will clamp to real EOF 63 + el.currentTime = 1e101; 64 + } 65 + 43 66 let mediaRecorder: MediaRecorder | null = null; 44 67 let stream: MediaStream | null = null; 45 68 let chunks: Blob[] = []; ··· 49 72 const elapsedDisplay = $derived(formatTime(elapsedSeconds)); 50 73 51 74 function formatTime(seconds: number): string { 52 - const mm = Math.floor(seconds / 60).toString().padStart(2, '0'); 53 - const ss = Math.floor(seconds % 60).toString().padStart(2, '0'); 75 + if (!Number.isFinite(seconds) || seconds < 0) seconds = 0; 76 + const mm = Math.floor(seconds / 60) 77 + .toString() 78 + .padStart(2, '0'); 79 + const ss = Math.floor(seconds % 60) 80 + .toString() 81 + .padStart(2, '0'); 54 82 return `${mm}:${ss}`; 55 83 } 56 84 ··· 160 188 title = ''; 161 189 tags = []; 162 190 elapsedSeconds = 0; 191 + currentTime = 0; 192 + duration = 0; 193 + isPlaying = false; 163 194 uiState = 'idle'; 164 195 } 165 196 ··· 291 322 bind:currentTime 292 323 bind:duration 293 324 src={previewUrl} 325 + onloadedmetadata={handleLoadedMetadata} 294 326 onplay={() => (isPlaying = true)} 295 327 onpause={() => (isPlaying = false)} 296 328 onended={() => (isPlaying = false)}
+1 -1
loq.toml
··· 256 256 257 257 [[rules]] 258 258 path = "frontend/src/routes/record/+page.svelte" 259 - max_lines = 648 259 + max_lines = 680