If the host's mic publish lands after the listener's initial DJ auto-
enrol attempt, the 404 from the SFU was permanent — no event re-
triggered enrol. 4s interval re-checks every speaker and starts streams
that are still missing. Stops on transition out of listener.
Replaces the regressive tap-anywhere-to-resume queue with
primeAudioOnGesture(): the entry-button click handler plays a 1-frame
silent WAV through a hidden <audio>, blessing the page for audio.
Auto-enrolled DJ streams then play() without further user gesture.
No more "tap anywhere" message; no more flip-flop on stalled.
Calls a.load() inside the gesture before a.play() so the retry isn't
no-op'ing on an aborted element. Adds 'click' to the listened events
since Firefox Android only grants gesture-activation on click in some
configs. Logs retry success/failure so we know what's happening.
When mobile Firefox/Safari blocks audio.play() on auto-enrolled DJ
streams, queue the elements and retry on the first pointerdown/
touchstart anywhere on the page. WebRTC stays unmuted as fallback
so the listener isn't silent in the meantime.
Three coupled fixes to the DJ-stream toggle path:
- await setSinkId() before setting .src so it doesn't race + abort
the in-flight media load
- drop the 'stalled' handler that was ping-ponging WebRTC ↔ stream
on every transient mobile-cellular buffer pause
- make startStream idempotent on the same URL so a peer-joined
auto-enrollment race can't reset .src and self-abort
Plus preload="auto" and playsInline on the dynamic <audio> for
mobile compat. Fixes "no music on phone stream stalled" and the
"autoplay blocked: aborted at user's request" log spam from host
self-monitor clicks.
Moves the ◉/○ DJ-stream button out of the bottom mod-actions stack
into the dedicated `strm` grid column on the top row (next to the
mic icon). No more extra sub-row per speaker for the self-monitor or
host-controlled stream toggle — fixes the vertical scroll regression
when many members were in the room.
Swap grid-auto-flow:column to grid-template-columns:repeat(auto-fit,max-content)
so the now-5-button row wraps to a second line on narrow panels instead
of overflowing horizontally.
DJ can preview the ~2s delayed Ogg/Opus stream of their own mic to
confirm what listeners hear. Closed headphones recommended; open
speakers + music mode = feedback (title attr warns).
Each speaker row carries a stream toggle that swaps their audio from
WebRTC subscribe (~700ms latency, glitchy under loss) to a plain
HTTP <audio> pulling Ogg/Opus from the SFU's new /stream endpoint
(~2s latency, browser-managed jitter buffer, bulletproof for DJ
playback). Requires matching proxy.unturf.com SFU build.
Choppy persisted after every other dial-back. The only remaining new
Chrome-publisher params were the fullband pin and the 320k bump. Drop
both. Chrome publisher's SDP now back to the pre-feature shape
(stereo + FEC + 256k, no maxplaybackrate / sprop-maxcapturerate / cbr).
Kept: Firefox SDP regex fix, UA-gated NACK, speaker picker.
Choppy persisted after NACK rollback. Dial back to 320k (transparent
threshold, much less stress on uploads alongside screen+camera). Drop
a=maxptime:120 — receivers + 700ms playout buffer don't handle larger
frames well. Fix the SDP regex that silently no-op'd on Firefox
publishers (Firefox's Opus fmtp lacks minptime=10 — the matcher
required it). Firefox publishers will now actually carry the music
codec params.
RTCRtpSender.getCapabilities probe decides whether to advertise NACK
in our Opus offers. Chromium publishes it (responds to retransmits),
Firefox/Safari omit (can't respond). Receivers honor only what the
sender promised — mixed-browser rooms stay clean, same-browser rooms
get the full reactive recovery.
Chrome listeners requested NACK retransmits that Firefox publishers
never replied to (Firefox never implemented the audio NACK responder).
Chrome's jitter buffer waited for recovery that never arrived, then
skipped. useinbandfec + 700ms playout buffer remain.
Codec headroom + loss resilience without DSP filters:
- Opus 256k → 510k (spec ceiling) on mic SFU + mesh + screen + game audio
- Fullband pin: maxplaybackrate / sprop-maxcapturerate = 48000, cbr=0
- Audio NACK feedback on Opus rtpmap (reactive packet recovery)
- a=maxptime:120 advertises tolerance for larger peer frames
Playback chain:
- New speaker-output selector (setSinkId) routes peer audio to a
chosen sink (studio monitors / external DAC). Hidden on Safari.
Hard refresh required for the new speaker-picker row to appear.
Mirror of zebra-report 1dde43f + proxy.unturf.com edb1009. Closes
the 'backgrounded tab squats in room' gap that WS-level pings
can't catch (browsers auto-pong below JS).
Mirror of zebra-report 740ed2b + proxy.unturf.com 0733d61.
- moderation: split single 'boot' into 'kick' (rejoinable) and 'ban'
(blocks pubkey). Server adds matching kick wire-type alongside
renamed boot→ban.
- tab-close lifecycle: pagehide handler fires a 'bye' so closing a
tab triggers a strong-leave (peer-left + SFU evict immediately)
instead of waiting 8s for the hiccup grace. Fix for the case
where Will's phone-tab close left audio leaking until the host
manually kicked him.
Mirror of zebra-report fb7228b. Will refreshed; the supplant ontrack
hit autoplay block on srcObject swap. Fresh <video> dodges the
mid-life play() rejection.
Mirror of zebra-report 3ca9042 + proxy.unturf.com bc04362.
- moderation: boot failures now log to the moderator's page instead
of looking like the button did nothing (stale uuid case).
- diagnostic: watchFirstFrame fires 'still black after 2500ms — no
keyframe?' if a fresh SFU video tile never decodes a first frame.
- proxy-side companion: longer keyframe-burst on subscriber attach
so a dropped PLI no longer leaves the tile permanently black.