fix: unblock speaker page audio when loadedmetadata never fires on opus blob
All checks were successful
Build and Push / build (push) Successful in 1m20s
All checks were successful
Build and Push / build (push) Successful in 1m20s
Stop gating the UI on the blob's loadedmetadata (Firefox never fires it for large Ogg/Opus blobs). Release audioLoading right after load(), seed duration from getSession().audio_duration / max turn end, and fall back to the direct audio URL (HTTP Range streaming) via an 8s readyState check or blob onerror. playAt waits for loadeddata/canplay instead of audioLoading.
This commit is contained in:
@@ -18,6 +18,8 @@ export default function Speakers() {
|
||||
const audioRef = useRef<HTMLAudioElement>(null);
|
||||
const paletteRef = useRef(new Map<string, string>());
|
||||
const blobUrlRef = useRef<string | null>(null);
|
||||
const directRef = useRef(false);
|
||||
const fallbackTimerRef = useRef<number | null>(null);
|
||||
|
||||
const [audioLoading, setAudioLoading] = useState(true);
|
||||
const [audioProgress, setAudioProgress] = useState(0);
|
||||
@@ -31,16 +33,35 @@ export default function Speakers() {
|
||||
sp.forEach((s: any) => (n[s.raw_label] = s.display_name || ""));
|
||||
setNames(n);
|
||||
});
|
||||
api.getTurns(id!).then(setTurns);
|
||||
api.getTurns(id!).then((tr) => {
|
||||
setTurns(tr);
|
||||
setDuration((d) => d || tr.reduce((m, t: any) => Math.max(m, t.end || 0), 0));
|
||||
});
|
||||
api.getSession(id!).then((s) => {
|
||||
if (s?.audio_duration) setDuration((d) => (d > 0 ? d : s.audio_duration));
|
||||
});
|
||||
};
|
||||
useEffect(load, [id]);
|
||||
|
||||
const useDirectAudio = () => {
|
||||
const audio = audioRef.current;
|
||||
if (!audio) return;
|
||||
directRef.current = true;
|
||||
if (blobUrlRef.current) {
|
||||
URL.revokeObjectURL(blobUrlRef.current);
|
||||
blobUrlRef.current = null;
|
||||
}
|
||||
audio.src = api.audioUrl(id!);
|
||||
audio.load();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
setAudioLoading(true);
|
||||
setAudioProgress(0);
|
||||
setTotalKnown(false);
|
||||
setAudioError(null);
|
||||
directRef.current = false;
|
||||
let cancelled = false;
|
||||
const url = api.audioUrl(id);
|
||||
|
||||
@@ -52,7 +73,7 @@ export default function Speakers() {
|
||||
const total = parseInt(res.headers.get("Content-Length") || "0", 10);
|
||||
setTotalKnown(total > 0);
|
||||
const reader = res.body!.getReader();
|
||||
const chunks: Uint8Array[] = [];
|
||||
const chunks: BlobPart[] = [];
|
||||
let received = 0;
|
||||
|
||||
while (true) {
|
||||
@@ -71,17 +92,29 @@ export default function Speakers() {
|
||||
const audio = audioRef.current;
|
||||
if (!audio || cancelled) {
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
blobUrlRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
audio.onloadedmetadata = () => resolve();
|
||||
audio.onerror = () => reject(new Error("Audio decode failed"));
|
||||
audio.onerror = () => {
|
||||
if (cancelled) return;
|
||||
if (directRef.current) {
|
||||
setAudioError("Audio decode failed");
|
||||
setAudioLoading(false);
|
||||
} else {
|
||||
useDirectAudio();
|
||||
}
|
||||
};
|
||||
|
||||
audio.src = blobUrl;
|
||||
audio.load();
|
||||
});
|
||||
|
||||
if (!cancelled) setAudioLoading(false);
|
||||
|
||||
fallbackTimerRef.current = window.setTimeout(() => {
|
||||
if (cancelled || directRef.current) return;
|
||||
if (audio.readyState >= 2) return;
|
||||
useDirectAudio();
|
||||
}, 8000);
|
||||
} catch (err: any) {
|
||||
if (!cancelled) {
|
||||
setAudioError(err.message || "Failed to load audio");
|
||||
@@ -92,6 +125,10 @@ export default function Speakers() {
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (fallbackTimerRef.current) {
|
||||
clearTimeout(fallbackTimerRef.current);
|
||||
fallbackTimerRef.current = null;
|
||||
}
|
||||
if (blobUrlRef.current) {
|
||||
URL.revokeObjectURL(blobUrlRef.current);
|
||||
blobUrlRef.current = null;
|
||||
@@ -105,11 +142,28 @@ export default function Speakers() {
|
||||
|
||||
const playAt = (t: number) => {
|
||||
const audio = audioRef.current;
|
||||
if (!audio || audioLoading) return;
|
||||
if (!audio) return;
|
||||
const seek = () => {
|
||||
audio.currentTime = t;
|
||||
const p = audio.play();
|
||||
if (p) p.catch(() => {});
|
||||
};
|
||||
if (audio.readyState >= 1) {
|
||||
seek();
|
||||
return;
|
||||
}
|
||||
const done = () => {
|
||||
audio.removeEventListener("loadeddata", done);
|
||||
audio.removeEventListener("canplay", done);
|
||||
seek();
|
||||
};
|
||||
audio.addEventListener("loadeddata", done);
|
||||
audio.addEventListener("canplay", done);
|
||||
window.setTimeout(() => {
|
||||
audio.removeEventListener("loadeddata", done);
|
||||
audio.removeEventListener("canplay", done);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
const toggleSelected = (label: string) => {
|
||||
const next = new Set(selected);
|
||||
@@ -190,7 +244,7 @@ export default function Speakers() {
|
||||
duration={duration}
|
||||
currentTime={currentTime}
|
||||
onSeek={playAt}
|
||||
disabled={audioLoading || !!audioError}
|
||||
disabled={!!audioError}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -244,7 +298,7 @@ export default function Speakers() {
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => playAt(s.start)}
|
||||
disabled={audioLoading}
|
||||
disabled={!!audioError}
|
||||
className="block text-left text-sm text-ink/60 hover:text-ink w-full truncate disabled:opacity-30 disabled:pointer-events-none"
|
||||
>
|
||||
▶ {s.text}
|
||||
|
||||
Reference in New Issue
Block a user