* fix: prevent probeAudioDuration from hanging on unresolved Promise
The original Promise constructor only accepted `resolve` — no `reject`
callback. The `error` event handler called `resolve(null)` instead of
rejecting. If the Audio element never emits `loadedmetadata` or
`error` (rare browser conditions, GC races, invalid blob URLs), the
Promise hangs forever with no settlement path.
Added:
- 10-second timeout that rejects if neither event fires
- Proper rejection on error event
- `{ once: true }` on listeners to prevent double-invocation
- Dedicated cleanup function
* fix: settle probeAudioDuration with null on error/timeout instead of rejecting
The 10s timeout stays (a media element that never fires any event
genuinely hung this promise forever — verified). But both failure paths
now resolve(null) rather than reject: the only caller, ingestRefAudio,
awaits without a try/catch, and a clip this webview can't decode must
still be accepted — the backend decodes it with ffmpeg (Tauri WebKit
lacks several codecs). Regression test covers all four behaviors,
fail-before verified against the reject() version.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* style: oxfmt pass on format.js
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: mergetest <nizam4103@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
42 lines
1.2 KiB
JavaScript
42 lines
1.2 KiB
JavaScript
export function formatTime(s) {
|
|
const m = Math.floor(s / 60);
|
|
const sec = (s % 60).toFixed(1);
|
|
return `${m}:${sec.padStart(4, '0')}`;
|
|
}
|
|
|
|
// Contract: settles with a number or null, NEVER rejects. null means
|
|
// "duration unknown — keep the file": callers (ingestRefAudio) have no
|
|
// try/catch and must still accept clips this webview can't decode, because
|
|
// the backend decodes them with ffmpeg (Tauri WebKit lacks several codecs).
|
|
// The timeout guards against media elements that never fire any event.
|
|
export async function probeAudioDuration(file) {
|
|
return new Promise((resolve) => {
|
|
const url = URL.createObjectURL(file);
|
|
const a = new Audio();
|
|
const cleanup = () => URL.revokeObjectURL(url);
|
|
const timeout = setTimeout(() => {
|
|
cleanup();
|
|
resolve(null);
|
|
}, 10000);
|
|
a.addEventListener(
|
|
'loadedmetadata',
|
|
() => {
|
|
clearTimeout(timeout);
|
|
cleanup();
|
|
resolve(isFinite(a.duration) ? a.duration : null);
|
|
},
|
|
{ once: true },
|
|
);
|
|
a.addEventListener(
|
|
'error',
|
|
() => {
|
|
clearTimeout(timeout);
|
|
cleanup();
|
|
resolve(null);
|
|
},
|
|
{ once: true },
|
|
);
|
|
a.src = url;
|
|
});
|
|
}
|