fix(sfu): enforce bounded root invariants (#179)
This commit is contained in:
1 parent
7f32dd16df
commit
12c686ea5e
20 files changed
+323
-37
No files matched your search
@@ -202,7 +202,16 @@ Targeted tests cover the revision controller, protocol authorization, relay
|
||||
capacity and deterministic admission rescue, ordered break-before-make client
|
||||
transitions, stale asynchronous work, server-restart
|
||||
resynchronization, one-shot credential recovery, peer failback, and optional
|
||||
standby warming. Chrome 151/LiveKit 1.13.5 localhost runs established route
|
||||
standby warming. The 2026-08-22 `gate:sfu-root-invariants` preflight hard-checks
|
||||
that controller state and token allowlists accept no more than two central
|
||||
roots, one active Host SFU publication consumes one of the Host's two outbound
|
||||
media edges, and a committed zero-descendant root remains selected across
|
||||
reauthentication. A separate client transition check keeps its active subscriber
|
||||
when a newer same-kind assignment still has no children. The benchmark evaluator
|
||||
also rejects a third root or third Host media edge. This in-process evidence
|
||||
starts neither LiveKit nor a browser and therefore proves no packet flow,
|
||||
bandwidth adaptation, or resource cost. Chrome 151/LiveKit 1.13.5 localhost runs
|
||||
established route
|
||||
transition, resumed decoding, and the two-edge bound. Their elapsed times are
|
||||
diagnostics only and do not satisfy the regional recovery target. Public
|
||||
transport/audio/load/browser checks remain pending. The direction is accepted,
|
||||
|
||||
@@ -162,8 +162,11 @@ evidence windows remain limited to topology eligibility and diagnostics:
|
||||
simulcast/send encodings, leaves each LiveKit root's ceiling at `HIGH`, and
|
||||
first tests built-in per-subscriber SFU bandwidth adaptation on
|
||||
zero-descendant leaf viewers. Start with deterministic two-layer publication
|
||||
and Dynacast off; its pinned cumulative behavior can later be measured as the
|
||||
equivalent always-on case. Screener's
|
||||
and Dynacast off. Dynacast controls publisher-layer pausing from aggregated
|
||||
subscriber demand; disabling it does not disable server-side per-subscriber
|
||||
BWE or turn a `HIGH` ceiling into a guaranteed received layer. Its pinned
|
||||
cumulative behavior can later be measured as the equivalent always-on case.
|
||||
Screener's
|
||||
subscriber does not attach a `RemoteTrack`, so LiveKit `adaptiveStream` is not
|
||||
directly usable without changing that ownership; it is not required for the
|
||||
SFU bandwidth-adaptation candidate. Before default enablement, a separate
|
||||
@@ -175,12 +178,17 @@ evidence windows remain limited to topology eligibility and diagnostics:
|
||||
P2P/`HIGH` path is unchanged, no third layer appears, the expected layer is
|
||||
actually received, and hardware encoder, game FPS/p1 low, CPU/GPU, interval
|
||||
encode cost, host upload, and `HIGH+LOW` bytes fit budget. Every active SFU
|
||||
publication now configures exactly two ordered `q,h` encodings, leaves
|
||||
Dynacast at its default `false`, and sets each subscriber's ceiling to
|
||||
`HIGH`; the two-layer publication is not behind a separate quality flag.
|
||||
Per-subscriber BWE, hardware cost, and root-with-children behavior remain
|
||||
unverified, so no runtime performance claim follows. If the always-on cost
|
||||
fails, test manual standard sender activation/deactivation next;
|
||||
publication now configures exactly two ordered `q,h` encodings, constructs
|
||||
`Room({ dynacast: false })`, disables backup-codec publication, and sets each
|
||||
subscriber's ceiling to `HIGH`; the two-layer publication is not behind a
|
||||
separate quality flag. Client 2.22.0 also defaults Dynacast to `false`, but
|
||||
the explicit option prevents silent option drift and `backupCodec: false`
|
||||
prevents the pinned multi-codec path from enabling it automatically.
|
||||
The 2026-08-22 executable preflight verifies this configuration, retains a
|
||||
zero-child subscriber, and rejects excess central roots. Per-subscriber BWE,
|
||||
actual layer forwarding, hardware cost, and root-with-children behavior
|
||||
remain unverified, so no runtime performance claim follows. If the always-on
|
||||
cost fails, test manual standard sender activation/deactivation next;
|
||||
custom/native dual encoding follows only if built-in and manual standard
|
||||
primitives fail.
|
||||
3. **Current Web/LiveKit SVC: rejected
|
||||
@@ -296,7 +304,7 @@ Negative:
|
||||
|
||||
## References
|
||||
|
||||
Primary sources checked 2026-08-19:
|
||||
Primary sources checked through 2026-08-22:
|
||||
|
||||
- [W3C WebRTC Statistics](https://www.w3.org/TR/webrtc-stats/)
|
||||
- [W3C Media Capture and Streams](https://www.w3.org/TR/mediacapture-streams/)
|
||||
@@ -310,8 +318,12 @@ Primary sources checked 2026-08-19:
|
||||
- [Firefox WebRTC-SVC implementation status](https://bugzilla.mozilla.org/show_bug.cgi?id=1571470)
|
||||
- [WebKit `RTCRtpEncodingParameters` WebIDL](https://github.com/WebKit/WebKit/blob/main/Source/WebCore/Modules/mediastream/RTCRtpEncodingParameters.idl)
|
||||
- [LiveKit video simulcast and Dynacast](https://docs.livekit.io/transport/media/advanced/)
|
||||
- [LiveKit selective subscription](https://docs.livekit.io/transport/media/subscribe/)
|
||||
- [LiveKit client 2.22.0 room defaults](https://github.com/livekit/client-sdk-js/blob/v2.22.0/src/room/defaults.ts)
|
||||
- [LiveKit client 2.22.0 room and Dynacast options](https://github.com/livekit/client-sdk-js/blob/v2.22.0/src/options.ts)
|
||||
- [LiveKit client 2.22.0 room option merge](https://github.com/livekit/client-sdk-js/blob/v2.22.0/src/room/Room.ts)
|
||||
- [LiveKit client 2.22.0 SVC defaults](https://github.com/livekit/client-sdk-js/blob/v2.22.0/src/room/track/options.ts)
|
||||
- [LiveKit client 2.22.0 screen-share SVC override](https://github.com/livekit/client-sdk-js/blob/v2.22.0/src/room/participant/LocalParticipant.ts)
|
||||
- [LiveKit client 2.22.0 screen-share SVC and publish-option handling](https://github.com/livekit/client-sdk-js/blob/v2.22.0/src/room/participant/LocalParticipant.ts)
|
||||
- [LiveKit client 2.22.0 SVC encoding construction](https://github.com/livekit/client-sdk-js/blob/v2.22.0/src/room/participant/publishUtils.ts)
|
||||
- [LiveKit client 2.22.0 subscriber quality control](https://github.com/livekit/client-sdk-js/blob/v2.22.0/src/room/track/RemoteTrackPublication.ts)
|
||||
- [LiveKit server 1.13.5 per-subscriber layer application](https://github.com/livekit/livekit/blob/v1.13.5/pkg/rtc/subscribedtrack.go)
|
||||
|
||||
@@ -10,7 +10,7 @@ Last updated: 2026-08-22
|
||||
- Minimize server bandwidth. Ordinary peer ICE is STUN-only. Failure tries restart, rebuild, alternate peer and bounded SFU/UDP roots; only then may the controller authorize short-lived TURN for one selected exceptional edge before clear failure. TURN is transport, not topology.
|
||||
- Current release policy is Host2/ordinary Browser Viewer1. Server authority derives the limit from authenticated role; old or malicious Viewer advertisements 2/3 remain effective1. Deployment config defaults to two and accepts only 1/2 to tighten, never lift, these limits. Web assignment execution independently truncates physical children to Host2/Viewer1 as defense-in-depth. Active/provisional/selected physical overlays count, as does Host SFU publication; ordinary upstream receive is free. A deployment limit of one promotes an occupied Host branch root before reserving its SFU publication slot. UA/visibility do not change capacity. SFU normally feeds one or two roots that retain peer descendants; its root cap remains separate. Dynamic evidence-driven capacity still requires its own acceptance.
|
||||
- Two-tree packet/layer striping may reduce endpoint upload toward one stream bitrate, but needs a bounded multi-parent, loss, sync, churn, and latency experiment; it is not in the current full-stream chains.
|
||||
- Direct/peer keeps per-PC stock GCC. Active SFU publishes exactly `q,h` at a `HIGH` ceiling; built-in BWE may forward `LOW`. The zero-descendant quality/resource gate is next; if it passes, no app media selector is built. Explicit quality, manual activation, and custom/native remain fallbacks.
|
||||
- Direct/peer keeps per-PC stock GCC. Active SFU explicitly disables Dynacast, publishes exactly `q,h`, and gives each subscribed root a `HIGH` ceiling; built-in BWE may still forward `LOW`. The source gate proves roots <=2, Host-publication edge accounting, and retention of a zero-child subscription. Actual shaped BWE and resource evidence is next; if it passes, no app media selector is built. Explicit quality, manual activation, and custom/native remain fallbacks.
|
||||
- A later explicit quality fallback evacuates children under a generation guard first. Autonomous BWE enters `suspect`; confirmation evacuates children, and no confirmed `FALLBACK` parent remains. Root-with-children impact is a default-on gate; capacity returns after longer recovery plus cooldown. Self-report alone never triggers it.
|
||||
- Site access protects room creation, Host publication, and every code-only Viewer entry. A valid room-scoped fragment grant bypasses that site gate; otherwise site access is checked before public-watch or a private room password. Persist neither raw credential; keep accounts, ACLs, users, and session tables out.
|
||||
- Prefer primary sources, maintained implementations, target-device evidence and sanitized production stats. Ordinary-PC synthetic runs prove only correctness/interoperability; never use them to calibrate performance or block a reversible standard API. Keep accepted truth in Git and replace stale facts in place.
|
||||
@@ -52,7 +52,7 @@ Last updated: 2026-08-22
|
||||
- Production requires independent `SITE_ACCESS_PASSWORD`: its stateless cookie authorizes creation/Host and code-only Viewer attempts; private grants stay direct, while public/private code entry needs site access alone/plus room password. Env/endpoint/cookie/nginx naming migrated atomically; failures remain neutral; there are no accounts/JWT/session rows. Local `gate:access-privacy` proves fragment isolation, site/grant transport-log absence, and raw room-password SQLite absence; room-password WebSocket/log ingress and live/headful evidence remain open.
|
||||
- Production uses nginx, Node.js 24.19.0, coturn 4.17.2, and LiveKit 1.13.5 on UDP 7882 (TCP fallback off). Ordinary peer ICE receives no TURN; the selected-edge tuple is configured only for the current controller edge, with no credential pre-advertised to ordinary peers. Coturn retains the old authenticated-relay ports. Services are healthy with zero restarts.
|
||||
- ADR-0005 rejects `PEER_ASSISTED_ROOM_IDS`; `PEER_ASSISTED_MEDIA=true` enables every room and ordinary peers stay STUN-only. Initial or active Host ingress may consume one bound relay-only grant; ready/abort clears it and failure restores peer baseline. Stale room-ID config fails startup. Production and source-only Native use `screener-v5`; Native does not subscribe to Web presence.
|
||||
- Defaults are balanced with automatic codec and 128 kbps screen audio; clarity/fluid, H.264/VP8, and 64/128/256 kbps audio remain explicit. Codec and audio quality are locked during a share. LiveKit 2.22.0 SFU uses `q,h`; production exposes one Host-local current-`h` A+B snapshot and clears it on publisher identity changes. Target-browser field availability, actual media values, BWE, and route observation remain open.
|
||||
- Defaults are balanced with automatic codec and 128 kbps screen audio; clarity/fluid, H.264/VP8, and 64/128/256 kbps audio remain explicit. Codec and audio quality are locked during a share. LiveKit 2.22.0 SFU explicitly disables Dynacast and uses ordered active `q,h`; the source gate rejects root3, charges the Host publication edge, and retains a zero-child root/subscription. Production exposes one Host-local current-`h` A+B snapshot and clears it on publisher identity changes. Target-browser field availability, actual media values, BWE, and route observation remain open.
|
||||
- Chrome 151 local functional evidence includes five-Viewer capacity-two and sixteen-Viewer cap2/cap3 runs. These are retained historical experiments, not release policy. All Viewers decoded, and a ten-second follow-up recorded guarded sender and aggregate Chromium CPU evidence, but resolution/FPS stayed low and the same-machine ordinary-PC runs do not rank caps or close resource, visual-quality, endurance, heterogeneous-network, or 20-viewer gates.
|
||||
|
||||
## Provisional Quality Targets
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Low-Server-Cost Media Routes
|
||||
|
||||
- Research date: 2026-08-21
|
||||
- Research date: 2026-08-22
|
||||
- Scope: one broadcaster, explicit admission up to sixteen trusted viewers,
|
||||
low latency, and bounded host media fanout
|
||||
- Status: ADR-0005 accepts STUN-only direct/peer UDP, bounded SFU/UDP roots,
|
||||
@@ -80,6 +80,17 @@ effects. Its roughly 86% result must not be extrapolated to a public network;
|
||||
internet DNS/TLS reuse, RTT/loss, audio, transport fallback, and browser variance
|
||||
still require measurement.
|
||||
|
||||
The 2026-08-22 in-process `gate:sfu-root-invariants` closes the corresponding
|
||||
source-accounting preflight: controller state and LiveKit token allowlists reject
|
||||
a third normal root; an active Host SFU publication counts as one outbound media
|
||||
edge in addition to direct children; and the benchmark acceptance evaluator
|
||||
rejects either three roots or three Host media edges. A committed root with zero
|
||||
peer descendants remains selected across reauthentication; a separate client
|
||||
transition check keeps the active subscriber on a newer same-kind empty-child
|
||||
assignment. This is executable topology and accounting evidence only; without a
|
||||
LiveKit process, browser RTC stats, and packet counters it does not measure
|
||||
`B_pub`, root egress, forwarding, or congestion behavior.
|
||||
|
||||
## Traffic Conservation And The Impossible Triangle
|
||||
|
||||
This is an engineering accounting identity, not a named theorem. Let `B` be
|
||||
|
||||
@@ -467,17 +467,41 @@ gate must inject an autonomous downshift, observe `suspect`, evacuate after
|
||||
bounded confirmation, and limit temporary descendant impact; no confirmed
|
||||
`FALLBACK` root may retain children. Screener's current publisher always
|
||||
configures exactly two ordered `q`/`h` encodings whenever an SFU publication is
|
||||
active, leaves Dynacast at `false`, disables backup-codec publication, and its
|
||||
subscriber sets a `HIGH` ceiling after subscribing. The publication is not
|
||||
behind a separate quality flag; its per-subscriber BWE and resource acceptance
|
||||
gates remain unverified. This is implemented configuration, not browser or
|
||||
performance evidence. The subscriber also does not attach a
|
||||
active, explicitly constructs `Room({ dynacast: false })`, disables backup-codec
|
||||
publication, and its subscriber sets a `HIGH` ceiling after selectively
|
||||
subscribing. Client 2.22.0 also defaults Dynacast to `false`; the explicit option
|
||||
prevents silent drift, while `backupCodec: false` prevents the pinned
|
||||
multi-codec publish path from enabling Dynacast automatically. Dynacast off
|
||||
prevents the pinned SDK's subscribed-quality handler from toggling publisher
|
||||
encoding activation. It does not disable per-subscriber SFU BWE, force the
|
||||
server to forward `HIGH`, or prove that both encoders consume resources
|
||||
continuously. The publication is not behind a separate quality flag. The
|
||||
subscriber also does not attach a
|
||||
`RemoteTrack`, so SDK `adaptiveStream` is not directly usable without changing
|
||||
that ownership; built-in SFU bandwidth adaptation does not depend on enabling
|
||||
that feature. If built-in selection fails the product gates, test explicit
|
||||
standard subscriber quality selection before manual sender
|
||||
activation/deactivation.
|
||||
|
||||
### SFU Root Invariant Preflight
|
||||
|
||||
The 2026-08-22 `gate:sfu-root-invariants` source gate proves that the publisher
|
||||
passes explicit `dynacast: false`, exposes exactly the ordered active `q,h`
|
||||
encodings, and the selectively subscribed screen publication retains a `HIGH`
|
||||
ceiling. Server route state and token allowlists reject a third SFU root. A Host
|
||||
with one direct child plus one active SFU publication is charged two outbound
|
||||
media edges, while a third edge fails. A committed SFU root with zero peer
|
||||
children stays selected across reauthentication, and its active subscriber is
|
||||
reconciled with an empty child list without deactivation. The benchmark
|
||||
acceptance evaluator independently rejects a third root or Host media edge.
|
||||
|
||||
This preflight does not start LiveKit 1.13.5, a browser, or a network shaper. It
|
||||
therefore does not prove packet receipt/forwarding, autonomous BWE downshift and
|
||||
recovery, actual `LOW`/`HIGH` dimensions, per-layer bytes, encoder count, CPU/GPU,
|
||||
game frame time, or upload cost. Those remain acceptance requirements for the
|
||||
isolated zero-descendant run below; root-with-children behavior remains a later
|
||||
default-on gate.
|
||||
|
||||
### Per-Flow Shaping Preflight
|
||||
|
||||
The 2026-08-19 Windows preflight stopped before downloading LiveKit, writing a
|
||||
@@ -514,7 +538,7 @@ inbound bitrate, dimensions, FPS, and decode progress must remain within the
|
||||
unshaped baseline envelope. Finally remove the weak qdisc and require recovery
|
||||
to `HIGH/HIGH` while the healthy leaf remains unchanged.
|
||||
|
||||
Retain gate evidence for the publisher's ordered `q`/`f` encodings and
|
||||
Retain gate evidence for the publisher's ordered `q`/`h` encodings and
|
||||
per-layer bytes, both subscribers' actual dimensions, FPS, bytes, codec and
|
||||
decode progress, correlated A+B/C windows, Host upload and interval encode
|
||||
cost, and available system CPU/GPU/game proxies. A third layer, an affected
|
||||
@@ -749,8 +773,12 @@ is a separate optimization.
|
||||
- [LiveKit screen-share presets](https://github.com/livekit/client-sdk-js/blob/main/src/room/track/options.ts)
|
||||
- [LiveKit degradation defaults](https://github.com/livekit/client-sdk-js/blob/main/src/room/participant/publishUtils.ts)
|
||||
- [LiveKit video simulcast and Dynacast](https://docs.livekit.io/transport/media/advanced/)
|
||||
- [LiveKit selective subscription](https://docs.livekit.io/transport/media/subscribe/)
|
||||
- [LiveKit client 2.22.0 room defaults](https://github.com/livekit/client-sdk-js/blob/v2.22.0/src/room/defaults.ts)
|
||||
- [LiveKit client 2.22.0 room and Dynacast options](https://github.com/livekit/client-sdk-js/blob/v2.22.0/src/options.ts)
|
||||
- [LiveKit client 2.22.0 room option merge](https://github.com/livekit/client-sdk-js/blob/v2.22.0/src/room/Room.ts)
|
||||
- [LiveKit client 2.22.0 SVC defaults](https://github.com/livekit/client-sdk-js/blob/v2.22.0/src/room/track/options.ts)
|
||||
- [LiveKit client 2.22.0 screen-share SVC and republish lifecycle](https://github.com/livekit/client-sdk-js/blob/v2.22.0/src/room/participant/LocalParticipant.ts)
|
||||
- [LiveKit client 2.22.0 screen-share SVC, Dynacast enablement, and republish lifecycle](https://github.com/livekit/client-sdk-js/blob/v2.22.0/src/room/participant/LocalParticipant.ts)
|
||||
- [LiveKit client 2.22.0 start-bitrate negotiation](https://github.com/livekit/client-sdk-js/blob/v2.22.0/src/room/PCTransport.ts)
|
||||
- [LiveKit client 2.22.0 SVC encoding construction](https://github.com/livekit/client-sdk-js/blob/v2.22.0/src/room/participant/publishUtils.ts)
|
||||
- [LiveKit client 2.22.0 subscriber quality control](https://github.com/livekit/client-sdk-js/blob/v2.22.0/src/room/track/RemoteTrackPublication.ts)
|
||||
|
||||
+2
-1
@@ -18,6 +18,7 @@ This file is the current execution index. Load [verification status](./verificat
|
||||
- The deployed pre-share self-check uses disposable, no-room resources for health, WSS, and STUN `srflx` checks and treats configured SFU as unverified until a real route. Expanded connection details provide click-only, versioned JSON export; its fixed allowlist excludes candidate addresses/ports, participant and room identity, raw signaling, URLs, and credentials, and no report is uploaded or persisted.
|
||||
- Private fragment grants enter directly; code-only Viewers need site access and private rooms additionally need their password. Credentials remain unstored. Web names and presence are session-only; duplicate names alone show an ID suffix.
|
||||
- Rooms default to eight Viewers and accept an explicit admission limit from one through sixteen. Current source is server-authoritative and role-aware: Host physical media edges are bounded to two, ordinary Browser Viewer downstream edges to one, and upstream receive is free. Active/provisional/selected overlays count, as does Host SFU publication. Deployment values 1/2 may only tighten these limits; old/malicious Viewer advertisements 2/3 cannot lift them. Web assignment execution also truncates physical children to Host2/Viewer1 as defense-in-depth, and deployment limit one promotes an occupied Host branch root before reserving the SFU publication slot. The wire retains `0/1/2/3` only as a future envelope, and SFU roots remain separately bounded to two.
|
||||
- Local `gate:sfu-root-invariants` proves the source/control-plane boundary: controller and token allowlists reject root3, Host publication consumes one of two upload edges, the benchmark evaluator rejects root3/edge3, and a zero-child SFU root keeps its assignment and selective subscriber. The publisher explicitly disables Dynacast and exposes ordered active `q,h`; no LiveKit process, browser, network shaper, or production route ran.
|
||||
- Relay quality handling compares fresh child FPS with parent input. Two-child corroboration is retained only for a future accepted capacity tier and is unreachable under the ordinary Browser Viewer1 release clamp. Deployed MBB keeps the triggering child's old edge while one ordinary Viewer with peer or SFU upstream and a strict spare slot owns the provisional child PC. Matching current-generation RTP, decoded frames, live video, revision, and identity promote that same PC; Host remains excluded. Failure, timeout, session/share change, or no candidate keeps the old edge without SFU/TURN. Sibling MBB remains open.
|
||||
- A generation-bound 15-second initial-connect deadline enters ICE restart. One-root healthy-SFU make-before-break is deployed but media-unverified; capacity `0 -> 1`, multi-root, browser, and mobile gates remain open.
|
||||
|
||||
@@ -25,7 +26,7 @@ This file is the current execution index. Load [verification status](./verificat
|
||||
|
||||
1. Verify the deployed Share audio presets and local observability on target browsers and real routes: field availability, actual values, negotiated/observed bitrate, audible quality, A/V synchronization evidence, candidate endpoints, and route switches.
|
||||
2. Run one representative production-room UDP/frame/cap/stop canary. Validate initial ingress, `peer-selected`, bad-relay reparenting, and healthy SFU reselection without changing the STUN-only ordinary-peer policy.
|
||||
3. Run exactly-two/Dynacast-off zero-child SFU BWE/resource evidence before implementing explicit evacuation or another media selector.
|
||||
3. Run pinned LiveKit with isolated per-leaf shaping to prove zero-child SFU BWE downshift/recovery and resources before implementing explicit evacuation or another media selector; the exactly-two/Dynacast-off source preflight passes.
|
||||
4. Complete Native package download, Viewer2/FIFO, hardware/endurance, and game A/V evidence; use target-device or production measurements for performance decisions.
|
||||
5. Run the Host2/ordinary Browser Viewer1 relay resource/quality and heterogeneous-network gates. ADR-0004 cap2/cap3 results remain historical experiments. A 20-viewer matrix is required before any accepted scale-default change; mobile remains a compatibility observation, not a capacity class.
|
||||
|
||||
|
||||
+1
-1
@@ -206,7 +206,7 @@ SVC 的标准能力门禁已经静态关闭为 `no-go-web-svc-cross-path-hardwar
|
||||
|
||||
应用不得稳定保留经交叉证据确认的 `FALLBACK` parent。SFU BWE 可能先自主 downshift;该观测进入 `suspect`,跨有界窗口确认后再迁走 children 并撤销 capacity,不能承诺 packet-level 先迁后降。Root-with-children gate 必须限制 descendants 的短暂影响。计划或显式质量 fallback 只在内建路径失败后考虑,并先在 assignment/connection generation guard 下迁走全部 children;失败或 stale 操作保持原 assignment 与 quality。更长恢复窗口和 cooldown 后才能重新发布 capacity。Viewer 自报不能单独改变拓扑。详细 wire 由后续 routing owner 持有。
|
||||
|
||||
标准表示能力排在任何自定义双表示媒体实现之前。当前 Web P2P simulcast 因 receiver 不能选 RID 且跨 PeerConnection 无 shared-encode 契约而静态 no-go;当前 Web/LiveKit SVC 因 direct/peer 无独立选层、硬件不可证明且 pinned screen share 为 `L1T3` 而静态 no-go。下一 gate 优先验证 pinned LiveKit 的标准 exactly-two screen-share simulcast:Dynacast off 常驻 `HIGH+LOW`,zero-descendant SFU leaf 以 `HIGH` 为 maximum,由 SFU BWE 独立降层和恢复;通过后不实现自定义媒体 selector。Root-with-children 的 suspect/确认/有界迁移是 default-on 前硬门。只有内建选择不满足门槛时才显式设置标准 subscriber quality;常驻成本违反硬件、游戏或上传预算时才测标准 sender 手动启停,全部标准路径失败后才进入 custom/native dual encode。当前 subscriber 未 attach `RemoteTrack`,因此不直接启用 `adaptiveStream`,但这不阻塞 SFU BWE leaf 候选。Cumulative Dynacast-on 只作为另一种 always-on 形态。本 SVC 静态审查不跑 browser。具体门槛由 ADR-0007 和[实时质量调研](./research/realtime-quality-adaptation.md)持有,任何方案都不得复用 PR #28 的 minimum-of-two 或绕过 stock WebRTC congestion control。
|
||||
标准表示能力排在任何自定义双表示媒体实现之前。当前 Web P2P simulcast 因 receiver 不能选 RID 且跨 PeerConnection 无 shared-encode 契约而静态 no-go;当前 Web/LiveKit SVC 因 direct/peer 无独立选层、硬件不可证明且 pinned screen share 为 `L1T3` 而静态 no-go。Pinned LiveKit 的标准 exactly-two screen-share simulcast 已显式配置 `dynacast: false` 并通过 roots <=2、Host publication 上传边记账和 zero-descendant assignment/subscription 保留的 source preflight;Dynacast off 阻止 pinned SDK 按 subscribed-quality 更新切换 publisher encoding,不关闭 SFU per-subscriber BWE,也不保证实际收到 `HIGH`。下一 gate 在隔离整形下验证 zero-descendant SFU leaf 从 `HIGH` maximum 由 SFU BWE 独立降层和恢复;通过后不实现自定义媒体 selector。Root-with-children 的 suspect/确认/有界迁移是 default-on 前硬门。只有内建选择不满足门槛时才显式设置标准 subscriber quality;常驻成本违反硬件、游戏或上传预算时才测标准 sender 手动启停,全部标准路径失败后才进入 custom/native dual encode。当前 subscriber 未 attach `RemoteTrack`,因此不直接启用 `adaptiveStream`,但这不阻塞 SFU BWE leaf 候选。Cumulative Dynacast-on 只作为另一种 always-on 形态。本 SVC 静态审查不跑 browser。具体门槛由 ADR-0007 和[实时质量调研](./research/realtime-quality-adaptation.md)持有,任何方案都不得复用 PR #28 的 minimum-of-two 或绕过 stock WebRTC congestion control。
|
||||
|
||||
## 移动分享端能力边界与排期
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"build:server": "tsc -p tsconfig.server.json",
|
||||
"start": "node dist/server/server/index.js",
|
||||
"gate:peer-topology-loopback": "tsx scripts/peer-assisted-benchmark.ts",
|
||||
"gate:sfu-root-invariants": "vitest run tests/media-route-controller.test.ts tests/livekit-token.test.ts tests/sfu-client.test.ts tests/route-transition.test.ts tests/peer-assisted-benchmark.test.ts tests/server-signal.test.ts -t \"SFU root invariant gate\"",
|
||||
"gate:access-privacy": "npm run build:client && vitest run tests/server-signal.test.ts && tsx scripts/access-privacy-gate.ts",
|
||||
"gate:turn-udp-allocation": "tsx scripts/turn-allocation-gate.ts",
|
||||
"probe:native-one-viewer": "tsx scripts/native-one-viewer-gate.ts",
|
||||
|
||||
@@ -108,7 +108,7 @@ export class SfuPublisher {
|
||||
return false;
|
||||
}
|
||||
|
||||
const room = new sdk.Room();
|
||||
const room = new sdk.Room({ dynacast: false });
|
||||
this.room = room;
|
||||
this.sdk = sdk;
|
||||
room.on(sdk.RoomEvent.Disconnected, () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
CURRENT_HOST_MEDIA_EDGE_LIMIT,
|
||||
CURRENT_SFU_ROOT_LIMIT,
|
||||
DEFAULT_PEER_RELAY_DOWNSTREAM_EDGES,
|
||||
MAX_ICE_SERVER_URLS,
|
||||
MAX_VIEWERS_PER_ROOM_LIMIT,
|
||||
@@ -17,8 +18,7 @@ const MAX_SELECTED_EDGE_TURN_SECRET_BYTES = 128;
|
||||
const MIN_SELECTED_EDGE_TURN_TTL_SECONDS = 60;
|
||||
const MAX_SELECTED_EDGE_TURN_TTL_SECONDS = 10 * 60;
|
||||
const DEFAULT_MAX_VIEWERS_PER_ROOM = 8;
|
||||
const DEFAULT_MAX_SFU_ROOTS_PER_ROOM = 2;
|
||||
const MAX_SFU_ROOTS_PER_ROOM = 2;
|
||||
const DEFAULT_MAX_SFU_ROOTS_PER_ROOM = CURRENT_SFU_ROOT_LIMIT;
|
||||
const VISIBLE_ASCII_PATTERN = /^[\x21-\x7e]+$/;
|
||||
const REMOVED_ENVIRONMENT_VARIABLES = [
|
||||
"TURN_URLS",
|
||||
@@ -171,7 +171,7 @@ function parseLiveKitFallback(
|
||||
DEFAULT_MAX_SFU_ROOTS_PER_ROOM,
|
||||
"MAX_SFU_ROOTS_PER_ROOM",
|
||||
1,
|
||||
MAX_SFU_ROOTS_PER_ROOM,
|
||||
CURRENT_SFU_ROOT_LIMIT,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { randomBytes } from "node:crypto";
|
||||
import {
|
||||
CURRENT_BROWSER_RELAY_DOWNSTREAM_EDGE_LIMIT,
|
||||
CURRENT_HOST_MEDIA_EDGE_LIMIT,
|
||||
CURRENT_SFU_ROOT_LIMIT,
|
||||
MAX_MEDIA_ROUTE_REVISION,
|
||||
type ClientMessage,
|
||||
type MediaAssignment,
|
||||
@@ -303,7 +304,8 @@ export class HybridMediaRouter {
|
||||
if (
|
||||
!Number.isSafeInteger(fallback.maxRoots) ||
|
||||
fallback.maxRoots < 1 ||
|
||||
fallback.maxRoots > Math.min(2, options.roomStore.maxViewersPerRoom) ||
|
||||
fallback.maxRoots >
|
||||
Math.min(CURRENT_SFU_ROOT_LIMIT, options.roomStore.maxViewersPerRoom) ||
|
||||
(fallback.prepareTimeoutMs !== undefined &&
|
||||
(!Number.isSafeInteger(fallback.prepareTimeoutMs) ||
|
||||
fallback.prepareTimeoutMs <= 0))
|
||||
|
||||
@@ -5,12 +5,12 @@ import {
|
||||
} from "livekit-server-sdk";
|
||||
|
||||
import {
|
||||
CURRENT_SFU_ROOT_LIMIT,
|
||||
MAX_VIEWERS_PER_ROOM_LIMIT,
|
||||
type Role,
|
||||
} from "../shared/protocol.js";
|
||||
|
||||
const LIVEKIT_TOKEN_TTL_SECONDS = 5 * 60;
|
||||
const MAX_SFU_ROOTS_PER_ROOM_LIMIT = 2;
|
||||
const ROOM_ID_PATTERN = /^[1-9]\d{0,11}$/;
|
||||
const OPAQUE_ID_PATTERN = /^[A-Za-z0-9_-]{8,128}$/;
|
||||
|
||||
@@ -48,7 +48,7 @@ export class LiveKitTokenIssuer implements SfuTokenIssuer {
|
||||
if (
|
||||
!Number.isSafeInteger(options.maxSfuRootsPerRoom) ||
|
||||
options.maxSfuRootsPerRoom < 1 ||
|
||||
options.maxSfuRootsPerRoom > MAX_SFU_ROOTS_PER_ROOM_LIMIT
|
||||
options.maxSfuRootsPerRoom > CURRENT_SFU_ROOT_LIMIT
|
||||
) {
|
||||
throw new Error("LiveKit SFU root limit is invalid");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
CURRENT_BROWSER_RELAY_DOWNSTREAM_EDGE_LIMIT,
|
||||
CURRENT_HOST_MEDIA_EDGE_LIMIT,
|
||||
CURRENT_SFU_ROOT_LIMIT,
|
||||
DEFAULT_PEER_RELAY_DOWNSTREAM_EDGES,
|
||||
MAX_MEDIA_ROUTE_REVISION,
|
||||
MAX_PEER_RELAY_DOWNSTREAM_EDGES,
|
||||
@@ -300,7 +301,7 @@ function assertRouteInvariants(
|
||||
const rootPeerIds = new Set(route.sfu.rootPeerIds);
|
||||
if (
|
||||
rootPeerIds.size !== route.sfu.rootPeerIds.length ||
|
||||
rootPeerIds.size > MAX_VIEWERS_PER_ROOM_LIMIT
|
||||
rootPeerIds.size > CURRENT_SFU_ROOT_LIMIT
|
||||
) {
|
||||
throw new Error("SFU root participants must be unique and bounded");
|
||||
}
|
||||
|
||||
@@ -207,6 +207,7 @@ export const DEFAULT_QUALITY_SETTINGS = {
|
||||
export const DEFAULT_PEER_RELAY_DOWNSTREAM_EDGES = 2;
|
||||
export const CURRENT_HOST_MEDIA_EDGE_LIMIT = 2;
|
||||
export const CURRENT_BROWSER_RELAY_DOWNSTREAM_EDGE_LIMIT = 1;
|
||||
export const CURRENT_SFU_ROOT_LIMIT = 2;
|
||||
// The wire retains the accepted future 0/1/2/3 capability envelope. Current
|
||||
// release policy is enforced server-side by role and is intentionally lower.
|
||||
export const MAX_PEER_RELAY_DOWNSTREAM_EDGES = 3;
|
||||
|
||||
@@ -7,6 +7,8 @@ const apiKey = "test-api-key";
|
||||
const apiSecret = "s".repeat(32);
|
||||
const publicationGeneration = "publication_12345678";
|
||||
const rootPeerId = "viewer_root_12345678";
|
||||
const secondRootPeerId = "viewer_root_23456789";
|
||||
const thirdRootPeerId = "viewer_root_34567890";
|
||||
|
||||
function issuer(
|
||||
maxViewersPerRoom = 8,
|
||||
@@ -21,6 +23,41 @@ function issuer(
|
||||
}
|
||||
|
||||
describe("LiveKitTokenIssuer", () => {
|
||||
it("SFU root invariant gate: allows exactly two roots and rejects a third", async () => {
|
||||
const tokenIssuer = issuer();
|
||||
await expect(
|
||||
tokenIssuer.issueToken({
|
||||
roomId: "42",
|
||||
role: "host",
|
||||
peerId: "host_peer_12345678",
|
||||
publicationGeneration,
|
||||
allowlistedRootPeerIds: [rootPeerId, secondRootPeerId],
|
||||
}),
|
||||
).resolves.toEqual(expect.any(String));
|
||||
await expect(
|
||||
tokenIssuer.issueToken({
|
||||
roomId: "42",
|
||||
role: "viewer",
|
||||
peerId: secondRootPeerId,
|
||||
publicationGeneration,
|
||||
allowlistedRootPeerIds: [rootPeerId, secondRootPeerId],
|
||||
}),
|
||||
).resolves.toEqual(expect.any(String));
|
||||
await expect(
|
||||
tokenIssuer.issueToken({
|
||||
roomId: "42",
|
||||
role: "host",
|
||||
peerId: "host_peer_12345678",
|
||||
publicationGeneration,
|
||||
allowlistedRootPeerIds: [
|
||||
rootPeerId,
|
||||
secondRootPeerId,
|
||||
thirdRootPeerId,
|
||||
],
|
||||
}),
|
||||
).rejects.toThrow("root allowlist is invalid");
|
||||
});
|
||||
|
||||
it("issues a generation-bound host token limited to screen sharing", async () => {
|
||||
const claims = await new TokenVerifier(apiKey, apiSecret).verify(
|
||||
await issuer().issueToken({
|
||||
|
||||
@@ -11,6 +11,7 @@ const DIRECT_ROOT = "viewer_direct_12345678";
|
||||
const SFU_ROOT_A = "viewer_sfu_a_12345678";
|
||||
const SFU_ROOT_B = "viewer_sfu_b_12345678";
|
||||
const THIRD_CHILD = "viewer_third_12345678";
|
||||
const SFU_ROOT_C = "viewer_sfu_c_12345678";
|
||||
const GENERATION_A = "generation_a_12345678";
|
||||
const GENERATION_B = "generation_b_12345678";
|
||||
|
||||
@@ -72,6 +73,65 @@ function chainedAssignments(): Map<string, ParticipantRouteAssignment> {
|
||||
}
|
||||
|
||||
describe("MediaRouteController", () => {
|
||||
it("SFU root invariant gate: accepts two roots, rejects a third, and charges one Host publication edge", () => {
|
||||
const assignments = new Map<string, ParticipantRouteAssignment>([
|
||||
[
|
||||
HOST,
|
||||
assignment({ kind: "none" }, [DIRECT_ROOT], GENERATION_A),
|
||||
],
|
||||
[DIRECT_ROOT, assignment({ kind: "peer", peerId: HOST })],
|
||||
[SFU_ROOT_A, assignment({ kind: "sfu" })],
|
||||
[SFU_ROOT_B, assignment({ kind: "sfu" })],
|
||||
]);
|
||||
const routes = new MediaRouteController({
|
||||
hostPeerId: HOST,
|
||||
assignments,
|
||||
sfuPublicationGeneration: GENERATION_A,
|
||||
sfuRootPeerIds: [SFU_ROOT_A, SFU_ROOT_B],
|
||||
});
|
||||
|
||||
expect(routes.getActiveRoute().sfu.rootPeerIds).toEqual([
|
||||
SFU_ROOT_A,
|
||||
SFU_ROOT_B,
|
||||
]);
|
||||
expect(routes.hostActiveMediaEdges()).toBe(2);
|
||||
|
||||
const excessiveRoots = new Map(assignments);
|
||||
excessiveRoots.set(SFU_ROOT_C, assignment({ kind: "sfu" }));
|
||||
expect(
|
||||
() =>
|
||||
new MediaRouteController({
|
||||
hostPeerId: HOST,
|
||||
assignments: excessiveRoots,
|
||||
sfuPublicationGeneration: GENERATION_A,
|
||||
sfuRootPeerIds: [SFU_ROOT_A, SFU_ROOT_B, SFU_ROOT_C],
|
||||
}),
|
||||
).toThrow("SFU root participants must be unique and bounded");
|
||||
|
||||
const excessiveHostEdges = new Map(assignments);
|
||||
excessiveHostEdges.set(
|
||||
HOST,
|
||||
assignment(
|
||||
{ kind: "none" },
|
||||
[DIRECT_ROOT, SFU_ROOT_C],
|
||||
GENERATION_A,
|
||||
),
|
||||
);
|
||||
excessiveHostEdges.set(
|
||||
SFU_ROOT_C,
|
||||
assignment({ kind: "peer", peerId: HOST }),
|
||||
);
|
||||
expect(
|
||||
() =>
|
||||
new MediaRouteController({
|
||||
hostPeerId: HOST,
|
||||
assignments: excessiveHostEdges,
|
||||
sfuPublicationGeneration: GENERATION_A,
|
||||
sfuRootPeerIds: [SFU_ROOT_A, SFU_ROOT_B],
|
||||
}),
|
||||
).toThrow("Host active media edge budget exceeded");
|
||||
});
|
||||
|
||||
it("atomically commits only after every expected participant is ready", () => {
|
||||
const routes = controller();
|
||||
const revision = routes.prepare(hybridPlan())!;
|
||||
|
||||
@@ -476,6 +476,98 @@ describe("peer topology loopback observations", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("SFU root invariant gate: accepts two roots and detects excess root or Host media edges", () => {
|
||||
const generation = "publication_generation_12345678";
|
||||
const host = page("host", "host", 2, 0);
|
||||
const direct = page("viewer", "viewer-1", 0, 1);
|
||||
const firstRoot = page("viewer", "viewer-2", 0, 1);
|
||||
const secondRoot = page("viewer", "viewer-3", 0, 1);
|
||||
host.routeRevision = 7;
|
||||
host.routeAssignment.childPeerIds = [direct.peerId];
|
||||
host.routeAssignment.sfuPublicationGeneration = generation;
|
||||
host.maxAssignedChildren = 1;
|
||||
direct.routeRevision = 7;
|
||||
direct.routeAssignment.upstream = {
|
||||
kind: "peer",
|
||||
peerId: host.peerId,
|
||||
};
|
||||
for (const root of [firstRoot, secondRoot]) {
|
||||
root.routeRevision = 7;
|
||||
root.routeAssignment.upstream = { kind: "sfu" };
|
||||
}
|
||||
for (const participant of [host, firstRoot, secondRoot]) {
|
||||
markActiveRouteReady(participant);
|
||||
}
|
||||
|
||||
const initial = [host, direct, firstRoot, secondRoot];
|
||||
const final = structuredClone(initial);
|
||||
for (const viewer of final.slice(1)) {
|
||||
viewer.connections.at(-1)!.receiveTotals!.framesTotal = 20;
|
||||
}
|
||||
const summary = summarizeSamples(
|
||||
[
|
||||
{ atEpochMs: 2_000, elapsedMs: 0, pages: initial },
|
||||
{ atEpochMs: 4_000, elapsedMs: 2_000, pages: final },
|
||||
],
|
||||
3,
|
||||
);
|
||||
const checks = buildRunChecks(summary, 3, "720p30");
|
||||
|
||||
expect(summary).toMatchObject({
|
||||
maxHostActiveMediaEdges: 2,
|
||||
maxHostAssignedChildren: 1,
|
||||
sfuRootCount: 2,
|
||||
sfuPublicationCoherent: true,
|
||||
everyViewerDecoded: true,
|
||||
});
|
||||
expect(
|
||||
checks.find((check) => check.name === "sfu-route-consistency")?.passed,
|
||||
).toBe(true);
|
||||
expect(
|
||||
checks.find((check) => check.name === "host-active-media-edges")?.passed,
|
||||
).toBe(true);
|
||||
|
||||
const thirdRoot = page("viewer", "viewer-4", 0, 1);
|
||||
thirdRoot.routeRevision = 7;
|
||||
thirdRoot.routeAssignment.upstream = { kind: "sfu" };
|
||||
markActiveRouteReady(thirdRoot);
|
||||
const excessiveRootInitial = [...structuredClone(initial), thirdRoot];
|
||||
const excessiveRootFinal = structuredClone(excessiveRootInitial);
|
||||
for (const viewer of excessiveRootFinal.slice(1)) {
|
||||
viewer.connections.at(-1)!.receiveTotals!.framesTotal = 20;
|
||||
}
|
||||
const excessiveRootSummary = summarizeSamples(
|
||||
[
|
||||
{ atEpochMs: 2_000, elapsedMs: 0, pages: excessiveRootInitial },
|
||||
{ atEpochMs: 4_000, elapsedMs: 2_000, pages: excessiveRootFinal },
|
||||
],
|
||||
4,
|
||||
);
|
||||
expect(excessiveRootSummary.sfuRootCount).toBe(3);
|
||||
expect(
|
||||
buildRunChecks(excessiveRootSummary, 4, "720p30").find(
|
||||
(check) => check.name === "sfu-route-consistency",
|
||||
)?.passed,
|
||||
).toBe(false);
|
||||
|
||||
const excessiveHostInitial = structuredClone(initial);
|
||||
const excessiveHostFinal = structuredClone(final);
|
||||
excessiveHostInitial[0]!.maxActiveOutboundMediaEdges = 3;
|
||||
excessiveHostFinal[0]!.maxActiveOutboundMediaEdges = 3;
|
||||
const excessiveHostSummary = summarizeSamples(
|
||||
[
|
||||
{ atEpochMs: 2_000, elapsedMs: 0, pages: excessiveHostInitial },
|
||||
{ atEpochMs: 4_000, elapsedMs: 2_000, pages: excessiveHostFinal },
|
||||
],
|
||||
3,
|
||||
);
|
||||
expect(
|
||||
buildRunChecks(excessiveHostSummary, 3, "720p30").find(
|
||||
(check) => check.name === "host-active-media-edges",
|
||||
)?.passed,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("fails SFU continuity without one same-revision Host publication", () => {
|
||||
function summarizeSfu(
|
||||
hostGeneration: string | null,
|
||||
|
||||
@@ -1012,6 +1012,30 @@ describe("HostSfuRoute", () => {
|
||||
});
|
||||
|
||||
describe("ViewerSfuRoute", () => {
|
||||
it("SFU root invariant gate: retains an active subscriber when the root has zero peer children", async () => {
|
||||
const reconcileSfuChildren = vi.fn();
|
||||
const { route, subscriber } = await activateViewerSfuRoute({
|
||||
activatePeer: () => undefined,
|
||||
reconcileSfuChildren,
|
||||
onSfuStream: () => undefined,
|
||||
send: () => true,
|
||||
});
|
||||
reconcileSfuChildren.mockClear();
|
||||
|
||||
route.accept({
|
||||
revision: 2,
|
||||
phase: "active",
|
||||
assignment: sfuAssignment(),
|
||||
});
|
||||
|
||||
await vi.waitFor(() =>
|
||||
expect(reconcileSfuChildren).toHaveBeenCalledWith([]),
|
||||
);
|
||||
expect(subscriber.activate).toHaveBeenCalledOnce();
|
||||
expect(subscriber.deactivate).not.toHaveBeenCalled();
|
||||
expect(subscriber.disconnect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reports a subscriber prepare failure without requesting refresh", async () => {
|
||||
const messages: ClientMessage[] = [];
|
||||
const subscriber = createFakeSubscriber(
|
||||
|
||||
@@ -5595,7 +5595,7 @@ describe("WebSocket signaling", () => {
|
||||
expect(failedPrepare.assignment.upstream).toEqual({ kind: "sfu" });
|
||||
});
|
||||
|
||||
it("reparents failed peer edges before committing a bounded SFU fallback", async () => {
|
||||
it("SFU root invariant gate: retains a zero-child root across commit and reauthentication", async () => {
|
||||
const issued: Array<
|
||||
Parameters<SfuTokenIssuer["issueToken"]>[0]
|
||||
> = [];
|
||||
@@ -5752,6 +5752,7 @@ describe("WebSocket signaling", () => {
|
||||
expect(hostActive.assignment.childPeerIds).toEqual([secondAuth.peerId]);
|
||||
expect(hostActive.assignment.sfuPublicationGeneration).toBeTruthy();
|
||||
expect(rootActive.assignment.upstream).toEqual({ kind: "sfu" });
|
||||
expect(rootActive.assignment.childPeerIds).toEqual([]);
|
||||
expect(new Set(issued.map(({ peerId }) => peerId))).toEqual(
|
||||
new Set([hostAuth.peerId, firstAuth.peerId]),
|
||||
);
|
||||
@@ -5783,7 +5784,10 @@ describe("WebSocket signaling", () => {
|
||||
expect(reconnectedRootAuth).toMatchObject({
|
||||
peerId: firstAuth.peerId,
|
||||
routeRevision: hostPrepare.revision,
|
||||
routeAssignment: { upstream: { kind: "sfu" } },
|
||||
routeAssignment: {
|
||||
upstream: { kind: "sfu" },
|
||||
childPeerIds: [],
|
||||
},
|
||||
mediaAssignment: { parentPeerId: null },
|
||||
});
|
||||
expect(await reconnectedRoot.inbox.next("sfu-config")).toMatchObject({
|
||||
|
||||
@@ -539,13 +539,13 @@ describe("SfuPublisher", () => {
|
||||
expect(updates.at(-1)).toBeNull();
|
||||
});
|
||||
|
||||
it("prepares a connection without publishing media", async () => {
|
||||
it("SFU root invariant gate: explicitly keeps Dynacast off while preparing", async () => {
|
||||
const publisher = new SfuPublisher();
|
||||
|
||||
await expect(publisher.connect(connection)).resolves.toBe(true);
|
||||
|
||||
const room = livekit.state.rooms[0];
|
||||
expect(room.options).toEqual({});
|
||||
expect(room.options).toEqual({ dynacast: false });
|
||||
expect(room.connect).toHaveBeenCalledWith(connection.url, connection.token, {
|
||||
autoSubscribe: false,
|
||||
});
|
||||
@@ -598,7 +598,7 @@ describe("SfuPublisher", () => {
|
||||
expect(livekit.state.rooms[0]?.disconnect).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it("publishes one screen video and optional audio only while active", async () => {
|
||||
it("SFU root invariant gate: publishes exactly the ordered q and h video encodings", async () => {
|
||||
const publisher = new SfuPublisher();
|
||||
const video = track("video", "video-1");
|
||||
const audio = track("audio", "audio-1");
|
||||
@@ -654,6 +654,9 @@ describe("SfuPublisher", () => {
|
||||
scaleResolutionDownBy: 1,
|
||||
}),
|
||||
]);
|
||||
expect(
|
||||
sender.parameters.encodings.every((encoding) => encoding.active !== false),
|
||||
).toBe(true);
|
||||
expect(publisher.getSenderParameters()).toEqual({
|
||||
requested: {
|
||||
maxBitrate: 8_000_000,
|
||||
@@ -1226,7 +1229,7 @@ describe("SfuPublisher", () => {
|
||||
});
|
||||
|
||||
describe("SfuSubscriber", () => {
|
||||
it("stays inactive while prepared and selectively subscribes after activation", async () => {
|
||||
it("SFU root invariant gate: keeps the assigned screen subscription at a HIGH ceiling", async () => {
|
||||
const gate = deferred();
|
||||
livekit.state.connectGate = gate.promise;
|
||||
const streams: Array<MediaStream | null> = [];
|
||||
|
||||
Reference in new issue
Block a user