diff --git a/frontend/src/pages/Speakers.tsx b/frontend/src/pages/Speakers.tsx index b9b316e..6840a40 100644 --- a/frontend/src/pages/Speakers.tsx +++ b/frontend/src/pages/Speakers.tsx @@ -18,6 +18,8 @@ export default function Speakers() { const audioRef = useRef(null); const paletteRef = useRef(new Map()); const blobUrlRef = useRef(null); + const directRef = useRef(false); + const fallbackTimerRef = useRef(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((resolve, reject) => { - audio.onloadedmetadata = () => resolve(); - audio.onerror = () => reject(new Error("Audio decode failed")); - audio.src = blobUrl; - audio.load(); - }); + 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,10 +142,27 @@ export default function Speakers() { const playAt = (t: number) => { const audio = audioRef.current; - if (!audio || audioLoading) return; - audio.currentTime = t; - const p = audio.play(); - if (p) p.catch(() => {}); + 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) => { @@ -190,7 +244,7 @@ export default function Speakers() { duration={duration} currentTime={currentTime} onSeek={playAt} - disabled={audioLoading || !!audioError} + disabled={!!audioError} /> @@ -244,7 +298,7 @@ export default function Speakers() {