[GL-RM10] V1.10.0 video juddering/freezes in WebRTC(Native) + mono audio in legacy WebRTC — reverting to V1.9.1

Hi,

I'm reporting audio/video regressions on the Comet Pro (GL-RM10) after upgrading to V1.10.0 (stable). I've reverted to V1.9.1 as a workaround. Details below so it can be triaged.

Setup

  • Device: GL.iNet Comet Pro (GL-RM10)
  • Target device: both Windows 11 and Linux (tested on each)
  • Client: Meta Quest 3 using the built-in Quest Browser app

V1.10.0 behavior:

  • WebRTC (Native): Stereo sound, but in full-screen mode, after variable delay (sometimes after several minutes) there are very noticeable video judders, which can escalate into freezes lasting several seconds, right up to a complete stall (completely frozen image). Switching to non-fullscreen mode and back to fullscreen often fixed the problem for a while, but not always. Mic input was reliable.
  • WebRTC (legacy): Reasonably good video, but not as smooth as 1.9.1; audio came out mono. Mic not tested in this mode.
  • WebRTC (FEC): Tested briefly — behaved similarly to legacy WebRTC (mono, less smooth).

V1.9.1 behavior (both before the upgrade to V1.10.0 and after the downgrade):

  • WebRTC: Smooth video, better even than 1.10.0's WebRTC legacy mode, both in fullscreen and non-fullscreen mode. However, mic input became terribly unreliable: long delay before first audio, or no audio at all, even though the input was detected.

Happy to test fixes or provide more data. Thanks for your work on this.

EDIT: Tested V1.10.0 release4: The judder problem is still present, but I haven’t experienced any complete freezes yet. The workaround of switching to non-fullscreen mode and back to fullscreen still works most of the time.

How do you determine whether an audio track is mono or stereo?

Can you record screen to demonstrate the screen judder issue you mentioned?

Thanks for your quick reply, I want to give you as much information as I can, the workflow I use the Comet Pro for is critical to me. I will also update the post as I discover more.

To your question: how do I determine that an audio track is mono or stereo?

On my target PC running Linux, I use the speaker test tool in GNOME Settings → Sound → Output → Test… , on my target PC running Windows, I use a YouTube stereo test video with the narrator naming which channel plays the test sound. Naturally, in GLKVM I select a Video → EDID setting which supports audio over HDMI.

The Quest plays stereo sounds using a “virtual soundscape” where sound appears to come out of the browser window floating in space, as if it were a big TV with a stereo soundbar. So the test tool or video normally plays the left channel from the left side and the right channel from the right side. Of course, I verified that the Quest can play in stereo on its own by watching a YouTube test video directly in the browser, which works well.

Running firmware version V1.9.1, test sounds play in stereo without issues Left channel matches left side, right channel matches right side, and music has “depth”.

Running firmware version V1.10.0 release4, in WebRTC non-Native mode, the test sounds play in both speakers simultaneously, regardless of the channel the sound is supposed to be coming from, so it sounds as if the sound is coming from the center of the window, and it has no “depth”. In WebRTC(Native) mode, the test sounds play in stereo, even during video stream “freezes”.

As far as I understand, this mono audio playback is called “downmixing to mono”.

Here is a screen recording showing the screen judder issue. The framerate drops to less than 1 frame per second.

GLKVM-stutter_trimmed

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_createtype: offer); the Quest browser answers it.

3. The browser's answer strips stereo=1minptime=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):
    1. In appendStereoToSdp, make the match case-insensitive: e.toLowerCase().includes("opus/48000"). This is the actual bug — it currently never fires.
    2. 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.)

1 Like

I think this is caused by an issue with the implementation of the built-in browser on your Quest 3 itself. I tested all modes and they all correctly output dual-channel audio.

I suggest you test it on a regular computer, as I'm not sure what the browser situation is like inside your Quest 3.

I gave you a firmware with Stereo=1 forcibly configured in WebRTC FEC mode. You can try it and let me know if it works. In version 1.10, we granted the WebRTC Offer permission to the browser. So essentially, it's your browser that prefers mono audio.

Thanks for the build. We tested it in both WebRTC(FEC) and WebRTC (non-Native) mode, and the audio is still mono in both.

Restored to V1.10.0 release4, applied our patch, and stereo audio is working again.

We verified the audio path on the Comet Pro. The device runs a genuine PiKVM ustreamer v6.37 build — both the ustreamer binary and the libjanus_ustreamer.so Janus plugin carry the ustreamer/Maxim Devaev/PiKVM banners and version 6.37, and the SDP offer template embedded in the plugin is byte-identical to upstream v6.37: a=rtpmap:111 OPUS/48000/2 with a=fmtp:111 sprop-stereo=1. The Opus encoder is configured for true stereo (2ch/48 kHz, 128 kbps, fullband, music).

Please refer to: ustreamer/janus/src/rtpa.c at v6.37 · pikvm/ustreamer · GitHub (Note the uppercase “OPUS”.)

In other words, the server side is correct: it offers genuine stereo and declares sprop-stereo=1. The uppercase OPUS in the SDP is the standard/correct codec-name form (RFC 3555/4566), not a defect. The mono/dual-mono result comes from the client not handling the SDP case-insensitively — searching for a lowercase opus/48000 against the canonical OPUS/48000. The correct, general fix on the client is case-insensitive matching, which we've applied and verified produces channels: 2 with stereo=1 in webrtc-internals.

We have a working fix for the video judders / freezes in WebRTC(Native) mode as well, more information to come soon.

With our patch: no more video stutters, even during many hours of use, see screen recording:

GLKVM-no-stutter

Hi everyone,

Following up on this thread — I've dug a bit deeper into the full-screen video judder / freezes that shows up on V1.10.0 (smooth on V1.9.1), and I think I've found the trigger and a workaround. Sharing in case it helps anyone stuck on V1.10.0, including folks on the built-in Quest Browser who can't touch anything.

The symptom. In full-screen the video periodically "freezes then jumps," and it builds up over several minutes. Toggling out of and back into full-screen often buys some time. It is not a network problem — 1080p, near-zero packet loss, good jitter.

What I found. Using chrome://webrtc-internals on the Quest, the inbound-video playout buffer sits at about 5 seconds during the juddering state (43 freezes in ~10 s). That deep buffer tracks a WebRTC thing called the playout-delay extension. The Quest is only offering a standard, spec'd extension (that's correct); something in the negotiation engages it, and once it's negotiated the browser applies a very deep playout delay to a stream that never actually uses it. Native mode advertises no such extension and doesn't judder, which pointed me right at the negotiation as the switch. I confirmed the "before" reproduces on a factory-reset, unpatched V1.10.0, so it's not contamination.

The interesting part. GL.iNet's own source fork already contains a near-identical fix, a playoutDelayHint = 0 line for video, commented out, with a note that it reduces the delay but introduces keyframe stutter. So the team was already this close to solving it; force-zeroing just trades one problem for another.

The workaround that works for me. Instead of zeroing the buffer, a small client-side patch that stops the playout-delay extension from being negotiated drops the buffer from ~5 s to ~524 ms (43 → 2 freezes, a ~10× reduction) with network/decode unchanged.

A few honest caveats:

  • It requires editing the minified web bundle on the Comet Pro, so it breaks on firmware updates and has to be reapplied.
  • This is really a browser/WebRTC quirk (Chrome offers that extension by default and the
    behavior has been murky since ~2019), not a GLKVM-specific bug.

I've sent the full evidence (webrtc-internals exports, an A/B pair, the patch) directly to GL.iNet.

If anyone else is experiencing similar issues, let me know your firmware + mode, the more data points the better. And if you want the exact patch / steps for the Comet Pro, I'm happy to share.