From dddf35560d42e017999043b81093cfc5185e8a1e Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 6 Nov 2026 15:51:44 -0800 Subject: [PATCH 1/5] quest: Safari and WebSocket publishers wait for WebKit's send estimate Co-Authored-By: Claude Opus 5.5 --- quest/m2/probe/browser-send-estimate.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/quest/m2/probe/browser-send-estimate.md b/quest/m2/probe/browser-send-estimate.md index d9c58207d42..10d9bce0f03 100644 --- a/quest/m2/probe/browser-send-estimate.md +++ b/quest/m2/probe/browser-send-estimate.md @@ -24,15 +24,22 @@ Reason: one rule decided at setup fixes the RTT, the polling, and the counters. What remains once the waits clear: - In stable Chrome (no flag) and Firefox, verify through a shaped link that a publisher's bitrate follows a capacity drop. Reuse the [probe epic](/quest/m2/probe/README.md)'s shaped-uplink harness if it has landed. -- The WebSocket fallback has no estimate whatever the browsers ship, so its publishers still hold their rate; the maintainer decision below covers it. +- The WebSocket fallback has no estimate whatever the browsers ship, so its publishers still hold their rate, as decided below. -Public API: none expected. Wire: none, unless the decision below picks a receiver report. +Decided 2026-11-07 (maintainer, /quest-unblock): Safari publishers and the WebSocket fallback get no estimate before WebKit reports one. + +- ✅ (a) Wait for WebKit, as today. +- (b) A lite-07 receiver report, where the relay sends back the rate it receives from us, which can only lower the estimate while app-limited. +- (c) Estimate locally from how fast the transport drains our writes, which depends on each browser's buffering. + +Reason: Chrome and Firefox need nothing, and (b) and (c) each build an app-layer controller above the browser's own that a later WebKit release makes redundant. + +Public API: none expected. Wire: none. ## Waiting - Chrome: ships `WebTransport.getStats()` with `estimatedSendRate` enabled for all users in 159, per the 2026-11-05 Intent to Ship. Check that stable Chrome's `getStats()` reports a non-null `estimatedSendRate` on a connection that has been sending media for a few seconds. - WebKit: Safari's `getStats()` reports a non-null `estimatedSendRate`. Check the `return_object_property_estimatedSendRate` entry under `api.WebTransport.getStats` in browser-compat-data, or a Safari publisher's `Reservation.rate`. -- Maintainer: whether Safari publishers (and the WebSocket fallback) get an estimate before WebKit reports one. An agent can't pick this because each option changes the wire or adds a congestion estimator above the browser's own. Options: (a) wait for WebKit, as today; (b) a lite-07 receiver report, where the relay sends back the rate it receives from us, which can only lower the estimate while app-limited; (c) estimate locally from how fast the transport drains our writes, which depends on each browser's buffering. Recommendation: (a), since Chrome and Firefox need nothing, and (b) and (c) each build an app-layer controller that a later WebKit release makes redundant. ## Related From cdd5cdaffb2f03b153469b0db9f55235cad86528 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 6 Nov 2026 16:28:59 -0800 Subject: [PATCH 2/5] quest: scope the WebSocket fallback out of the send-estimate goal Co-Authored-By: Claude Opus 5.5 --- quest/m2/probe/browser-send-estimate.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/quest/m2/probe/browser-send-estimate.md b/quest/m2/probe/browser-send-estimate.md index 10d9bce0f03..ca61f4b9524 100644 --- a/quest/m2/probe/browser-send-estimate.md +++ b/quest/m2/probe/browser-send-estimate.md @@ -4,6 +4,8 @@ A browser publisher on shipping Chrome, Firefox, and Safari gets a send-rate estimate, so its bandwidth reservations (`Reservation.rate`) follow the path and `@moq/publish` encoders adapt, instead of holding their ceiling because the estimate is always `undefined`. +The WebSocket fallback is out of scope: it has no browser estimate to read, so its publishers keep holding their rate (decided 2026-11-07, below). + ## Plan Found by [moq-dev/moq#7524](https://github.com/moq-dev/moq/pull/7524): `js/net`'s `Allocator` only reads `WebTransport.getStats()`'s `estimatedSendRate`, which shipping Chromium lacks (it is behind `--enable-blink-features=WebTransportStats`). qmux reports no counters, and WebRTC's `getStats` throws. @@ -32,7 +34,7 @@ Decided 2026-11-07 (maintainer, /quest-unblock): Safari publishers and the WebSo - (b) A lite-07 receiver report, where the relay sends back the rate it receives from us, which can only lower the estimate while app-limited. - (c) Estimate locally from how fast the transport drains our writes, which depends on each browser's buffering. -Reason: Chrome and Firefox need nothing, and (b) and (c) each build an app-layer controller above the browser's own that a later WebKit release makes redundant. +Reason: Chrome and Firefox need nothing. For Safari, (b) and (c) each build an app-layer controller above the browser's own that a later WebKit release makes redundant. For the WebSocket fallback, (b) or (c) would be the only source, since it never reads WebTransport stats; building one waits until fallback adaptation is worth it on its own. Public API: none expected. Wire: none. From 9dfc6b62bbd74210c32d7220f2ab51fbd24b02f6 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 6 Nov 2026 16:44:58 -0800 Subject: [PATCH 3/5] quest: plan native WebSocketStream datagram shedding (8054 follow-up) Co-Authored-By: Claude Opus 5.5 --- quest/m1/README.md | 1 + quest/m1/websocket-stream-datagrams.md | 45 ++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 quest/m1/websocket-stream-datagrams.md diff --git a/quest/m1/README.md b/quest/m1/README.md index c772a56bdbb..42bc5efe160 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -42,6 +42,7 @@ moves what remains of them once their claims merge. - [Datagram tracks](/quest/m1/datagram-tracks/README.md) - a track is datagrams or streams; datagram tracks have their own request and sequence, no ranges, no cache, and map to object ID 0 on moq-transport - [Stream credit by priority](/quest/m1/stream-credit/README.md) - every stream opens through one priority entry point, and a higher-priority group takes over or resets a lower one when credit runs out - [Audio jitter target](/quest/m1/audio-jitter-target/README.md) - the playout target is default in both languages; time-stretched browser playback without steady-state underruns and a native trace replay remain +- [Native WebSocketStream keeps datagrams flowing](/quest/m1/websocket-stream-datagrams.md) - on Chromium's native `WebSocketStream`, QMux over WebSocket sheds a datagram only on a real send backlog, proven in headless Chromium - [Container reads drop backfill](/quest/m1/read-backfill.md) - the JS container consumer drops groups below its read cursor like Rust - [Buffered playback subscribes in ascending order](/quest/m1/buffered-ascending.md) - a buffered `` asks for the oldest group first, so a voice reply's opening arrives first, and audio's head hold shrinks to the floor - [Push-to-talk loses no speech](/quest/m1/push-to-talk/README.md) - an unmute is heard from its first frame with no catalog round trip: mute moves from holding a disabled rendition to suppressed silence with comfort noise, after a route spike; capture delivers frames on a steady cadence diff --git a/quest/m1/websocket-stream-datagrams.md b/quest/m1/websocket-stream-datagrams.md new file mode 100644 index 00000000000..9d8ef1aa9a7 --- /dev/null +++ b/quest/m1/websocket-stream-datagrams.md @@ -0,0 +1,45 @@ +# [S] Native WebSocketStream keeps datagrams flowing + +## Goal + +On Chromium's native `WebSocketStream`, a QMux session over WebSocket sheds a +datagram only when the socket has a real send backlog, not whenever any write +is in flight. A headless-Chromium test writes datagrams back to back on an idle +socket and proves every one arrives, and qmux is fixed if they don't. + +## Plan + +Planned 2026-11-07 (maintainer, /quest-complete) as the follow-up of +[moq-dev/moq#8054](https://github.com/moq-dev/moq/pull/8054). Decision prompts: + +- ✅ Plan it, goal as stated +- Plan it, different goal +- Drop it + +Milestone: + +- ✅ m1 +- m0 +- m4 + +8054 found that the `WebSocketStream` ponyfill reported backpressure during +every write, because its writable used the default one-chunk high-water mark. +`WritableStreamSink.wantsMore()` in `js/qmux/src/scheduler.ts` sheds a datagram +as soon as `writer.desiredSize` reaches 0, so two datagrams written back to +back lost one. 8054 fixed only the ponyfill. When the platform has a native +`WebSocketStream` (Chromium), qmux uses it instead, and its writable may report +the same in-flight `desiredSize` of 0. + +- Measure first: drive a QMux session over native `WebSocketStream` in + headless Chromium, as the ponyfill's case in 8054's `test/datagram-tracks` + does over Node, and record whether `desiredSize` drops to 0 while a write is + in flight with nothing buffered. +- If it does, fix the native path in qmux so that only a real socket backlog + sheds datagrams, and keep the test as the regression. If it doesn't, keep the + test and finish. + +Public API: none expected. Wire: none. + +## Related + +- [Datagram tracks](https://github.com/moq-dev/moq/pull/8054) - the PR whose ponyfill fix this extends to the native API From d128e8f5a5998c0a23825262c03ef433687945eb Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 6 Nov 2026 16:48:36 -0800 Subject: [PATCH 4/5] quest: Safari's send estimate follows its WebTransport gate Co-Authored-By: Claude Opus 5.5 --- quest/m2/probe/browser-send-estimate.md | 5 ++--- quest/m5/safari-webtransport.md | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/quest/m2/probe/browser-send-estimate.md b/quest/m2/probe/browser-send-estimate.md index ca61f4b9524..55898a60348 100644 --- a/quest/m2/probe/browser-send-estimate.md +++ b/quest/m2/probe/browser-send-estimate.md @@ -2,9 +2,9 @@ ## Goal -A browser publisher on shipping Chrome, Firefox, and Safari gets a send-rate estimate, so its bandwidth reservations (`Reservation.rate`) follow the path and `@moq/publish` encoders adapt, instead of holding their ceiling because the estimate is always `undefined`. +A browser publisher on shipping Chrome and Firefox gets a send-rate estimate, so its bandwidth reservations (`Reservation.rate`) follow the path and `@moq/publish` encoders adapt, instead of holding their ceiling because the estimate is always `undefined`. -The WebSocket fallback is out of scope: it has no browser estimate to read, so its publishers keep holding their rate (decided 2026-11-07, below). +The WebSocket fallback is out of scope: it has no browser estimate to read, so its publishers keep holding their rate (decided 2026-11-07, below). That includes every WebKit browser, which `js/net` routes to the fallback (`isWebTransportUserAgentSupported` in `js/net/src/connection/browser.ts`) until [Safari WebTransport](/quest/m5/safari-webtransport.md) lifts the gate; that quest owns Safari's estimate. ## Plan @@ -41,7 +41,6 @@ Public API: none expected. Wire: none. ## Waiting - Chrome: ships `WebTransport.getStats()` with `estimatedSendRate` enabled for all users in 159, per the 2026-11-05 Intent to Ship. Check that stable Chrome's `getStats()` reports a non-null `estimatedSendRate` on a connection that has been sending media for a few seconds. -- WebKit: Safari's `getStats()` reports a non-null `estimatedSendRate`. Check the `return_object_property_estimatedSendRate` entry under `api.WebTransport.getStats` in browser-compat-data, or a Safari publisher's `Reservation.rate`. ## Related diff --git a/quest/m5/safari-webtransport.md b/quest/m5/safari-webtransport.md index 04c018e981c..e3deb862bbf 100644 --- a/quest/m5/safari-webtransport.md +++ b/quest/m5/safari-webtransport.md @@ -30,6 +30,7 @@ tracking since PR #3345 and closes with this. Playwright has a worker `MediaStreamTrackProcessor` or transfers a `MediaStreamTrack`; the harness's capability probe already reports the path taken, and the fixture must take it. +- Safari publishers then pace from `getStats()`'s `estimatedSendRate`, which Safari 26.4 still reports as null, so they hold their rate until WebKit fills it (decided 2026-11-07 in [Browser publishers adapt to their send estimate](/quest/m2/probe/browser-send-estimate.md)). Check the `return_object_property_estimatedSendRate` entry under `api.WebTransport.getStats` in browser-compat-data, or a Safari publisher's `Reservation.rate`, and record which. - Update `doc/lib/js/index.md`. - `parseUserAgent` in `js/net/src/connection/browser.ts` already returns the Safari and iOS versions (bowser left in From d5a24ea8e9b13cf180be86b9d3134823f2c12e17 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 6 Nov 2026 17:04:32 -0800 Subject: [PATCH 5/5] quest: name /quest-plan in the WebSocketStream quest's origin Co-Authored-By: Claude Opus 5.5 --- quest/m1/websocket-stream-datagrams.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quest/m1/websocket-stream-datagrams.md b/quest/m1/websocket-stream-datagrams.md index 9d8ef1aa9a7..75b408af974 100644 --- a/quest/m1/websocket-stream-datagrams.md +++ b/quest/m1/websocket-stream-datagrams.md @@ -9,7 +9,7 @@ socket and proves every one arrives, and qmux is fixed if they don't. ## Plan -Planned 2026-11-07 (maintainer, /quest-complete) as the follow-up of +Planned 2026-11-07 (maintainer, /quest-plan of the follow-ups from the /quest-complete run) as the follow-up of [moq-dev/moq#8054](https://github.com/moq-dev/moq/pull/8054). Decision prompts: - ✅ Plan it, goal as stated