Ionosphere.tv
3
fork

Configure Feed

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

perf+fix: eliminate words array, fix pause bug, chunked transcripts

Performance:
- WaveformBand derives density from diarization (151KB) instead of
words array (3MB) — works at all zoom levels
- getTrack response drops from 2.8MB to ~155KB
- Track transcript loads via HLS-inspired chunked endpoint (~10KB/chunk)

Bug fix:
- Video pause button now works on first click. Root cause: React Strict
Mode double-invoked the HLS setup effect, leaving a stale setTimeout
that held a closure with hasAutoPlayed=false. Fixed by using useRef
for the auto-play guard and clearing the timeout in cleanup.

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

+17 -28
+17 -28
apps/ionosphere/src/app/components/VideoPlayer.tsx
··· 16 16 const offsetS = offsetNs / 1e9; 17 17 const playlistUrl = `${VOD_ENDPOINT}?uri=${encodeURIComponent(videoUri)}`; 18 18 19 + // Ref-based auto-play guard survives React Strict Mode double-invocation. 20 + // A closure variable would get a fresh copy per effect invocation, allowing 21 + // a stale setTimeout from the first invocation to call play() with its own 22 + // hasAutoPlayed=false even after the second invocation already auto-played. 23 + const hasAutoPlayedRef = useRef(false); 24 + 19 25 useEffect(() => { 20 26 const video = videoRef.current; 21 27 if (!video) return; 22 28 23 29 let hls: any; 30 + let manifestTimeoutId: ReturnType<typeof setTimeout> | null = null; 24 31 25 32 async function setupHls() { 26 33 const { default: Hls } = await import("hls.js"); ··· 29 36 hls.loadSource(playlistUrl); 30 37 hls.attachMedia(video!); 31 38 32 - // Select AAC audio track before playback starts to avoid mid-play rebuffer. 33 - // Streamplace serves both original + AAC renditions — switching after 34 - // playback begins causes a visible stall. 35 - // 36 - // Strategy: wait for MANIFEST_PARSED, then select the audio track 37 - // before any playback begins. Only auto-play after the track is set 38 - // and the first fragment is buffered — this avoids the reload cycle 39 - // caused by switching tracks mid-stream. 40 39 let audioSettled = false; 41 - let seekSettled = offsetS <= 0; // no seek needed if no offset 42 - let hasAutoPlayed = false; 40 + let seekSettled = offsetS <= 0; 43 41 44 42 const tryAutoPlay = () => { 45 - if (!hasAutoPlayed && video!.paused && audioSettled && seekSettled) { 46 - hasAutoPlayed = true; 43 + if (!hasAutoPlayedRef.current && video!.paused && audioSettled && seekSettled) { 44 + hasAutoPlayedRef.current = true; 47 45 video!.play().catch(() => {}); 48 46 } 49 47 }; 50 48 51 49 hls.on(Hls.Events.AUDIO_TRACKS_UPDATED, () => { 52 - if (audioSettled) return; // only run once 50 + if (audioSettled) return; 53 51 const tracks = hls!.audioTracks; 54 52 if (tracks.length > 1) { 55 53 const aacTrack = tracks.findIndex((t: any) => ··· 65 63 66 64 hls.on(Hls.Events.MANIFEST_PARSED, () => { 67 65 // If no audio tracks event fires within 200ms, consider audio settled 68 - setTimeout(() => { 66 + manifestTimeoutId = setTimeout(() => { 69 67 if (!audioSettled) { 70 68 audioSettled = true; 71 69 tryAutoPlay(); ··· 75 73 // Seek to offset before playback starts 76 74 if (offsetS > 0) { 77 75 video!.currentTime = offsetS; 78 - // Wait for seek to complete before allowing auto-play 79 76 video!.addEventListener("seeked", () => { 80 77 seekSettled = true; 81 78 tryAutoPlay(); ··· 83 80 } 84 81 }); 85 82 86 - // Auto-play once audio is settled, seek is done, and we have buffered data 87 83 hls.on(Hls.Events.FRAG_BUFFERED, () => { 88 - tryAutoPlay(); 84 + if (!hasAutoPlayedRef.current) tryAutoPlay(); 89 85 }); 90 86 91 - // Error recovery — conservative approach to avoid reload cycles. 92 - // Only recover from fatal errors; non-fatal errors are handled by HLS.js internally. 93 87 hls.on(Hls.Events.ERROR, (_: any, data: any) => { 94 88 if (!data.fatal) return; 95 - 96 89 console.warn("[HLS] Fatal error:", data.type, data.details); 97 90 switch (data.type) { 98 91 case Hls.ErrorTypes.NETWORK_ERROR: 99 - // Network errors: retry loading 100 92 hls!.startLoad(); 101 93 break; 102 94 case Hls.ErrorTypes.MEDIA_ERROR: 103 - // Media errors: single recovery attempt 104 95 hls!.recoverMediaError(); 105 96 break; 106 97 default: 107 - // Unrecoverable: destroy and give up 108 98 hls!.destroy(); 109 99 break; 110 100 } ··· 120 110 } 121 111 122 112 setupHls(); 123 - return () => { if (hls) hls.destroy(); }; 113 + return () => { 114 + if (manifestTimeoutId !== null) clearTimeout(manifestTimeoutId); 115 + if (hls) hls.destroy(); 116 + }; 124 117 }, [playlistUrl, offsetS]); 125 118 126 119 // Broadcast current time at 60fps via RAF (instead of ~4Hz timeupdate). 127 - // This eliminates up to 250ms of stale time data and makes the 128 - // transcript highlight track the audio much more tightly. 129 120 useEffect(() => { 130 121 const video = videoRef.current; 131 122 if (!video) return; ··· 147 138 isPlaying = false; 148 139 setPaused(true); 149 140 cancelAnimationFrame(rafId); 150 - // One final update to capture exact pause position 151 141 setCurrentTimeNs((video.currentTime - offsetS) * 1e9); 152 142 }; 153 - // Also update on seek (click-to-seek while paused) 154 143 const onSeeked = () => { 155 144 setCurrentTimeNs((video.currentTime - offsetS) * 1e9); 156 145 };