fix(web): room stream propagation and track-switch sync#72
Conversation
Three real-time defects in host-to-listener room streaming: - Listeners joining by link/QR often heard nothing: captureAudioStream threw when the captured MediaStream had zero audio tracks, which happens when capture runs at loadedmetadata before playback starts. That aborted the host stream and no offer was sent. Capture no longer throws on zero tracks; the caller checks for a live track and retries on play (PersistentAudio now also prepares the stream in onPlay). - Loop/replay was not propagated: repeat replayed on the `ended` event, but an element firing `ended` also ends its captureStream() audio track, silencing the room on loop. Repeat now uses the element's native `loop`, which never fires `ended`, so the captured track stays live across the loop. - Clicking next kept playing the old track during a visible delay: selectTrack resolves the new source asynchronously (access check + decrypt) and only then swaps audioSource, so the old source played through the whole gap. selectTrack now pauses the current element immediately; the new source autoplays on load. Also stops peer churn: prepareLocalStream records the captured source and skips re-capture/renegotiation on play/pause/seek, only re-offering listeners on a real source change. Access doctrine unchanged (host satisfies policy, listeners get only the stream). Rationale and manual QA in docs/design/room-stream-sync-fixes.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
✅ Deploy Preview for muzinga ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
@claude please review this PR |
|
Claude finished @knzeng-e's task in 5m 33s —— View job Claude's review
Traced all three symptoms end-to-end against the code (not just the design doc). Symptoms A and B check out; the WebRTC/room-access boundary is untouched as claimed. One regression found in the Symptom C fix. Bug: reselecting the currently-playing track pauses it with no auto-resume
const outgoingAudio = localAudioRef.current;
if (outgoingAudio && !outgoingAudio.paused) {
outgoingAudio.pause();
}This runs unconditionally, before the async access-check/decrypt that resolves the new This isn't a browser-timing case, so it's unit-testable, unlike the QA items in the design doc. Suggest guarding the pause (and/or forcing a reload) when the track being selected is already the active one, e.g. skip when Minor / non-blocking
Not independently verifiedCould not run |
…review) Three non-blocking review items on the room-stream fixes: - captureAudioStream dropped its "no audio track" throw in favor of retry-on- play, but a source that never produces a live track (unsupported codec, genuinely silent asset) would retry forever with no error. Add a bounded counter (nextCaptureAttempt, ceiling MAX_CAPTURE_ATTEMPTS): after a few trackless attempts for the same source, surface "Capture unavailable"; a live track resets it. Normal pre-play trackless attempts are still tolerated. - Extract the capture decisions into a pure module (features/rooms/streamCapture) - shouldReuseCapture (the no-churn guard) and nextCaptureAttempt - and cover them with unit tests (9 cases), since they are plain logic. - Comment the handleEnded repeat branch: it is listener-only now, because the host element loops natively (audio.loop = repeatEnabled) and never fires `ended` while repeat is on. The pause-on-select change is a direct DOM side effect (pause the outgoing element) with no pure logic to extract, so it is left without a bespoke test. 83 unit tests (9 new), lint 0 errors, build/fmt/signaling green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes three reported real-time defects in host-to-listener room streaming. All live in the WebRTC/audio path; none change the room access doctrine (host satisfies the policy, listeners receive only the ephemeral stream). Rationale + manual QA in
docs/design/room-stream-sync-fixes.md.Symptom A - listeners joining by link/QR often hear nothing
captureAudioStreamthrew when the capturedMediaStreamhad zero audio tracks. Capture runs atloadedmetadata, before the element is actually playing, so a transient zero-track capture aborted the whole host stream and no offer was ever sent. Fix: capture no longer throws on zero tracks; the caller checks for a live track and retries onplay(PersistentAudionow also prepares the stream inonPlay). The genuine autoplay-policy case (no gesture) already surfaces a "Start audio" control and is left as-is - that is honest, expected mobile behavior.Symptom B - host changes track or loops, listeners do not hear it
loadedmetadata->prepareLocalStreamre-captures and renegotiates. The old throw-on-zero-tracks could make this silently fail; fixed as in A.endedevent, but when an element firesendeditscaptureStream()audio track ends too, silencing the room on loop. Fix: repeat now uses the element's nativeloop, which seeks back without firingended, so the captured track stays live across the loop.Symptom C - clicking next keeps playing the old track (visible delay)
selectTrackupdates title/cover synchronously but only sets the newaudioSourceafter an async access check + decrypt/fetch, so the old source played through the whole gap while the UI already showed the new track. Fix:selectTrackpauses the current element immediately; the new source autoplays on load.Efficiency: no peer churn on play/pause/seek
prepareLocalStreamnow records whichaudioSourcethe live stream was captured from (capturedSourceRef) and skips re-capture/renegotiation when the source is unchanged and the stream still has a live track. Only a real source change re-offers listeners - removing the teardown/rebuild a naive re-capture on every event would cause.Verification
lint0 errors,test:unit74/74,test:signal30/30,buildclean,fmt:checkclean. ASCII-only.Pre-existing unrelated
contracts/working-tree changes are excluded.