Telemetry on the Firefox Android phone listener exposed the real
mechanism: every <audio>.play() rejects with "play method is not
allowed by the user agent", the audio pool exhausts as the wedge-
recovery cycles, and every fresh element produced fails identically.
Pre-blessing the pool via SILENCE_WAV at entry click does NOT carry
over when srcObject is later swapped to a WebRTC MediaStream —
Firefox Android grants <audio>.play() engagement PER ELEMENT PER
SOURCE, and the gesture window expires before the SFU subscribe
round-trip finishes.
AudioContext has a different model: one resume() inside the user
gesture covers every MediaStreamSource subsequently connected to its
destination. No per-source re-engagement needed.
Implementation:
- attachListenerStreamViaAudioContext(uuid, stream): creates a
MediaStreamSource + GainNode, connects through to
audioCtx.destination
- detachListenerStream(uuid): disconnects on peer-left
- attachSfuTrack: if myRole === 'listener', try the AudioContext
path first; on failure (older browsers) falls through to the
existing <audio> path
- primeAudioOnGesture now CREATES audioCtx if absent (listeners
never grab a mic so it wouldn't otherwise exist) + resumes it
inside the gesture
- peer-left handler also calls detachListenerStream
Listener-only because speakers/cohosts/hosts have an active mic +
setSinkId speaker-picker requirements that still want <audio>
elements. Listeners don't pick speaker output devices and don't
talk — Web Audio is the simpler path that survives the mobile
autoplay regime.
Meter unaffected — startMeter creates its own MediaStreamSource for
analysis (multiple sources per stream is allowed in Web Audio).