Bug fox reported: late joiner sees host's screen + camera initially.
The moment they share their own screen/camera, they lose the host's
tiles. The host never sees their stuff either. Symptoms point at the
renegotiation flow being raced.
Root cause: SSE delivers offers via async onmessage handlers. JS is
single-threaded but each `await` yields. When a speaker publishes
mic + screen + camera in quick succession, the SFU's addPubToSub
serialises and fires three SSE offers. The browser handler picks up
offer 1 with setRemoteDescription (state → have-remote-offer), then
awaits createAnswer. During that await another onmessage fires for
offer 2 and tries setRemoteDescription — which throws because the PC
is in have-remote-offer state. Offer 2 is dropped, the new tracks for
that publish never register on the browser side. SFU still forwards
RTP for those tracks but the browser has no receiver, so they vanish.
Existing tiles can also stop receiving RTP when the SFU's track set
diverges from the browser's transceiver set.
Fix:
- chain onmessage handlers through a single Promise queue
(`renegQueue = renegQueue.then(...)`) so each renegotiation fully
completes (SRD → answer → SLD → /answer POST) before the next starts.
Browser PC always returns to stable between offers.
- ontrack mic-path now also short-circuits when pubHex === own pubHex.
Without this, a speaker who subscribes to the SFU would attach their
OWN mic to a remote-audio sink and hear themselves.