We have found a working fix for the downmixed to mono audio in WebRTC mode. For some yet unknown reason, WebRTC(FEC) mode still outputs mono audio.
After connecting my Quest 3 via ADB over Wi-Fi and setting up Quest Browser debugging, my AI agent and I collaborated on finding out what was happening with audio transport, by opening the chrome://webrtc-internals page in Quest Browser, using a remote Developer Tools connection to the (Chrome-based) Quest Browser, and examining files on the Comet Pro device itself.
In short: the problem with mono audio in WebRTC (non-Native) mode is due to how the browser and the Comet Pro negotiate how audio should be streamed: because of a case sensitive matching bug, the link never negotiates stereo audio, and the browser plays back downmixed mono audio on both channels instead.
I’ve confirmed the fix works by manually patching the JavaScript bundle file on the device, refreshing the browser tab, and playing some music on the target PC.
In my AI agent’s own words:
Investigation complete. Here's the definitive picture.
Authoritative current state (from chrome://webrtc-internals, codec stats)
| Stream |
ssrc |
a=fmtp |
channels |
Result |
| Inbound (what Quest hears) |
1594680898 |
minptime=10;useinbandfec=1 |
2 |
dual-mono — no stereo=1 |
| Outbound (Quest mic) |
3225893482 |
sprop-stereo=1 |
2 |
stereo |
a=rtpmap (both) |
— |
OPUS/48000/2 |
2 |
declares 2ch |
The rtpmap says 2 channels but the fmtp lacks stereo=1 (RFC 6464), so both channels carry the identical mono signal. That's the mono you hear.
Root cause — stereo is offered but dropped at the browser's answer
1. The source is genuinely stereo. VM audio rides HDMI, captured by the Janus ustreamer acap from multi_hdmi_input (ALSA, 2ch/48 kHz). The encoder is configured for stereo: OPUS_SET_SIGNAL(OPUS_SIGNAL_MUSIC), OPUS_SET_MAX_BANDWIDTH(OPUS_BANDWIDTH_FULLBAND), 128 kbps.
2. The plugin correctly offers stereo. libjanus_ustreamer.so (identical in usr and rom) builds the media with a=fmtp:%u sprop-stereo=1 + OPUS/48000/2. The plugin makes the offer (us_sdp_create → type: offer); the Quest browser answers it.
3. The browser's answer strips stereo=1 → minptime=10;useinbandfec=1. Neither the plugin nor Janus emits that fmtp, so it originates from the browser's answer generation.
4. The app's only stereo-enforcement mechanism is broken. The app's appendStereoToSdp() is the sole thing meant to force stereo, and it has a case-sensitivity bug:
appendStereoToSdp(e){
const t=e.split("\r\n"),
a=t.find((e=>e.startsWith("a=rtpmap:")&&e.includes("opus/48000"))); // ← lowercase
...
}
Every SDP in this session uses OPUS/48000 (uppercase). String.includes is case-sensitive, so the search never matches and the function returns early — it never stamps stereo=1 onto the inbound answer. Confirmed: the bundle contains zero references to channelCount/numberOfChannels/.channels, and the getUserMedia constraint has no channelCount.
So: source is stereo, plugin offers stereo, but the app's SDP-munging silently no-ops and the browser answers with a mono fmtp → dual-mono.
Fix directions
- Client (fastest, no server access):
- In
appendStereoToSdp, make the match case-insensitive: e.toLowerCase().includes("opus/48000"). This is the actual bug — it currently never fires.
- Add
channelCount: 2 to the getUserMedia constraint so Chrome's local track is stereo and it honors the stereo negotiation for the whole m=audio section.
- Server-side (Janus/VM): Not the problem here — the plugin already negotiates stereo correctly. The loss is entirely on the Quest's answer side.
(Written in collaboration with Ornith 1.5 35B A3B using the Pi coding agent.)