feat(sfu): enable native representation adaptation
This commit is contained in:
1 parent
aa95f5674f
commit
89db0b47ba
17 files changed
+256
-109
No files matched your search
@@ -19,6 +19,9 @@ rtc:
|
||||
stun_servers:
|
||||
- stun.example.com:3478
|
||||
use_external_ip: true
|
||||
congestion_control:
|
||||
# Let LiveKit select the forwarded layer from subscriber bandwidth.
|
||||
use_send_side_bwe: true
|
||||
|
||||
room:
|
||||
# Screener creates exact-generation rooms through the private RoomService API.
|
||||
|
||||
+2
-2
@@ -17,7 +17,7 @@
|
||||
- [Native shared-encode sender](./research/native-shared-encode-sender.md): consolidated Draft #16/#18/#22/#23/#25/#28 ladder, unclassified product-gate failure, staged revalidation, and stop line.
|
||||
- [Native H.264 hardware decision](./research/native-h264-hardware-decision.md): bounded WebCodecs no-go, proved Media Foundation/NVIDIA hardware encode, the default-fmtp `42c01f` stop, and the retained interop boundary.
|
||||
- [Native H.264 opt-in path](./research/native-h264-opt-in-path.md): explicit H.264 sender mode, exact Pion profile registration, and one bounded Host/Viewer loopback with hardware caveats.
|
||||
- [Realtime quality adaptation](./research/realtime-quality-adaptation.md): fixed Browser VP8/no-video-hint evidence, manual quality ceilings, diagnostic-only capture/send/receive correlation, and LiveKit-owned SFU `HIGH+LOW` adaptation evidence.
|
||||
- [Realtime quality adaptation](./research/realtime-quality-adaptation.md): fixed Browser VP8/no-video-hint evidence, manual quality ceilings, diagnostic-only capture/send/receive correlation, and LiveKit-owned SFU dual-representation adaptation evidence.
|
||||
- [Browser background capture diagnostics](./research/browser-background-capture.md): browser lifecycle evidence, a privacy-bounded capture/send/receive experiment, interpretation gates, and non-goal keepalive mechanisms.
|
||||
- [Browser screen-audio quality](./research/browser-screen-audio-quality.md): capture/source compatibility, live sender-ceiling readback, Opus negotiation boundaries, and remaining audible/device evidence.
|
||||
- [Display name and presence boundaries](./research/display-name-presence.md): browser-local storage failure behavior, Unicode normalization, control-character policy, and the Web-only presence capability boundary.
|
||||
@@ -27,7 +27,7 @@
|
||||
- [ADR-0004](./adr/0004-peer-assisted-media-experiment.md): historical standard-WebRTC peer-assisted experiment and reusable evidence.
|
||||
- [ADR-0005](./adr/0005-automatic-hybrid-media-routing.md): accepted P2P-first single-reconcile route model, including deployed Browser SFU ICE-server isolation; remaining real-network validation is tracked in verification status.
|
||||
- [ADR-0006](./adr/0006-fixed-high-native-sender-canary.md): proposed fixed-`HIGH` native canary, current no-go result, and staged revalidation boundary.
|
||||
- [ADR-0007](./adr/0007-path-isolated-representation-quality.md): accepted LiveKit-owned VP8 `HIGH+LOW` SFU adaptation, pinned evidence, and application-controller/SVC boundaries.
|
||||
- [ADR-0007](./adr/0007-path-isolated-representation-quality.md): accepted LiveKit-owned pinned-default VP8 SFU adaptation, pinned evidence, and application-controller/SVC boundaries.
|
||||
- [ADR-0008](./adr/0008-window-scoped-audio-capture.md): browser window-audio hint and the Windows WASAPI process-loopback boundary.
|
||||
|
||||
Documentation and project memory are part of the product source of truth. Update them in the same change that alters the corresponding requirement or architecture.
|
||||
@@ -367,8 +367,8 @@ deployment-wide.
|
||||
|
||||
Production runs exact deployed application/runtime revision
|
||||
`1d8761528d0dba43fb6d818df3934483ba2f5340`, release `1d87615`; canonical `main`
|
||||
contains the same application/runtime tree plus the current truth checkpoint, including exact
|
||||
Browser SFU ICE-server-isolation implementation
|
||||
contains newer source pending deployment, including the active-SFU cadence correction
|
||||
and exact Browser SFU ICE-server-isolation implementation
|
||||
`ae09c760adec76fd26da611d4928486d105c6d3b`. Both run strict `screener-v12` and
|
||||
the direct/STUN peer plus LiveKit SFU/UDP route model. Browser SFU PCs use empty
|
||||
external ICE-server lists. Chrome 151 on that exact production verified one
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# ADR-0007: LiveKit-Owned SFU Representation Adaptation
|
||||
|
||||
- Status: Accepted; not implemented or deployed
|
||||
- Status: Accepted and implemented in current source; not deployed
|
||||
- Date: 2026-08-19
|
||||
- Last reviewed: 2026-08-25
|
||||
|
||||
@@ -11,13 +11,14 @@ controllers. The SFU path is different: one Host publication serves several
|
||||
subscribers, so a weak subscriber needs a lower encoded representation without
|
||||
lowering every healthy subscriber or creating one encoder per Viewer.
|
||||
|
||||
A single non-scalable `HIGH` representation cannot provide that spatial
|
||||
A single non-scalable full-resolution representation cannot provide that spatial
|
||||
downshift because an SFU forwards encoded packets and does not transcode them.
|
||||
The earlier production A/B compared an always-active `LOW+HIGH` publication
|
||||
against `LOW` disabled. It showed that the always-active lower representation
|
||||
competed with `HIGH` on that constrained Host-to-SFU path; it did not test
|
||||
The earlier production A/B compared an always-active two-encoding publication
|
||||
against its lower representation disabled. It showed that the always-active
|
||||
lower representation competed with the full representation on that constrained
|
||||
Host-to-SFU path; it did not test
|
||||
LiveKit-owned demand-driven layer control and does not justify a permanent
|
||||
single-`HIGH` contract.
|
||||
single-encoding contract.
|
||||
|
||||
The earlier application-layer controller discussion started from the false
|
||||
premise that LiveKit/WebRTC did not already own per-subscriber bandwidth
|
||||
@@ -31,16 +32,18 @@ removing the representation required by constrained Viewers.
|
||||
|
||||
1. Direct and peer paths retain independent stock WebRTC congestion control.
|
||||
Viewer feedback is never aggregated into a room-wide target.
|
||||
2. The Browser Host SFU publication provides bounded VP8 `HIGH` and `LOW`
|
||||
simulcast representations. The representation count is fixed and does not
|
||||
2. The Browser Host SFU publication provides the pinned SDK's bounded VP8
|
||||
screen-share representations: one half-resolution encoding and the original
|
||||
full-resolution encoding. The representation count is fixed and does not
|
||||
grow with Viewer count.
|
||||
3. LiveKit Dynacast and its per-subscriber stream allocator/BWE own publication
|
||||
layer activation and the actual layer forwarded to each SFU subscriber.
|
||||
Screener does not implement a quality score, bandwidth estimator, periodic
|
||||
layer controller, or per-Viewer encoder.
|
||||
4. `HIGH` is a subscriber ceiling, not a forced delivery layer. A weak downlink
|
||||
may receive `LOW` while another subscriber remains on `HIGH` when the pinned
|
||||
LiveKit stack can sustain that contract.
|
||||
4. A subscriber's `HIGH` request is a ceiling, not a forced delivery layer. A
|
||||
weak downlink may receive the lower representation while another subscriber
|
||||
receives the highest available representation when the pinned LiveKit stack
|
||||
can sustain that contract.
|
||||
5. AdaptiveStream is a display-demand input, not a network detector. Current
|
||||
Screener subscribers keep it disabled because any Viewer may become a relay
|
||||
and LiveKit cannot see that Viewer's peer children. Network adaptation uses
|
||||
@@ -48,16 +51,21 @@ removing the representation required by constrained Viewers.
|
||||
6. ADR-0005 remains the only route owner. Layer choice does not change topology,
|
||||
endpoint capacity, SFU admission, or decoded-stall recovery authority.
|
||||
|
||||
Current source and production remain the deployed single-`HIGH` implementation
|
||||
until a separate implementation is integrated and deployed. That is deployment
|
||||
state, not the accepted quality target.
|
||||
Current source leaves the screen-share simulcast layers unset so pinned client
|
||||
`2.22.0` supplies its default half-resolution plus original-resolution pair,
|
||||
enables Dynacast, and tracks the required server send-side-BWE configuration.
|
||||
Those two encodings use RIDs `q,h`; LiveKit's protocol labels them `LOW,MEDIUM`,
|
||||
while a subscriber's default `HIGH` ceiling still selects the highest available
|
||||
encoding. Production remains the deployed single-encoding implementation until
|
||||
cutover.
|
||||
|
||||
## Pinned Physical Result
|
||||
|
||||
The 2026-08-25 gate used LiveKit server `1.13.5`, client `2.22.0`, Chrome 151,
|
||||
a continuously changing VP8 source, and one `HIGH+LOW` screen-share publication.
|
||||
Manual selection delivered `1280x720` HIGH and `640x360` LOW at about 30 fps,
|
||||
and the same connection recovered from LOW to HIGH.
|
||||
a continuously changing VP8 source, and one full-resolution plus half-resolution
|
||||
screen-share publication. Manual maximum/lower selection delivered `1280x720`
|
||||
and `640x360` at about 30 fps, and the same connection recovered from the lower
|
||||
to the full-resolution representation.
|
||||
|
||||
With the server default receiver-side BWE, an approximately 0.96 Mbps subscriber
|
||||
remained on HIGH for about 21 seconds and ended at zero decoded fps. Enabling
|
||||
@@ -68,14 +76,16 @@ connection's automatic upgrade after removing shaping was not isolated because
|
||||
Chrome bound that test condition when creating the PeerConnection; manual and
|
||||
AdaptiveStream same-connection LOW-to-HIGH recovery were independently proven.
|
||||
|
||||
Dynacast paused layers with no subscribers after about five seconds. LOW-only
|
||||
demand disabled HIGH, while any HIGH demand kept both LOW and HIGH encoding; the
|
||||
second VP8 simulcast encode is therefore an accepted bounded cost, not eliminated
|
||||
by Dynacast. AdaptiveStream selected HIGH for a large attached element, LOW for a
|
||||
Dynacast paused layers with no subscribers after about five seconds. Lower-only
|
||||
demand disabled the full-resolution encoding, while maximum demand kept both
|
||||
encodings active; the second VP8 simulcast encode is therefore an accepted
|
||||
bounded cost, not eliminated by Dynacast. AdaptiveStream selected the full
|
||||
representation for a large attached element, the lower representation for a
|
||||
small element, paused a hidden element, and recovered when visible. With no
|
||||
attached element it received only LOW, confirming that it cannot own relay
|
||||
ingress. The accepted product combination is VP8 HIGH+LOW simulcast, Dynacast,
|
||||
server send-side BWE, and `adaptiveStream: false` for Screener subscribers.
|
||||
attached element it received only the lower representation, confirming that it
|
||||
cannot own relay ingress. The accepted product combination is pinned-default
|
||||
VP8 screen-share simulcast, Dynacast, server send-side BWE, and
|
||||
`adaptiveStream: false` for Screener subscribers.
|
||||
|
||||
## Evidence Boundary
|
||||
|
||||
@@ -96,10 +106,11 @@ mutation or predict another parent.
|
||||
- A weak SFU Viewer can receive a real lower spatial representation when the
|
||||
framework selects it; the SFU does not transcode.
|
||||
- The Host publishes at most two SFU representations regardless of Viewer
|
||||
count. Dynacast pauses layers above aggregate demand and all layers when no
|
||||
subscriber remains; HIGH demand keeps both VP8 simulcast encodings active.
|
||||
count. Dynacast pauses representations above aggregate demand and all
|
||||
representations when no subscriber remains; maximum demand keeps both VP8
|
||||
simulcast encodings active.
|
||||
- Host encode capacity is handled through the existing explicit share profiles,
|
||||
not by falling back to single HIGH and abandoning constrained subscribers.
|
||||
not by falling back to one encoding and abandoning constrained subscribers.
|
||||
- An SFU-fed relay keeps a subscription ceiling sufficient for its subtree;
|
||||
local DOM visibility alone cannot pause that ingress.
|
||||
- A configured 60 fps or bitrate remains a ceiling, not a delivery guarantee.
|
||||
|
||||
+3
-1
@@ -466,7 +466,9 @@ deployment's self-hosted STUN listener for server-side public-IP discovery and
|
||||
to prevent pinned LiveKit from substituting a public default in its join
|
||||
response. Current Screener source gives Browser publisher/subscriber connects an
|
||||
explicit empty list, so those SFU PCs do not use either response after the
|
||||
pending application cutover. Do not add Redis for this one-node workload.
|
||||
pending application cutover. The tracked `rtc.congestion_control` enables
|
||||
`use_send_side_bwe` for LiveKit-owned per-subscriber simulcast selection. Do not
|
||||
add Redis for this one-node workload.
|
||||
The service journal is the diagnostic log; keep its retention finite and access
|
||||
restricted. Pinned LiveKit 1.13.5 includes raw PublisherOffer SDP in info-level
|
||||
join records, so the tracked production baseline uses `logging.level: warn` and
|
||||
|
||||
@@ -17,7 +17,7 @@ Last updated: 2026-08-25
|
||||
- One Host publication may serve all SFU subscriptions. Its exact generation owns one ingress handle and exact Viewer subscription handles own egress; later SFU children reuse the publication, while reserved, committed, and draining handles remain charged until the managed room is deleted and proven absent. Actual SFU usage release wakes only rooms that previously lost a candidate to global admission, and SFU-fed and peer-fed endpoints use the same provisional-child transaction. Parent choice hard-filters authority, reachability, cycles, effective capacity, and producer transition, then orders by shallowest result, remaining steady sender capacity, stable join order, and peer identity; server admission is checked at the current cursor. Acyclicity and room admission bound maximum depth; depth is observed but has no separate hard cap. Healthy committed edges remain sticky: the controller repairs waiting, invalid, departed-parent, and overflow children but does not periodically rebalance the graph. Raw IP, claimed NAT type, UA, geography, all-pairs probes, weighted scores, and periodic room rebalancing are not route inputs.
|
||||
- Ordinary peer ICE is STUN-only. Browser LiveKit publisher and subscriber PCs use no external ICE server, retain LiveKit-signaled UDP candidates, and let standard ICE nominate a non-relay pair; deployment STUN remains available to ordinary peers and LiveKit server-side public-IP discovery. This Browser SFU ICE-server isolation avoids the reproduced external-STUN/SFU association conflict without constraining candidate type or count and without adding TURN, another route, a port, or a timeout. Credentials, room secrets, candidate details, and diagnostic data remain private and narrowly scoped. A future strict-firewall transport requires separate evidence and must remain inside LiveKit rather than becoming another Screener route candidate.
|
||||
- Browser peers use existing periodic WebRTC/LiveKit sampling to maintain one exact-route decoded-progress timestamp. New frames or route/connection changes reset it; a non-paused deadline reports once. An active SFU cadence treats an absent track/report or stats failure as no progress without fabricating metrics and stops only on deactivation/teardown. Other current-path stats remain diagnostic and cannot prove another parent better.
|
||||
- ADR-0007 accepts VP8 `HIGH+LOW`, Dynacast, and server send-side BWE. Gate proved LOW for constrained and HIGH for unshaped subscribers; HIGH demand keeps both encodings. AdaptiveStream stays off because any Viewer may relay. Source/prod remain single-`HIGH`.
|
||||
- ADR-0007 accepts pinned-default VP8 half-resolution plus original-resolution SFU representations, Dynacast, server send-side BWE, and no AdaptiveStream for potential relays. Current source implements it; production remains single-encoding until cutover.
|
||||
- Browser capture requests available share audio by default on initial capture and source switch, with an explicit video-only warning when no audio track is returned. Its screen-audio control offers live-switchable 64/128/256 kbps sender ceilings and defaults to 128. The last-wins quality wire owns desired state; each endpoint serially applies and reads back current Host/relay/SFU audio senders, exposes local failures, and gives future senders the latest desired value. Opus remains fixed, and without an applied-ack wire the Host does not claim room-wide atomic convergence. These are configuration ceilings, not fidelity claims.
|
||||
- Browser display video leaves `contentHint` unset so Chromium retains its screen-capture classification; returned audio tracks keep `contentHint = "music"`. Browser video is fixed to VP8 across direct, browser-relay, and SFU paths; codec selection, fallback to another media codec, quality-state/wire fields, and share-lifetime codec branches are not product features. Actual codec/profile/encoder stats remain diagnostic only.
|
||||
- Recommended quality remains exactly `1080p60`, `1080p30`, and `720p30`, with the middle `1080p30` as the accepted default. `480p` means only an advanced `854x480` resolution choice whose FPS and bitrate remain independently selected; it is not a fourth recommended profile.
|
||||
@@ -29,18 +29,18 @@ Last updated: 2026-08-25
|
||||
|
||||
## Current Source And Production
|
||||
|
||||
- Production runs exact application/runtime revision `1d8761528d0dba43fb6d818df3934483ba2f5340`, release `1d87615`, on strict `screener-v12`; canonical `main` contains the same application/runtime tree plus the current truth checkpoint.
|
||||
- Current source and production admit up to `20` Viewers and implement the uniform `1/2/3` endpoint cap, exact prepare candidate, first-decoded-frame commit, strict rollback revision, decoded-progress stall, a two-stage direct/SFU operation deadline, and one event-driven room controller. A short-lived exact-generation observer proves only the pending candidate inside the existing total deadline. Both still cease to drive the active SFU deadline when its track/report disappears; the accepted cadence is not implemented or deployed. Ordinary peer ICE is STUN-only and the sole application fallback is SFU/UDP; stale TURN environment keys fail startup. SFU admission uses exact publication and Viewer-subscription handles with explicit no-default ingress/egress capacities and a listener-fenced dedicated LiveKit owner. Browser SFU publisher/subscriber connects use fresh empty ICE-server options; Chrome 151 on exact production `1d87615` verified one no-RID publisher encoding and progressing publisher/subscriber frames over LiveKit UDP, closing the current single-representation gate.
|
||||
- Production runs exact application/runtime revision `1d8761528d0dba43fb6d818df3934483ba2f5340`, release `1d87615`, on strict `screener-v12`; canonical `main` is newer and pending deployment.
|
||||
- Current source and production admit up to `20` Viewers and implement the uniform `1/2/3` endpoint cap, exact prepare candidate, first-decoded-frame commit, strict rollback revision, decoded-progress stall, one direct/SFU operation deadline, and one event-driven controller. Current source keeps active-SFU sampling alive through an absent track/report or stats failure; production retains the reproduced gap. Ordinary peer ICE is STUN-only and the sole application fallback is SFU/UDP. Exact SFU admission and empty Browser SFU ICE-server options remain unchanged; Chrome 151 closed release `1d87615`'s single-representation UDP gate.
|
||||
- Source and production use random four-digit memory rooms, dormant leases, exact-Host resume, restart loss, local preference replay, orthogonal grants/code entry, and no SQLite runtime. V12 uses a 22-character room-incarnation grant, `ROOM_NOT_FOUND`, no diagnostic-file export, and a compact UI showing the complete shorter URL.
|
||||
- Exact production artifacts, services, ports, and any change-scoped infrastructure recovery boundary are owned by [deployment](./deployment.md) and indexed by [status](./status.md).
|
||||
- Source and production require v12, fixed VP8, and no video hint. Both publish exactly one SFU `HIGH` encoding without simulcast or a subscriber layer selector and keep Dynacast disabled. Current Pause/Resume, live audio ceilings, exact-sender metrics, relay/SFU audio continuity, typed Viewer presentation, and quality profiles match the product truths above.
|
||||
- Source and production require v12, fixed VP8, and no video hint. Current source uses pinned-default SFU half-resolution plus original-resolution simulcast and Dynacast; production retains one encoding with Dynacast off. Other current media and UI behavior matches the truths above.
|
||||
- Browser SFU subscribers reconcile eligible Host screen publications across connect/activate event ordering, participant/track arrival, and reconnect. Production LiveKit logs only warn/error, and the application service has a bounded same-host LiveKit readiness preflight.
|
||||
- Exact `main@1d87615` passed TypeScript, 607 Web tests, both builds, hygiene, three reviews, production postflight, one no-RID SFU encoding gate, and a controlled exact-candidate rollback/SFU commit gate with progressing publisher/subscriber frames over LiveKit UDP. Earlier gates closed direct/browser-relay VP8 cadence and active-audio ceilings; representative networks, all-UDP-blocked failure, public SFU 60 fps, and real-game synchronization remain unproven. Exact metrics live in [verification status](./verification-status.md).
|
||||
- Exact deployed source `1d87615` passed TypeScript, 607 Web tests, both builds, hygiene, three reviews, production postflight, one no-RID SFU encoding gate, and a controlled exact-candidate rollback/SFU commit gate with progressing publisher/subscriber frames over LiveKit UDP. Earlier gates closed direct/browser-relay VP8 cadence and active-audio ceilings; representative networks, all-UDP-blocked failure, public SFU 60 fps, and real-game synchronization remain unproven. Exact metrics live in [verification status](./verification-status.md).
|
||||
- Chrome loopback, cap2/cap3, short resource, recovery, MBB, and A/V fixtures are bounded evidence only. They do not establish quality, resource limits, or product policy.
|
||||
|
||||
## Current Priority
|
||||
|
||||
1. Implement the accepted LiveKit-native `HIGH+LOW`/Dynacast/send-side-BWE SFU representation model; then deploy the active-SFU decoded-progress cadence, repeat controlled recovery, and finish real-network route, screen-audio, and real-game A/V acceptance. VP8 acceleration, mobile lifecycle, Native, distribution, and repository simplification remain later.
|
||||
1. Deploy the implemented LiveKit-native dual-representation/Dynacast/send-side-BWE model and active-SFU decoded-progress cadence, repeat controlled recovery, and finish real-network route, screen-audio, and real-game A/V acceptance. VP8 acceleration, mobile lifecycle, Native, distribution, and repository simplification remain later.
|
||||
|
||||
The desktop Host background/minimized report is deferred until it reproduces on current production with a real game and synchronized media plus CPU/GPU evidence. The bounded current-Chrome screening found no immediate Host-page lifecycle drop, and removal of the old harmful `contentHint = "motion"` remains a plausible confound rather than a proven background fix. Mobile Viewer playback and assigned-relay survival remain a separate lifecycle gate.
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ may instead lower frame rate. Neither preference overrides congestion control.
|
||||
|
||||
Production runs exact deployed application/runtime revision
|
||||
`1d8761528d0dba43fb6d818df3934483ba2f5340`, release `1d87615`; canonical
|
||||
`main` contains the same runtime code. Current Browser source and production use
|
||||
`main` contains newer SFU publication source pending deployment. Current Browser source and production use
|
||||
strict `screener-v12`, fixed VP8, no video `contentHint`, and no codec UI,
|
||||
quality state, or wire field. They use `balanced` as the recommended profile
|
||||
and advanced default;
|
||||
@@ -79,17 +79,18 @@ real-game quality, heterogeneous networks, or endurance.
|
||||
## 2026-08-25 Pinned Native-Adaptation Gate
|
||||
|
||||
LiveKit server `1.13.5`, JS client `2.22.0`, and Chrome 151 exercised a changing
|
||||
VP8 `HIGH+LOW` screen-share publication. Manual subscriber selection delivered
|
||||
`1280x720` HIGH and `640x360` LOW at about 30 fps, and the same connection
|
||||
returned from LOW to HIGH. AdaptiveStream selected HIGH for a large attached
|
||||
element, LOW for a small one, paused when hidden, and recovered when visible.
|
||||
Without an attached element it received only LOW, so it cannot govern a Viewer
|
||||
that may forward the stream to peer children.
|
||||
VP8 screen-share publication with full- and half-resolution representations.
|
||||
Manual maximum/lower subscriber selection delivered `1280x720` and `640x360`
|
||||
at about 30 fps, and the same connection returned from the lower to the full
|
||||
representation. AdaptiveStream selected full resolution for a large attached
|
||||
element, half resolution for a small one, paused when hidden, and recovered when
|
||||
visible. Without an attached element it received only the lower representation,
|
||||
so it cannot govern a Viewer that may forward the stream to peer children.
|
||||
|
||||
Dynacast stopped layers with no subscribers after about five seconds. LOW-only
|
||||
demand stopped HIGH, but any HIGH demand kept LOW and HIGH encoding together.
|
||||
It therefore avoids unused work but cannot remove VP8's dual-encode cost while a
|
||||
healthy HIGH subscriber exists.
|
||||
Dynacast stopped representations with no subscribers after about five seconds.
|
||||
Lower-only demand stopped the full representation, but maximum demand kept both
|
||||
encodings active. It therefore avoids unused work but cannot remove VP8's
|
||||
dual-encode cost while a healthy maximum-quality subscriber exists.
|
||||
|
||||
With LiveKit's default receiver-side BWE, an approximately 0.96 Mbps subscriber
|
||||
remained on HIGH for about 21 seconds and ended at zero decoded fps. With
|
||||
@@ -100,7 +101,8 @@ unshaping on the same PeerConnection was not isolated because the Chrome DevTool
|
||||
condition was bound when that connection was created; manual and AdaptiveStream
|
||||
same-connection upward transitions independently proved layer recovery.
|
||||
|
||||
The result accepts VP8 HIGH+LOW simulcast, Dynacast, and server send-side BWE.
|
||||
The result accepts pinned-default VP8 dual-representation simulcast, Dynacast,
|
||||
and server send-side BWE.
|
||||
Screener does not add a bandwidth estimator or layer controller. AdaptiveStream
|
||||
stays disabled for current subscribers because every Viewer may become a relay.
|
||||
The earlier dual-encode cost is handled through explicit Host share profiles,
|
||||
@@ -301,7 +303,7 @@ wire expose no codec choice. Codec/profile/encoder stats remain diagnostic and
|
||||
do not authorize automatic switching, route changes, or another controller.
|
||||
Production runs exact deployed application/runtime revision
|
||||
`1d8761528d0dba43fb6d818df3934483ba2f5340`, release `1d87615`; canonical
|
||||
`main` contains the same runtime code. Current source and production implement
|
||||
`main` contains newer SFU source pending deployment. Current source and production implement
|
||||
this strict `screener-v12` contract.
|
||||
|
||||
Chromium maps video `contentHint = "motion"` to libwebrtc `kFluid`, and
|
||||
@@ -489,10 +491,9 @@ the requested value remains null and a browser-reported default is not called
|
||||
a mismatch; multiple encodings remain unknown. Inbound stats provide no current
|
||||
standard `scalabilityMode` source, so C does not carry a null-only placeholder.
|
||||
The earlier exact-production gate rejected its ordered `q,h` publication because
|
||||
the active lower representation reduced `HIGH`. Current source and production
|
||||
still implement one Browser SFU `HIGH`; the exact deployment gate above proved
|
||||
that no-RID baseline, while the later native-adaptation gate established the
|
||||
accepted two-representation target.
|
||||
the active lower representation reduced `HIGH`. Current source implements the
|
||||
later accepted LiveKit-owned two-representation target; production remains the
|
||||
exact no-RID single-`HIGH` baseline until cutover.
|
||||
|
||||
Official W3C text checked 2026-08-19 defines names ending in `Id` as stats-object
|
||||
references. In particular, outbound [`mediaSourceId`](https://www.w3.org/TR/webrtc-stats/#dom-rtcoutboundrtpstreamstats-mediasourceid)
|
||||
@@ -519,14 +520,15 @@ member, which remains null rather than a capability conclusion.
|
||||
|
||||
## LiveKit Native Representation Boundary
|
||||
|
||||
ADR-0007 owns the accepted target: fixed `HIGH+LOW` VP8 simulcast with layer
|
||||
activation and per-subscriber selection delegated to LiveKit. The pinned gate
|
||||
passed with explicit server send-side BWE. Current source and production remain
|
||||
the not-yet-migrated single-`HIGH` baseline.
|
||||
ADR-0007 owns the accepted target: the pinned client's default VP8 screen-share
|
||||
simulcast pair with representation activation and per-subscriber selection
|
||||
delegated to LiveKit. The pinned gate passed with explicit server send-side BWE.
|
||||
Current source implements the target; production remains the not-yet-migrated
|
||||
single-encoding baseline.
|
||||
|
||||
The candidate assumed one shared `HIGH+LOW` SFU publication could preserve
|
||||
healthy `HIGH` while LiveKit selected a lower representation per constrained
|
||||
subscriber. Static review established two limits before the runtime gate:
|
||||
The candidate assumed one shared dual-representation SFU publication could
|
||||
preserve full resolution while LiveKit selected a lower representation per
|
||||
constrained subscriber. Static review established two limits before the runtime gate:
|
||||
ordinary WebRTC receivers have no portable per-RID selection API across
|
||||
separate PeerConnections. Runtime evidence confirmed that LiveKit 1.13.5
|
||||
Dynacast cumulatively enables qualities below the highest requested quality, so
|
||||
|
||||
@@ -58,9 +58,12 @@ Current high-signal boundary:
|
||||
physically verified direct/browser-relay/SFU screen-audio continuity and all
|
||||
three sender ceilings. An exact-production VP8 gate sustained about 59.5 fps
|
||||
on direct and Browser-relay paths and rejected the then-deployed `bf32859`
|
||||
SFU `q,h` publication because active `LOW` reduced `HIGH`. ADR-0007 accepts one Browser
|
||||
SFU `HIGH`; current source and production implement it. Chrome 151 verified
|
||||
one no-RID encoding and progressing publisher/subscriber frames over LiveKit UDP.
|
||||
SFU `q,h` publication because active `LOW` reduced `HIGH`. ADR-0007 accepts
|
||||
LiveKit-owned half-resolution plus original-resolution simulcast, Dynacast,
|
||||
and server send-side BWE. Current source implements that combination while
|
||||
production remains single-encoding; Chrome
|
||||
151 verified the production no-RID baseline and separately proved the accepted
|
||||
native adaptation on the pinned stack.
|
||||
- Ordinary browser ICE owns direct reachability. Do not build port prediction,
|
||||
guessed candidates, NAT classification, TCP probes, quality scores, or
|
||||
quality-driven reparenting. Current-path quality is diagnostic; a healthy
|
||||
|
||||
+4
-4
@@ -19,17 +19,17 @@ This is the current execution index. Git history owns completed timelines; [veri
|
||||
|
||||
- Canonical root `main` is the integration truth; auxiliary branches and older worktrees do not supersede it. Current source and production use the single strict Browser `screener-v12` wire and reject every other wire before room authority.
|
||||
- Current source implements one Host plus up to `20` Viewers, one steady outbound media-copy cap for every non-server endpoint (default `2`, static `1/2/3`), and the process-memory room model with random four-digit codes, a default 24-hour dormant lease, exact-Host resume, restart loss, local Host preference replay, exactly `open | private` code entry, and no SQLite runtime. Every room defaults to `open` without a room password and independently receives a Viewer grant: one 22-character opaque value bound to that room incarnation. Private rooms without a password are invitation-only; configuring a password additionally permits matching code-only entry. Unallocated/reclaimed/expired code-only entry returns `ROOM_NOT_FOUND` without a password prompt.
|
||||
- One event-driven controller owns the committed graph and at most one room-serial child operation. It uses one deterministic candidate list/cursor and one total direct-then-SFU deadline; exact admission and physical resources remain charged through drain, and the pending candidate commits only after an exact-generation decoded-frame proof. A short-lived 100 ms stats observer proves only that candidate inside the existing deadline. The active-path 2-second sampler currently ceases to drive its 15-second decoded-progress deadline when an SFU subscriber has no video track, no merged report, or a failed stats read; the accepted correction keeps that same cadence delivery alive without adding a timer or fabricating metrics. Healthy edges remain sticky and current-path quality does not authorize reparenting.
|
||||
- One event-driven controller owns the committed graph and at most one room-serial child operation. It uses one deterministic candidate list/cursor and one total direct-then-SFU deadline; exact admission and physical resources remain charged through drain, and the pending candidate commits only after an exact-generation decoded-frame proof. A short-lived 100 ms stats observer proves only that candidate inside the existing deadline. The active-path 2-second sampler now keeps its existing 15-second decoded-progress cadence when an SFU subscriber has no video track, no merged report, or a failed stats read, without adding a timer or fabricating metrics. Healthy edges remain sticky and current-path quality does not authorize reparenting.
|
||||
- Ordinary peer ICE is STUN-only and the only application fallback is the dedicated LiveKit SFU over UDP. SFU generations use explicit no-default ingress/egress capacities, exact `reserved | committed | draining` accounting, delete-plus-absence release, one Host publication, and exact Viewer subscription handles. No Screener TURN, ICE/TCP, media TCP, or TLS-relayed media route exists.
|
||||
- Current Browser SFU publisher and subscriber connects supply fresh `autoSubscribe: false` plus `rtcConfig: { iceServers: [] }` options. Ordinary peers retain deployment STUN; LiveKit signaling candidates, server-side public-IP discovery, route deadlines, ports, and retry behavior are unchanged.
|
||||
- Current source leaves video `contentHint` unset and keeps audio `contentHint = "music"`. Browser direct and browser-relay offers contain VP8 as their only media codec, and SFU publication uses exactly one VP8 `HIGH` encoding with `simulcast: false`, no backup codec, no subscriber quality selector, and explicit `dynacast: false`. Codec UI, quality state, wire fields, fallback media codecs, and share-lifetime codec branches are absent. Live 64/128/256 kbps audio-ceiling mutation remains unchanged; ordinary Host and SFU publisher metrics select the exact current sender, relays reconcile audio-track additions and removals on the persistent stream, and an SFU reconnect reacquires the current publication/sender or fails the route closed.
|
||||
- Current source leaves video `contentHint` unset and keeps audio `contentHint = "music"`. Browser direct and browser-relay offers contain VP8 as their only media codec. SFU publication fixes VP8 with no backup codec, leaves screen-share simulcast layers unset so pinned client `2.22.0` supplies its half-resolution plus original-resolution pair, enables Dynacast, retains LiveKit's default maximum subscriber ceiling, and keeps AdaptiveStream disabled. Codec UI, quality state, wire fields, fallback media codecs, and share-lifetime codec branches are absent. Live 64/128/256 kbps audio-ceiling mutation remains unchanged; ordinary Host and SFU publisher metrics select the exact current sender, relays reconcile audio-track additions and removals on the persistent stream, and an SFU reconnect reacquires the exact current publication/sender, avoids unchanged parameter/stat resets, or fails the route closed.
|
||||
- Current source implements typed first-frame Viewer presentation and recovery, Chinese user-facing route/access failures, local connection details without a diagnostic-file export, authenticated Host-only on-demand route snapshots for automatic acceptance, explicit missing-room presentation, and responsive entry controls. Host invitation controls remain independent from two-state room-code entry.
|
||||
- Recommended quality remains exactly `1080p60`, `1080p30`, and `720p30`; current source defaults to `1080p30`. Advanced resolution adds `480p` as `854x480` without adding a fourth preset, and advanced FPS and bitrate remain independent.
|
||||
- Exact `main@1d87615` passed repository hygiene, TypeScript, all 607 Web tests in 44 files, both production builds, three independent reviews, immutable deployment postflight, the Chrome 151 single-representation SFU gate, and the controlled direct-rollback-to-SFU transaction. Earlier exact-production gates exercised direct, browser-relay, SFU audio continuity and all three audio ceilings, and sustained about 59.5 fps on direct and browser-relay paths; exact applicability remains in [verification status](./verification-status.md).
|
||||
- Exact deployed source `1d87615` passed repository hygiene, TypeScript, all 607 Web tests in 44 files, both production builds, three independent reviews, immutable deployment postflight, the Chrome 151 single-representation SFU gate, and the controlled direct-rollback-to-SFU transaction. Earlier exact-production gates exercised direct, browser-relay, SFU audio continuity and all three audio ceilings, and sustained about 59.5 fps on direct and browser-relay paths; exact applicability remains in [verification status](./verification-status.md).
|
||||
|
||||
## Current Milestone
|
||||
|
||||
1. Implement ADR-0007's accepted VP8 `HIGH+LOW` simulcast with Dynacast and LiveKit server send-side BWE; keep AdaptiveStream disabled for potential relays and add no application layer controller. Then correct and deploy the active-SFU decoded-progress cadence, repeat the controlled recovery transaction, and finish representative ICE/STUN/SFU acceptance including screen-audio continuity and real-game A/V sync on mobile and heterogeneous networks. Browser port prediction, NAT classification, TCP probing, fake page keepalive, and quality-driven reparenting remain outside the accepted model.
|
||||
1. Deploy current source with ADR-0007's pinned-default VP8 dual representation, Dynacast, server send-side BWE, and active-SFU decoded-progress cadence; repeat the controlled recovery transaction and finish representative ICE/STUN/SFU acceptance including screen-audio continuity and real-game A/V sync on mobile and heterogeneous networks. Browser port prediction, NAT classification, TCP probing, fake page keepalive, and quality-driven reparenting remain outside the accepted model.
|
||||
|
||||
## Active Boundaries
|
||||
|
||||
|
||||
+2
-2
@@ -6,8 +6,8 @@ Only items in **Now** are executable after their stated decision gate. A branch
|
||||
|
||||
## Now
|
||||
|
||||
1. **Implement native SFU representation adaptation.** Publish fixed VP8 `HIGH+LOW` simulcast, enable Dynacast and LiveKit server send-side BWE, and keep AdaptiveStream disabled for current Viewers because any may become a relay. Preserve LiveKit-owned per-subscriber selection and the existing Host quality profiles; do not add a Screener quality controller or fall back to single HIGH for Host resource protection.
|
||||
2. **Finish real-network route acceptance.** First restore the existing active-SFU decoded-progress cadence when its track or stats report disappears, deploy it, and repeat the controlled recovery transaction. Then measure standard ICE/STUN direct paths, peer relay, SFU ingress/subscription, relay-ingress repair with subtree retention, disconnect/capacity drain, Pause/Resume, screen-audio continuity and real-game A/V sync across representative IPv4/IPv6 NATs and mobile networks. Include an all-UDP-blocked case to establish bounded explicit failure. Do not add another watchdog, port prediction, NAT classification, TCP probes, guessed candidates, route scores, or quality-driven reparenting.
|
||||
1. **Deploy native SFU representation adaptation.** Current source publishes the pinned-default VP8 half-resolution plus original-resolution screen-share representations, enables Dynacast, and tracks LiveKit server send-side BWE while keeping AdaptiveStream disabled because any Viewer may become a relay. Deploy and verify that exact combination without adding a Screener quality controller or falling back to one encoding for Host resource protection.
|
||||
2. **Finish real-network route acceptance.** Deploy the current-source active-SFU decoded-progress cadence correction and repeat the controlled recovery transaction. Then measure standard ICE/STUN direct paths, peer relay, SFU ingress/subscription, relay-ingress repair with subtree retention, disconnect/capacity drain, Pause/Resume, screen-audio continuity and real-game A/V sync across representative IPv4/IPv6 NATs and mobile networks. Include an all-UDP-blocked case to establish bounded explicit failure. Do not add another watchdog, port prediction, NAT classification, TCP probes, guessed candidates, route scores, or quality-driven reparenting.
|
||||
|
||||
## Accepted Later Roadmap
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ Capacity and routing entries below describe the exact tested source or release o
|
||||
|
||||
## Retained Functional Evidence
|
||||
|
||||
- Exact deployed `main@1d87615` passed repository hygiene, TypeScript typecheck, all 607 Web tests in 44 files, client and server production builds, three independent reviews, and production artifact/service postflight. Exact predecessor `b68c471` separately passed the 261-test SFU admission gate and explicit-Chrome access/privacy gate. Retained Chrome 20-Viewer and responsive visual checks apply only to their recorded predecessor revisions.
|
||||
- Exact deployed source `1d87615` passed repository hygiene, TypeScript typecheck, all 607 Web tests in 44 files, client and server production builds, three independent reviews, and production artifact/service postflight. Exact predecessor `b68c471` separately passed the 261-test SFU admission gate and explicit-Chrome access/privacy gate. Retained Chrome 20-Viewer and responsive visual checks apply only to their recorded predecessor revisions.
|
||||
- Chrome 151 on exact production `1d87615` used real dynamic-tab `getDisplayMedia()` at 1822x1080 and made only the ordinary STUN peer attempt unusable so the existing route operation reached its SFU suffix. Publisher and subscriber LiveKit PCs retained empty external ICE-server lists and selected UDP. The publisher exposed one video sender encoding and one outbound video stats object, both without RID; over six seconds it encoded 180 frames while the subscriber decoded and rendered 182 frames, with bytes advancing at both ends. This closes the single-representation implementation/deployment gate without claiming public SFU 60 fps, real-game quality, heterogeneous-network behavior, or endurance.
|
||||
- Chrome 151 on exact production `1d87615` used one continuously changing synthetic Browser stream while test-side policy made only ordinary STUN peer PCs unusable and left LiveKit unchanged. The Viewer observed direct prepare revision `1`, explicit rollback active revision `2`, SFU prepare revision `3`, exact-revision first-frame ready, then SFU active revision `3`; no ready was sent for the failed direct candidate. The final Host snapshot had no operation, parent/final route `sfu`, rejection `none`, and first-frame/final timing `10703/10703` ms within the unchanged 20-second total deadline. Host and Viewer LiveKit PCs had zero external ICE URLs, selected UDP, and advanced `58/57` frames plus bytes over the next two seconds. This closes the exact deployed controlled candidate rollback/SFU commit transaction, not physical capture, heterogeneous-network reachability, all-UDP-blocked failure, quality, or endurance.
|
||||
- Chrome 151 on exact production `1d87615` formed `Host -> SFU -> relay -> child` plus a separate direct Viewer, passed Pause/Resume, and then locally closed the relay's subscriber PC. After 45,234 ms the relay remained assigned to SFU and sent no `route-media-unavailable`, `refresh-sfu`, or `route-failed`; its downstream child independently reported its own decoded stall and recovered through the other Viewer. Receiver-stats calls had stopped about 28.9 seconds before the final sample. Source inspection confirms that no video track stops SFU sampling while an empty merged report or failed stats read produces no deadline sample. This bounded fault injection exposes an active-cadence implementation gap; local `RTCPeerConnection.close()` does not reproduce physical UDP loss or prove LiveKit's real reconnect path, so relay-ingress recovery remains open.
|
||||
@@ -37,12 +37,12 @@ Capacity and routing entries below describe the exact tested source or release o
|
||||
|
||||
## Open Proof Boundaries
|
||||
|
||||
- Routing: exact v10 local direct loopback remains historical evidence for admission-20 initial ingress. Exact v12 production closes one controlled ordinary-direct exhaustion, explicit rollback, and exact SFU first-frame commit transaction; the later subscriber-PC closure injection found the active-SFU cadence gap without proving physical loss recovery. Representative real-network direct/peer/SFU behavior, relay-ingress reparent with subtree retention, disconnect/effective-capacity drain, pause/resume, and bounded all-UDP-blocked failure remain open. Silent control partitions may wait 30 to 60 seconds for heartbeat detection before the default five-second grace; a non-paused decoded-frame stall may invalidate only that exact child edge.
|
||||
- Quality: exact-production direct and Browser-relay VP8/no-hint cadence plus the deployed single-`HIGH` SFU baseline are closed. ADR-0007's pinned gate separately proved LiveKit-owned `HIGH+LOW` selection with server send-side BWE; its product implementation/deployment, public SFU 60 fps, real-game quality, heterogeneous devices, and endurance remain open. Startup ramp is stock BWE behavior and does not authorize an SDP or application bitrate workaround.
|
||||
- Routing: exact v10 local direct loopback remains historical evidence for admission-20 initial ingress. Exact v12 production closes one controlled ordinary-direct exhaustion, explicit rollback, and exact SFU first-frame commit transaction; the later subscriber-PC closure injection found the active-SFU cadence gap without proving physical loss recovery. Current source corrects that cadence while physical recovery remains open. Representative real-network direct/peer/SFU behavior, relay-ingress reparent with subtree retention, disconnect/effective-capacity drain, pause/resume, and bounded all-UDP-blocked failure remain open. Silent control partitions may wait 30 to 60 seconds for heartbeat detection before the default five-second grace; a non-paused decoded-frame stall may invalidate only that exact child edge.
|
||||
- Quality: exact-production direct and Browser-relay VP8/no-hint cadence plus the deployed single-encoding SFU baseline are closed. ADR-0007's pinned gate separately proved LiveKit-owned selection between full- and half-resolution representations with server send-side BWE, and current source implements that combination; deployment, public SFU 60 fps, real-game quality, heterogeneous devices, and endurance remain open. Startup ramp is stock BWE behavior and does not authorize an SDP or application bitrate workaround.
|
||||
- Host background: deferred until the report reproduces on exact production with a continuously changing real game or replay, a separate direct wired Viewer, synchronized capture/outbound/inbound plus CPU/GPU evidence, and repeated focused/unfocused/hidden/minimized windows. The current-Chrome Browser `getDisplayMedia()` WPF-window screening did not observe an immediate Host-page media drop, and the old harmful `contentHint = "motion"` release remains a plausible confound rather than a proven background cause. Current evidence authorizes no keepalive, Wake Lock, timer, codec switch, route change, or Native fallback.
|
||||
- Relay resources: current source and production expose the authenticated `1/2/3` endpoint-cap boundary, and retained v10 local loopback evidence covers 20 Viewers at cap `2` only for that source. CPU/GPU/upload and heterogeneous-network measurements remain diagnostics, not authority to change the contract.
|
||||
- Room scale: current source and production admit 20 Viewers plus one Host, and the retained v10 local Browser convergence gate covers that bound on direct loopback. A real public-network 1:20 session, quality, and sustained resource matrix remain open.
|
||||
- SFU: source and production use one Host `HIGH` encoding without simulcast or a subscriber layer selector, with exact Viewer subscriptions and no fixed root-count or room-wide lease rule. Chrome 151 physically closed the empty-external-ICE-server connection hold and exact single-representation publisher/subscriber gate. Real LiveKit lifecycle/JWT replay, resource use, SFU-fed first-child behavior, and broader heterogeneous-network acceptance remain open.
|
||||
- SFU: current source uses pinned-default half-resolution plus original-resolution simulcast, Dynacast, and the tracked server send-side-BWE setting without an application layer selector; production remains one Host encoding. Both use exact Viewer subscriptions and no fixed root-count or room-wide lease rule. Chrome 151 physically closed the empty-external-ICE-server connection hold and exact-production single-representation gate, while the separate pinned-stack gate proved native representation selection. Real LiveKit lifecycle/JWT replay, resource use, SFU-fed first-child behavior, and broader heterogeneous-network acceptance remain open.
|
||||
- Audio: current v12 source and production select the exact active ordinary Host or SFU publisher audio sender, reset samples across track changes or absence, keep relay children synchronized with persistent-stream audio changes, and reacquire exact SFU publications/senders after reconnect. Chrome 151 on exact production `bf32859` physically closed the reported active-source `1 kbps` observation and all three live ceilings across direct, browser-relay and SFU. Audible game quality, heterogeneous-network changes and real-game A/V synchronization remain part of route acceptance; a muted/idle Opus track may still legitimately remain near 1 kbps.
|
||||
- Observability: current v12 source and production keep local connection details but no Host/Viewer diagnostic downloads or Browser file-export plumbing. The Host-only bounded snapshot stays for automatic acceptance. Chrome 151 supplied selected candidate endpoints and ordinary-route A/V playout timestamps in the current gate, but omitted the SFU A/V timestamp. Target-browser availability and actual values remain open for codec/fmtp and encoder implementation/power efficiency, media-source and encoded FPS, and broader jitter/concealment cases. All retained fields diagnose and have no recovery authority; they do not by themselves prove hardware encode, synchronization quality, or route quality.
|
||||
- Mobile: Android/iOS remain Viewer-only. Autoplay, rotation, iOS lock-screen/page reclamation, background reconnection, and network changes need device evidence. AirPlay/system mirroring is Viewer-local output, not a portable in-app WebRTC output contract.
|
||||
|
||||
+3
-3
@@ -188,7 +188,7 @@ Viewer 本地连接详情只从当前 route 的唯一 inbound screen video/audio
|
||||
|
||||
`frameRate` 约束和 sender 的 `maxFramerate` 都是目标或上限;浏览器仍控制实际捕获、重复帧处理、编码和拥塞降级。静态画面可能由浏览器减少实际编码数据,但标准没有保证每个实现以同一种方式节省采集或 GPU 开销。本 PoC 不添加画面差分、定时改约束或自定义动态 FPS 状态机,先用 `framesEncoded`、实际发送码率、`totalEncodeTime`、质量受限原因和主机资源占用测量真实问题。
|
||||
|
||||
质量自适应采用 ADR-0007 的路径隔离框架能力,而不是全房间按最差链路降档。Direct/peer 保留每条 PeerConnection 的 stock WebRTC GCC。Browser Host 的唯一 SFU publication 以固定 `HIGH+LOW` VP8 simulcast 提供两种空间表示。LiveKit Dynacast 按全房最高需求停用其上的发布层,无订阅时停用全部层;启用 send-side BWE 的 server stream allocator 独立选择每个 subscriber 的实际下行层,`HIGH` 只是上限。Screener 不实现质量评分、带宽估计、周期调档或应用媒体层 selector。当前任一 Viewer 都可能承担 peer children,因此 subscriber 统一保持 `adaptiveStream: false`,relay ingress 不跟随本地 DOM 可见性。Pinned gate 已证明受限下游原生选择 LOW、非受限下游取得 HIGH、手动及元素驱动的同连接恢复和 Dynacast 启停;也证明任何 HIGH 需求会让 LOW+HIGH 同时编码。双编码是支持受限下游的固定成本,不是回退到单层 HIGH 的理由。当前源码与生产尚未迁移,仍是单层 `HIGH`。
|
||||
质量自适应采用 ADR-0007 的路径隔离框架能力,而不是全房间按最差链路降档。Direct/peer 保留每条 PeerConnection 的 stock WebRTC GCC。Browser Host 的唯一 SFU publication 不手写层列表,由锁定的 LiveKit client 以半分辨率和原始分辨率两路 VP8 screen-share encoding 提供空间表示。LiveKit Dynacast 按全房最高需求停用其上的发布层,无订阅时停用全部层;启用 send-side BWE 的 server stream allocator 独立选择每个 subscriber 的实际下行层,订阅端最高质量请求只是上限。Screener 不实现质量评分、带宽估计、周期调档或应用媒体层 selector。当前任一 Viewer 都可能承担 peer children,因此 subscriber 统一保持 `adaptiveStream: false`,relay ingress 不跟随本地 DOM 可见性。Pinned gate 已证明受限下游原生选择半分辨率、非受限下游取得原始分辨率、手动及元素驱动的同连接恢复和 Dynacast 启停;也证明最高需求会让两路同时编码。双编码是支持受限下游的固定成本,不是回退到单路的理由。当前源码已完成迁移,生产仍是单路 encoding。
|
||||
|
||||
质量证据只用于诊断,不形成全房分数或 parent authority。路由只消费 exact child edge 的 `failed/closed` 和非暂停 decoded-frame stall。prepare/commit/abort 绑定 authenticated sessions、share generation、base/pending revision 与 candidate connection;candidate child 的第一帧新 decoded video 是唯一应用层 media-ready,ICE connected 不能单独提交。
|
||||
|
||||
@@ -212,7 +212,7 @@ SVC 的标准能力门禁已经静态关闭为 `no-go-web-svc-cross-path-hardwar
|
||||
|
||||
路由状态只有 committed graph 和一个 pending child operation;该对象聚合 candidate list/cursor、一个 current candidate/预留和一个总 deadline。relay 自身 ingress 失效时,它作为 child 进入同一 reparent 并保留 subtree;disconnect 或 effective capacity 降低时让 direct/overflow children 逐个回到 reconcile。多个下游 edge 若各自失效,会独立迁移并自然排空 relay。权威 pause abort operation、保持 active graph、抑制 stall;resume 重新 reconcile。
|
||||
|
||||
标准表示能力排在任何自定义媒体实现之前。Web P2P simulcast 缺少 receiver 选 RID 与跨 PeerConnection shared-encode 契约;LiveKit SFU 则原生拥有 per-subscriber 层选择与 Dynacast。ADR-0007 的 pinned-stack 门禁已通过,Browser SFU 的接受目标是固定 `HIGH+LOW`、Dynacast 和 server send-side BWE,并完全委托框架启停和选择;不得把该能力扩散到 direct/peer,也不得绕过 stock WebRTC congestion control。
|
||||
标准表示能力排在任何自定义媒体实现之前。Web P2P simulcast 缺少 receiver 选 RID 与跨 PeerConnection shared-encode 契约;LiveKit SFU 则原生拥有 per-subscriber 层选择与 Dynacast。ADR-0007 的 pinned-stack 门禁已通过,Browser SFU 的接受目标是锁定 SDK 默认的两路 screen-share 表示、Dynacast 和 server send-side BWE,并完全委托框架启停和选择;不得把该能力扩散到 direct/peer,也不得绕过 stock WebRTC congestion control。
|
||||
|
||||
## 移动分享端能力边界与排期
|
||||
|
||||
@@ -362,7 +362,7 @@ Token-free SFU standby prewarm 保持可选且不创建 participant、publicatio
|
||||
|
||||
## 当前交付边界
|
||||
|
||||
- Production 使用严格单一 `screener-v12` wire 和精确 deployed application/runtime revision `1d8761528d0dba43fb6d818df3934483ba2f5340`、release `1d87615`;current source 含相同 application/runtime tree 与当前 truth checkpoint。两者均已实现短 room-lived grant、明确 missing-room 结果、产品诊断下载移除、完整短 URL 邀请 UI、exact-current audio stats 与音轨跨 relay/SFU reconnect 连续性,固定 Browser VP8 且 video 不设置 `contentHint`,codec selector、quality-state/wire 字段与分享生命周期 codec 分支不存在;Browser SFU 只发布一个 `HIGH` encoding,不启用 simulcast 或 Dynacast。
|
||||
- Production 使用严格单一 `screener-v12` wire 和精确 deployed application/runtime revision `1d8761528d0dba43fb6d818df3934483ba2f5340`、release `1d87615`;current source 含待部署的新 application 与 LiveKit template。两者均已实现短 room-lived grant、明确 missing-room 结果、产品诊断下载移除、完整短 URL 邀请 UI、exact-current audio stats 与音轨跨 relay/SFU reconnect 连续性,固定 Browser VP8 且 video 不设置 `contentHint`,codec selector、quality-state/wire 字段与分享生命周期 codec 分支不存在。Current source 的 Browser SFU 使用 pinned-default 半分辨率加原始分辨率 simulcast 与 Dynacast;production 仍只发布一个 encoding。
|
||||
- Pause/Resume 由当前 Host session 和 `shareGeneration` 约束。Host 后台性能和 VP8 hardware acceleration 分别按受控根因证据诊断;标准 ICE/SFU 按真实异构网络验收。
|
||||
- 当前发布面只包含 Web Host、Viewer 和 browser relay。Native sender、capture helper、shared-encode executable 与两个分发包仍由各自后续决策和物理证据驱动,不属于当前 Browser release。
|
||||
|
||||
|
||||
+3
-3
@@ -28,7 +28,7 @@ SFU 的 publication、subscription、server-resource accounting、临时 overlap
|
||||
- P2P 优先:每条普通逻辑媒体边先尝试 direct/peer UDP,服务器不得默认中转可直连媒体。更多观看者优先沿满足统一端点容量的无环 peer 图分布;每名 Viewer 同时至多一个 active upstream,失败时只影响需要重选的 child。任何 source-reachable endpoint 在 effective capacity 有空位时都可作为 parent candidate;Peer-fed 与 SFU-fed endpoint 使用同一个 provisional-child transaction,并在 candidate child 解码出第一帧新画面后独立提交该 edge。Host 已满且首次需要 SFU 时,同一 reconcile 将 affected Host direct child(若无则稳定顺序中最新的 connected Host direct child)作为本次 child operation,创建唯一 Host publication 并把该 child 变为首个 SFU subscriber,subtree 不动;publication 取代释放的 Host peer slot,下一次扫描再处理原受限 Viewer,不建立 bootstrap 状态机。
|
||||
- NAT 与传输边界:STUN 必须可用,所有 ordinary peer `RTCPeerConnection` 只接收 STUN ICE servers。Browser LiveKit publisher/subscriber PC 必须显式使用空 `iceServers`,保留 LiveKit signaling 交付的授权 SFU UDP candidates,并由标准 ICE 选择 nominated non-relay pair;不得把 ordinary-peer STUN、LiveKit join response 中的 STUN 或默认公共 STUN混入该 PC,也不得把某一种 candidate type 或数量写成成功条件。部署 STUN 仍供 ordinary peers 和 LiveKit server-side 公网地址发现使用。浏览器 ICE connectivity checks 是 direct UDP 可达性的权威;Screener 不合成 remote candidate、不预测端口、不分类 NAT,也不使用 TCP 探测推断 UDP 媒体可达性。所有恢复和候选授权必须绑定 authenticated room/endpoint session、share generation、base/pending route revision 与 candidate connection identity;成功即停止,失败、超时或绑定失效即撤销,direct 阶段耗尽后在同一 operation deadline 内进入 SFU/UDP,全部可用路径耗尽后明确等待或失败。已接受的完整顺序只有保持或尝试 direct/STUN peers,再复用或建立 direct-ingress Host publication/SFU subscription。Screener 不配置 TURN、ICE/TCP、media TCP 或 TLS-relayed media;当前 LiveKit 也保持 UDP-only。未来若真实严格防火墙矩阵证明需要另一种传输,必须把它作为 LiveKit 内部能力重新决策,不能增加第三种应用 route candidate。LiveKit join JWT 仅按协议进入不得记录的 WebSocket request target;HTTPS/WSS 继续独立使用 TLS/TCP。
|
||||
- 质量控制:推荐档位始终只有 1080p60(8 Mbps)、1080p30(5 Mbps)、720p30(3 Mbps)三档,默认选择中档 `1080p30`。高级设置的分辨率选项增加 `480p`(854x480),其 FPS 与码率仍由高级控件独立选择;不增加 `480p` 推荐档、preset ID 或固定的 480p FPS/码率组合。三档与高级设置初始值统一采用 `balanced`(平衡),`maintain-resolution`(清晰优先)、`maintain-framerate`(流畅优先)保留为显式选择。Browser 视频固定使用 VP8;界面、质量状态和 wire 不提供 codec 选择或切换。分享期间可直接换视频档位和 64/128/256 kbps 音频 sender ceiling,不重新选择来源或重建健康连接。Peer-assisted relay 与已配置的 SFU 必须接收同一个严格校验的房间级权威设置,覆盖当前和以后建立的 sender;普通 P2P 信令结构保持不变。任何设置都只是浏览器上限和偏好,具体降级由浏览器决定;必须读回实际 sender 参数并通过 stats 观测结果、显示参数拒绝、改写和持续受限原因。Host 资源不足时由 Host 主动选择较低的现有档位,不得以删除 SFU LOW 表示来静默牺牲受限下游。不做自定义动态 FPS、综合评分或周期性全房间调档。
|
||||
- 路径隔离表示:direct/peer 继续使用每条 PeerConnection 独立的 stock WebRTC GCC,不把 Viewer 反馈合成全房目标。Browser Host 的唯一 SFU publication 提供固定两层 VP8 `HIGH+LOW` simulcast;LiveKit Dynacast 按全房最高需求停用其上的发布层,无订阅时停用全部层,任何 `HIGH` 需求则保留两层编码;启用 send-side BWE 的 server stream allocator 为每个 subscriber 选择实际下行层。Screener 不实现质量评分、带宽估计、周期调档或应用媒体层 selector。所有当前 Viewer 都可能承担 peer children,因此 subscriber 保持 `adaptiveStream: false`,不得让本地元素尺寸或可见性控制 relay ingress。表示选择不改变拓扑、端点容量或路由失效权威。ADR-0007 的 pinned-stack 物理门禁已经通过;当前源码和生产仍是尚未迁移的单层 `HIGH`。
|
||||
- 路径隔离表示:direct/peer 继续使用每条 PeerConnection 独立的 stock WebRTC GCC,不把 Viewer 反馈合成全房目标。Browser Host 的唯一 SFU publication 将层配置留空,由锁定的 LiveKit client 提供半分辨率和原始分辨率两路 VP8 screen-share encoding;LiveKit Dynacast 按全房最高需求停用其上的发布层,无订阅时停用全部层,最高需求则保留两路编码;启用 send-side BWE 的 server stream allocator 为每个 subscriber 选择实际下行层。Screener 不实现质量评分、带宽估计、周期调档或应用媒体层 selector。所有当前 Viewer 都可能承担 peer children,因此 subscriber 保持 `adaptiveStream: false`,不得让本地元素尺寸或可见性控制 relay ingress。表示选择不改变拓扑、端点容量或路由失效权威。ADR-0007 的 pinned-stack 物理门禁已经通过;当前源码已实现该组合,生产仍是待迁移的单路 encoding。
|
||||
- 路由失效输入:应用不推断 parent-wide 质量,也不计算综合分数。当前 authenticated child 只在 PeerConnection `failed/closed`,或非权威暂停时现有 periodic stats 在统一 deadline 内没有新 decoded frame 时,使自己的 exact edge 失效并唤醒同一个 reconcile。decoded progress 只保存 route-keyed `lastDecodedAt`,route/connection变化或新帧重置、暂停抑制,同一 edge只报告一次,不增加轮询器。Active SFU route 暂时没有 video track、stats report 为空或 stats 读取失败时,现有采样 cadence 仍把该次作为无 decoded progress 推进 deadline;它不得停表、伪造连接指标或立即判定 edge 失效,只有 route 停用或 teardown 才停止该 cadence。该 operation 把失败的 exact direct parent 在当前 fact version 直接视为已尝试,先选择其他 direct parent,再进入有界 SFU suffix;新的外部事实才允许原 direct tuple 重新进入。Track availability、低码率、低 FPS、模糊、sender/viewer stats 与 SFU layer只用于stock WebRTC/LiveKit恢复和诊断。relay自身ingress失效时,它作为child换父并保留subtree;disconnect或effective downstream capacity降低只让direct/overflow children逐个走同一事务。
|
||||
- 断线恢复:临时网络变化先交给 WebRTC/LiveKit 的 ICE、consent、重连和流状态。Direct edge 可在 exact current parent/child sessions 内采用 framework rebuild 的新 connection ID而不改变topology。应用只在上述exact edge失效后运行一次有界reconcile;任一步成功即停止。候选耗尽时hard-invalid、confirmed-departed或overflow edge被退休并进入明确waiting,healthy bootstrap edge保留。所有异步结果受authenticated session、share generation、base/pending route revision与candidate connection identity约束,未受影响分支保持sticky。权威暂停abort未提交candidate、保留active graph、抑制stall,并在resume后重新reconcile。
|
||||
- 同一 operation 的一个总 deadline 只按实际存在的 `direct | sfu` 语义阶段等分,不配置每个 candidate 的 magic timeout。阶段内的硬失败继续推进 cursor;silent ICE 到达 direct 阶段边界时跳过剩余 direct tuple,必须把派生份额留给 SFU。20 名 Viewer 同时加入时,每名 child 的 operation deadline 不会被人数除分,但单一 room-serial operation 可能形成线性排队尾延迟;在接受另一种并发模型前,只测量 queue wait、candidate start、first decoded frame 与 final route 的 p50/p95/max,不用 NAT 猜测、TCP probe 或 magic timeout 绕过排队。
|
||||
@@ -151,12 +151,12 @@ SFU 的 publication、subscription、server-resource accounting、临时 overlap
|
||||
### 质量和恢复
|
||||
|
||||
- Given 用户选择任一推荐档位或高级质量参数,When 接收端实际分辨率、帧率或码率持续显著低于该目标,Then 诊断数据能区分采集约束、发送编码、带宽/拥塞、当前 peer/SFU 路径和接收端限制;产品不得只改变档位标签却继续隐藏实际降级。
|
||||
- Given Browser Host 建立 SFU publication,When sender 参数和 RTCStats 可读,Then 最多存在固定的 VP8 `LOW` 与 `HIGH` 两个 outbound video encoding,表示数不随 Viewer 数增长;LiveKit Dynacast 按聚合层需求停用其上的层,启用 send-side BWE 的 server stream allocator 为每个 SFU subscriber 选择实际下行层,弱下行不能改变健康路径、拓扑或 direct/peer 各自的 stock WebRTC GCC。
|
||||
- Given Browser Host 建立 SFU publication,When sender 参数和 RTCStats 可读,Then 最多存在固定的半分辨率与原始分辨率两个 VP8 outbound video encoding,表示数不随 Viewer 数增长;LiveKit Dynacast 按聚合层需求停用其上的层,启用 send-side BWE 的 server stream allocator 为每个 SFU subscriber 选择实际下行层,弱下行不能改变健康路径、拓扑或 direct/peer 各自的 stock WebRTC GCC。
|
||||
- Given 当前 Viewer 的 exact edge 进入 `failed/closed`,或在非权威暂停时超过统一 deadline 没有新 decoded frame,When 服务端确认 authenticated child、share generation、active revision 与 connection identity 仍匹配,Then 只把该 child 送入同一个 reserve/prepare/first-frame/commit-or-abort 事务;低码率、低 FPS、模糊和 SFU layer 只进入诊断。
|
||||
- Given 当前 edge 仍在解码但分辨率、FPS、码率、freeze ratio、RTT、jitter、loss 或 limitation evidence 持续影响观感,When 评估是否自动换父,Then 当前产品只诊断而不迁移:现有路径证据不能证明未连接 parent 的反事实质量,WebRTC/LiveKit 继续拥有拥塞适应,应用不建立 alternative-parent probe、hysteresis/cooldown、加权分数或周期性重排。
|
||||
- Given 同一 Viewer 同时是某人的 child 和其他人的 parent,When 自身 ingress 失效,Then 它作为 child 换父、在新 ingress 首帧提交前保留可用旧 edge,并保持 subtree。Given endpoint disconnect 或 `effectiveDownstreamCapacity` 降低,Then controller 只保留稳定顺序内可容纳的 children,并将 deterministic overflow children 逐个送入同一事务;childless 后删除 disconnected relay。
|
||||
- Given 当前分享权威 paused,When 新 Viewer 加入、edge 失效或已有 child operation 尚未提交,Then abort 整个 pending route operation 及其 current candidate/预留、保留 active graph、让新 Viewer waiting,并且不因没有 decoded frame 触发恢复。普通 Pause/Resume 只通过当前 Host session 与 `shareGeneration` 约束的一次 paused-state 更新同步本地 capture tracks、服务端快照、`host-status` 和 route pause;Resume 重新启用同一批 tracks并从当前 graph 唤醒 fresh reconciliation,不建立第二次授权、source acknowledgement、媒体 proof 或回滚状态。较早 share generation、已替换 Host session、stop 或新分享的迟到更新不得改变当前状态。
|
||||
- Given 当前 Browser SFU publication,When 进行质量验收,Then 受限 SFU subscriber 必须能由 LiveKit 原生选择 `LOW`,非受限 subscriber 仍可取得 `HIGH`;Dynacast 在 LOW-only 或无订阅时停用更高层或全部层,但任何 `HIGH` 需求允许 `LOW+HIGH` 同时编码。不得退回全房间最低目标、启用 AdaptiveStream 控制潜在 relay ingress,或增加应用媒体层 controller。
|
||||
- Given 当前 Browser SFU publication,When 进行质量验收,Then 受限 SFU subscriber 必须能由 LiveKit 原生选择半分辨率表示,非受限 subscriber 仍可取得原始分辨率表示;Dynacast 在只有较低需求或无订阅时停用更高表示或全部表示,但任何最高需求允许两路同时编码。不得退回全房间最低目标、启用 AdaptiveStream 控制潜在 relay ingress,或增加应用媒体层 controller。
|
||||
- Given 同一时刻采集、host outbound 与 viewer inbound 证据,When 进行根因分类,Then 关联派生后的实际协商视频 codec/profile/parameters、适用时的 `scalabilityMode` 与真实解码表现,依次区分 Capture 低、Capture 高但 outbound 因 CPU 低、Capture 高但 outbound 因 bandwidth 低、outbound 正常但 inbound 低、以及 inbound 正常但肉眼模糊五类;不用 UA 或设备型号代替运行时证据,也不上传 raw SDP。
|
||||
- Given Viewer 上报当前路径质量证据,When 报告未鉴权、过频、重复、过期或没有匹配的 sender/viewer 统计,Then 报告被丢弃;有效报告也只能用于诊断,不能改变 Host 表示或路径状态。
|
||||
- Given 受控 RTT/丢包测试条件,When 连续分享高动态游戏画面 30 分钟,Then 延迟、帧率与丢帧达到上述目标,且没有不可恢复的冻结。
|
||||
|
||||
@@ -246,8 +246,9 @@ function requestedScaleResolutionDownBy(
|
||||
function readVideoSenderParameters(
|
||||
parameters: RTCRtpSendParameters,
|
||||
includeAppliedScalabilityMode: boolean,
|
||||
encodingIndex = 0,
|
||||
): VideoSenderParameterValues {
|
||||
const encoding = parameters.encodings[0];
|
||||
const encoding = parameters.encodings[encodingIndex];
|
||||
const scalabilityMode = (
|
||||
encoding as
|
||||
| (RTCRtpEncodingParameters & { scalabilityMode?: unknown })
|
||||
@@ -302,15 +303,23 @@ export async function configureVideoSender(
|
||||
if (parameters.encodings.length === 0) {
|
||||
parameters.encodings = [{}];
|
||||
}
|
||||
parameters.encodings[0]!.maxBitrate = profile.maxBitrate;
|
||||
parameters.encodings[0]!.maxFramerate = profile.maxFramerate;
|
||||
parameters.encodings[0]!.scaleResolutionDownBy =
|
||||
requestedScaleResolutionDownBy(sender, profile);
|
||||
// Pinned LiveKit orders simulcast encodings by increasing spatial resolution.
|
||||
const encodingIndex = parameters.encodings.length - 1;
|
||||
parameters.encodings[encodingIndex]!.maxBitrate = profile.maxBitrate;
|
||||
parameters.encodings[encodingIndex]!.maxFramerate = profile.maxFramerate;
|
||||
if (parameters.encodings.length === 1) {
|
||||
parameters.encodings[encodingIndex]!.scaleResolutionDownBy =
|
||||
requestedScaleResolutionDownBy(sender, profile);
|
||||
}
|
||||
parameters.degradationPreference = profile.degradationPreference;
|
||||
|
||||
const requested = readVideoSenderParameters(parameters, false);
|
||||
const requested = readVideoSenderParameters(parameters, false, encodingIndex);
|
||||
await sender.setParameters(parameters);
|
||||
const applied = readVideoSenderParameters(sender.getParameters(), true);
|
||||
const applied = readVideoSenderParameters(
|
||||
sender.getParameters(),
|
||||
true,
|
||||
encodingIndex,
|
||||
);
|
||||
return senderParameterReadback(requested, applied);
|
||||
}
|
||||
|
||||
|
||||
+37
-14
@@ -124,7 +124,7 @@ export class SfuPublisher {
|
||||
return false;
|
||||
}
|
||||
|
||||
const room = new sdk.Room({ dynacast: false });
|
||||
const room = new sdk.Room({ dynacast: true });
|
||||
this.room = room;
|
||||
this.sdk = sdk;
|
||||
room.on(sdk.RoomEvent.Disconnected, () => {
|
||||
@@ -713,43 +713,52 @@ export class SfuPublisher {
|
||||
const previousAudio = this.audio;
|
||||
const previousVideoSender =
|
||||
previousVideo?.publication.videoTrack?.sender ?? null;
|
||||
const video = previousVideo
|
||||
const previousAudioSender =
|
||||
previousAudio?.publication.audioTrack?.sender ?? null;
|
||||
const currentVideo = previousVideo
|
||||
? currentPublishedTrack(
|
||||
room,
|
||||
sdk.Track.Source.ScreenShare,
|
||||
previousVideo.rawTrack,
|
||||
)
|
||||
: null;
|
||||
const videoSender = video?.publication.videoTrack?.sender ?? null;
|
||||
if (!video || !videoSender) {
|
||||
const videoSender = currentVideo?.publication.videoTrack?.sender ?? null;
|
||||
if (!currentVideo || !videoSender) {
|
||||
this.failureStage = "transport";
|
||||
await this.failClosed(room, generation);
|
||||
return false;
|
||||
}
|
||||
const audio = previousAudio
|
||||
const currentAudio = previousAudio
|
||||
? currentPublishedTrack(
|
||||
room,
|
||||
sdk.Track.Source.ScreenShareAudio,
|
||||
previousAudio.rawTrack,
|
||||
)
|
||||
: null;
|
||||
const audioSender = audio?.publication.audioTrack?.sender ?? null;
|
||||
if (previousAudio && (!audio || !audioSender)) {
|
||||
const audioSender = currentAudio?.publication.audioTrack?.sender ?? null;
|
||||
if (previousAudio && (!currentAudio || !audioSender)) {
|
||||
this.failureStage = "transport";
|
||||
await this.failClosed(room, generation);
|
||||
return false;
|
||||
}
|
||||
this.stopStats();
|
||||
const videoChanged =
|
||||
currentVideo.publication !== previousVideo?.publication ||
|
||||
videoSender !== previousVideoSender;
|
||||
const audioChanged =
|
||||
currentAudio?.publication !== previousAudio?.publication ||
|
||||
audioSender !== previousAudioSender;
|
||||
const video = videoChanged ? currentVideo : previousVideo;
|
||||
const audio = audioChanged ? currentAudio : previousAudio;
|
||||
if (videoChanged) {
|
||||
this.stopStats();
|
||||
}
|
||||
this.video = video;
|
||||
this.audio = audio;
|
||||
if (
|
||||
video.publication !== previousVideo?.publication ||
|
||||
videoSender !== previousVideoSender
|
||||
) {
|
||||
if (videoChanged) {
|
||||
this.senderParameters = null;
|
||||
this.videoQualityWarning = null;
|
||||
this.startStats(video);
|
||||
}
|
||||
this.startStats(video);
|
||||
|
||||
const profile = this.desiredProfile;
|
||||
const profileRevision = this.profileRevision;
|
||||
@@ -776,6 +785,21 @@ export class SfuPublisher {
|
||||
}
|
||||
try {
|
||||
const ownedAudioSender = audioSender;
|
||||
const requestedMaxBitrate = screenAudioBitrate(
|
||||
profile.screenAudioQuality,
|
||||
);
|
||||
const appliedMaxBitrate =
|
||||
ownedAudioSender.getParameters().encodings[0]?.maxBitrate ?? null;
|
||||
if (appliedMaxBitrate === requestedMaxBitrate) {
|
||||
this.audioSenderParameters = {
|
||||
requestedMaxBitrate,
|
||||
appliedMaxBitrate,
|
||||
mismatch: false,
|
||||
};
|
||||
this.audioParametersSender = ownedAudioSender;
|
||||
this.audioQualityWarning = null;
|
||||
return true;
|
||||
}
|
||||
const configured = await configurePublishedAudio(
|
||||
audio,
|
||||
ownedAudioSender,
|
||||
@@ -1194,7 +1218,6 @@ function videoPublishOptions(profile: QualityProfile): TrackPublishOptions {
|
||||
return {
|
||||
backupCodec: false,
|
||||
videoCodec: "vp8",
|
||||
simulcast: false,
|
||||
screenShareEncoding: {
|
||||
maxBitrate: profile.maxBitrate,
|
||||
maxFramerate: profile.maxFramerate,
|
||||
|
||||
@@ -174,6 +174,56 @@ describe("realtime quality controls", () => {
|
||||
expect(setParameters).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("applies a profile to the highest-resolution simulcast encoding", async () => {
|
||||
let applied = {
|
||||
encodings: [
|
||||
{
|
||||
rid: "q",
|
||||
maxBitrate: 1_250_000,
|
||||
maxFramerate: 30,
|
||||
scaleResolutionDownBy: 2,
|
||||
},
|
||||
{
|
||||
rid: "h",
|
||||
maxBitrate: 5_000_000,
|
||||
maxFramerate: 30,
|
||||
scaleResolutionDownBy: 1,
|
||||
},
|
||||
],
|
||||
} as unknown as RTCRtpSendParameters;
|
||||
const sender = {
|
||||
track: {
|
||||
getSettings: () => ({ width: 1920, height: 1080 }),
|
||||
},
|
||||
getParameters: () => applied,
|
||||
setParameters: vi.fn(async (parameters: RTCRtpSendParameters) => {
|
||||
applied = parameters;
|
||||
}),
|
||||
} as unknown as RTCRtpSender;
|
||||
|
||||
await expect(
|
||||
configureVideoSender(sender, QUALITY_PROFILES["1080p60"]),
|
||||
).resolves.toMatchObject({
|
||||
requested: { maxBitrate: 8_000_000, maxFramerate: 60 },
|
||||
applied: { maxBitrate: 8_000_000, maxFramerate: 60 },
|
||||
mismatches: [],
|
||||
});
|
||||
expect(applied.encodings).toEqual([
|
||||
{
|
||||
rid: "q",
|
||||
maxBitrate: 1_250_000,
|
||||
maxFramerate: 30,
|
||||
scaleResolutionDownBy: 2,
|
||||
},
|
||||
{
|
||||
rid: "h",
|
||||
maxBitrate: 8_000_000,
|
||||
maxFramerate: 60,
|
||||
scaleResolutionDownBy: 1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["saver", 64_000],
|
||||
["music", 128_000],
|
||||
|
||||
@@ -107,6 +107,18 @@ const livekit = vi.hoisted(() => {
|
||||
) => {
|
||||
const localTrack = new FakeLocalTrack(rawTrack);
|
||||
localTrack.publishOptions = options;
|
||||
const audioPreset = options.audioPreset;
|
||||
if (
|
||||
rawTrack.kind === "audio" &&
|
||||
typeof audioPreset === "object" &&
|
||||
audioPreset !== null &&
|
||||
"maxBitrate" in audioPreset &&
|
||||
typeof audioPreset.maxBitrate === "number"
|
||||
) {
|
||||
localTrack.sender.parameters.encodings = [
|
||||
{ maxBitrate: audioPreset.maxBitrate },
|
||||
];
|
||||
}
|
||||
const screenShareEncoding = options.screenShareEncoding;
|
||||
if (
|
||||
rawTrack.kind === "video" &&
|
||||
@@ -606,13 +618,13 @@ describe("SfuPublisher", () => {
|
||||
expect(replacementSender.getStats).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("explicitly keeps Dynacast off while preparing SFU fallback", async () => {
|
||||
it("enables Dynacast while preparing SFU fallback", async () => {
|
||||
const publisher = new SfuPublisher();
|
||||
|
||||
await expect(publisher.connect(connection)).resolves.toBe(true);
|
||||
|
||||
const room = livekit.state.rooms[0];
|
||||
expect(room.options).toEqual({ dynacast: false });
|
||||
expect(room.options).toEqual({ dynacast: true });
|
||||
expect(room.connect).toHaveBeenCalledWith(connection.url, connection.token, {
|
||||
autoSubscribe: false,
|
||||
rtcConfig: { iceServers: [] },
|
||||
@@ -635,7 +647,7 @@ describe("SfuPublisher", () => {
|
||||
expect(livekit.state.rooms[0]?.disconnect).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it("publishes exactly one VP8 video encoding", async () => {
|
||||
it("leaves VP8 screen-share simulcast layers to LiveKit defaults", async () => {
|
||||
const publisher = new SfuPublisher();
|
||||
const video = track("video", "video-1");
|
||||
const audio = track("audio", "audio-1");
|
||||
@@ -651,7 +663,6 @@ describe("SfuPublisher", () => {
|
||||
source: Track.Source.ScreenShare,
|
||||
backupCodec: false,
|
||||
videoCodec: "vp8",
|
||||
simulcast: false,
|
||||
screenShareEncoding: {
|
||||
maxBitrate: 8_000_000,
|
||||
maxFramerate: 60,
|
||||
@@ -660,7 +671,7 @@ describe("SfuPublisher", () => {
|
||||
});
|
||||
expect(
|
||||
room.localParticipant.publishTrack.mock.calls[0]?.[1],
|
||||
).toHaveProperty("videoCodec", "vp8");
|
||||
).not.toHaveProperty("simulcast");
|
||||
expect(room.localParticipant.publishTrack).toHaveBeenNthCalledWith(2, audio, {
|
||||
source: Track.Source.ScreenShareAudio,
|
||||
audioPreset: { maxBitrate: 128_000 },
|
||||
@@ -978,8 +989,9 @@ describe("SfuPublisher", () => {
|
||||
room.emit(RoomEvent.Reconnected);
|
||||
|
||||
await vi.waitFor(() =>
|
||||
expect(audioPublication.track.sender.setParameters).toHaveBeenCalledOnce(),
|
||||
expect(publisher.getAudioSenderParameters()).not.toBeNull(),
|
||||
);
|
||||
expect(audioPublication.track.sender.setParameters).not.toHaveBeenCalled();
|
||||
expect(audioPublication.track.sender.parameters.encodings[0]?.maxBitrate).toBe(
|
||||
256_000,
|
||||
);
|
||||
@@ -1026,13 +1038,45 @@ describe("SfuPublisher", () => {
|
||||
room.emit(RoomEvent.Reconnected);
|
||||
|
||||
await vi.waitFor(() =>
|
||||
expect(audioSender.setParameters).toHaveBeenCalledTimes(audioUpdates + 1),
|
||||
expect(publisher.getAudioSenderParameters()?.appliedMaxBitrate).toBe(
|
||||
128_000,
|
||||
),
|
||||
);
|
||||
expect(audioSender.setParameters).toHaveBeenCalledTimes(audioUpdates);
|
||||
expect(publisher.getSenderParameters()).toBe(senderParameters);
|
||||
expect(publisher.getQualityWarning()).toBe(qualityWarning);
|
||||
expect(room.disconnect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps publisher stats continuous across a signal-only reconnect", async () => {
|
||||
vi.useFakeTimers();
|
||||
const updates: Array<ConnectionMetrics | null> = [];
|
||||
const publisher = new SfuPublisher({
|
||||
onStats: (metrics) => updates.push(metrics),
|
||||
});
|
||||
const video = track("video", "video-1");
|
||||
await publisher.connect(connection);
|
||||
await publisher.activate(stream(video), qualityProfile);
|
||||
const room = livekit.state.rooms[0];
|
||||
const sender = room.localParticipant.publications[0].track.sender;
|
||||
sender.getStats
|
||||
.mockResolvedValueOnce(senderReport(video.id, 1_000, 100_000, 60))
|
||||
.mockResolvedValueOnce(senderReport(video.id, 3_000, 300_000, 180))
|
||||
.mockResolvedValueOnce(senderReport(video.id, 5_000, 500_000, 300));
|
||||
|
||||
await vi.advanceTimersByTimeAsync(4_000);
|
||||
const resets = updates.filter((update) => update === null).length;
|
||||
room.emit(RoomEvent.Reconnected);
|
||||
await Promise.resolve();
|
||||
await vi.advanceTimersByTimeAsync(2_000);
|
||||
|
||||
expect(updates.filter((update) => update === null)).toHaveLength(resets);
|
||||
expect(updates.at(-1)).toMatchObject({
|
||||
bitrateKbps: 800,
|
||||
intervalFramesEncoded: 120,
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["publication", "sender"] as const)(
|
||||
"fails the SFU route when republished audio loses its %s",
|
||||
async (missing) => {
|
||||
|
||||
Reference in new issue
Block a user