From bcc6d7c1881932738c1eaf908b9ddc7d016316fe Mon Sep 17 00:00:00 2001 From: AAA <35992542+TNTcraftHIM@users.noreply.github.com> Date: Mon, 24 Aug 2026 01:34:15 +0800 Subject: [PATCH] docs(route): retire application TURN (#215) --- AGENTS.md | 12 +- docs/README.md | 4 +- .../0005-automatic-hybrid-media-routing.md | 128 ++++++------ docs/deployment.md | 182 ++++++------------ docs/project-memory.md | 24 +-- docs/research/built-in-peer-ice-turn.md | 23 +-- docs/research/low-server-media-routes.md | 126 ++++++------ docs/session-handoff.md | 14 +- docs/status.md | 34 ++-- docs/todo.md | 18 +- docs/verification-status.md | 14 +- docs/方案设计.md | 105 +++++----- docs/需求理解.md | 73 +++---- 13 files changed, 342 insertions(+), 415 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 58bf7b4..9040761 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,8 +12,8 @@ ## 2. Safety And Privacy -- Never commit credentials, TURN shared secrets, TLS private keys, access tokens, or real user data. Commit only placeholder configuration and document local secret injection. -- Production TURN credentials are short-lived and narrowly authorized; rooms use authentication or unguessable expiring invitations. Do not log or persist private media-path identifiers beyond their accepted owner. +- Never commit credentials, TLS private keys, access tokens, or real user data. Commit only placeholder configuration and document local secret injection. +- Server-assisted media credentials are short-lived and narrowly authorized; rooms use authentication or unguessable expiring invitations. Do not log or persist private media-path identifiers beyond their accepted owner. - Block the current phase on P0 security, privacy, authorization, irreversible-data, generation, bounded-resource, or rollback failures and on P1 failures of its core path. ## 3. Product Contract @@ -21,10 +21,10 @@ - Build private, low-latency game screen sharing for one broadcaster and up to `20` authenticated Viewers, not a public or large-scale streaming service. - A normal desktop or mobile browser is the initial Viewer target. A packaged sender or native capture helper is a later optimization. - Keep media distributed and automatic. Central services own rooms, authentication, signaling, STUN, observability, and bounded fallback resources; do not silently make the product always-SFU. -- STUN and direct/peer UDP remain first. Ordinary peer edges receive no TURN candidates by default, and every accepted path ends in bounded success or clear failure. HTTPS/WSS transport is independent of media transport. -- Every non-server endpoint uses one server-authoritative steady outbound media-copy cap: default `2`, configurable as `1`, `2`, or `3`. A peer child or the Host's single SFU publication consumes one slot; upstream receive is free, selected TURN replaces the transport of the same copy, and SFU subscriber egress is accounted at the server. Browser role, UA, visibility, and client advertisement do not create another tier. -- The accepted route model is one committed graph, one event-driven reconciliation loop, and at most one room-serial child operation. That operation owns one deterministic candidate list/cursor, one current candidate with its reservations, and one total deadline; its exact child commits on the first newly decoded frame. Join/waiting, child reparent, relay ingress repair with subtree retention, confirmed departure, and effective-capacity reduction all use this operation. A failed exact `parent + transport` tuple is already tried for that operation, so other direct parents come first; another transport on the old parent may follow, and no persistent parent blacklist is created. -- WebRTC and LiveKit own ICE/DTLS/consent, congestion control, transient reconnect, and SFU stream state. Screener does not add all-pairs endpoint probing, parent-wide quality inference, a weighted route score, periodic rebalancing, or an independent depth cap. SFU is one Host publication with per-Viewer subscriptions; TURN is a selected transport for an authorized edge or Host-SFU ingress, not a topology node. Endpoint overlap and SFU/TURN resources remain independently admitted and bounded. +- STUN and direct/peer UDP remain first; the only application fallback is the dedicated LiveKit SFU over UDP. Screener configures no TURN, ICE/TCP, media TCP, or TLS-relayed media path. Every accepted path ends in bounded success or clear failure, and HTTPS/WSS transport is independent of media transport. +- Every non-server endpoint uses one server-authoritative steady outbound media-copy cap: default `2`, configurable as `1`, `2`, or `3`. A peer child or the Host's single SFU publication consumes one slot; upstream receive is free, and SFU subscriber egress is accounted at the server. Browser role, UA, visibility, and client advertisement do not create another tier. +- The accepted route model is one committed graph, one event-driven reconciliation loop, and at most one room-serial child operation. That operation owns one deterministic candidate list/cursor, one current candidate with its reservations, and one total deadline; its exact child commits on the first newly decoded frame. Join/waiting, child reparent, relay ingress repair with subtree retention, confirmed departure, and effective-capacity reduction all use this operation. A failed exact candidate is already tried for that operation, so other direct parents come first and SFU is the bounded suffix; no persistent parent blacklist is created. +- WebRTC and LiveKit own ICE/DTLS/consent, congestion control, transient reconnect, and SFU stream state. Screener does not add all-pairs endpoint probing, parent-wide quality inference, a weighted route score, periodic rebalancing, or an independent depth cap. SFU is one Host publication with per-Viewer subscriptions, and endpoint overlap plus SFU resources remain independently admitted and bounded. Any future strict-firewall transport must be accepted as a LiveKit-internal capability from real evidence, not added as another application route candidate. ## 4. Canonical Repository And Integration diff --git a/docs/README.md b/docs/README.md index 23cfffc..82c4764 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,8 +11,8 @@ - [Maintenance guide](./maintenance.md): Git workflow, context hygiene, document lifecycle, research policy, and automation. - [P2P WebRTC research](./research/webrtc-p2p-screen-sharing.md): evidence, bandwidth model, browser constraints, reference implementations, and feasibility assessment. - [Peer-assisted media research](./research/peer-assisted-media.md): browser shared-encode limits, standard relay re-encoding, deterministic sticky topology, runtime relay-capacity extension, and abandon gates. -- [Low-server-cost media routes](./research/low-server-media-routes.md): Host-publication/SFU distribution, selected-edge TURN, hop-level accounting, privacy-safe ICE evidence, and bounded transport gates. -- [Built-in peer ICE TURN candidate](./research/built-in-peer-ice-turn.md): rejected canary record, retained standards/resource findings, and the selected-edge replacement boundary. +- [Low-server-cost media routes](./research/low-server-media-routes.md): Host-publication/SFU distribution, direct-to-SFU conclusion, hop-level accounting, privacy-safe ICE evidence, and bounded transport gates. +- [Built-in peer ICE TURN candidate](./research/built-in-peer-ice-turn.md): historical evidence for the rejected participant-wide and selected-edge TURN candidates. - [Advanced peer distribution](./research/advanced-peer-distribution.md): generic local-reconcile evidence plus multi-tree/SVC, encoded-object relay, native RTP forwarding, FEC/network-coding, and MoQ gates. - [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. diff --git a/docs/adr/0005-automatic-hybrid-media-routing.md b/docs/adr/0005-automatic-hybrid-media-routing.md index 3c0a5e8..c5a5223 100644 --- a/docs/adr/0005-automatic-hybrid-media-routing.md +++ b/docs/adr/0005-automatic-hybrid-media-routing.md @@ -53,11 +53,9 @@ set. Capacity counts active outbound media copies produced by a non-server endpoint: an ordinary peer child consumes one slot and the Host's single SFU publication -consumes one Host slot. An upstream receive edge is free. A committed selected -TURN transport replaces the transport of its logical edge and consumes the same -single steady slot; a parallel media-producing candidate requires a separately -reserved transition slot. SFU subscriptions consume server egress, not endpoint -capacity. +consumes one Host slot. An upstream receive edge is free. A parallel +media-producing candidate requires a separately reserved transition slot. SFU +subscriptions consume server egress, not endpoint capacity. ### Active topology @@ -76,9 +74,10 @@ revision: bounded failure. Direct or peer UDP remains the first media choice. Ordinary peer -`RTCPeerConnection` instances receive STUN candidates only; TURN candidates are -not distributed to ordinary peer edges by default. HTTPS and WSS continue to -use TLS/TCP independently of media transport. +`RTCPeerConnection` instances receive STUN candidates only. The sole +application fallback is the dedicated LiveKit SFU over UDP; Screener configures +no TURN, ICE/TCP, media TCP, or TLS-relayed media. HTTPS and WSS continue to use +TLS/TCP independently of media transport. Routing is event-driven. Join, capacity release or reduction, endpoint departure, current-edge hard failure, and a non-paused decoded-frame stall wake the same @@ -106,20 +105,19 @@ so blocked state never hides a physical copy or server resource. A healthy edge used only for SFU bootstrap remains committed if bootstrap cannot start. There is no independent maximum-depth policy. Acyclicity and room admission -bound the graph, while shallowest-first ordering minimizes depth. Depth remains -an observed acceptance metric. +bound the graph, while shallowest-first ordering minimizes depth. If a departed +Host root releases a Host slot, a new or orphaned child therefore prefers that +shallower Host result over another root's deeper slot. Healthy committed edges +remain sticky; the controller does not periodically rebalance the graph. Depth +remains an observed acceptance metric. -A candidate identity is one logical upstream plus one transport. An operation -opened by a failed edge seeds that exact current tuple as already tried, so the -same parent and transport cannot immediately repeat. Other eligible direct/STUN -parents use deterministic order first; a different transport on the old parent, -such as admitted selected TURN after direct failure, may appear with the later -transport candidates, followed by SFU fallback. A new external fact starts a new -operation and may make the old tuple eligible again. If the Host has no usable direct first-level peer path, the operation -prefers its single Host publication, using selected TURN for that ingress when -needed, rather than enumerating Host-TURN Viewer edges. The operation records -only exact tuples already tried; failure does not globally exclude that parent -from later room events. +A candidate identity is one logical upstream path. An operation opened by a +failed edge seeds that exact current candidate as already tried, so the same +parent cannot immediately repeat. Other eligible direct/STUN parents use +deterministic order first, followed by SFU reuse or direct Host publication +ingress. A new external fact starts a new operation and may make the old +candidate eligible again. The operation records only exact candidates already +tried; failure does not globally exclude that parent from later room events. If an SFU subscription is needed while the Host has no publication and all Host slots are occupied, reconciliation uses the same child operation to convert one @@ -145,23 +143,22 @@ resetting that deadline. These fields do not become separate gates or state machines, and this route wave adds no assignment, media-binding, or proof generation to the wire. The same one timer derives wake boundaries from the route classes actually -present in the deterministic list: direct peer, selected TURN, and SFU. The -total deadline is divided equally between those semantic stages, without fixed -per-candidate milliseconds. Hard failures may advance through multiple tuples -inside a stage; a silent tuple at its boundary skips the remaining tuples of -that class so it cannot consume the selected/SFU suffix. -A `prepare` route update names that operation's exact child, selected transport, -and server-issued candidate connection identity. Parent and child therefore -prepare the same connection even when the parent's child set is unchanged by a -direct/selected-transport replacement; neither endpoint infers candidate -authority from an assignment-list difference. -The current Browser runtime uses the single `screener-v8` wire. On each WebSocket, -the server sends the exact prepare before its TURN grant or SFU configuration; -the candidate child is queued before a peer parent is allowed to start its -offer. WebSocket ordering is the companion-delivery contract, so clients keep -no reordering inbox. Duplicate current companions are idempotent and stale ones -are ignored. Native v6 and executable senders are outside this release and fail -the protocol boundary rather than receiving a compatibility path. +present in the deterministic list: direct peer and SFU. The total deadline is +divided equally between those semantic stages, without fixed per-candidate +milliseconds. Hard failures may advance through multiple candidates inside a +stage; a silent direct candidate at its boundary skips the remaining direct +candidates so it cannot consume the SFU suffix. +A `prepare` route update names that operation's exact child, route kind, and +server-issued candidate connection identity. Parent and child therefore +prepare the same connection; neither endpoint infers candidate authority from +an assignment-list difference. +The current Browser runtime uses one internal wire. On each WebSocket, the +server sends the exact prepare before its SFU configuration; the candidate child +is queued before a peer parent is allowed to start its offer. WebSocket ordering +is the companion-delivery contract, so clients keep no reordering inbox. +Duplicate current companions are idempotent and stale ones are ignored. Native +and executable senders are outside this release and fail the protocol boundary +rather than receiving a compatibility path. A stale or mismatched asynchronous result fails closed and cannot revive an old edge. Successful candidate `P` is broadcast as active revision `P`. Failure, timeout, @@ -171,9 +168,7 @@ clients never infer rollback from silence or from an older revision. One room-wide monotonic allocator owns active, prepare, rollback, retirement, and prune revisions. A direct peer transport may adopt a fresh WebRTC connection identity during its framework-owned rebuild only for the exact current -parent/child sessions; that identity update does not change topology. Selected -TURN remains session-bound and must retire through the same rollback primitive -when either authenticated session changes. +parent/child sessions; that identity update does not change topology. A route replacement uses make-before-break only when the typed endpoint and server-resource ledger atomically admits the required reservations. The old @@ -220,15 +215,13 @@ second mutable graph. candidate; SFU-fed and peer-fed endpoints use the same provisional-child transaction and each child edge commits independently. Viewer republishing into a second SFU publication is outside the current product. -- TURN is a selected transport for an existing authorized logical edge or the - Host-to-SFU ingress. It is not a topology node, a second source, or a global - room lease. Existing direct/STUN edges remain preferred; a failed selected - edge may use exact TURN, and an unavailable logical peer path may fall back to - an SFU subscription. If the Host has no direct first-level path, one Host - publication over direct or selected TURN is preferred to repeated Host-TURN - peer edges. +- The only server-assisted route is an SFU subscription backed by the Host's + one direct-ingress publication. Screener configures no TURN or media TCP + transport. An all-UDP-blocked network reaches a clear bounded failure; any + future strict-firewall transport requires its own evidence and belongs inside + LiveKit rather than becoming another application candidate. - Endpoint sender capacity is accounted independently from server ingress/egress, SFU - subscriptions, TURN allocations, and one bounded transition-overlap slot. + subscriptions, and one bounded transition-overlap slot. Steady capacity is `1`, `2`, or `3` (default `2`); a transition may use `min(steadyCap + 1, 3)` only for one fenced, deadline-bound handoff and must return to steady bounds at commit. There is no fixed SFU-root count or @@ -254,24 +247,14 @@ second mutable graph. stall decisions, and leaves new participants waiting. Resume wakes a fresh reconciliation. Healthy unaffected edges remain sticky. -SFU and TURN remain bounded fallback resources with independent deployment-wide -admission. Resource exhaustion produces the next bounded candidate, an explicit -wait, or failure; it never creates unbounded central fanout. +SFU remains a bounded fallback resource with independent deployment-wide +admission. Resource exhaustion produces an explicit wait or failure; it never +creates unbounded central fanout. Rooms that actually lose a candidate to deployment-wide admission register in -one waiter set. An actual SFU/TURN usage decrease drains that set once, advances +one waiter set. An actual SFU usage decrease drains that set once, advances each waiting controller's external fact, and schedules normal reconciliation; there is no periodic capacity poll or resource-specific route controller. -For the single-process deployment, selected TURN uses one injected allocation -authority with an explicitly configured positive safe-integer capacity and no -product default. Each exact `peer-selected` transport and Host-SFU ingress -transport consumes one unit. The controller reserves that unit before issuing a -credential, commits only the matching current identity, and keeps reserved, -committed, and draining entries charged until their logical authorization is -released. Independent logical edges may coexist up to this deployment capacity; -the capacity is not derived from endpoint media-copy capacity, Viewer admission, -or a room-wide lease count. - For the single-process deployment, SFU admission is one injected authority with deployment-wide ingress and egress counters. Enabling LiveKit requires explicit positive safe-integer `SFU_INGRESS_CAPACITY` and `SFU_EGRESS_CAPACITY` values; @@ -315,10 +298,12 @@ deployment-wide. ## Current Deployment Boundary -Production release `d3ff9e7` runs the `screener-v8` Browser runtime. Its exact -configuration, rollback artifacts, and postflight evidence are owned by the -deployment document; real heterogeneous-network and SFU/TURN media validation -remains open. +Production release `a5b1fc6` runs the `screener-v8` Browser runtime and still +contains the superseded selected-TURN surface. Its exact configuration, rollback +artifacts, and postflight evidence are owned by the deployment document. The +direct-to-SFU contract is accepted but not deployed until the selected-TURN +source and production configuration are removed atomically; real +heterogeneous-network and SFU media validation remains open. ## Acceptance Boundary @@ -342,9 +327,9 @@ Before a revised controller ships: - focused controller tests cover first-frame commit, candidate failure and stale ready, relay-ingress reparent with its subtree intact, disconnected relay and nested-disconnect convergence, effective capacity `0..C` and overflow drain, - SFU-fed first-child use, selected TURN on the same edge, and pause/resume; -- real-browser tests cover direct peer media, peer relay, server-assisted media, - selected transport when configured, failure, and recovery; + SFU-fed first-child use and pause/resume; +- real-browser tests cover direct peer media, peer relay, SFU media, bounded + failure, and recovery; - deployment preflight proves the LiveKit instance is dedicated, uses `room.auto_create: false`, exposes `RoomService` only on its accepted private control origin, and can drain its managed namespace before traffic or @@ -387,7 +372,7 @@ Negative: - production intentionally diverges until the implemented source is released; - make-before-break consumes explicit endpoint and server reservations and may require a bounded-gap cutover when no overlap slot exists; and -- real SFU/TURN and target-network evidence is still required. +- real SFU and target-network evidence is still required. ## Relationship to other ADRs @@ -402,7 +387,6 @@ Negative: - [WebRTC](https://w3c.github.io/webrtc-pc/) - [WebRTC statistics](https://www.w3.org/TR/webrtc-stats/) - [WebRTC transports, RFC 8835](https://www.rfc-editor.org/rfc/rfc8835.html) -- [TURN, RFC 8656](https://www.rfc-editor.org/rfc/rfc8656.html) - [RTP topologies, RFC 7667](https://www.rfc-editor.org/rfc/rfc7667.html) - [PIM-SM Join/Prune behavior, RFC 7761](https://www.rfc-editor.org/rfc/rfc7761.html) - [ICE connectivity checks and candidate checklists, RFC 8445](https://www.rfc-editor.org/rfc/rfc8445.html) diff --git a/docs/deployment.md b/docs/deployment.md index d59ac4d..ccd434e 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -1,8 +1,8 @@ # Minimal Deployment -Last verified against upstream documentation: 2026-08-23. +Last verified against upstream documentation: 2026-08-24. -This page records exact release `d3ff9e7` production facts and the current source +This page records exact release `a5b1fc6` production facts and the current source deployment contract. Product direction and pending migrations are owned by [project memory](./project-memory.md) and [the TODO ledger](./todo.md). @@ -17,21 +17,19 @@ as the current controller's automatic final media fallback. This capacity is dormant unless the complete public URL, private API URL, key, secret, and capacity tuple is configured. It does not replace the P2P path, the peer-assisted experiment, or required STUN discovery. LiveKit remains -ICE/UDP only. Source no longer contains the rejected participant-wide Peer ICE TURN -candidate; stale `PEER_ICE_TURN_*` keys fail startup even when blank. The -selected-edge source tuple is enabled in production only for the controller's -single exceptional edge. Production ordinary peer ICE remains STUN-only and the -tracked coturn example remains `stun-only`; the shared host retains its older -authenticated-relay daemon configuration and firewall range. The application -does not pre-advertise TURN credentials to ordinary peers. +ICE/UDP only. Ordinary peer ICE remains STUN-only and the tracked coturn example +is STUN-only with TCP/TLS disabled. The accepted source contract configures no +TURN, ICE/TCP, media TCP, or TLS-relayed media. Production still retains the +superseded selected-edge environment tuple, authenticated-relay coturn +configuration, TCP listener, and relay firewall range until the direct-to-SFU +release removes them atomically. -Production runs exact `d3ff9e7b7b4a8fe58db700565971aaeda638d2e9`, release -`d3ff9e7`, from `/opt/screener/releases/d3ff9e7`. The immutable runtime archive +Production runs exact `a5b1fc62f609b996ead54f2a5db42ddd2f374fb9`, release +`a5b1fc6`, from `/opt/screener/releases/a5b1fc6`. The immutable runtime archive SHA-256 is -`a7c3b7b7cf593f3933a1c194a07370131fd76b3d4c30768b5206e84ccf93b63d`. -Immediate rollback is `/opt/screener/releases/352c457`, with the verified -pre-cutover environment/LiveKit/unit backup at -`/opt/screener/backups/d3ff9e7-precutover-20260823T150618Z`. Local and public +`2c6223f13b1ae44e737f4088af3f62259581c58c20eb9583f4f4900f58387519`. +Its verified pre-cutover boundary is +`/opt/screener/backups/a5b1fc6-precutover-20260823T160319Z`. Local and public health return 200; Screener, LiveKit, coturn, and nginx are active with `NRestarts=0`. @@ -42,7 +40,8 @@ one-controller exact-candidate route runtime, and stale-v7 rejection before room authority. The service unit has no writable room StateDirectory and the old live SQLite path is absent. LiveKit is dedicated, has `room.auto_create: false` and `max_participants: 21`, and is admitted to one global publication ingress plus -twenty subscription egress handles. Selected TURN has two logical allocations. +twenty subscription egress handles. The deployed selected-TURN allocation cap +of two is a superseded production fact, not accepted behavior. These bounds are fail-safe admissions, not throughput or quality claims. The retained local 20-Viewer Browser smoke and open real-network boundaries are owned by [verification status](./verification-status.md). @@ -142,19 +141,14 @@ subscriptions and allocations use independent deployment admission rather than an endpoint-cap or fixed-root rule. The repository requires STUN and ordinary peer connections receive STUN-only -ICE. An existing logical edge prefers direct/peer UDP and may use exact selected -TURN when configured; only an unavailable logical ingress uses the Host -publication/SFU path. A Host-SFU ingress may itself use selected TURN. LiveKit -participants receive only revision-bound `sfu-config` URL/token messages and -negotiate within LiveKit's separate ICE domain. The selected-edge TURN -config/wire is deployed in the current `352c457` release with one UDP URL, a -120-second credential TTL, and two logical allocations. It was not exercised by -a real media session during this cutover. +ICE. An existing logical edge prefers direct/peer UDP; the only application +suffix is the Host publication/SFU path. LiveKit participants receive only +revision-bound `sfu-config` URL/token messages and negotiate within LiveKit's +separate UDP-only ICE domain. `PEER_ASSISTED_MEDIA=true` is the process-wide topology/SFU switch. Every normal -room gets its own bounded controller state; ordinary peer connections remain -STUN-only and only the selected-edge source path may grant TURN to the -controller's then-current edge. Run candidate releases on an isolated +room gets its own bounded controller state and ordinary peer connections remain +STUN-only. Run candidate releases on an isolated instance/hostname and keep the old release unchanged for rollback. If the candidate fails, roll back the release or instance; do not add a permanent dual-transport branch. @@ -168,19 +162,14 @@ recorded old release plus its SQLite environment/config backup unchanged for rollback. Rolling back restores that exact old environment and database with the old binary; it does not attempt to carry candidate rooms into SQLite. -A shared-public-IP instance can test candidate behavior while the old TURN -service stays live, but it cannot prove the clean-port boundary or approve broad -migration. The room-1 smoke uses this shape: nginx owns 443, LiveKit 1.13.5 owns -UDP 7882, Peer ICE TURN is disabled, and LiveKit is fail-closed under a 192 MiB +A shared-public-IP instance can test candidate behavior, but it cannot prove the +clean-port boundary or approve broad migration. The room-1 smoke uses this +shape: nginx owns 443, LiveKit 1.13.5 owns UDP 7882, and LiveKit is fail-closed under a 192 MiB high/256 MiB hard cgroup limit with restart disabled. Prefer a separate VM/IP for the full gate and verify representative external networks and devices. -TURN REST authentication proves only HMAC and expiry to coturn. Coturn cannot -verify a room, route, edge, or connection generation, so the application must -bind issuance and parent/child rebuild to the current controller state, keep the -attempt one-use and short-lived, and revalidate every asynchronous boundary. -Never describe coturn itself as cryptographic selected-edge enforcement. -HTTPS/WSS always remains TLS/TCP independently. +Coturn provides STUN binding only and must reject allocation requests. +HTTPS/WSS always remains TLS/TCP independently of the UDP-only media ladder. The 2026-08-20 exact-room canary used source `a11a73dfa79d`, inactive release `/opt/screener/releases/a11a73dfa79d-r4`, artifact SHA-256 @@ -242,61 +231,24 @@ SFU_INGRESS_CAPACITY= SFU_EGRESS_CAPACITY= ``` -To enable exact selected TURN after bounded peer and SFU recovery, add its -complete tuple: - -```dotenv -SELECTED_EDGE_TURN_URLS=turn:turn.example.com:3478?transport=udp -SELECTED_EDGE_TURN_SHARED_SECRET= -SELECTED_EDGE_TURN_CREDENTIAL_TTL_SECONDS=120 -SELECTED_EDGE_TURN_ALLOCATION_CAPACITY= -``` - -The rejected `PEER_ICE_TURN_*` participant-wide tuple is removed; supplying any -stale key, even blank, fails startup. Current source requires the complete -four-value `SELECTED_EDGE_TURN_*` tuple with one explicit TURN/UDP URI, an -independent secret, bounded TTL, and positive safe-integer logical-allocation -capacity. Each exact peer-selected edge and Host-SFU ingress consumes one unit -from the same process ledger before credential issuance; reserved, committed, -and draining authorizations stay charged until exact logical release. The -production release remains on its recorded configuration until the four-value -tuple is supplied atomically with current source. Only a controller-selected -edge may receive a short-lived grant. Roll back by -removing the application tuple before changing coturn or firewall state. -Credentials never enter URLs, logs, durable server storage, or browser -`localStorage`. - -Application release does not prove deletion of the matching physical coturn -allocation. Keep the credential TTL short and configure coturn `user-quota` and -`total-quota` to bound allocations left by disconnect or process restart. A -restarted single process begins a fresh logical ledger while old physical -allocations expire under those coturn bounds; do not claim cross-restart -reconstruction or per-allocation readback. - -Before a media canary, `npm run gate:turn-udp-allocation` provides the bounded -UDP allocation check. It proves only that an authenticated relay candidate can -be allocated over UDP; it does not prove a media route, recovery, quality, or -performance. Set `CHROME_PATH` in the process environment and supply the -complete selected-edge tuple either in that environment or as newline-delimited -`KEY=value` records on stdin. Values containing `=` are preserved. The script -uses an isolated Chrome profile and loopback-only in-memory credential response, -then emits only status, relay-candidate count, transport protocol, and coarse ICE -error-code buckets. Do not echo, log, or persist raw production environment -input; never add TURN URLs, usernames, credentials, candidate addresses, or raw -errors to the result. +Supplying any stale `PEER_ICE_TURN_*` or `SELECTED_EDGE_TURN_*` key, even blank, +fails startup. The cutover must remove all four production selected-edge keys +before starting the new application, install the tracked STUN-only coturn +configuration, and close TCP 3478/5349 plus every relay range. Keep the old +release, its exact environment, coturn configuration, and firewall snapshot as +one rollback unit; do not restore only part of that obsolete transport. `PEER_ASSISTED_ROOM_IDS` is retired. Supplying it, even blank, fails startup so that a stale room-1 deployment cannot silently retain the old scope. With `PEER_ASSISTED_MEDIA=true`, every normal room receives peer-assisted routing, optional LiveKit fallback, and the same deployment resource/fanout/failure guards. -Every ordinary peer connection remains STUN-only; selected-edge TURN is still -issued only to a current controller-selected edge. There is no browser control, +Every ordinary peer connection remains STUN-only. There is no browser control, percentage rollout, or second router. `ALLOWED_ORIGINS` must list exact `http` or `https` origins, never `*`. `SITE_ACCESS_PASSWORD` is required in production and must contain 8 through 128 visible ASCII bytes (`0x21` through `0x7e`). It must not be reused for -LiveKit, TLS, TURN, or another service. It authorizes room creation, Host role, +LiveKit, TLS, or another service. It authorizes room creation, Host role, and code-only Viewer attempts; it does not replace a private room grant or password. Local development and tests may omit it. Supplying a removed access key, even blank, fails startup. @@ -305,27 +257,24 @@ lifetime. An actively connected Host prevents expiry; explicit stop or Host disconnect starts the lease, and only the exact Host token renews it before expiry. Viewer activity never renews ownership. `ROOM_DATABASE_PATH` and `ROOM_TTL_SECONDS` fail startup even when blank. -Production `352c457` accepts 1 through 20 and explicitly selects 20. This is an +Production `a5b1fc6` accepts 1 through 20 and explicitly selects 20. This is an admission limit, not evidence that every publisher, network, or quality profile can sustain that many streams. `ENDPOINT_MEDIA_COPY_CAPACITY` defaults to 2 and accepts only 1, 2, or 3. It is the single server-authoritative steady outbound media-copy cap for Host and Viewer endpoints; role, browser, UA, and visibility do not create another tier. -A peer child or the Host's single SFU publication consumes one copy, upstream -receive is free, a committed selected TURN transport for the same child does not -consume a second copy, and an uncommitted selected carry does. Transition work +A peer child or the Host's single SFU publication consumes one copy and upstream +receive is free. Transition work may reach only `min(C + 1, 3)`. A Host already sending three copies at `C=3` -must fail or wait before a fourth endpoint copy is issued; changing an existing -copy to selected TURN remains the same endpoint copy and separately requires -TURN allocation admission. +must fail or wait before a fourth endpoint copy is issued. Supplying the removed `MAX_PEER_RELAY_DOWNSTREAM_EDGES`, even blank, fails startup. -Production release `352c457` runs the deployed server and Browser assets +Production release `a5b1fc6` runs the deployed server and Browser assets atomically on `screener-v8`; stale v7 Browser and executable-sender wires fail before room authority. Native senders and helpers are outside this release. -Restore release `6b87732` with the exact precutover environment, unit, LiveKit -configuration, and SQLite backup when rolling back. +Restore only an exact recorded release with its matching environment, unit, +LiveKit, coturn, and firewall snapshot when rolling back. The four `LIVEKIT_*` values must either all be absent or all be present, and a complete tuple requires `PEER_ASSISTED_MEDIA=true` plus explicit positive @@ -362,20 +311,13 @@ second normal room contain room state or capacity bypass. Exercise join, offer and answer, stale-token reconnect after abort, Host signaling loss with and without a live Host participant, stop, restart, and room deletion in both rooms. Confirm each drain -keeps capacity charged until the room is absent. Ordinary Web and -Native-shaped clients remain STUN-only. With the selected-edge tuple, only -the current controller-selected edge may receive a one-use grant; all other -sessions and connections remain STUN-only. Roll back by disabling application -issuance, stopping Screener, and proving every managed LiveKit room absent before -restoring the exact recorded application/LiveKit configuration and release; or -direct traffic to the unchanged old instance. Restore the coturn/firewall -baseline only after the application no longer issues its selected-edge grant. -Do not treat disabling -`PEER_ASSISTED_MEDIA` alone as TURN rollback: disable the selected-edge tuple -independently, and no removed old -TURN wire is restored. During migration, remove the retired room-ID variable in -the same coherent change; the process flag explicitly enables the all-room -controller. +keeps capacity charged until the room is absent. Ordinary Web clients remain +STUN-only. Roll back by stopping Screener, proving every managed LiveKit room +absent, and restoring the exact recorded application environment, application +release, LiveKit configuration, coturn configuration, and firewall snapshot as +one unit; or direct traffic to the unchanged old instance. During migration, +remove retired route configuration in the same coherent change; the process +flag explicitly enables the all-room controller. Only `POST /api/site-access` accepts the site access secret in an `Authorization: Bearer` header from an exact allowed Origin. Success returns a @@ -442,12 +384,9 @@ any key is present, including with an empty value. Remove those obsolete keys from the candidate environment; startup fails instead of pretending they enable compatibility. -The rejected candidate names `PEER_ICE_TURN_URLS`, -`PEER_ICE_TURN_SHARED_SECRET`, and -`PEER_ICE_TURN_CREDENTIAL_TTL_SECONDS` must remain absent from production and -now fail startup even when blank. They are not aliases for the selected-edge -tuple; ordinary peer edges remain STUN-only while only the current controller -edge may use the configured selected-edge transport. +The rejected `PEER_ICE_TURN_*` and `SELECTED_EDGE_TURN_*` candidate names must +remain absent from production and fail startup even when blank. Ordinary peer +edges remain STUN-only and the application has no TURN configuration surface. ## HTTPS and WSS ingress @@ -543,14 +482,11 @@ canary pass silently. ## Self-hosted STUN -This subsection describes the temporary STUN-only deployment template, not the -enabled selected-edge production environment or the current shared-host coturn -state. That host retains an older authenticated-relay configuration and TCP/UDP -3478 plus UDP 49152-49251 firewall range. In this template Screener advertises -no TURN credential and the canary audit found zero allocations; production -selected-edge issuance remains limited to the controller's one configured edge -and has one active Host-ingress forced-relay functional acceptance. Initial -Host ingress and `peer-selected` last mile remain open. +This subsection describes the accepted STUN-only deployment. Before the +direct-to-SFU cutover, the shared production host still retains an older +authenticated-relay configuration and TCP/UDP 3478 plus UDP 49152-49251 +firewall range; the atomic cutover replaces that configuration and closes those +obsolete listeners/ranges. Copy [`deploy/coturn/turnserver.conf.example`](../deploy/coturn/turnserver.conf.example) to an untracked service-owned location and use the tracked @@ -593,10 +529,8 @@ Run these checks from real external networks before calling the deployment usabl publication serves only the admitted SFU subscriptions over LiveKit UDP 7882. Peer descendants stay on ordinary direct UDP and every non-server endpoint obeys the configured `ENDPOINT_MEDIA_COPY_CAPACITY`. -4. Force one controller-eligible edge past SFU/UDP and verify only that edge gets - the short-lived TURN server plus relay policy and selects TURN/UDP. Ordinary - peer PCs must stay STUN-only. Then block all UDP and verify bounded recovery - ends clearly without ICE/TCP, TURN/TCP, or a long pseudo-connected path. +4. Block all UDP and verify bounded recovery ends clearly without ICE/TCP, + TURN, or a long pseudo-connected path. 5. Exercise root departure, reconnect, SFU unavailable, route prepare rollback, stop, and source/profile changes. Unaffected subtrees must not migrate. 6. Repeat at 1, 3, 5, and 20 viewers across representative consumer networks. diff --git a/docs/project-memory.md b/docs/project-memory.md index 7ac883f..93513cc 100644 --- a/docs/project-memory.md +++ b/docs/project-memory.md @@ -1,25 +1,25 @@ # Project Memory -Last updated: 2026-08-23 +Last updated: 2026-08-24 ## Current Product Truth - Build private, low-latency game screen sharing for one broadcaster and a small group of trusted friends. Public or large broadcasts belong on OBS/Twitch-class services. -- A room supports one Host and up to `20` authenticated Viewers. Admission is independent of endpoint fanout and SFU/TURN capacity; the event-driven route controller must remain efficient at that bound without all-pairs probing. +- A room supports one Host and up to `20` authenticated Viewers. Admission is independent of endpoint fanout and SFU capacity; the event-driven route controller must remain efficient at that bound without all-pairs probing. - Authenticated Hosts and shipped clients are trusted media participants in these private rooms; route authority, generation fencing, and server admission remain fail-closed. - Rooms use one bounded, process-memory model: a random free code from `1000..9999`, default 24-hour configurable dormant lease, and no database or cross-restart recovery. Active sharing never expires; stop/disconnect starts the lease, the exact Host token may resume before expiry, Viewer activity does not renew it, and expiry releases the code. Process restart invalidates every room, token, invitation, password verifier, and participant. - Viewer grants and code entry are orthogonal. Every room has an expiring token-bearing invitation; code-only entry is `open | password | disabled` and defaults to `open` with no room password after site access. The Host browser locally retains its display name, code-entry policy, optional room password, and raw Host ownership token; the server keeps only current-process digests/verifiers and never uses IP, UA, device properties, or a fingerprint as identity. Recreating after restart reapplies local preferences but produces a new code, Host token, and grant. - Web is the current delivery target. The route release covers Browser Host, Viewer, and relay endpoints; packaged/native senders, capture helpers, shared-encode executables, and their acceptance runs are lowest-priority later work outside this milestone. -- Keep routing automatic and media distributed. Direct/peer UDP is preferred; centralized media is fallback infrastructure, not the default topology. -- Every non-server endpoint uses one server-authoritative steady outbound media-copy capacity: default `2`, statically configurable as `1`, `2`, or `3`. A peer child or the Host publication consumes one slot; upstream receive is free, selected TURN replaces the same copy's transport, and SFU subscriber egress is accounted at the server. Browser role/UA/visibility does not create a separate tier, and clients cannot raise the deployment value. +- Keep routing automatic and media distributed. Direct/peer UDP is preferred; the dedicated LiveKit SFU/UDP path is the only application fallback and is not the default topology. Screener configures no TURN, ICE/TCP, media TCP, or TLS-relayed media path. +- Every non-server endpoint uses one server-authoritative steady outbound media-copy capacity: default `2`, statically configurable as `1`, `2`, or `3`. A peer child or the Host publication consumes one slot; upstream receive is free, and SFU subscriber egress is accounted at the server. Browser role/UA/visibility does not create a separate tier, and clients cannot raise the deployment value. - The stable routing invariants are one authoritative upstream per Viewer, an acyclic source-reachable active graph, exact authenticated route attempts, endpoint and server admission, one room-serial child operation, and bounded success, wait, or failure. - One room controller reconciles allocation, child reparenting, and relay drain through the same operation. It finds one waiting Viewer or invalid edge and builds one deterministic tuple list. The operation owns its cursor, current candidate and reservations, fact version, plus one total deadline; only its current candidate may produce media. A `screener-v8` prepare names that exact child, selected transport, and server-issued candidate connection ID. A usable old edge remains until the exact candidate child decodes its first new video frame. `settleReady` synchronously commits admission before graph promotion; success commits prepare revision `P`, while failure releases resources, broadcasts rollback `R > P` from one monotonic allocator, and advances the cursor without resetting the deadline. Exhaustion retires hard-invalid, departed-parent, or overflow edges into explicit waiting but retains a healthy bootstrap edge. Bounded-gap preflights server resources, replans the untried suffix after retirement, and appends one ordinary restore candidate when it cut healthy media. A relay with a bad ingress reparents itself as a child and retains its subtree. A disconnected endpoint, or one whose effective downstream capacity falls below its current child count, accepts no excess children; its direct or overflow children enter the same operation and a disconnected endpoint is removed when childless. Authoritative pause aborts the operation, suppresses decoded-progress stall decisions, and resumes from a fresh reconciliation. -- 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/TURN usage release wakes only rooms that previously lost a candidate to global admission. SFU-fed and peer-fed endpoints use the same provisional-child transaction, and selected TURN changes only the transport of an authorized logical edge. 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. 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. Credentials, room secrets, candidate details, and diagnostic data remain private and narrowly scoped. +- 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, and current LiveKit media is UDP-only. 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 stats to maintain one exact-route decoded-progress timestamp. New frames or route/connection changes reset it; a non-paused deadline reports the edge once. Track availability, bitrate, FPS, blur, sender stats, and SFU layer remain framework or diagnostic inputs. - Browser capture already requests available share audio by default on initial capture and source switch, with an explicit video-only warning when no audio track is returned. Screen audio offers live-switchable 64/128/256 kbps sender ceilings and defaults to 128. The existing 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. - Video codec selection remains `automatic | H.264 | VP8`. The accepted first switching boundary is a generation-fenced renegotiation/republish transaction while sharing is explicitly paused; unpaused hot switching is later reuse of that transaction, not an independent mechanism. -- Two distribution artifacts are accepted only after the current feature, NAT, and real-network ledger is complete. A public-server one-click deployment package installs the current application plus STUN/SFU/TURN and proxy components on a user-owned server. A Windows/macOS/Linux local package runs the Host, application server, and local state on the broadcaster's machine without source, Node, or any external Screener/network service; it keeps workable direct P2P but reports the reachability loss imposed by public ingress, TLS, gateway, NAT, or firewall limits rather than promising universal connectivity. +- Two distribution artifacts are accepted only after the current feature, NAT, and real-network ledger is complete. A public-server one-click deployment package installs the current application plus STUN/SFU and proxy components on a user-owned server. A Windows/macOS/Linux local package runs the Host, application server, and local state on the broadcaster's machine without source, Node, or any external Screener/network service; it keeps workable direct P2P but reports the reachability loss imposed by public ingress, TLS, gateway, NAT, or firewall limits rather than promising universal connectivity. - Current UI, presence, route labels, and diagnostics describe observed state only; they do not create route authority. - Before an explicit public-release decision, the private deployment and repository keep only the current product contract. Incompatible wire, configuration, parser, and API changes ship atomically; stale clients fail before authority, replaced compatibility surfaces are deleted, and Git history owns rollback. - Viewer startup feedback remains on the stage until current-generation frame presentation is proven, distinguishes peer, fallback, media, autoplay, recovery, and failure phases, and keeps `Play` specific to autoplay rejection. The strict-NAT guessed-candidate idea remains an isolated emulator gate; only a pass may authorize one manually approved, kill-switched canary in an operator-owned test room with explicit consent from both endpoints and exact room/edge/generation/budget bounds. @@ -27,19 +27,19 @@ Last updated: 2026-08-23 ## Current Source And Production - Canonical source is the root `main` branch. Audit/integration boundaries require it to be clean, and new branches/worktrees start from its exact commit only after accepted truth is merged. -- Current source admits up to `20` Viewers and implements the uniform `1/2/3` endpoint cap, single Browser `screener-v8` wire, exact prepare candidate, first-decoded-frame commit, strict rollback revision, decoded-progress stall, and one event-driven room controller. Older Browser and executable-sender wires fail before room authority. SFU admission uses exact publication and Viewer-subscription handles with explicit no-default ingress/egress capacities and a listener-fenced dedicated LiveKit owner. Selected TURN uses its own explicit no-default deployment capacity and exact logical-allocation ledger shared by independent peer edges and Host-SFU ingress. +- Canonical `main` at `a5b1fc62f609b996ead54f2a5db42ddd2f374fb9` admits up to `20` Viewers and implements the uniform `1/2/3` endpoint cap, single Browser `screener-v8` wire, exact prepare candidate, first-decoded-frame commit, strict rollback revision, decoded-progress stall, staged operation deadline, and one event-driven room controller. Its source still contains the superseded selected-TURN path; removing that path and retaining only direct peer plus SFU is the active implementation boundary. SFU admission uses exact publication and Viewer-subscription handles with explicit no-default ingress/egress capacities and a listener-fenced dedicated LiveKit owner. - Current source and production implement the memory-resident room model: random free four-digit codes, the configurable dormant lease, exact-Host resume, restart loss, local creation-preference replay, orthogonal Viewer grants and `open | password | disabled` code entry, and direct invitation/policy controls. SQLite room persistence, `ROOM_DATABASE_PATH`, `ROOM_TTL_SECONDS`, and the writable room StateDirectory are removed from the v8 runtime; the stopped v7 SQLite snapshot exists only in the verified rollback backup. -- Production runs exact `d3ff9e7b7b4a8fe58db700565971aaeda638d2e9`, release `d3ff9e7`, wire `screener-v8`; `352c457` is the immediate rollback release. -- Production implements the current endpoint-cap and route-controller contract, admits 20 Viewers, uses LiveKit admission of one publication ingress plus 20 subscription egress handles on a dedicated non-auto-create instance, and bounds selected TURN to two logical allocations. +- Production runs exact `a5b1fc62f609b996ead54f2a5db42ddd2f374fb9`, release `a5b1fc6`, wire `screener-v8`; `/opt/screener/backups/a5b1fc6-precutover-20260823T160319Z` is its verified pre-cutover boundary. +- Production implements the endpoint-cap and staged route-controller contract, admits 20 Viewers, and uses LiveKit admission of one publication ingress plus 20 subscription egress handles on a dedicated non-auto-create instance. It still has the superseded four-value selected-TURN environment tuple and authenticated coturn relay configuration; accepted truth is not deployed until both are removed atomically. - 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. -- Production retains access controls, self-check, diagnostic export, signaling watchdog behavior, 64/128/256 audio choices, and selected-edge transport. Real heterogeneous-network and SFU/TURN media evidence remains open. +- Production retains access controls, self-check, diagnostic export, signaling watchdog behavior, and 64/128/256 audio choices. Real heterogeneous-network and SFU media evidence remains open. - `screener`, LiveKit, coturn, and nginx are healthy; local and public health return 200 and `screener` has `NRestarts=0`. Deployment detail is owned by [deployment](./deployment.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. Keep this truth set and [the TODO ledger](./todo.md) concise and internally consistent. -2. Release live audio and paused video-codec switching next, follow with evidence-led H.264 diagnosis, the strict-NAT emulator gate, connection feedback, and the remaining functional roadmap. Distribution packaging starts only after those TODOs are complete. Parallel branches may finish earlier but do not change this merge/deployment order. +2. Remove application selected TURN from source and production first. Then resume live audio and paused video-codec switching, evidence-led H.264 diagnosis, the strict-NAT emulator gate, connection feedback, and the remaining functional roadmap. Distribution packaging starts only after those TODOs are complete. Parallel branches may finish earlier but do not change this merge/deployment order. ## Working Rules diff --git a/docs/research/built-in-peer-ice-turn.md b/docs/research/built-in-peer-ice-turn.md index c2631d1..8a86682 100644 --- a/docs/research/built-in-peer-ice-turn.md +++ b/docs/research/built-in-peer-ice-turn.md @@ -10,27 +10,20 @@ transport design is owned by [ADR-0005](../adr/0005-automatic-hybrid-media-routi ## Decision -Do not deploy or continue the built-in participant-wide candidate. Ordinary -peer connections remain STUN-only. Exact release `9461e20` used its dated +Do not deploy or continue either the participant-wide or selected-edge +application candidate. Ordinary peer connections remain STUN-only and the sole +application fallback is LiveKit SFU/UDP. Exact release `9461e20` used its dated SFU-then-selected-TURN recovery slice; current route authority is ADR-0005. -TURN remains a compatibility transport, not a topology. The controller may -authorize it only for an exact logical edge transport or the single Host-SFU -publication path. No room, participant, or ordinary peer connection receives -TURN candidates by default. - This supersedes this document's former recommendation to place STUN plus TURN on every capable exact-room Web peer connection. Git history retains that analysis; the durable findings that still apply are summarized below. -The current source no longer contains the rejected participant-wide config, -issuer, authentication capability, refresh wire, or client propagation. Supplying any -stale `PEER_ICE_TURN_*` key, even blank, fails startup. The replacement -selected-edge config, wire, and rebuild now exist and are configured in -production. They remain controller-selected. An isolated Host-SFU ingress canary -proved selected TURN/UDP media; ordinary peer-selected media, deployment -capacity, expiry, heterogeneous networks, and production-route behavior remain -open. +The participant-wide and selected-edge implementation sections below are dated +evidence only. Their allocation, credential, authorization, rebuild, UI, +configuration, and deployment surfaces have no current product authority. An +isolated Host-SFU ingress canary proved historical selected TURN/UDP media, but +did not establish a unique product need beyond the accepted SFU fallback. ## Production Canary diff --git a/docs/research/low-server-media-routes.md b/docs/research/low-server-media-routes.md index b7ec1ff..847c188 100644 --- a/docs/research/low-server-media-routes.md +++ b/docs/research/low-server-media-routes.md @@ -1,13 +1,40 @@ # Low-Server-Cost Media Routes -- Research date: 2026-08-22 -- Scope: one broadcaster, explicit admission up to sixteen trusted viewers, +- Research date: 2026-08-22; transport conclusion updated 2026-08-24 +- Scope: one broadcaster, explicit admission up to twenty trusted viewers, low latency, and bounded host media fanout - Status: research and dated route evidence. Current invariants and assisted transport roles are in [ADR-0005](../adr/0005-automatic-hybrid-media-routing.md); the Browser source migration is deployed, while real-network validation remains in [verification status](../verification-status.md). +## Current Transport Conclusion + +Screener's application route ladder is only direct/STUN peer followed by the +dedicated LiveKit SFU over UDP. The selected peer-edge/Host-ingress TURN/UDP +candidate is rejected: when outbound UDP can reach a public server, SFU/UDP +covers the product's bounded central fallback while one Host publication serves +multiple Viewers; when all outbound UDP is blocked, TURN/UDP adds no reachability. +The extra credential issuer, route tuple, logical-allocation ledger, relay-only +rebuild, configuration, UI, deployment, and test surfaces have no remaining +consumer that justifies their whole-system cost. + +This conclusion does not claim that the TURN standard has no use. WebRTC +requires TURN support for endpoint-dependent NAT combinations, and LiveKit can +offer ICE/TCP or TURN/TLS for strict firewalls. Screener currently accepts a +smaller UDP-only boundary and fails clearly when it is unreachable. If real +target-network evidence later requires broader coverage, transport selection +belongs inside LiveKit rather than becoming a third Screener route class. The +pinned LiveKit 1.13.5 configuration exposes `rtc.tcp_port`, external TURN, and +embedded TURN/TLS as framework capabilities; none is enabled by this decision. + +Primary sources checked 2026-08-24: [WebRTC transports, RFC +8835](https://www.rfc-editor.org/rfc/rfc8835.html), [LiveKit connection +reliability](https://docs.livekit.io/intro/basics/connect/), [LiveKit self-hosted +deployment](https://docs.livekit.io/transport/self-hosting/deployment/), and the +[pinned LiveKit 1.13.5 configuration +sample](https://github.com/livekit/livekit/blob/v1.13.5/config-sample.yaml). + ## Historical Candidate Route Ladder At this dated checkpoint, the candidate plan used direct Host P2P first, then a @@ -30,8 +57,8 @@ RIDs in `q,h` order; Screener's former `q,f` guard caused the Host publisher to fail closed at `sender-config`. After correcting that contract, Viewer inbound packets and decoded/rendered frames increased, endpoint edges stayed bounded, and both clients left cleanly. TURN did not participate in that first run. A -later exact-source canary against the production media tuple proved the active -Host-ingress selected relay described below. Both are functional, not +later exact-source canary against the then-production media tuple proved the +historical Host-ingress selected relay described below. Both are functional, not performance, evidence; public-room and browser-diversity validation remain open. ## Token-Free SFU Standby Prewarm @@ -95,24 +122,22 @@ representations; if simulcast `HIGH` and `LOW` both remain active, that may be `B_HIGH+B_LOW`. Publisher-to-SFU and every SFU subscription are independent ICE connections. Whether a subscribed endpoint also sends peer edges is represented only by its actual committed children and those edges are already counted in -`V`. Ordinary peer edges are STUN-only; selected coturn is issued only for an -exact authorized edge or Host-SFU ingress. -If a -publisher leg separately uses LiveKit TURN, retain host upload +`V`. Ordinary peer edges are STUN-only. In a historical or external deployment +where a publisher leg separately uses TURN, retain host upload `B_pub`, TURN ingress `B_pub`, TURN egress `B_pub`, and SFU ingress `B_pub` as distinct interface/service traffic. Any separately TURN-relayed LiveKit subscription or controller-selected peer edge adds TURN ingress and egress equal to that leg's measured bitrate. These are one logical useful payload copy but real physical hops, so NIC, service, and billing counters must never be folded. SFU -subscriptions are never hidden inside endpoint child capacity. Their ingress, -egress, and TURN allocations are admitted explicitly; `S` is a measured route +subscriptions are never hidden inside endpoint child capacity. Their ingress +and egress are admitted explicitly; `S` is a measured route result, not a fixed root limit. -RTP/RTCP/SRTP, DTLS, ICE/TURN and IP headers, retransmission, FEC, and redundant +RTP/RTCP/SRTP, DTLS, ICE and IP headers, retransmission, FEC, and redundant paths only add traffic. W3C candidate-pair byte counters exclude some transport overhead, and a provider may bill ingress and egress differently, so formulas -screen candidates but never replace host NIC, TURN/SFU, and billing counters +screen candidates but never replace host NIC, SFU, and billing counters from the same run. Encoding once can reduce compute and memory bandwidth, but it cannot remove the network copy delivered to each viewer. @@ -120,9 +145,9 @@ it cannot remove the network copy delivered to each viewer. TURN is an ICE transport and an SFU is a media service; neither fact alone defines logical parentage or resource admission. Ordinary peer PCs gather only -STUN candidates. The holistic model must bind any SFU subscription or selected -TURN transport to current route authority while preserving sticky unaffected -subtrees, the configured ordinary endpoint cap, and bounded failure. +STUN candidates. The current model binds each SFU subscription to route +authority while preserving sticky unaffected subtrees, the configured ordinary +endpoint cap, and bounded failure. This distinction follows TURN's allocation/relay role in RFC 8656 and the media topology boundary in RFC 7667. A relay candidate proves transport for one edge; @@ -142,15 +167,15 @@ network changes, but their selected ICE path must be measured. | --- | --- | --- | | Endpoint-independent mapping with UDP | Full ICE can check host, server-reflexive, and peer-reflexive paths | Keep direct/peer UDP first | | One endpoint-dependent mapper | Coordinated checks can sometimes create a peer-reflexive path; success is not guaranteed | Exhaust the existing bounded restart/rebuild/alternate-parent steps | -| Both peer endpoints use endpoint-dependent mapping | A direct peer path is not reliable; RFC 8835 requires TURN support for this case | A public SFU/UDP or authorized TURN path may still work when outbound UDP reaches that server; ADR-0005 owns exact edge and resource authority | +| Both peer endpoints use endpoint-dependent mapping | A direct peer path is not reliable; RFC 8835 requires browser TURN capability for general WebRTC interoperability | Use the public SFU/UDP fallback when outbound UDP reaches it; otherwise fail clearly | | All outbound UDP is blocked | The UDP media ladder has no reachable candidate | End with a clear bounded failure | | Wi-Fi/cellular or address change | Old mappings and candidate pairs can become invalid | Use a new opaque generation and bounded ICE restart/rebuild, then re-run the same priority ladder | Peer-reflexive discovery records an address only after a connectivity check succeeds; it does not cross two incompatible mappings by itself. A restricted -endpoint may still reach SFU or TURN infrastructure over outbound UDP. The -holistic route model must decide publication ownership, subscription placement, -selected transport, and resource admission without changing this NAT boundary. +endpoint may still reach SFU infrastructure over outbound UDP. The route model +must decide publication ownership, subscription placement, and resource +admission without changing this NAT boundary. Do not infer endpoint-dependent mapping from a `cellular` or `CGNAT` label. A 2023 experiment reached three of four Dutch mobile carriers, but that cohort is @@ -172,17 +197,17 @@ change or explicit reconnect; it does not support periodic probing, a carrier ### Deployable Browser-First Priorities -**1. Dual-stack Web, STUN, SFU, and selected TURN** +**1. Dual-stack Web, STUN, and SFU** - Smallest change: publish working `AAAA` records, bind every public endpoint on IPv6, and open the same bounded UDP paths. Browser ICE already gathers and intermixes IPv4/IPv6 candidates; do not add application candidate ordering. Product code can remain unchanged; any retained evidence is only a sanitized selected-address-family enum tied to the existing opaque generation. -- Dependency: routed IPv6 at the provider, dual-stack STUN/SFU/TURN listeners, +- Dependency: routed IPv6 at the provider, dual-stack STUN/SFU listeners, correct firewall rules, and no IPv4-only hostname hidden in the media ladder. - Acceptance: owned desktop, Wi-Fi, and cellular probes select an IPv6 P2P path - and an IPv6 SFU/selected-TURN path where available; broken IPv6 still reaches + and an IPv6 SFU path where available; broken IPv6 still reaches the IPv4 ladder within the current route deadline. Retain only an `ipv4|ipv6` enum with the exact candidate first-frame transaction, never a raw address. - Stop line: if any required public media endpoint has no routed IPv6, record the @@ -267,7 +292,7 @@ not authorize a guessed production path. connections, browser-specific SDP rewriting, more than eight total guesses, unbounded retries, a longer black-screen deadline, or fails the two-engine repeatability gate. A failure closes this candidate in favor of bounded - SFU/UDP and selected TURN/UDP; it does not justify a native sender by itself. + SFU/UDP; it does not justify a native sender by itself. ### Ordinary-Browser Hard Boundaries @@ -299,10 +324,9 @@ not authorize a guessed production path. The shared acceptance matrix remains EIM/EIM, one endpoint-dependent mapper, two endpoint-dependent mappers including cellular-to-cellular, all UDP blocked, -and a Wi-Fi-to-cellular change. Record only sanitized selected transport, -address family, generation, time to first decoded frame, loss, RTT, bitrate, and -relay/SFU bytes. Keep credentials short-lived and edge-scoped and retain -allocation, relay-port, and egress caps. +and a Wi-Fi-to-cellular change. Record only sanitized route kind, address family, +generation, time to first decoded frame, loss, RTT, bitrate, and SFU bytes. Keep +credentials short-lived and generation-scoped and retain ingress/egress caps. ## Privacy-Safe ICE Evidence Candidate @@ -356,8 +380,8 @@ and participant connection, which would create the room/SFU state this pre-share check deliberately avoids. The UI therefore reports configured SFU as unknown until an actual controller-selected route proves current-generation media. The public, `no-store` self-check config returns only the existing STUN-only -`iceConfig` and an SFU-configured boolean. It returns no SFU or TURN URL, token, -username, credential, address, candidate, or selected-edge grant. +`iceConfig` and an SFU-configured boolean. It returns no SFU URL, token, +credential, address, or candidate. ## Historical SFU/UDP And Selected-Edge TURN Slice @@ -393,18 +417,15 @@ explicit ICE servers when constructing the publisher PeerConnection. A selected Host ingress can therefore supply only its coturn server plus relay policy without advertising that credential to ordinary peer PCs. -Ordinary peer ICE remains STUN-only. The participant-wide TURN -config, capability and refresh wire are removed; any stale `PEER_ICE_TURN_*` -key, including an empty value, fails startup. Selected-edge TURN is deployed as -a configured, controller-issued exceptional transport. Its active Host-ingress -media path is canary-proven; initial ingress and `peer-selected` remain open. -Every ordinary Web peer and Native-shaped client remains STUN-only. The tracked coturn example is UDP +Ordinary peer ICE remains STUN-only. Both the participant-wide and selected-edge +TURN candidates are rejected product paths; their canary evidence remains below +only to explain the measured tradeoff. Every ordinary Web peer remains STUN-only. The tracked coturn example is UDP `stun-only`; the LiveKit example exposes only ICE/UDP mux 7882, explicitly sets `tcp_port: 0` and `allow_tcp_fallback: false`, supplies the self-hosted STUN endpoint, and configures no external or embedded TURN. Candidate validation and rollback use isolated instances rather than a process-wide old-release compatibility branch. HTTPS/WSS remains TLS/TCP. -### Active Host-Ingress Functional Evidence +### Historical Host-Ingress Functional Evidence On 2026-08-21 the tracked standalone gate reused the production selected-edge tuple parser and credential issuer, supplied the short-lived credential only to @@ -463,32 +484,29 @@ same `2B` egress. Mixed representations replace `B` with measured `B_pub` and `sum(B_s)`; a HIGH+LOW publication may erase that advantage. Public LiveKit benchmarks and the Jitsi profiling breakdown establish capacity and component categories only. Different machines and implementations cannot support a -universal coturn/SFU CPU ratio or a claimed fixed percentage saving. +universal relay/SFU CPU ratio or a claimed fixed percentage saving. One bounded exact-room gate owns rollout evidence: -1. On the same host and NIC, compare coturn UDP and LiveKit SFU UDP at measured - 8 and 12 Mbps with one and two roots. Record CPU seconds/GiB, RX/TX bytes, - packets/s, RSS, host upload, p95/p99 forwarding latency, loss/recovery, and - final decoded quality. +1. On the same host and NIC, measure LiveKit SFU UDP at 8 and 12 Mbps with one + publication and one, two, and twenty subscriptions. Record CPU seconds/GiB, + RX/TX bytes, packets/s, RSS, host upload, forwarding latency, loss/recovery, + and final decoded quality. 2. Cover representative consumer networks with independent STUN-only - direct/peer, SFU/UDP subscription, exact selected-edge TURN, and Host-SFU TURN - gates. The test order does not define the controller's per-edge route choice. - All ordinary peer connections must remain STUN-only. -3. Measure allocation count/relay ports/RSS/CPU/latency for one and two selected - attempts, plus relay RX/TX/loss/latency. There is no participant-count idle - allocation target because ordinary peer PCs do not receive TURN candidates. - Block all UDP and show a bounded explicit failure; TURN/UDP is not media TCP. -4. Exercise assisted-route departure, reconnect, service unavailability, and + direct/peer and SFU/UDP subscription gates. All ordinary peer connections + must remain STUN-only. +3. Block all UDP and show a bounded explicit failure; current media has no TCP + path. +4. Exercise assisted-route departure, reconnect, SFU unavailability, and rollback. Non-server ordinary downstream edges stay within the configured - cap, server allocations stay within accepted resource admission, and + cap, SFU resources stay within accepted admission, and unaffected peer subtrees do not migrate. -5. Correlate only selected candidate type/protocol/relayProtocol and opaque +5. Correlate only route kind, candidate type/protocol, and opaque generations; never upload raw SDP, candidate/address/IP, credentials, or device identifiers. -Selected-edge TURN retains endpoint-to-endpoint DTLS-SRTP. Ordinary SFU transport -terminates DTLS-SRTP on both sides, so its operator can access media unless +Direct peer transport retains endpoint-to-endpoint DTLS-SRTP. Ordinary SFU +transport terminates DTLS-SRTP on both sides, so its operator can access media unless Screener later implements application E2EE and key distribution. That accepted tradeoff remains visible in deployment and UI claims. @@ -596,7 +614,7 @@ contract and is outside this wave. | Native shared-encode host | Host targets one encode for standard WebRTC edges | libwebrtc public-API proxy risk spike, with Pion as fallback | Research evidence; still pays per-edge upload | | Native volunteer encoded-RTP relay | Each volunteer forwards one encoded copy | Native install, RTP/RTCP forwarding, packaging, and opt-in relay policy | Research only; no current product authorization | | SFU service | SFU emits authorized subscription copies | Service pays bounded egress; an authoritative Host publisher supplies media | Source topology/admission are deployed; real LiveKit media and network validation remain open | -| Additional server-assisted paths | SFU/TURN emits authorized copies | Additional central ingress, egress, or allocation cost | Accepted resource-admission input; validate each bounded path at release | +| SFU fallback | SFU emits authorized copies | Additional central ingress and egress cost | Accepted resource-admission input; validate the bounded path at release | | SVC plus multiple trees | Peers emit striped layer copies across several trees | Layer scheduling, reassembly, redundancy, and more churn state | Separate conditional spike; target endpoint upload near `B` | | Network coding | Peers or servers emit coded blocks | Generations, buffering, decoding, integrity, and a custom media plane | Trace/FEC spike only; optimize loss recovery, not clean bandwidth | | MoQ | Publishers and MoQ relays emit object copies | New transport, packaging, player, relay, and auth stack | Optional central-fallback benchmark; still pays server egress | diff --git a/docs/session-handoff.md b/docs/session-handoff.md index 074b124..1e1014b 100644 --- a/docs/session-handoff.md +++ b/docs/session-handoff.md @@ -1,6 +1,6 @@ # Session Handoff -Last updated: 2026-08-23 +Last updated: 2026-08-24 This file is a bootstrap prompt, not a product-truth owner. The linked documents replace any duplicated or conflicting statement here. Update or remove this file @@ -31,17 +31,19 @@ Current high-signal boundary: - docs/status.md owns the exact source/production/hold state. - docs/todo.md is the only executable work ledger and owns ordering. -- ADR-0005 owns the accepted automatic route model. The Browser route remains - implemented and deployed in release `d3ff9e7`; real external-network - and SFU/TURN media evidence remains open. Do not rerun broad or +- ADR-0005 owns the accepted automatic route model: direct/STUN peer followed + only by the dedicated LiveKit SFU/UDP fallback. Production release `a5b1fc6` + has the staged route deadline but still contains the superseded selected-TURN + surface; its atomic removal is the active boundary. Real external-network + and SFU media evidence remains open. Do not rerun broad or executable/native test suites unless the current acceptance boundary specifically requires them. - ADR-0002 owns the source-implemented room model: one process-memory RoomStore, random free four-digit codes, configurable 24-hour dormant leases, restart loss, local Host preference replay, an independent expiring Viewer grant, and open/password/disabled code entry. Current source and production use the - single `screener-v8` wire and no SQLite runtime; `6b87732` plus the verified - precutover SQLite snapshot remains the immediate rollback boundary. + single `screener-v8` wire and no SQLite runtime; exact rollback artifacts are + owned by the deployment document. - docs/maintenance.md owns the reusable mechanism-simplification review. SQLite is its reference case because the bounded restart-experience loss did not justify the persistence surface; accepting that tradeoff changed the contract, diff --git a/docs/status.md b/docs/status.md index 4c9ee88..7d532c0 100644 --- a/docs/status.md +++ b/docs/status.md @@ -1,37 +1,37 @@ # Current Status -Last updated: 2026-08-23 +Last updated: 2026-08-24 This is the current execution index. Git history owns completed timelines; [verification status](./verification-status.md) owns evidence boundaries. ## Production -- `https://share.bonfire.icu` runs exact `d3ff9e7b7b4a8fe58db700565971aaeda638d2e9`, release `d3ff9e7`, wire `screener-v8`; `352c457` is the immediate rollback release and `/opt/screener/backups/d3ff9e7-precutover-20260823T150618Z` retains its verified environment/unit/LiveKit boundary. +- `https://share.bonfire.icu` runs exact `a5b1fc62f609b996ead54f2a5db42ddd2f374fb9`, release `a5b1fc6`, wire `screener-v8`; `/opt/screener/backups/a5b1fc6-precutover-20260823T160319Z` retains its verified pre-cutover boundary and the immutable archive SHA-256 is `2c6223f13b1ae44e737f4088af3f62259581c58c20eb9583f4f4900f58387519`. - `screener`, LiveKit, coturn, and nginx are active; local/public health return 200 and all services report `NRestarts=0`. The v8 unit has no writable room `StateDirectory`, the process has no SQLite descriptor, and the old live SQLite path is absent. -- Production admits one Host plus 20 Viewers, applies endpoint capacity `2`, rejects stale v7 Browser and executable-sender wires before room authority, and runs the one-controller exact-candidate route model. Rooms use random free four-digit codes, a 24-hour dormant lease, restart loss, local Host creation preferences, independent Viewer grants, and `open | password | disabled` code entry. LiveKit remains dedicated with `room.auto_create: false`, `max_participants: 21`, global admission `1` publication ingress / `20` subscription egress; selected TURN has `2` logical allocations. -- Production still has access/privacy controls, self-check, diagnostic export, signaling watchdog behavior, 64/128/256 audio choices, and selected-edge transport. These deployment facts do not close the real heterogeneous-network, SFU/TURN media, mobile, or endurance evidence boundaries. +- Production admits one Host plus 20 Viewers, applies endpoint capacity `2`, rejects stale Browser and executable-sender wires before room authority, and runs the one-controller exact-candidate route model with one staged total deadline. Rooms use random free four-digit codes, a 24-hour dormant lease, restart loss, local Host creation preferences, independent Viewer grants, and `open | password | disabled` code entry. LiveKit remains dedicated with `room.auto_create: false`, `max_participants: 21`, and global admission `1` publication ingress / `20` subscription egress. +- Production still has the superseded four-value selected-TURN environment tuple and authenticated coturn relay configuration. Accepted truth is now direct/STUN peer followed only by LiveKit SFU/UDP; this is not implemented or deployed until source, environment, coturn relay configuration, and firewall range are removed atomically. These deployment facts do not close the real heterogeneous-network, SFU, mobile, or endurance evidence boundaries. - SFU subscribers reconcile Host screen publications across connect, activation, participant arrival, track publication, and reconnect. A controlled production canary forced two SFU roots; both committed `route-ready`, reached active SFU assignments, and decoded 1920x1080 video. LiveKit runs at `warn`/Pion `error`, and the Screener unit waits boundedly for the same-host LiveKit control listener before startup. ## Current Source - Canonical source is the root `main` branch; auxiliary branches and worktrees do not own current truth. Audit/integration boundaries require a clean root, and dependent work starts only from its exact commit. -- Current source implements one steady outbound media-copy cap for every non-server endpoint: default `2`, static `1/2/3`, a peer child or Host publication consumes one slot, upstream receive is free, a committed selected TURN transport replaces the same copy while hidden carry consumes another, and no Browser/UA/visibility tier exists. One shared guard owns the accounting and transition work is bounded by `min(C + 1, 3)`. +- Current source implements one steady outbound media-copy cap for every non-server endpoint: default `2`, static `1/2/3`, a peer child or Host publication consumes one slot, upstream receive is free, and no Browser/UA/visibility tier exists. One shared guard owns the accounting and transition work is bounded by `min(C + 1, 3)`. - Current source and production admission support one Host plus up to `20` Viewers. Focused controller coverage includes `C=1/2/3`, a 20-Viewer graph, and the passed local 20-Viewer Browser smoke; real external-network media and resource measurements remain open. - The single Browser `screener-v8` boundary rejects stale Browser and executable-sender wires before room authority and carries the exact deployment cap plus exact prepare candidate in the authenticated route contract. -- Current source also implements explicit no-default SFU ingress/egress capacities and exact `reserved | committed | draining` accounting. The listener owner reconciles its dedicated non-auto-create LiveKit namespace before serving, creates each generation before token issuance, releases only after delete-plus-absence proof, and reclaims Host-offline generations through bounded exact-participant checks. Selected TURN has an independent explicit no-default deployment capacity; one process-local ledger charges every exact peer-selected edge and Host-SFU ingress through reserve, commit, drain, and release. +- Current source also implements explicit no-default SFU ingress/egress capacities and exact `reserved | committed | draining` accounting. The listener owner reconciles its dedicated non-auto-create LiveKit namespace before serving, creates each generation before token issuance, releases only after delete-plus-absence proof, and reclaims Host-offline generations through bounded exact-participant checks. The superseded selected-TURN source remains present at this truth checkpoint and is the active removal scope. - Current source implements automatic P2P-first routing with one active upstream, an acyclic source-reachable graph, exact Browser v7 prepare tuple, first-decoded-frame make-before-break, strict rollback revision, and bounded wait or failure. -- One event-driven controller owns the committed graph and at most one room-serial child operation. The operation owns one deterministic tuple list/cursor, one current candidate and reservations, fact version, and one total deadline. Failed tuples are operation-local; exhaustion retires invalid/overflow edges but retains healthy bootstrap media. Bounded-gap preflights resources, replans the remaining suffix, and restores a healthy retired edge through one final ordinary candidate. Admission commits synchronously before graph promotion; release uses the resource-set difference and room disposal returns all resources. One Host publication serves exact Viewer subscription handles, selected TURN changes only transport, and actual global resource release wakes registered waiting rooms. -- Parent selection is deterministic rather than random or score-based: hard eligibility filters, then shallowest result, remaining sender capacity, stable join order, and peer identity. The exact candidate child's first new decoded frame is the only application media-ready event. Moderate bitrate, FPS, or visual degradation remains with stock WebRTC/LiveKit adaptation and diagnostics; only hard connection failure or a non-paused decoded-frame stall invalidates a route edge. Viewer connection feedback and the strict-NAT emulator gate are accepted follow-up work, not deployed behavior. +- One event-driven controller owns the committed graph and at most one room-serial child operation. The operation owns one deterministic candidate list/cursor, one current candidate and reservations, fact version, and one total deadline split only across direct and SFU stages. Failed candidates are operation-local; exhaustion retires invalid/overflow edges but retains healthy bootstrap media. Bounded-gap preflights resources, replans the remaining suffix, and restores a healthy retired edge through one final ordinary candidate. Admission commits synchronously before graph promotion; release uses the resource-set difference and room disposal returns all resources. One Host publication serves exact Viewer subscription handles, and actual global SFU release wakes registered waiting rooms. +- Parent selection is deterministic rather than random or score-based: hard eligibility filters, then shallowest result, remaining sender capacity, stable join order, and peer identity. When a departed Host root frees a Host slot, a new or reparable child prefers that shallower Host result over another root's free slot; healthy old edges remain sticky and the controller does not periodically rebalance the whole graph. The exact candidate child's first new decoded frame is the only application media-ready event. Moderate bitrate, FPS, or visual degradation remains with stock WebRTC/LiveKit adaptation and diagnostics; only hard connection failure or a non-paused decoded-frame stall invalidates a route edge. Quality-driven local reparenting and topology evidence are accepted research/TODO work, not current behavior. - Browser initial capture and source switching already request available share audio through one shared capture boundary and visibly retain video-only sharing when no audio track is returned. - Live 64/128/256 audio-ceiling mutation and paused-share video codec switching are accepted but not yet implemented on canonical `main`. H.264 startup blur/low-FPS reports remain an evidence-led diagnosis, not a presumed codec defect. Public-server and fully local distribution packages are later work after the functional, NAT, and real-network ledger is complete. - Current source and production implement one process-memory RoomStore with random four-digit codes, a default 24-hour dormant lease, exact-Host resume, restart loss, local Host preference replay, independent token invitations and `open | password | disabled` code entry, and no SQLite runtime. The Host UI separates the invitation, room-code entry policy, password, grant update, and grant revoke controls. ## Current Milestone -1. Release live audio-ceiling mutation and paused-share codec switching on the deployed generation/transition boundary. -2. Reproduce the H.264 startup report before any codec-specific repair, and build the strict-NAT emulator gate before any manually approved canary in an operator-owned test room. -3. Add first-frame-driven Viewer connection stages, then finish the remaining accepted functional and real-network work. -4. Validate and deploy each coherent slice in this order with its focused browser/network/resource and rollback gates; distribution packaging remains outside this milestone. +1. Remove selected TURN from the application, wire, configuration, UI, tests, production environment, coturn relay mode, and relay firewall range while retaining STUN and SFU. +2. Release live audio-ceiling mutation and paused-share codec switching on the deployed generation/transition boundary. +3. Reproduce the H.264 startup report before any codec-specific repair, research quality-driven local reparenting, and build the strict-NAT emulator gate before any manually approved canary in an operator-owned test room. +4. Add first-frame-driven Viewer connection stages, then finish the remaining accepted functional and real-network work. ## Active Boundaries @@ -39,11 +39,11 @@ This is the current execution index. Git history owns completed timelines; [veri - Open PR #192 and the Native stack are evidence/research, not pending product releases. Native senders, capture helpers, shared-encode executables, and their test binaries are outside the current Browser milestone and are not built or run. - Deployed surfaces and retained candidates that still need product decisions are indexed only in [the TODO ledger](./todo.md); do not extend or roll them back automatically. - Room entry, sharing controls, ID lifetime, storage semantics, v8 stale-client rejection, restart loss, and rollback restoration are aligned between current source and production. -- Real SFU/TURN recovery, heterogeneous networks, mobile lifecycle, audio/A-V device behavior, and endurance/resource measurements remain external acceptance evidence, not blockers for unrelated reversible work. +- Real SFU recovery, heterogeneous networks, mobile lifecycle, audio/A-V device behavior, and endurance/resource measurements remain external acceptance evidence, not blockers for unrelated reversible work. ## Current Hold -The Browser route, SFU subscription reconciliation, and memory-room slices are deployed, but room -`9403` exposed a P1 fallback-budget regression: silent direct/selected candidates can consume the -whole operation before an existing SFU publication is reused. Fix and deploy the one-timer staged -deadline invariant before resuming live-media, Native/executable, or broad cleanup work. +The Browser route, SFU subscription reconciliation, staged deadline, and memory-room slices are +deployed. The active bounded change is removal of selected TURN from source and production; do not +resume live-media, Native/executable, or broad repository cleanup until direct-to-SFU source and +production postflight are coherent. diff --git a/docs/todo.md b/docs/todo.md index 8aea445..beb6c84 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -1,30 +1,32 @@ # Current TODO Ledger -Last reviewed: 2026-08-23 +Last reviewed: 2026-08-24 Only items in **Now** are executable after their stated decision gate. A branch name, unchecked requirement, experiment, review suggestion, or deployed behavior is not a TODO by itself. ## Now -1. **Restore stage budget for SFU fallback.** Room `9403` proved that silent direct recovery can consume the whole child-operation deadline: Host-full bootstrap correctly moved one existing direct child to SFU, but the cellular child's next operation never reached the healthy publication's `sfu:reuse` tuple. Divide the one total deadline equally across the route classes actually present, use the same timer to skip silent candidates at each derived boundary, and prove direct/selected stages cannot starve SFU. Deploy and recheck the exact cellular Viewer before resuming later work. +1. **Remove selected TURN and retain direct-to-SFU routing.** Delete the application selected-TURN candidate, signaling/configuration surface, credential issuer, logical-allocation ledger, browser relay-only rebuild, labels, gate, and tests. Ordinary peer ICE remains STUN-only and the sole route suffix is the dedicated LiveKit SFU over UDP. Atomically remove the production `SELECTED_EDGE_TURN_*` tuple, restore coturn to tracked STUN-only/no-TCP/no-TLS mode, close its relay range and TCP listener, and prove direct peer, Host-full SFU bootstrap/reuse, bounded failure, configuration absence, services, health, and rollback. Do not add an application media-TCP substitute; any later strict-firewall transport requires separate evidence and belongs inside LiveKit. 2. **Switch screen-audio ceilings live.** Keep Opus fixed and serialize 64/128/256 kbps desired profile updates across every current Host, relay, and SFU audio sender. Each endpoint uses fresh `getParameters()`/`setParameters()` readback, ignores stale completions, retains media plus its prior applied value on failure, and applies the latest desired value to future edges. Do not add a remote applied-ack protocol or claim room-wide atomic convergence in this slice. 3. **Switch video codec while paused.** Serialize the change with route mutation, freeze exact current route/media bindings, hold new joins, renegotiate P2P/relay and republish SFU while paused, then verify actual codec plus decoded frames on resume. A binding invalidated by leave or hard route failure exits this proof set and waits for routing to rebuild from the final committed preference after the bounded codec transaction settles. On failure pause again and perform bounded rollback; rollback proves the prior preference was reapplied plus a newly negotiated actual codec and decoded progress, not literal equality with the old actual codec. Zero targets update only the future-edge preference. Reset `automatic` with empty codec preferences. This is a paused bounded-gap transaction; unpaused make-before-break is a later extension. 4. **Diagnose the production H.264/blur report.** Reproduce exact release `769de201f7cc` against the deployed route release on the same Host/browser/driver/game and one wired direct Viewer, changing one variable at a time. Implement a codec-specific repair only if codec/encoder evidence identifies it; otherwise fix the proven capture, sender, BWE, or lifecycle cause. 5. **Diagnose desktop Host background/minimized performance.** On the same machine, dynamic scene, direct Viewer, browser, and fresh share, compare foreground, occluded, minimized, and hidden states while separately verifying actual negotiated VP8/H.264. Correlate capture settings/FPS, frames captured/encoded/sent/decoded, bitrate, `qualityLimitationReason`, encode time, browser/game CPU and GPU, and the existing local-preview pause boundary. Treat browser scheduling/throttling and codec effects as hypotheses until measured. Do not spoof page activity, add keepalive timers, or misuse Wake Lock; if current Chrome/Edge cannot preserve the accepted Host behavior through standard Web APIs, record that capability boundary before considering the packaged/native Host. 6. **Gate the strict-NAT guessed-candidate experiment.** Build only the isolated endpoint-dependent NAT emulator and browser harness first. Standard ICE must fail in the owned fixture, and every guessed candidate must remain inside the existing direct-stage share rather than extending the room operation. The capped set must pass the documented repeatability, privacy, cleanup, and total-deadline gates in stable Chrome and Firefox, while fallback remains unchanged. Only then may one manually approved, operator-owned test room run a default-off, kill-switched canary with explicit consent from both endpoints and exact room/edge/ICE-generation/deadline/aggregate-budget bounds; failure closes the candidate. Do not ship guessed candidates to ordinary rooms or infer NAT type from IP, UA, carrier, or two STUN observations. -7. **Make Viewer connection progress and failure recovery explicit.** Derive one stable stage from existing access, Host presence, route, peer/SFU, media, autoplay, recovery, and bounded-failure state. Keep the overlay until a composited current-generation frame, show `Play` only for actual autoplay rejection, preserve a still-valid old frame during recovery, and expose retry only through the bounded controller. Use one Chinese user-facing vocabulary and map server restart/room loss, Host stop, Host signaling loss with healthy media, Host crash/offline, route exhaustion, SFU/TURN admission, autoplay, and stale-client refresh to distinct actionable presentations without exposing private topology identifiers. A failed mobile page must retain privacy-safe stage/revision/code buckets for diagnostic export instead of collapsing everything into `No usable media route is available`. Measure the existing token-free SFU prewarm and route stages before adding another connection mechanism. +7. **Make Viewer connection progress and failure recovery explicit.** Derive one stable stage from existing access, Host presence, route, peer/SFU, media, autoplay, recovery, and bounded-failure state. Keep the overlay until a composited current-generation frame, show `Play` only for actual autoplay rejection, preserve a still-valid old frame during recovery, and expose retry only through the bounded controller. Use one Chinese user-facing vocabulary and map server restart/room loss, Host stop, Host signaling loss with healthy media, Host crash/offline, route exhaustion, SFU admission, autoplay, and stale-client refresh to distinct actionable presentations without exposing private topology identifiers. A failed mobile page must retain privacy-safe stage/revision/code buckets for diagnostic export instead of collapsing everything into `No usable media route is available`. Measure the existing token-free SFU prewarm and route stages before adding another connection mechanism. 8. **Clarify malformed and unusable room entry without enumeration.** Reject non-four-digit input locally with a direct format message. For a well-formed code that cannot be admitted, stop automatically implying that a password is required; show one neutral result covering an unknown room, disabled code entry, wrong/absent password, or bounded admission failure, and offer an optional password retry plus the invitation-link path. Keep `open` rooms direct and do not expose room existence or code-entry policy through distinct server errors. 9. **Refine recommended quality presets.** Change the default recommended profile to the middle `1080p30` option and add one fully specified 480p preset only after its exact resolution, frame rate, and bitrate ceiling are accepted. Update capture constraints, current/future peer/relay/SFU sender settings, local preference/default selection, readback, and tests together; do not add a label that leaves old sender parameters active. +10. **Finish the room-code entry UI as one responsive control.** Preserve the invitation-link controls that are already direct, but redesign the code-only policy/password area to match the compact work surface instead of a nested form panel. Keep the `Start sharing` and `Join room` action rows at stable dimensions when disclosure content opens; prove desktop/mobile wrapping, focus, password and disabled states, and no horizontal overflow before integrating the retained UI candidate. +11. **Research quality-driven local reparenting without room-wide churn.** First measure exact child-edge decoded progress, freeze ratio, RTT, jitter, loss, received bitrate, resolution, FPS, and sender/receiver limitation evidence against the desired room profile. Compare standards/framework algorithms and primary literature for hysteresis, make-before-break handoff, application-layer multicast repair, and local cost/benefit decisions. Any accepted mechanism must be event-driven, exact-edge scoped, bounded by the existing room-serial operation, and resistant to oscillation; it must not infer parent-wide quality from one child, use a weighted room score, or periodically rebalance healthy subtrees. Separately prove the current deterministic rule: after a Host root leaves, a waiting or orphaned child prefers the newly free shallower Host slot, while already healthy descendants remain sticky. Add privacy-safe route-decision snapshots before diagnosing another live room because production currently cannot reconstruct a room such as `3793` after the fact. ## Accepted Later Roadmap These items are real product work, ordered after the current release boundary; they are not permission to resume an old branch wholesale. 1. **Mobile Viewer lifecycle.** Run Android Chrome and iOS Safari device matrices for autoplay gesture, foreground/background audio, foreground video recovery, lock/page reclamation, rotation, Wi-Fi/cellular migration, and an assigned relay's outbound-media survival or controller failover across those transitions. Preserve the same media element and current reconnect path; do not promise background video composition, background relay continuity, or unsupported lock-screen behavior. -2. **Real network acceptance.** Measure standard ICE/STUN, IPv4/IPv6, independent STUN destinations, selected TURN, SFU ingress, and bounded failure across representative NATs and mobile networks. Browser port prediction remains experiment-only until its emulator and allowlisted-canary gates pass; raw-IP ranking and a room-wide quality score remain out of scope. Parent choice uses exact edge evidence and the accepted route controller. +2. **Real network acceptance.** Measure standard ICE/STUN, IPv4/IPv6, independent STUN destinations, SFU ingress/subscription, and bounded failure across representative NATs and mobile networks. Include an all-UDP-blocked case to measure the current explicit failure boundary. Browser port prediction remains experiment-only until its emulator and allowlisted-canary gates pass; raw-IP ranking and a room-wide quality score remain out of scope. Parent choice uses exact edge evidence and the accepted route controller. 3. **Platform output only when real.** Revisit AirPlay/Cast only when a target browser and physical receiver prove the live `MediaStream` contract. System mirroring remains external; no placeholder button, custom receiver, or server transcode is scheduled. -4. **Public-server one-click deployment package.** After every current functional and real-network item is settled, package the exact application, STUN/SFU/TURN, reverse proxy, secrets, health checks and rollback flow for a user-owned public server. Require real domain/TLS/firewall/capacity inputs and never ship credentials or call a partial deployment ready. -5. **Fully local one-click Host-server package.** Package Windows/macOS/Linux Host capture, the exact application server and local state without requiring source, Node or any external Screener/STUN/SFU/TURN/tunnel/relay service. Preserve usable direct P2P and use the completed NAT/network evidence to maximize reachability, while reporting public-origin, gateway, NAT and firewall limits truthfully; loopback/LAN success is not universal Internet success. +4. **Public-server one-click deployment package.** After every current functional and real-network item is settled, package the exact application, STUN/SFU, reverse proxy, secrets, health checks and rollback flow for a user-owned public server. Require real domain/TLS/firewall/capacity inputs and never ship credentials or call a partial deployment ready. +5. **Fully local one-click Host-server package.** Package Windows/macOS/Linux Host capture, the exact application server and local state without requiring source, Node or any external Screener/STUN/SFU/tunnel/relay service. Preserve usable direct P2P and use the completed NAT/network evidence to maximize reachability, while reporting public-origin, gateway, NAT and firewall limits truthfully; loopback/LAN success is not universal Internet success. 6. **Native Host capture backend.** At lowest priority, productize a proven Windows native capture path before expanding capture/audio backends to macOS and Linux. Its capture, hardware encode, audio, identity and lifecycle evidence becomes an input to later packages; a source checkout, helper executable, or evaluation ZIP is not a distributable product. 7. **Native shared encode, Windows first.** At lowest priority, prove one hardware encoder output can serve two independent Viewer transports and the Host-SFU publication while each connection retains its own RTP/RTCP, pacing, encryption, feedback, and upload accounting. Productize only after actual access-unit identity, Viewer2/FIFO, heterogeneous feedback, game-resource, browser/hardware, A/V, packaging, and licensing gates pass. Web Host/relay remains standard per-`RTCPeerConnection` encoding because no portable shared-encoder contract exists. 8. **Repository simplification audit.** After the current route and room checkpoints and their scoped implementation work settle, inventory components, configuration, migrations, gates, timers, compatibility paths, and tests using the [maintenance review](./maintenance.md#机制减负审查). Begin read-only: identify the user behavior each mechanism preserves, quantify the impact of removing it, and compare that value with its whole-system cost. Any accepted feature tradeoff updates its owning contract before complete unused surfaces are removed. This later audit must not become a broad refactor or block current product work. @@ -36,7 +38,7 @@ These items are real product work, ordered after the current release boundary; t | #169 connection self-check | Keep, revise, or remove the user-facing pre-share probe. Do not extend it automatically. | | #171 diagnostic JSON export | Confirm a support workflow or remove the extra surface. | | #181 selected-pair response counter | Keep as local observation or remove; it has no route authority. | -| #182 signaling watchdog | Confirm Host, mobile/background, and selected-edge session semantics before calling it complete. | +| #182 signaling watchdog | Confirm Host and mobile/background session semantics before calling it complete. | These decisions are not permission to continue old branches, and current deployment remains unchanged until a scoped decision is made. @@ -45,7 +47,7 @@ These decisions are not permission to continue old branches, and current deploym - Open PR #192 measures synthetic flash/tone timing; it is not an A/V correction feature. - The Native PR stack #16/#18/#22/#23/#25/#28 and related fanout branches are one research program, not six product TODOs. - Cap3, resource, route-recovery, Viewer-MBB, signaling-blackhole, Host-generation, SFU-shaping, NAT, mobile, and endurance artifacts remain bounded evidence. Merge or retain them only when a current decision consumes them. The isolated strict-NAT harness authorizes no product path until its gate passes; raw-IP ranking, AirPlay/Cast receivers, and mobile background guarantees are not implied by the current implementation batch. -- Real heterogeneous-network SFU/TURN, mobile lifecycle, audio/A-V device, and endurance/resource acceptance are scheduled at the relevant release boundary. +- Real heterogeneous-network SFU, mobile lifecycle, audio/A-V device, and endurance/resource acceptance are scheduled at the relevant release boundary. ## Candidate Handling diff --git a/docs/verification-status.md b/docs/verification-status.md index 2955c7c..5efeddd 100644 --- a/docs/verification-status.md +++ b/docs/verification-status.md @@ -1,6 +1,6 @@ # Current Verification Status -Last updated: 2026-08-23 +Last updated: 2026-08-24 ## Purpose @@ -10,21 +10,21 @@ Capacity and routing entries below describe the exact tested source or release o ## Deployment Evidence -- Production runs exact `d3ff9e7b7b4a8fe58db700565971aaeda638d2e9`, release `d3ff9e7`, wire `screener-v8`, from `/opt/screener/releases/d3ff9e7`; the immutable runtime archive SHA-256 is `a7c3b7b7cf593f3933a1c194a07370131fd76b3d4c30768b5206e84ccf93b63d`. `352c457` is the immediate rollback release, and `/opt/screener/backups/d3ff9e7-precutover-20260823T150618Z` contains the verified environment/unit/LiveKit boundary. -- The 2026-08-23 postflight found `screener`, LiveKit, coturn, and nginx active with zero restarts; local and public `/healthz` returned 200 and the Screener journal contained no error line. The deployed browser bundle is `assets/index-BVK8yQk2.js`, SHA-256 `388a093734e8dc37a07b859651ea15d4098a8c8f13864ca9d70eb99128a911a3`, matching the locally built artifact and containing one `screener-v8` and zero `screener-v7` literals. -- Production uses endpoint capacity `2`, room admission `20`, random free four-digit memory rooms, `ROOM_LEASE_SECONDS=86400`, a dedicated LiveKit instance with `room.auto_create: false` and `max_participants: 21`, global SFU admission `1` ingress / `20` egress, and selected TURN admission `2`. The v8 service has no StateDirectory or SQLite descriptor; the old live SQLite file/directory is absent and LiveKit listed zero rooms after startup. +- Production runs exact `a5b1fc62f609b996ead54f2a5db42ddd2f374fb9`, release `a5b1fc6`, wire `screener-v8`, from `/opt/screener/releases/a5b1fc6`; the immutable runtime archive SHA-256 is `2c6223f13b1ae44e737f4088af3f62259581c58c20eb9583f4f4900f58387519`, and `/opt/screener/backups/a5b1fc6-precutover-20260823T160319Z` is its verified pre-cutover boundary. +- A 2026-08-24 read-only check found `screener`, LiveKit, coturn, and nginx active with zero restarts; local and public `/healthz` returned 200. +- Production uses endpoint capacity `2`, room admission `20`, random free four-digit memory rooms, `ROOM_LEASE_SECONDS=86400`, a dedicated LiveKit instance with `room.auto_create: false` and `max_participants: 21`, and global SFU admission `1` ingress / `20` egress. The superseded selected-TURN admission remains `2` until the direct-to-SFU cutover removes its environment, coturn relay mode, and firewall range atomically. The v8 service has no StateDirectory or SQLite descriptor and the old live SQLite path is absent. - A public control-plane smoke passed site access, four-digit creation, Host v8 authentication, stale-v7 rejection before authority, independent code-only and grant Viewer admission, grant rotation that preserved the code Viewer, signaling challenge, and exact room abandonment. A separate service-restart smoke rejected the old Host token and Viewer grant, atomically created a password-policy replacement room, and abandoned it. -- The first reported SFU failure showed Host screen tracks published and the Viewer active over LiveKit UDP, but the candidate was deleted before a usable route committed. Exact `d3ff9e7` fixed missing subscription reconciliation across pending connect, participant/track arrival, activation, and reconnect; 550 Web tests passed, and a production Host plus three zero-downstream Viewer canary forced one peer and two active 1920x1080 SFU roots. Room `9403` then exposed a separate earlier blocker: repeated cellular attempts never entered LiveKit. With Host peer slots full, bootstrap correctly converted one existing direct child into a healthy Host publication plus one SFU subscriber, but the cellular child's next operation again exhausted in direct/selected work before `sfu:reuse`. This is the active P1 stage-budget regression, not evidence that the publication or LiveKit UDP path is unavailable. +- The first reported SFU failure showed Host screen tracks published and the Viewer active over LiveKit UDP, but the candidate was deleted before a usable route committed. Exact `d3ff9e7` fixed missing subscription reconciliation across pending connect, participant/track arrival, activation, and reconnect; 550 Web tests passed, and a production Host plus three zero-downstream Viewer canary forced one peer and two active 1920x1080 SFU roots. Room `9403` then exposed a separate blocker: repeated cellular attempts never entered LiveKit because silent earlier route classes consumed the total deadline before `sfu:reuse`. Exact `a5b1fc6` derives semantic-stage wake boundaries from the one total timer and is deployed; an exact heterogeneous-network recheck remains open. The accepted follow-up removes the selected-TURN stage entirely, leaving only direct and SFU shares. - The first hotfix cutover start raced LiveKit ActiveState before TCP 7880 and produced one supervised Screener restart. A production-only `After/Wants` plus bounded listener-ready `ExecStartPre` was installed; a simultaneous LiveKit/Screener restart then completed with both `NRestarts=0`. Pinned LiveKit info records were confirmed to contain raw PublisherOffer SDP, so production now uses warn/Pion-error. Three archived journal files containing prior raw SDP (90,404,248 bytes total) were deleted; the active journal and the post-hardening SFU canary contain zero `PublisherOffer` record. - Rollback evidence confirms release `6b87732` and its old environment/runtime imports are readable, every precutover backup checksum passes, the backed-up database remains schema v3/integrity-ok with six rooms, and the current and rollback release regular-file sets share zero inodes. -- Share audio presets, local-only diagnostics, self-check, diagnostic export, signaling-partition recovery, and selected-edge transport remain deployed surfaces. Their real-route media behavior and target-browser field availability remain unverified. +- Share audio presets, local-only diagnostics, self-check, diagnostic export, signaling-partition recovery, and the superseded selected-edge transport remain deployed surfaces. The latter is pending atomic removal; the remaining real-route media behavior and target-browser field availability remain unverified. ## Retained Functional Evidence - Exact source `31e323e2754fcfde15f7b4d69e925ec71e2795cb` passed repository hygiene, TypeScript typecheck, client/server builds, all 660 Web tests, all Native Go packages, `go vet`, and two independent P0/P1 reviews. The tests cover strict v6 endpoint-cap authentication, ordinary active/waiting authorization and grace promotion, Native cap propagation and admission, and peer-assisted cap rejection. This is local state-machine/build evidence, not a complete Native physical-slot matrix or production, browser-quality, or heterogeneous-network acceptance. - Exact Wave B implementation source `815c6174d411c55648daf5cd6ac4c42f64effff9`, based on `d1ba2e52919225e67ad0c8dd2b660f333fbc88fd`, passed its 372-test SFU-admission gate, all 690 Web tests, TypeScript typecheck, and client/server builds. Focused tests cover reserved/committed/draining accounting, reserve-before-token enforcement, explicit room lifecycle, listener-owner startup fencing, restart cleanup, stale-token replay boundaries, Host-offline reclamation, shutdown races, and two-room global admission. This is fake-control-plane and local state-machine evidence, not a real LiveKit process/JWT replay, packet-flow, multi-process, deployment-capacity, or production proof. - Exact route source `4e0a922` passed repository hygiene, TypeScript typecheck, and 84 focused controller, runtime, protocol, transition, decoded-stall, and SFU-admission tests. During the same convergence, the full 557-test Web suite and client/server builds passed before the final no-consumer cleanup. The two independent route audits' model and wire findings are incorporated, and final static review found no remaining P0/P1. No Browser, Native, Go, helper, hardware, or executable test ran in the final pass; this is source and state-machine evidence, not production or heterogeneous-network proof. -- Exact source `6b87732` passed the required one-Host/20-Viewer local Browser smoke on Windows Node 24.19.0 / Chrome 151.0.7922.174, using 720p30 synthetic canvas media, endpoint cap `2`, a 30-second measurement window, and report SHA-256 `1347E036BB9AC35D51B4ED9B215290E742715128C7F3056D8FEB185CC84456D6`. All six checks passed: 20/20 Viewers had live clean upstreams and increasing decoded frames, Host and relay sender counts stayed at two, topology was coherent, and no orphan SFU publication appeared. Maximum authenticate-to-first-decoded latency was 958 ms; the final graph had two Host children and eighteen peer descendants. The application Node process consumed about 0.95 additional CPU seconds over the 54-second observation window, peaked near 284 MiB RSS, and settled near 162 MiB; isolated Chromium averaged 761% CPU over its valid 25.97-second process interval. The harness did not configure LiveKit or TURN, so their observed usage was zero and this loopback is not a public-network, SFU/TURN, quality, or endurance claim. +- Exact source `6b87732` passed the required one-Host/20-Viewer local Browser smoke on Windows Node 24.19.0 / Chrome 151.0.7922.174, using 720p30 synthetic canvas media, endpoint cap `2`, a 30-second measurement window, and report SHA-256 `1347E036BB9AC35D51B4ED9B215290E742715128C7F3056D8FEB185CC84456D6`. All six checks passed: 20/20 Viewers had live clean upstreams and increasing decoded frames, Host and relay sender counts stayed at two, topology was coherent, and no orphan SFU publication appeared. Maximum authenticate-to-first-decoded latency was 958 ms; the final graph had two Host children and eighteen peer descendants. The application Node process consumed about 0.95 additional CPU seconds over the 54-second observation window, peaked near 284 MiB RSS, and settled near 162 MiB; isolated Chromium averaged 761% CPU over its valid 25.97-second process interval. The harness did not configure LiveKit, so its observed SFU usage was zero and this loopback is not a public-network, SFU, quality, or endurance claim. - Exact memory-room source `2ba155782bd5c4a193484c007ee69007e562ddb9` passed repository hygiene, TypeScript typecheck, all 548 Web tests, and client/server builds. The focused room/access set contributed 239 passing tests across allocation, dormant leases, grant/code orthogonality, password KDF bounds, stale generation atomicity, protocol v8, HTTP/signaling, local ownership, and creation-profile replay. A fresh Chrome access/privacy gate also passed all fixed checks: pre-DOM fragment consumption, room-scoped session grant, zero request/nginx/application-log secret hits, zero persistent room files, strong grant rotate/revoke coverage, and cleanup. This is local source, state-machine, and loopback-browser evidence; it is not production deployment, restart/rollback, heterogeneous-network, or media-quality evidence. - Current source TURN-admission unit and signaling tests cover independent peer and Host-SFU logical edges, shared capacity exhaustion, exact reserve/commit/drain/release identity, current-share cleanup, and graceful shutdown. This is local authorization evidence, not real coturn, network-media, or production-capacity proof. - Chrome 151 synthetic `1/3/5/8` and 720p30 runs preserved bounded fanout and decoding. A `9461e20`-era three-Viewer rerun kept Host2/Browser Viewer1, decoded every Viewer, and recovered a hard first-level relay departure in 5,380 ms; it did not exercise quality-triggered MBB. Sixteen-Viewer cap2/cap3 loopbacks decoded all Viewers and a ten-second follow-up recorded guarded sender and aggregate Chromium CPU evidence, but resolution/FPS remained low and the ordinary-PC data cannot rank caps, select product policy, or prove sustainable heterogeneous-network quality. See [peer-assisted media](./research/peer-assisted-media.md). diff --git a/docs/方案设计.md b/docs/方案设计.md index 5ba43a7..327a960 100644 --- a/docs/方案设计.md +++ b/docs/方案设计.md @@ -6,7 +6,7 @@ ## 目标与边界 -产品面向一名分享者和小范围可信朋友的低延迟游戏画面分享。观看端使用普通桌面或手机浏览器;每条普通媒体边独立 ICE 并优先 direct/peer UDP。所有非服务器端点共享一个服务端权威稳态 outbound media-copy capacity,由唯一部署项 `ENDPOINT_MEDIA_COPY_CAPACITY` 配置,默认 `2` 且只接受 `1`、`2` 或 `3`;peer child 或 Host publication 各消耗一份,upstream receive 免费,selected TURN 只替换同一份 edge 的 transport,角色、浏览器、UA、设备和页面可见性不改变该值。活动媒体图无环,每名 Viewer 同时至多一个 active upstream。不兼容的 capacity 能力与单一 signaling wire 原子升级,旧客户端在取得房间 authority 前 fail closed。SFU subscriber egress、TURN allocation、temporary overlap 和 exact fallback sequence 由 ADR-0005 的整体模型统一定义。 +产品面向一名分享者和小范围可信朋友的低延迟游戏画面分享。观看端使用普通桌面或手机浏览器;每条普通媒体边独立 ICE 并优先 direct/peer UDP。所有非服务器端点共享一个服务端权威稳态 outbound media-copy capacity,由唯一部署项 `ENDPOINT_MEDIA_COPY_CAPACITY` 配置,默认 `2` 且只接受 `1`、`2` 或 `3`;peer child 或 Host publication 各消耗一份,upstream receive 免费,角色、浏览器、UA、设备和页面可见性不改变该值。活动媒体图无环,每名 Viewer 同时至多一个 active upstream。不兼容的 capacity 能力与单一 signaling wire 原子升级,旧客户端在取得房间 authority 前 fail closed。SFU subscriber egress、temporary overlap 和 direct-to-SFU fallback sequence 由 ADR-0005 的整体模型统一定义。 本阶段必须形成完整的最小纵向链路: @@ -30,15 +30,15 @@ | 房间状态 | 进程内 `RoomStore` | 重启本已中断媒体;新增房间号和邀请的有界回退不值得持续承担数据库、迁移和恢复分支 | | 运行时校验 | Zod | WebSocket 和 HTTP 输入不可信,需要与 TypeScript 类型一致的运行时边界 | | 静态文件 | `sirv` | 生产环境由同一 Node 进程提供前端构建结果,避免引入完整 Web 框架 | -| 地址发现/可选兼容传输 | coturn 4.17.2 或更新补丁版本 | 普通 peer 只下发 STUN;TURN 仅可服务整体路由模型明确授权的短期 edge,并受 UDP relay range、credential、quota 和真实媒体门禁约束 | +| 地址发现 | coturn 4.17.2 或更新补丁版本 | 只提供 STUN/UDP;tracked 配置使用 `stun-only`、`no-tcp` 与 `no-tls`,不分配媒体 relay | | 测试 | Vitest | 覆盖纯逻辑和 Node 集成测试;真实媒体能力另做浏览器/网络矩阵 | | TLS 入口 | Caddy 或 nginx,由部署者选择 | 统一 HTTPS/WSS;不把证书管理写进应用进程 | -不选 Go 的原因是 Node 不承载正常情况下的媒体流量,换语言不会减少分享者上行或 TURN 中继成本。不选 C++ 的原因是浏览器能力尚未被实测证明不足;只有捕获、应用音频或硬件编码出现明确瓶颈时才增加原生 helper。 +不选 Go 的原因是 Node 不承载正常情况下的媒体流量,换语言不会减少分享者上行或 SFU 成本。不选 C++ 的原因是浏览器能力尚未被实测证明不足;只有捕获、应用音频或硬件编码出现明确瓶颈时才增加原生 helper。 -最终分发有两个独立产物。公网服务器一键部署包面向用户自有服务器,编排同一版本的 Web/信令应用、易失房间状态、STUN/SFU/TURN 和反向代理,不建立第二套应用;域名、TLS、secret、端口、容量、健康检查和回滚继续 fail closed。它不提供托管基础设施,也不把占位配置写成可用服务。 +最终分发有两个独立产物。公网服务器一键部署包面向用户自有服务器,编排同一版本的 Web/信令应用、易失房间状态、STUN/SFU 和反向代理,不建立第二套应用;域名、TLS、secret、端口、容量、健康检查和回滚继续 fail closed。它不提供托管基础设施,也不把占位配置写成可用服务。 -本地自包含一键包在分享者电脑上同时承载 Host、当前 Web/信令应用服务器与本地状态,Viewer 仍使用普通浏览器。它不调用任何外部 Screener、STUN、SFU、TURN、隧道或 relay 服务,并复用唯一协议、房间、鉴权和路由实现;公网覆盖取决于本机入口、可信 origin、网关映射和 NAT 条件,direct P2P 可用时保留,受限时明确失败。具体 runtime、进程边界、签名、更新、证书、网关与 NAT 方案等当前功能和真实网络 TODO 完成后再由证据确定,不得先用某个打包器或自动映射技巧反向定义产品。 +本地自包含一键包在分享者电脑上同时承载 Host、当前 Web/信令应用服务器与本地状态,Viewer 仍使用普通浏览器。它不调用任何外部 Screener、STUN、SFU、隧道或 relay 服务,并复用唯一协议、房间、鉴权和路由实现;公网覆盖取决于本机入口、可信 origin、网关映射和 NAT 条件,direct P2P 可用时保留,受限时明确失败。具体 runtime、进程边界、签名、更新、证书、网关与 NAT 方案等当前功能和真实网络 TODO 完成后再由证据确定,不得先用某个打包器或自动映射技巧反向定义产品。 ## 系统结构 @@ -48,13 +48,13 @@ flowchart LR H <-->|WSS: 鉴权与信令| A V[观看者 Web 1..N] <-->|WSS: 鉴权与信令| A H <-->|ordinary direct/peer DTLS-SRTP UDP| V - H -.经 controller 授权.-> S[有界 SFU/TURN 资源] + H -.经 controller 授权.-> S[有界 SFU 资源] V -.经 controller 授权.-> S H -.地址发现.-> T[coturn STUN-only] V -.地址发现.-> T ``` -每个观看者的候选检查和最终路径互不影响;健康 direct/peer 子树不会因另一条弱路径被全房迁移。Node 房间服务不复制、解码或转发媒体。SFU/TURN 只按 ADR-0005 的明确 owner、edge 与 deployment budget 参与,不能由图中的连线另行推导 fallback 顺序或 server fanout。 +每个观看者的候选检查和最终路径互不影响;健康 direct/peer 子树不会因另一条弱路径被全房迁移。Node 房间服务不复制、解码或转发媒体。SFU 只按 ADR-0005 的明确 owner、edge 与 deployment budget 参与,不能由图中的连线另行推导 fallback 顺序或 server fanout。 ## 代码布局 @@ -70,7 +70,7 @@ tests/ RoomStore、ICE 配置、协议和 WebSocket 集成测试 ## 房间与访问控制 -`SITE_ACCESS_PASSWORD` 是简单的部署访问口令,不是 Viewer 密码或账号凭据。production 启动时必须配置;本地开发/测试可显式关闭。非空值必须包含 8 至 128 个可见 ASCII bytes(`0x21` 至 `0x7e`),并作为不复用 TURN、LiveKit、TLS 或其他凭据的独立 secret。`POST /api/site-access` 只在严格允许的 Origin 下接收 `Authorization: Bearer`,成功后签发 12 小时的无状态 site-access cookie:值只包含版本、到期时间和 HMAC-SHA256 签名,不创建账号、JWT 或服务端 session Map。cookie 使用 `HttpOnly`、`SameSite=Strict`、`Path=/`、有限 `Max-Age`;HTTPS 生产环境使用 `Secure` 和 `__Host-` 前缀。`POST /api/rooms` 只认该 cookie,不接受同一 Bearer 旁路。原子 deployment change 在 nginx 为该 admission endpoint 配置 per-source `limit_req`,初始边界为 `5r/m`、`burst=5 nodelay`;超限返回通用 429,允许到达 Node 的错误 secret 使用常量时间比较和统一 401。limiter 只使用 nginx shared memory,不新增 IP 持久化/日志字段;Node 不建立 IP key/LRU/session table,并保留现有全局/未鉴权连接上限。删除旧 `ACCESS_PASSWORD`、`/api/session` 与旧 cookie;旧环境变量出现时 fail-fast。 +`SITE_ACCESS_PASSWORD` 是简单的部署访问口令,不是 Viewer 密码或账号凭据。production 启动时必须配置;本地开发/测试可显式关闭。非空值必须包含 8 至 128 个可见 ASCII bytes(`0x21` 至 `0x7e`),并作为不复用 LiveKit、TLS 或其他凭据的独立 secret。`POST /api/site-access` 只在严格允许的 Origin 下接收 `Authorization: Bearer`,成功后签发 12 小时的无状态 site-access cookie:值只包含版本、到期时间和 HMAC-SHA256 签名,不创建账号、JWT 或服务端 session Map。cookie 使用 `HttpOnly`、`SameSite=Strict`、`Path=/`、有限 `Max-Age`;HTTPS 生产环境使用 `Secure` 和 `__Host-` 前缀。`POST /api/rooms` 只认该 cookie,不接受同一 Bearer 旁路。原子 deployment change 在 nginx 为该 admission endpoint 配置 per-source `limit_req`,初始边界为 `5r/m`、`burst=5 nodelay`;超限返回通用 429,允许到达 Node 的错误 secret 使用常量时间比较和统一 401。limiter 只使用 nginx shared memory,不新增 IP 持久化/日志字段;Node 不建立 IP key/LRU/session table,并保留现有全局/未鉴权连接上限。删除旧 `ACCESS_PASSWORD`、`/api/session` 与旧 cookie;旧环境变量出现时 fail-fast。 WebSocket upgrade 无法预知后续 role,因此仍允许没有 site-access cookie 的合法 grant Viewer 完成握手;握手前严格校验 `/signal` 无 query、Origin allowlist、总连接数和未鉴权连接数,并把当次 cookie 校验结果保存到 socket state。五秒内的第一条 `authenticate` 完成授权:Host 必须同时拥有 upgrade 时有效的 site-access 状态和当前房间 `hostToken`;Viewer 必须拥有目标房间合法未过期 grant,或者先通过 site access 再按 `open | password | disabled` code-only policy 校验。site-access cookie 只允许进行 code-only 尝试,不能替代 grant/房间密码;Viewer grant 也不能用于 HTTP 建房、Host role、其他房间或其他操作。 @@ -90,11 +90,11 @@ rotate/revoke 是 grant 路径的强撤销:服务端先同步提交新 digest/ 房间规则: -- 一名在线分享者、最多 `20` 名 authenticated Viewer;房间 admission 与 endpoint/SFU/TURN capacity 分离。Production `9461e20` 默认八名且只接受 1 至 16;当前 source 已接受 1 至 20,生产值在 20-Viewer smoke 通过后随 route release 提升; +- 一名在线分享者、最多 `20` 名 authenticated Viewer;房间 admission 与 endpoint/SFU capacity 分离; - 分享者明确停止或捕获轨结束时停止当前发布、通知观看者进入等待并开始 dormant lease,不立即删除房间;host 信令短断先交给既有恢复,在租期内仍可用同一 Host token 续租; - 活动分享不因租期到达而删除;dormant 房间过期后拒绝新连接、通知现有参与者、清理 route/resource 并释放号码; - 设置不超过四位号码池的全局房间上限;进程重启清空全部房间,不恢复任何 ownership、grant 或密码状态; -- 单个服务器内存控制面保存当前 room/participant/authorization/signaling 状态,但不记录 SDP、ICE candidate、IP 地址、媒体或 TURN 密钥;媒体面继续按 P2P-first 图分布,不把媒体压力移到应用服务器。 +- 单个服务器内存控制面保存当前 room/participant/authorization/signaling 状态,但不记录 SDP、ICE candidate、IP 地址或媒体;媒体面继续按 P2P-first 图分布,不把媒体压力移到应用服务器。 客户端用 `sessionStorage` 保存随机的 128-bit `clientId`,作为当前标签页内稳定的参与者 key。同一 role/clientId 的新 WebSocket 会原子替换旧 socket;观看者断线后保留五秒 grace,期间不发送 `peer-left` 且 `peerId` 不变,重新鉴权时重发幂等的 `peer-joined`,超时才释放名额。host 鉴权或重连后,`authenticated` 携带包含 grace 成员的权威 viewer ID 列表供客户端删除已离开的 peer,服务端再重放当前在线观看者的 `peer-joined`,因此观看者可以先于 host 打开邀请,host 离线期间的离开也不会遗留幽灵连接。 @@ -102,7 +102,7 @@ rotate/revoke 是 grant 路径的强撤销:服务端先同步提交新 digest/ Web Host 在 `authenticate` 中显式发送 `viewerPresence: true` capability,Viewer 也可显式订阅同一 participant snapshot;Native sender 不发送,因此继续只收到原有 exact server wire。两端可在 `authenticate.displayName` 发送名称,并以 `set-display-name` 更新当前会话。服务端只向已订阅的 Web participant 发送完整 `viewer-presence` snapshot,列表严格来自当前带 sessionId 的在线参与者,不含五秒媒体恢复 grace 成员;条目用 `role` discriminator 区分 Host/Viewer。加入、离线、会话替换、改名和 active route 改变都会重发快照;该只读广播不调用 connect/reconcile,也不建立任何媒体边。Host 的“在线”Viewer 总数只取 Viewer 条目数;最多两条本机媒体快照只补充当前以 Host 为 active upstream 的连接指标。 -每个 presence 条目包含 `role`、`peerId`、`displayName` 和服务端 active assignment 的精确 `upstream`:Host 为 `none`,Viewer 为 `none | peer(peerId) | sfu`。UI 默认只展示名称;房内出现同名时,仅为同名项追加 peerId 尾部至少六字符短后缀,并只在同名项后缀碰撞时逐字符有界延长至完整 peerId。卡片状态只依据本机连接或当前 Viewer 接收证据显示“已连接”,否则保持中性的“线路分配中”或具体连接状态;active assignment 本身不是连接成功证据。连接详情同样等待当前媒体证据,再显示绿色 `P2P` 或黄色 `SFU fallback`;实际 relay 另附 `TURN`,本地 relay protocol 可见时才显示 `TURN/UDP` 等实际值。Host 观看者卡片采用两级指标披露:一级固定保留 route/transport、协议、帧率、分辨率、码率、丢包、RTT 和发送质量限制,codec/fmtp、编解码深指标、sender 参数与音频细节由卡片内的二级按钮按需展开;Viewer 自身的独立详情页保持完整指标。Host 与 Viewer 都可通过独立拓扑按钮从 authoritative assignment snapshot 展示 Host、实际存在的 SFU 节点及精确上下游;Viewer 默认收起并放在视频下方,没有当前 Host presence 时不生成占位拓扑。拓扑不把计划线路当作连接成功,也不显示 IP 或唯一名称对应的 peerId。peerId 不因此成为授权身份或路由输入;名称、presence 与拓扑视图不改变授权、媒体或路由状态机。 +每个 presence 条目包含 `role`、`peerId`、`displayName` 和服务端 active assignment 的精确 `upstream`:Host 为 `none`,Viewer 为 `none | peer(peerId) | sfu`。UI 默认只展示名称;房内出现同名时,仅为同名项追加 peerId 尾部至少六字符短后缀,并只在同名项后缀碰撞时逐字符有界延长至完整 peerId。卡片状态只依据本机连接或当前 Viewer 接收证据显示“已连接”,否则保持中性的“线路分配中”或具体连接状态;active assignment 本身不是连接成功证据。连接详情同样等待当前媒体证据,再显示绿色 `P2P` 或黄色 `SFU fallback`。Host 观看者卡片采用两级指标披露:一级固定保留 route/transport、协议、帧率、分辨率、码率、丢包、RTT 和发送质量限制,codec/fmtp、编解码深指标、sender 参数与音频细节由卡片内的二级按钮按需展开;Viewer 自身的独立详情页保持完整指标。Host 与 Viewer 都可通过独立拓扑按钮从 authoritative assignment snapshot 展示 Host、实际存在的 SFU 节点及精确上下游;Viewer 默认收起并放在视频下方,没有当前 Host presence 时不生成占位拓扑。拓扑不把计划线路当作连接成功,也不显示 IP 或唯一名称对应的 peerId。peerId 不因此成为授权身份或路由输入;名称、presence 与拓扑视图不改变授权、媒体或路由状态机。 ## 信令协议 @@ -169,13 +169,13 @@ Web display-audio 请求显式关闭 `echoCancellation`、`noiseSuppression`、` 直播中切换质量不重新打开选源器:对当前视频 track 调用 `applyConstraints()`,再更新所有现有 sender 的参数;新加入、重建的 relay sender 和启用的 SFU publisher 使用同一权威设置。失败时保留当前分享并给出状态,不以重建整个房间补偿。临时暂停禁用当前 capture stream 的全部音视频 tracks;Host 通过当前 share generation 更新现有 `host-status`,`authenticated` 快照携带当前暂停位,Viewer 在舞台显示“分享者已暂停”。晚加入和信令重连从快照恢复,恢复、停止、新分享或会话替换清除旧状态;不得把 track mute、黑帧或网络停顿推断成主动暂停。房间与连接保持不变,恢复时重新启用同一批 tracks。 -Host 本地预览直接绑定现有 capture stream,不创建 viewer、`RTCPeerConnection` 或额外媒体边,也不产生 TURN/SFU/应用服务器出口。预览仍可能有本地合成、缩放和 GPU 显示成本;在同一动态场景对预览开/关采集 Host CPU、GPU、encode time 和实际 FPS。只有稳定收益且捕获、编码和远端画面不变时,才把直播后的默认动态预览改为静止缩略图或隐藏并允许按需展开;隐藏预览绝不停止 capture track 或 sender encode。 +Host 本地预览直接绑定现有 capture stream,不创建 viewer、`RTCPeerConnection` 或额外媒体边,也不产生 SFU/应用服务器出口。预览仍可能有本地合成、缩放和 GPU 显示成本;在同一动态场景对预览开/关采集 Host CPU、GPU、encode time 和实际 FPS。只有稳定收益且捕获、编码和远端画面不变时,才把直播后的默认动态预览改为静止缩略图或隐藏并允许按需展开;隐藏预览绝不停止 capture track 或 sender encode。 部署版 `769de201f7cc` 的模糊、游戏低帧、持续 `bandwidth` 提示和 Host 刷新恢复都只作为同一 release 的用户报告。首轮复现必须在相同机器、Chrome/Edge、驱动、游戏场景和单个有线 direct Viewer 上对照该 release 与最新 `main`。生命周期实验固定 `automatic`,每个独立 affected baseline 只运行 observe、受影响 `HostPeer` rebuild、capture replace、Host reload 或 preview cycle 中一个动作;codec A/B 另以全新 Host/share/PeerConnection 会话分别请求 automatic/H.264/VP8 并校验 actual codec。质量判断只使用同一 generation 且时间重叠的 capture A、sender B、Viewer C 窗口;stats identity poll 不推进采样 accumulator,可选 encoder 字段缺失保持 unknown。若实际 codec/encoder 不变而 BWE/发送恢复,则下调 codec 根因优先级;只有编码成本异常时才进入受控 codec repair。 -`track.getSettings()` 中存在的 EC/NS/AGC/voice-isolation、`channelCount` 和 `sampleRate` 只能作为当前捕获观测;缺失字段保持未知,Opus 协商仍是独立事实。每个 Web Host、浏览器 relay 或 selected-edge 的实际音频 sender 在首次绑定和换源后通过标准 `RTCRtpSender.setParameters()` 请求并读回当前 64/128/256 kbps ceiling;这只是上限,浏览器 WebRTC 拥塞控制仍可降低实际流量,不新增应用层音频降级器。128 kbps 保持默认,因为 RFC 7587 将 20 ms full-band stereo music 的建议区间列为 64--128 kbps,pinned LiveKit 2.22.0 也把 128 kbps 命名为 `musicHighQualityStereo`;256 kbps 是用户显式选择的较高上限,不是质量保证。Peer answer 声明 `stereo=1;maxaveragebitrate=256000`,让同一接收协商覆盖所有三档,实际 sender ceiling 仍由所选档位约束;普通 PC synthetic 不用于标定参数、听感或性能。 +`track.getSettings()` 中存在的 EC/NS/AGC/voice-isolation、`channelCount` 和 `sampleRate` 只能作为当前捕获观测;缺失字段保持未知,Opus 协商仍是独立事实。每个 Web Host 或浏览器 relay 的实际音频 sender 在首次绑定和换源后通过标准 `RTCRtpSender.setParameters()` 请求并读回当前 64/128/256 kbps ceiling;这只是上限,浏览器 WebRTC 拥塞控制仍可降低实际流量,不新增应用层音频降级器。128 kbps 保持默认,因为 RFC 7587 将 20 ms full-band stereo music 的建议区间列为 64--128 kbps,pinned LiveKit 2.22.0 也把 128 kbps 命名为 `musicHighQualityStereo`;256 kbps 是用户显式选择的较高上限,不是质量保证。Peer answer 声明 `stereo=1;maxaveragebitrate=256000`,让同一接收协商覆盖所有三档,实际 sender ceiling 仍由所选档位约束;普通 PC synthetic 不用于标定参数、听感或性能。 -Web peer 的最小 stereo 契约只改 Viewer 生成的 answer。实现把 MIT `sdp-transform` 2.15.0 与 `@types/sdp-transform` 声明为 root direct dependencies,不依赖 LiveKit 的 transitive install,也不用字符串或正则拼接 SDP。Helper 仅在解析出唯一非 rejected audio media、通过结构化 `rtp` entry 找到唯一 Opus payload 时,才在该 payload 的 fmtp 幂等 upsert `stereo=1` 与 `maxaveragebitrate=256000`;随后 `ViewerPeer` 把同一个结果交给 `setLocalDescription()` 和 signaling。首次连接、ICE restart、同 parent rebuild、浏览器 relay child 与 selected-edge TURN 都走该边界;TURN 只改变 ICE transport。空 SDP、无或歧义 Opus、多个有效 audio、重复目标参数、畸形 fmtp、parse/write 异常都按对象 identity 返回浏览器原 answer,保住仍可继续的 mono/video edge,不发送半修改 SDP。RFC 7587 说明这两个值都是可选的单向 receiver parameters;`maxaveragebitrate` 是 maximum 而非最低保证,实际发送仍可随拥塞下降。当前 libwebrtc 把缺省 full-band stereo codec bitrate 配成 64 kbps,而 `RTCRtpSender.maxBitrate` 只限制上界;本接收上限允许三档,实际发送预算仍由 64/128/256 kbps sender ceiling 与拥塞共同决定。`sprop-stereo` 只是 sender likelihood hint,`usedtx` 缺省已是 0,本 slice 不增加二者,也不改浏览器协商的 `useinbandfec`。 +Web peer 的最小 stereo 契约只改 Viewer 生成的 answer。实现把 MIT `sdp-transform` 2.15.0 与 `@types/sdp-transform` 声明为 root direct dependencies,不依赖 LiveKit 的 transitive install,也不用字符串或正则拼接 SDP。Helper 仅在解析出唯一非 rejected audio media、通过结构化 `rtp` entry 找到唯一 Opus payload 时,才在该 payload 的 fmtp 幂等 upsert `stereo=1` 与 `maxaveragebitrate=256000`;随后 `ViewerPeer` 把同一个结果交给 `setLocalDescription()` 和 signaling。首次连接、ICE restart、同 parent rebuild与浏览器 relay child 都走该边界。空 SDP、无或歧义 Opus、多个有效 audio、重复目标参数、畸形 fmtp、parse/write 异常都按对象 identity 返回浏览器原 answer,保住仍可继续的 mono/video edge,不发送半修改 SDP。RFC 7587 说明这两个值都是可选的单向 receiver parameters;`maxaveragebitrate` 是 maximum 而非最低保证,实际发送仍可随拥塞下降。当前 libwebrtc 把缺省 full-band stereo codec bitrate 配成 64 kbps,而 `RTCRtpSender.maxBitrate` 只限制上界;本接收上限允许三档,实际发送预算仍由 64/128/256 kbps sender ceiling 与拥塞共同决定。`sprop-stereo` 只是 sender likelihood hint,`usedtx` 缺省已是 0,本 slice 不增加二者,也不改浏览器协商的 `useinbandfec`。 SFU publisher 将 64 kbps 映射到 `AudioPresets.musicStereo`、128 kbps 映射到 `musicHighQualityStereo`,256 kbps 使用显式 `maxBitrate`;三者都保留 `forceStereo: true` 与 `dtx: false`。LiveKit 2.22.0 先合并 room publish defaults,其中 `red: true`;本实现不传 `red` 覆盖值,保留既有 RED request。RED 与 Opus in-band FEC 是不同修复机制,保留前者不声称实际启用、替代或等价于后者。直播中只改变档位时,不重新协商 Opus 或 republish;服务端沿现有 quality wire 保存 last-wins desired profile,各客户端 sender-mutation queue 从 fresh parameters 更新当前 P2P、relay 和 SFU audio sender并立即 readback,过期 completion 不覆盖新值,未来 sender 读取最新 desired profile。应用失败保留该 sender 的旧 applied 值和媒体并显示告警;无远端 applied 回执时 Host 不宣称全房原子收敛。Pinned LiveKit 2.22.0 的当前 Chrome/Edge screen-audio baseline 可直接更新现存 audio sender;其他浏览器以实际 readback 为准。Viewer 继续在同一个持久 `MediaStream`/媒体元素中增删 screen video/audio track;不新建 audio element、Web Audio mix/resample graph、第二 audio representation 或独立音频时钟。默认每条 outbound edge 的 audio RTP payload ceiling 为 128 kbps,显式最高档为 256 kbps;非服务器端点容量为 C 时最多 C × 128/C × 256 kbps(不含 RTP/SRTP/UDP/IP overhead)。stereo 仍可能增加编码与网络成本,且浏览器 relay 不保证跨 PeerConnection shared encode。未来 microphone voice 若进入范围,另建独立 source/track 与 AEC、降噪、gain、DTX 设计,不能作用于电影/游戏屏幕音频。详细证据与 Oopz 未知边界见 `docs/research/browser-screen-audio-quality.md`。 @@ -192,10 +192,10 @@ Viewer 本地连接详情只从当前 route 的唯一 inbound screen video/audio | 阶段 | 必要证据 | | --- | --- | | A. Host capture | `track.getSettings()` 的实际宽、高、FPS | -| B. Host outbound | 实际宽高/FPS/码率、可用或目标码率、区间 encode time、`qualityLimitationReason`、RTT、丢包、重传、direct/TURN 路径,以及派生后的实际协商视频 codec/profile/parameters 和适用时的 `scalabilityMode` | +| B. Host outbound | 实际宽高/FPS/码率、可用或目标码率、区间 encode time、`qualityLimitationReason`、RTT、丢包、重传、direct/SFU 路径,以及派生后的实际协商视频 codec/profile/parameters 和适用时的 `scalabilityMode` | | C. Viewer inbound | 实际宽高/FPS/码率、丢包、jitter、区间 decoded/dropped frame 和 freeze,以及对应派生 codec/profile/parameters、适用时的 `scalabilityMode` 和实际解码表现 | -判断顺序固定为:Capture 低归为采集或约束;Capture 高而 outbound 低且 reason=`cpu` 归为编码资源;Capture 高而 outbound 低且 reason=`bandwidth` 归为 GCC、上行或 TURN/路径;outbound 正常而 inbound 低归为传输或接收;inbound 指标正常但肉眼模糊归为码率/量化、协商 codec/profile/parameters、适用的 scalable layer 或显示缩放。B/C 必须关联同一派生协商结果与实际解码表现。累计统计只计算相邻、不重叠区间的 delta;缺字段、stats ID/媒体代次变化或计数器回退时重新建立基线,不能把未知当零。 +判断顺序固定为:Capture 低归为采集或约束;Capture 高而 outbound 低且 reason=`cpu` 归为编码资源;Capture 高而 outbound 低且 reason=`bandwidth` 归为 GCC、上行或当前路径;outbound 正常而 inbound 低归为传输或接收;inbound 指标正常但肉眼模糊归为码率/量化、协商 codec/profile/parameters、适用的 scalable layer 或显示缩放。B/C 必须关联同一派生协商结果与实际解码表现。累计统计只计算相邻、不重叠区间的 delta;缺字段、stats ID/媒体代次变化或计数器回退时重新建立基线,不能把未知当零。 探针按最小顺序落地:先在 host 同一个采样 tick 内关联 A+B,并携带明确 window、媒体/stats identity 和有效 delta;需要 Viewer 诊断时只增加受限、已鉴权 C 上报。不建设通用遥测 schema,只传派生后的协商字段,绝不上传 raw stats、raw SDP、candidate 地址或原始设备/网络标识;这些字段不产生路由 authority。 @@ -225,20 +225,19 @@ Viewer 向电视投屏不进入媒体拓扑;系统级屏幕镜像现在由平 `MediaInfo` 最终需要 `contentUrl` 或充当 URL 的 `contentId`,当前 P2P `MediaStream` 没有 该资源,因此不接 Cast sender/receiver SDK、不建转码或自有投屏协议。无论输出到本机还是电视,Viewer -都保留同一 upstream PeerConnection,TURN/SFU/peer route 与 Host fanout 完全不变。 +都保留同一 upstream PeerConnection,SFU/peer route 与 Host fanout 完全不变。 ## 媒体传输配置 Production `ServerConfig` 必须配置 `STUN_URLS`。所有 ordinary direct/peer -`RTCPeerConnection` 的 authenticated snapshot 只包含 STUN;不得把 TURN -candidate 预发给 participant、房间或普通 peer edge。HTTPS/WSS 始终使用 TLS/TCP, -不属于媒体 transport 策略。 +`RTCPeerConnection` 的 authenticated snapshot 只包含 STUN。HTTPS/WSS 始终使用 +TLS/TCP,不属于媒体 transport 策略。 `PEER_ASSISTED_MEDIA` 启用同一个 process-wide route controller,但每个房间拥有 独立的 assignment、session、revision、generation、connection 和 capacity 状态。 所有非服务器端点读取同一个 deployment steady outbound media-copy capacity,默认 `2`,只接受 `1`、`2` 或 `3`;peer child 或 Host publication 各消耗一份,upstream -receive 免费,committed selected TURN 不重复计数。服务端是权威,客户端广告只能 +receive 免费。服务端是权威,客户端广告只能 收紧,不能按 Host/Viewer、浏览器、UA、设备或页面状态放宽或分叉。 每个 candidate 和异步结果都绑定 authenticated room/endpoint sessions、share @@ -248,24 +247,21 @@ candidate child 解码出第一帧新画面时才原子 commit。ICE `connected` 签发或 assignment 本身都不是 media-ready。失败、超时、stale identity 或授权 撤销销毁 candidate,保留或恢复最后一个有效 route。 -SFU 与 TURN 是可选、有界的 server-assisted resources。一个 room controller +SFU 是可选、有界的 server-assisted resource。一个 room controller 通过同一个 reconcile 处理 allocate/distribute、child reparent 和 relay drain。 Host 拥有唯一 authoritative publication,所有 SFU-fed Viewer 订阅它;SFU-fed 与 peer-fed endpoint 的新 child 都使用同一 provisional first-frame transaction。 -TURN 只替换授权 logical edge 或 Host-SFU ingress 的 transport,不是拓扑节点。 -现有 edge 优先保持 direct/STUN,必要时使用 exact selected TURN,再按需要切换到 -SFU;Host 无 direct first-level path 时优先一条可经 TURN 的 Host-SFU -publication。端点容量、SFU/TURN ingress/egress、临时 overlap、server admission +现有 edge 优先保持 direct/STUN,唯一 suffix 是复用或建立 direct-ingress Host +publication 后的 SFU subscription。端点容量、SFU ingress/egress、临时 overlap、server admission 和 bounded failure 在同一 reserve/first-frame/commit-or-abort 事务中记账。 -短期 server-assisted credential 必须严格绑定当前 route identity、只存内存、受 TTL -与 quota 限界,并且不进入 URL、日志、browser storage 或任何持久存储。Coturn -只能验证 bearer 的 HMAC 与 expiry,应用仍需逐次重验 room/edge/generation。普通 -SFU 终止 DTLS-SRTP;没有应用层 E2EE 时 operator 可接触媒体,产品必须如实说明。 +短期 SFU credential 必须严格绑定当前 route identity、只存内存、受 TTL 与 quota +限界,并且不进入 URL、日志、browser storage 或任何持久存储。普通 SFU 终止 +DTLS-SRTP;没有应用层 E2EE 时 operator 可接触媒体,产品必须如实说明。 所有物理 hop 按[低服务器成本媒体路由](./research/low-server-media-routes.md)逐项计量。 -Production `9461e20` 的 exact behavior 只由 deployment 记录;新实现仅复用满足当前 -identity、media-proof 和 bounded-recovery 不变量的 scoped code 与 tests。 +精确 production behavior 只由 deployment 记录;新实现仅复用满足当前 identity、 +media-proof 和 bounded-recovery 不变量的 scoped code 与 tests。 ## 可观测性 @@ -273,14 +269,13 @@ identity、media-proof 和 bounded-recovery 不变量的 scoped code 与 tests Host 与 relay Viewer 复用同一个纯 presentation reducer 保存远端 `ViewerQualityEvidence`。代际 identity 是 exact route identity(room/share、viewer/parent、assignment/connection、route revision 和当前可用的 media-binding generation,及适用时 publication generation);只有同 identity 且 sequence 严格递增才可承接旧字段,其他输入从新 evidence 重建。每个非空 metric 保存自己的本地观测时间并在五秒边界清空,页面 timer 总是指向最早边界;整块详情不因某个采样缺字段而卸载。`new`/`connecting`/`disconnected` 保留同 identity shell 但立即标为非 fresh,`failed`/`closed` 或 route identity 变化清空。任何依赖当前状态的消费者只读取 fresh presentation;该层不新增 wire 字段或服务端 route authority。 -质量诊断必须能按顺序区分:捕获源设置、sender 实际参数、CPU 或带宽 `qualityLimitationReason`、selected candidate/TURN 路径、outbound 与 inbound 差异,以及接收端 dropped/freeze。当前有界实现显示 sender 参数的 requested/applied 读回;同一个非 `none` 原生 `qualityLimitationReason` 连续出现三个采样周期后显示单条原因提示,原因变化、缺失或恢复为 `none` 即重新计数。这只是去抖后的观察,不组合评分、不触发调档或迁移。完整跨路径分类仍由后续真实矩阵验收;首轮使用浏览器 WebRTC internals 导出和一份脱敏 run manifest,不新增服务端遥测。 +质量诊断必须能按顺序区分:捕获源设置、sender 实际参数、CPU 或带宽 `qualityLimitationReason`、当前 peer/SFU 路径、outbound 与 inbound 差异,以及接收端 dropped/freeze。当前有界实现显示 sender 参数的 requested/applied 读回;同一个非 `none` 原生 `qualityLimitationReason` 连续出现三个采样周期后显示单条原因提示,原因变化、缺失或恢复为 `none` 即重新计数。这只是去抖后的观察,不组合评分、不触发调档或迁移。完整跨路径分类仍由后续真实矩阵验收;首轮使用浏览器 WebRTC internals 导出和一份脱敏 run manifest,不新增服务端遥测。 分享者逐观看者显示: - `connectionState` / `iceConnectionState`; -- 选中候选对的 local/remote candidate type 与 ICE `protocol`,任一端为 `relay` 即标记中继; -- 本地 relay candidate 的 `relayProtocol`;规范禁止远端 candidate 暴露该字段,因此不推断远端使用 TURN/UDP、TCP 还是 TLS; -- SFU/UDP 与可选兼容 transport 只按[低服务器成本媒体路由](./research/low-server-media-routes.md)的 generation-baselined、privacy-safe ICE manifest 记录;先以本地脱敏 `webrtc-internals` 建立 ground truth,不新增后台遥测; +- 选中候选对的 local/remote candidate type 与 ICE `protocol`; +- SFU/UDP 只按[低服务器成本媒体路由](./research/low-server-media-routes.md)的 generation-baselined、privacy-safe ICE manifest 记录;先以本地脱敏 `webrtc-internals` 建立 ground truth,不新增后台遥测; - `currentRoundTripTime`、发送码率和 `availableOutgoingBitrate`; - 实际帧率、分辨率和相邻有效采样窗的 RTP 丢包率; - codec、编码实现、最近采样区间内的每帧编解码耗时和 `qualityLimitationReason`;同一连接不并发采样,首个样本、无新帧、统计对象换代或计数器重置时显示未知并重新建立基线,不用连接生命周期均值掩盖后续过载。 @@ -316,8 +311,8 @@ Token-free SFU standby prewarm 保持可选且不创建 participant、publicatio - Fragment 只避免其进入 HTTP/WebSocket request-target,不能把 capability 变成非敏感数据。客户端首次读取后立即清理地址栏,只在 room-scoped `sessionStorage` 留存;服务端和代理日志不得记录 WebSocket payload、Authorization、cookie、raw grant 或响应 body,异常只能记录安全路径、状态码和分类。 - HTTP body、WebSocket payload、总连接数、未鉴权连接数、输出缓冲、房间总数、观看人数和 candidate 缓存都有硬上限;无法解析的 upgrade request-target 在未鉴权边界返回 HTTP 400,不允许异常逃逸到进程顶层。 - 应用日志不得主动记录 IP;反向代理、STUN 和 LiveKit 为提供网络服务必然可见源地址,部署时需设置最短必要留存与访问控制。 -- tracked `stun-only` 与 `no-tcp`/`no-tls` 是当前 UDP-only 媒体边界。Ordinary peer 只接收 STUN;selected edge 使用独立 realm/secret、小 relay UDP range、allocation/bandwidth quotas 和单条短期 credential,不启用匿名 TURN。coturn 4.17.2 已弃用 `no-dtls`,tracked 配置不使用它。 -- 分享者直连会向可信观看者暴露网络地址。普通 peer PC 不配置 TURN;只有 controller 选择的异常 edge 可在重建时 relay-only。UI 和后台不得显示 bearer 或把 relay 宣称为匿名或 coturn 侧密码学 edge 隔离。 +- tracked coturn 的 `stun-only`、`no-tcp` 与 `no-tls` 加上 LiveKit 的 UDP-only 配置共同定义当前媒体边界。Coturn 不分配 relay,Screener 不配置 TURN、ICE/TCP、media TCP 或 TLS-relayed media。 +- 分享者直连会向可信观看者暴露网络地址。受限网络只尝试当前 direct 与 SFU/UDP 路径并在有界窗口后明确失败。 ## 验证计划 @@ -326,11 +321,11 @@ Token-free SFU standby prewarm 保持可选且不创建 participant、publicatio - SiteAccess 的 production 必配、8 至 128 visible-ASCII bytes/独立 secret、local-dev 显式关闭、12 小时签名 cookie、过期、篡改、cookie 属性、已移除 access key fail-fast 和无服务端 session 状态; - RoomStore 的四位空闲码随机分配/释放、活动分享免过期、dormant lease/续租、Host token/Viewer grant 摘要验证、`open/password/disabled` 正交 policy、grant 到期/跨房拒绝、rotate/revoke 强撤销、Viewer 上限和超额拒绝; - 进程重启后 room state 全空、旧 Host/grant/password fail closed、释放号码复用时旧 grant 仍失败;`ROOM_LEASE_SECONDS` 的默认/边界与 `MAX_ROOMS <= 9000` 校验,已删除的 `ROOM_DATABASE_PATH`/`ROOM_TTL_SECONDS` fail-fast; -- STUN/TURN UDP URL 严格校验、生产 STUN 必需、server-assisted tuple 完整性/TTL/secret 独立性、issuer HMAC/expiry/opaque tag、当前 route/session/revision/connection generation guard,以及 ordinary peer、未授权房间和 Native 的 STUN-only isolation;LiveKit `sfu-config` 保持独立边界; +- STUN UDP URL 严格校验、生产 STUN 必需、LiveKit tuple 完整性/TTL/secret 独立性、当前 route/session/revision/connection generation guard,以及 ordinary peer、未授权房间和 Native 的 STUN-only isolation;LiveKit `sfu-config` 保持独立边界; - 单一 `screener-v8` 信令 literal、任意旧客户端 mismatch 的一次性刷新终态,以及协议非法 JSON、未知消息、错误方向、跨房间目标和 payload 限制; - 质量协议的 host 授权、严格字段与数值范围、last-wins、当前/未来 relay、已配置 SFU 和普通 P2P wire 兼容;sender 的四字段 requested/applied 读回、拒绝与持续受限提示;screen audio SDP helper 覆盖 CRLF、existing/missing fmtp、unknown key preservation、幂等、payload number、rejected/non-audio,以及 empty/no-Opus/ambiguous/parser 异常原对象 fallback,`ViewerPeer` 覆盖 initial/restart/rebuild 时同一结果的 local/signaling 一致性,SFU publisher 精确断言 high-quality stereo/forceStereo/DTX 且不覆盖 RED; - HTTP/WS 集成:nginx per-source site-access `limit_req` 与统一错误不泄露 secret,应用不建立 IP limiter 状态;Viewer 可在 Origin/容量门下 upgrade;Host 首条鉴权同时要求 upgrade-captured site access 与 Host token,Viewer 要么持 exact-room grant,要么先有 site access 再按 `open/password/disabled`。覆盖原子创建 profile、同浏览器重建偏好、fragment/sessionStorage/replaceState、无 site access 的 code-only 统一拒绝、通过 site access 后的 open/password/disabled 行为、raw grant/密码不进入 URL/日志/服务端持久状态,以及 rotate/revoke 后 grace/route 清理、旧 Viewer/上游媒体边关闭和 reconnect 拒绝; -- 客户端策略与 stats 解析:会话替换不重连,ordinary ICE、LiveKit 与 selected-edge TURN protocol 分离,远端 TURN transport 保持未知; +- 客户端策略与 stats 解析:会话替换不重连,ordinary ICE 与 LiveKit protocol 分离; - route planner property tests:候选输入乱序仍产生相同 list,健康边 sticky,硬过滤拒绝 stale/cycle/effective-capacity/unadmitted parent,优先结果深度和剩余容量并稳定打破平局;失败候选清理后 cursor 前进且总 deadline 不重置; - Viewer stage reducer:peer/SFU prepare、remote track 无首帧、首个 composited frame、autoplay rejection、恢复和 bounded failure 不互相冒充,动态状态不改变布局或 route authority; - TypeScript 类型检查、Vite production build 和仓库 hygiene。 @@ -339,11 +334,11 @@ Token-free SFU standby prewarm 保持可选且不创建 participant、publicatio - 同机双标签页与同 LAN direct; - 真实跨网络、同房间 direct/peer UDP + SFU/UDP root; -- CGNAT、双 NAT、移动热点和普通家宽下按 accepted route model 验证 direct/peer、server-assisted path、selected transport、恢复和 bounded failure;记录 allocation/relay port/RSS/CPU/latency 与 RX/TX/loss。人为封锁全部 UDP 时仍必须有界明确失败。旧 production release 留在独立实例作为回滚,不在同进程保留旧 transport; +- CGNAT、双 NAT、移动热点和普通家宽下按 accepted route model 验证 direct/peer、SFU/UDP、恢复和 bounded failure;记录 RSS/CPU/latency 与 RX/TX/loss。人为封锁全部 UDP 时仍必须有界明确失败。旧 production release 留在独立实例作为回滚,不在同进程保留旧 transport; - guessed-candidate 只先在 owned endpoint-dependent NAT emulator 中运行 ordinary-ICE failure 与 bounded-candidate A/B;没有通过双浏览器重复性、首帧 deadline、privacy 和 cleanup gate 时不得进入任何 production room,通过后也只在一个人工批准的 operator-owned test room 运行一次 canary,并要求双方 endpoint 显式同意、default-off kill switch 以及 exact room/edge/ICE-generation/deadline/aggregate-budget 约束; - Windows Chrome/Edge 的 tab/window/monitor 音轨有/无、同时语音通话和 video-only;一个可区分左右声道的 fixture 只验证 Web P2P 与 SFU 的 channel correctness、协商 fmtp 和 route switch 后持久 `MediaStream`,不做听感/性能标定;Windows 11 原生候选另测 game process tree、通知、无渲染流与进程重启,Windows 10 记录明确不支持/未解决; - Android Chrome 与 iOS Safari 的播放手势、旋转、前后台和网络切换; -- 一名分享者、20 名观看者先运行 bounded smoke,记录路由深度、首帧、sender 数、应用 CPU/内存和 SFU/TURN 用量;端点 `C=1/2/3` 分开解释。30 分钟异构公网 endurance 仍是后续质量证据,不阻止已经有界失败的 20-Viewer admission。 +- 一名分享者、20 名观看者先运行 bounded smoke,记录路由深度、首帧、sender 数、应用 CPU/内存和 SFU 用量;端点 `C=1/2/3` 分开解释。30 分钟异构公网 endurance 仍是后续质量证据,不阻止已经有界失败的 20-Viewer admission。 - 真实桌面 Chrome 对 balanced 与 clarity 做控制传播和连接身份 smoke;该项只证明直播中应用与不重建,不代替清晰度、负载或公网矩阵。 真实矩阵未完成前,状态文档必须把这些列为待验证,而不是把设计目标写成产品能力。 @@ -353,14 +348,14 @@ Token-free SFU standby prewarm 保持可选且不创建 participant、publicatio 1. 建立单 package 工程、共享 schema、配置与 CI。 2. 完成 RoomStore、建房 API、WebSocket 鉴权和定向信令。 3. 完成屏幕捕获、逐观看者 PeerConnection、观看播放和手动质量档。 -4. 完成连接恢复和 stats 面板;随后以 STUN-only snapshot 替换旧全房 TURN credential/refresh wire。 -5. 在已合入的 endpoint cap/accounting、SFU lifecycle 与 TURN allocation admission 边界上,以单一 `screener-v7` wire 完成 route controller convergence:一个 event-driven reconcile 和一个 room-serial candidate 统一处理 deterministic allocation、child reparent 和 relay drain;该 wave 独立可回滚。 +4. 完成连接恢复和 stats 面板,并保持 ordinary peer 的 STUN-only snapshot。 +5. 在 endpoint cap/accounting 与 SFU lifecycle 边界上,以单一 Browser wire 完成 route controller convergence:一个 event-driven reconcile 和一个 room-serial candidate 统一处理 deterministic allocation、child reparent 和 relay drain;该 wave 独立可回滚。 6. 路由边界合入后依次落 live screen-audio ceiling 和 paused video-codec transaction;随后做 H.264 根因诊断与 strict-NAT emulator gate,门禁通过才允许 allowlisted canary。 7. 加入 first-frame-driven Viewer connection stages;随后把房间收敛为四位随机码、易失租约、正交 code/grant access 和本地 Host creation profile,并原子删除 SQLite 双模式。 8. 当前发布只验收 Web Host、Viewer 和 browser relay。Native sender、capture helper 与 shared-encode executable 排在整体 roadmap 最后,不参与当前构建、路由验收或发布。 9. 当前功能、NAT 与真实网络 TODO 完成后,再分别设计和交付公网服务器一键部署包与完全不依赖外部服务的本地 Host-server 包。 -生产已运行 `screener-v8` room/route release `352c457`。内部不兼容 wire/schema 原子切换,Git commit 与部署备份承担回滚,不保留兼容实现。 +生产当前运行 `screener-v8` room/route release `a5b1fc6`。内部不兼容 wire/schema 原子切换,Git commit 与部署备份承担回滚,不保留兼容实现。 任何一步只实现当前消费者需要的接口。不得为未来账号、第三种媒体拓扑或原生 sender 预建抽象层。 @@ -370,27 +365,27 @@ Token-free SFU standby prewarm 保持可选且不创建 participant、publicatio 服务端维护每个房间唯一的版本化 route owner,它独占 committed graph、一个 pending child operation 和 transition lifecycle。运行时只在该 owner 外保存一个串行 pump、一个 operation deadline timer 和资源生命周期任务;拓扑 helper 不持有第二份 graph。operation 内聚 tuple list/cursor、一个 current candidate/预留、当前 fact version 和一个总 deadline。committed media graph 必须无环且 source-reachable;Host 没有 upstream,每名 Viewer 同时只有零或一个 active upstream。加入、容量变化、节点离开、当前 edge `failed/closed`,以及非暂停时的 decoded-frame stall 都只唤醒同一个 `reconcileRoom(roomId)`;未受影响分支保持 sticky。 -`reconcileRoom` 在没有 pending operation 时先删除 disconnected childless participant,再找一个 waiting Viewer,或 parent offline/effective-capacity 不足、exact edge 已失效的 child。Candidate planning 拒绝 stale session、不可 source-reachable、成环或没有稳态/overlap/bounded-gap producer slot 的 tuple;server resource admission 在执行当前 cursor 时单独检查。由 failed edge 启动时只为当前 fact version 把它的 `parent + transport` tuple记为已尝试;新的外部事实允许重新评估。对剩余 upstream 依次比较结果深度、剩余 sender slots、稳定加入顺序与 peer identity,先建立其他 direct/STUN,再建立 selected TURN 与 SFU 的有界顺序;同一 parent 的不同 transport 仍是不同 tuple。每次只 reserve/prepare cursor 指向的 candidate。WebRTC 负责 ICE/DTLS/consent;exact candidate child 解码出第一帧新画面后发送一次 ready,controller 先同步 commit typed admission,再原子提交 graph。失败释放 candidate、广播严格更新的 rollback revision 并推进 cursor,operation 总 deadline 不重置。耗尽时 hard-invalid、confirmed-departed 或 deterministic overflow edge 被退休并进入明确 waiting;healthy bootstrap edge 保留。无环和房间 admission 已约束最大深度,不增加独立 depth hard cap。 +`reconcileRoom` 在没有 pending operation 时先删除 disconnected childless participant,再找一个 waiting Viewer,或 parent offline/effective-capacity 不足、exact edge 已失效的 child。Candidate planning 拒绝 stale session、不可 source-reachable、成环或没有稳态/overlap/bounded-gap producer slot 的 candidate;server resource admission 在执行当前 cursor 时单独检查。由 failed edge 启动时只为当前 fact version 把 exact candidate 记为已尝试;新的外部事实允许重新评估。对剩余 upstream 依次比较结果深度、剩余 sender slots、稳定加入顺序与 peer identity,先建立其他 direct/STUN,再复用或建立 SFU。Host root退出后新释放的Host槽位因此优先于另一root的更深槽位;健康旧边保持sticky,不做周期性整树平衡。每次只 reserve/prepare cursor 指向的 candidate。WebRTC 负责 ICE/DTLS/consent;exact candidate child 解码出第一帧新画面后发送一次 ready,controller 先同步 commit typed admission,再原子提交 graph。失败释放 candidate、广播严格更新的 rollback revision 并推进 cursor,operation 总 deadline 不重置。耗尽时 hard-invalid、confirmed-departed 或 deterministic overflow edge 被退休并进入明确 waiting;healthy bootstrap edge 保留。无环和房间 admission 已约束最大深度,不增加独立 depth hard cap。 -一个 operation 仍只有一个 timer 和一个总 deadline,但 timer 的下一次 wake 从当前 candidate list 派生。Controller 提取实际存在的 direct、selected-TURN、SFU 阶段并等分总窗口;显式失败可以在阶段内继续尝试其他 tuple,silent candidate 到达该阶段 wake boundary 后释放 reservation、广播 rollback 并跳到下一类。只有 direct 时仍使用完整 deadline;direct+SFU 时各用一半;三类同时存在时各用三分之一。这样不引入每-candidate magic milliseconds,也不允许一条 silent direct 把 SFU suffix 饿死。 +一个 operation 仍只有一个 timer 和一个总 deadline,但 timer 的下一次 wake 从当前 candidate list 派生。Controller 提取实际存在的 direct 与 SFU 阶段并等分总窗口;显式失败可以在阶段内继续尝试其他 candidate,silent candidate 到达 direct wake boundary 后释放 reservation、广播 rollback 并跳到 SFU。只有 direct 时仍使用完整 deadline;direct+SFU 时各用一半。这样不引入每-candidate magic milliseconds,也不允许一条 silent direct 把 SFU suffix 饿死。 首次需要 SFU 但 Host 已满时不建立另一条 rescue 流程。若 affected child 已是 Host direct child,就用它;否则用稳定加入顺序中最新的 connected Host direct child。`reconcileRoom` 为该 child 建立普通 SFU candidate,创建唯一 publication,保持其 subtree,并在该 child 首帧后提交。Host publication 取代释放的 peer slot,随后同一 fixed-point scan 再处理原 waiting/failed child。若旧 Host peer sender 在首帧前仍保留,candidate 必须持有同一 `TransitionOverlapSlot`。没有 overlap 时先 preflight server resource,再退休 exact producer;operation 保持原总 deadline、按新 graph 重算未尝试 suffix,并为被切断的 healthy direct edge追加一个最后 restore tuple。Forward candidate 与 restore 都失败才进入 waiting,不得产生第四份 endpoint copy或另一套恢复状态。 -所有非服务器端点共享一个 server-authoritative steady outbound media-copy cap `C`:唯一配置 `ENDPOINT_MEDIA_COPY_CAPACITY` 默认 `2`,只接受 `1`、`2` 或 `3`,不兼容旧键。服务端将 endpoint 当前广告的 `0..C` 可用值 clamp 为唯一 `effectiveDownstreamCapacity`;该值只表示当前可用 sender slots,不建立角色、浏览器、UA、设备或页面可见性 tier。peer child 或 Host publication 各占一份,upstream receive 免费,committed selected TURN 只替换同一份 edge transport;SFU subscriber egress 与 TURN allocation 使用独立 server admission。Browser `screener-v8` 的 `authenticated.endpointMediaCopyCapacity` 把部署 `C` 交给 Host 与 Viewer;两端用该值校验稳态 assignment,并让 active+pending 的物理 copy 只在 controller 已授权 overlap 时达到 `min(C + 1, 3)`。代码通过一个 shared accounting guard 执行,不重复 magic number;旧 wire 与 Native executable 在 authority 前拒绝,不保留兼容路径。Selected TURN 使用显式无默认值的正 safe-integer deployment capacity;同一个进程内 ledger 对每条 exact peer-selected edge 和 Host-SFU ingress 在 credential 签发前 reserve `1`,并让 reserved、committed、draining authorization 持续计费至 exact logical release。`C=3` 满载时先收到 active bounded-gap retirement再收到 prepare;任何会产生第四份 endpoint media copy 的 token/grant都不得签发。同一 edge 改用 TURN 不增加稳态 endpoint copy,但仍需独立 TURN admission。 +所有非服务器端点共享一个 server-authoritative steady outbound media-copy cap `C`:唯一配置 `ENDPOINT_MEDIA_COPY_CAPACITY` 默认 `2`,只接受 `1`、`2` 或 `3`,不兼容旧键。服务端将 endpoint 当前广告的 `0..C` 可用值 clamp 为唯一 `effectiveDownstreamCapacity`;该值只表示当前可用 sender slots,不建立角色、浏览器、UA、设备或页面可见性 tier。peer child 或 Host publication 各占一份,upstream receive 免费;SFU subscriber egress 使用独立 server admission。Browser `screener-v8` 的 `authenticated.endpointMediaCopyCapacity` 把部署 `C` 交给 Host 与 Viewer;两端用该值校验稳态 assignment,并让 active+pending 的物理 copy 只在 controller 已授权 overlap 时达到 `min(C + 1, 3)`。代码通过一个 shared accounting guard 执行,不重复 magic number;旧 wire 与 Native executable 在 authority 前拒绝,不保留兼容路径。`C=3` 满载时先收到 active bounded-gap retirement再收到 prepare;任何会产生第四份 endpoint media copy 的 token都不得签发。 -SFU admission 在应用单进程中只有一个注入实例。LiveKit credential tuple 完整时,必须同时显式配置私有 `LIVEKIT_API_URL`、正 safe integer `SFU_INGRESS_CAPACITY` 与 `SFU_EGRESS_CAPACITY`;容量没有产品默认值,也不从 Viewer 数、endpoint cap 或历史 root 数推导。专用 LiveKit 实例设置 `room.auto_create: false`。账本只保存全局已用计数,以及 exact room/share/publication generation 下的一份 publication handle 和按 Viewer identity 索引的 subscription handle。Publication 的 `reserved | committed | draining` 占 ingress `1`,每份 subscription 的同类状态占 egress `1`;离开 route 的 subscription 在该 generation 删除并确认 absent 前保持 draining,同一 Viewer handle 再次启用不重复计数。首个 SFU child 先 reserve publication 与自己的 subscription,再通过 `RoomService.CreateRoom` 创建 exact-generation room,成功后才 prepare 和签发 token;后续 child 只在同 generation reserve subscription 并复用 Host publication。exact candidate child 的 first decoded frame 后 commit 自己的资源;publication replacement 将旧 generation 及其全部 subscription 一起转入 draining,失败、超时、失联、分享换代、停止或删房按资源所有权进入同一生命周期。只有 `RoomService.DeleteRoom` 成功且 follow-up lookup 证明 room 不存在,账本才扣除该 generation 的 ingress/egress;auto-create 关闭后,旧 token 不能重建已删除 room。进程启动在接受流量前拒绝任何非 Screener room,删除全部遗留 Screener room 并确认 namespace 为空;关闭时先 drain 再遗忘计数。Host 信令离线期间若 exact room 中的 Host participant 仍存在就继续计费并保留健康媒体,participant 消失后在一个有界轮询周期内回收。稳态 reserve/commit/drain bookkeeping 仍是 Map lookup 与整数加减;只有 generation 创建、删除、启动清场和 Host 离线检查访问 LiveKit control plane。TURN allocation 使用独立的进程内 logical ledger;多应用进程部署必须先增加共享原子 admission/lifecycle owner。 +SFU admission 在应用单进程中只有一个注入实例。LiveKit credential tuple 完整时,必须同时显式配置私有 `LIVEKIT_API_URL`、正 safe integer `SFU_INGRESS_CAPACITY` 与 `SFU_EGRESS_CAPACITY`;容量没有产品默认值,也不从 Viewer 数、endpoint cap 或历史 root 数推导。专用 LiveKit 实例设置 `room.auto_create: false`。账本只保存全局已用计数,以及 exact room/share/publication generation 下的一份 publication handle 和按 Viewer identity 索引的 subscription handle。Publication 的 `reserved | committed | draining` 占 ingress `1`,每份 subscription 的同类状态占 egress `1`;离开 route 的 subscription 在该 generation 删除并确认 absent 前保持 draining,同一 Viewer handle 再次启用不重复计数。首个 SFU child 先 reserve publication 与自己的 subscription,再通过 `RoomService.CreateRoom` 创建 exact-generation room,成功后才 prepare 和签发 token;后续 child 只在同 generation reserve subscription 并复用 Host publication。exact candidate child 的 first decoded frame 后 commit 自己的资源;publication replacement 将旧 generation 及其全部 subscription 一起转入 draining,失败、超时、失联、分享换代、停止或删房按资源所有权进入同一生命周期。只有 `RoomService.DeleteRoom` 成功且 follow-up lookup 证明 room 不存在,账本才扣除该 generation 的 ingress/egress;auto-create 关闭后,旧 token 不能重建已删除 room。进程启动在接受流量前拒绝任何非 Screener room,删除全部遗留 Screener room并确认 namespace 为空;关闭时先 drain 再遗忘计数。Host 信令离线期间若 exact room 中的 Host participant 仍存在就继续计费并保留健康媒体,participant 消失后在一个有界轮询周期内回收。稳态 reserve/commit/drain bookkeeping 仍是 Map lookup 与整数加减;只有 generation 创建、删除、启动清场和 Host 离线检查访问 LiveKit control plane。多应用进程部署必须先增加共享原子 admission/lifecycle owner。 -只有实际因 deployment-wide SFU/TURN admission 失败的房间进入一个 `resourceWaiterRoomIds` Set。TURN logical release 或 LiveKit delete-plus-absent 使真实 usage 下降时,owner 一次 drain 该 Set,对每个仍存在的 controller 调用 `touchExternalFacts()` 并排入普通 reconcile。它不轮询、不按资源类型复制 route state,也不唤醒未等待资源的房间。 +只有实际因 deployment-wide SFU admission 失败的房间进入一个 `resourceWaiterRoomIds` Set。LiveKit delete-plus-absent 使真实 usage 下降时,owner 一次 drain 该 Set,对每个仍存在的 controller 调用 `touchExternalFacts()` 并排入普通 reconcile。它不轮询、不按资源类型复制 route state,也不唤醒未等待资源的房间。 普通 Web relay 把收到的 remote track 加入独立 downstream `RTCPeerConnection`,因此可能逐跳解码和重编码;浏览器不保证跨连接 shared encode。每条 ordinary child 是独立网络副本,实际 encode time、CPU/GPU、bitrate、loss、frame rate、depth 和 recovery 必须测量。 -迁移使用 authenticated room/endpoint sessions、share generation、base/pending route revision 与 candidate connection identity。`route-update/prepare` 显式携带当前 operation 的 exact child peer ID、selected transport 与服务端生成的 candidate connection ID;parent 与 child 直接使用该 candidate tuple,即使同一 parent 只替换 transport、child 列表不变,也不得从 assignment 差异猜测本次 candidate。服务端在每个 WebSocket 上先发 prepare,再发其 selected-TURN grant 或 SFU config,并在允许 parent offer前先排入 child 的 companion;客户端依赖 WebSocket顺序而不维护消息重排 inbox。可用旧 route 保持 active,candidate 在 active map 外准备;candidate child 的第一帧新 decoded video 触发 `settleReady(commitAdmissionSync)`,成功 candidate `P` 下发 active `P`。失败、超时、代次变化或授权撤销销毁 candidate,保持仍有效的 committed graph 内容并先下发单调递增的 active rollback revision `R > P`,再准备后续 candidate;一个 monotonic allocator同时拥有 prepare/commit/rollback/retirement/prune revision。Direct edge 的 stock WebRTC rebuild可在 exact current sessions内原子采用新 connection ID而不改变 topology;selected transport随session变化走统一 retirement。Make-before-break 的 overlap 由统一端点容量、单个有界 `TransitionOverlapSlot` 和 typed server-resource admission 同时决定;stop/delete通过controller `dispose()`一次返回所有current与committed资源。 +迁移使用 authenticated room/endpoint sessions、share generation、base/pending route revision 与 candidate connection identity。`route-update/prepare` 显式携带当前 operation 的 exact child peer ID、route kind 与服务端生成的 candidate connection ID;parent 与 child 直接使用该 candidate。服务端在每个 WebSocket 上先发 prepare,再发其 SFU config,并在允许 parent offer前先排入 child 的 companion;客户端依赖 WebSocket顺序而不维护消息重排 inbox。可用旧 route 保持 active,candidate 在 active map 外准备;candidate child 的第一帧新 decoded video 触发 `settleReady(commitAdmissionSync)`,成功 candidate `P` 下发 active `P`。失败、超时、代次变化或授权撤销销毁 candidate,保持仍有效的 committed graph 内容并先下发单调递增的 active rollback revision `R > P`,再准备后续 candidate;一个 monotonic allocator同时拥有 prepare/commit/rollback/retirement/prune revision。Direct edge 的 stock WebRTC rebuild可在 exact current sessions内原子采用新 connection ID而不改变 topology。Make-before-break 的 overlap 由统一端点容量、单个有界 `TransitionOverlapSlot` 和 typed server-resource admission 同时决定;stop/delete通过controller `dispose()`一次返回所有current与committed资源。 -SFU/TURN 的 publisher、subscriber、selected transport、central admission、resource accounting、temporary overlap 和 fallback sequence 由 ADR-0005 统一定义。普通端点 child count 不能成为 server cap,兜底也不能形成无界 whole-room fanout。一个 Host publication 服务所有 SFU subscriptions;SFU-fed 与 peer-fed endpoint 使用相同的 provisional-child first-frame transaction。TURN 只替换授权 edge 的 transport,Host 无 direct first-level path 时可作为单条 Host-SFU ingress 的 transport。 +SFU 的 publisher、subscriber、central admission、resource accounting、temporary overlap 和 fallback sequence 由 ADR-0005 统一定义。普通端点 child count 不能成为 server cap,兜底也不能形成无界 whole-room fanout。一个 Host publication 服务所有 SFU subscriptions;SFU-fed 与 peer-fed endpoint 使用相同的 provisional-child first-frame transaction。 relay 自身 ingress 失效时,reconcile 把它作为 child 换父并保留 subtree。endpoint disconnect 或 effective capacity 降低时,只保留稳定顺序内可容纳的 children,并把 overflow children 逐个送回同一 reconcile;offline childless participant 自然删除。Browser child复用现有 WebRTC/LiveKit stats sampling保存 exact active route的 `lastDecodedAt`;采样出现新 decoded frame就更新,route/connection变化就重置,超过统一 deadline只报告一次 exact edge。权威 pause abort pending operation、保留 active graph、让新参与者 waiting,并抑制/重置 decoded stall;resume重新从当前采样建立基线。Track availability、A/B/C、码率、FPS、模糊、sender stats和SFU layer只用于框架恢复或诊断。 -实现迁移以当前 ADR-0005 invariants 为准;生产 release `9461e20` 仅是部署快照,代码迁移只保留仍满足当前 identity、first-frame commit 和 bounded-recovery 约束的部分。 +实现迁移以当前 ADR-0005 invariants 为准;代码迁移只保留仍满足当前 identity、first-frame commit 和 bounded-recovery 约束的部分。 ## 官方与参考资料 访问日期为 2026-08-18 至 2026-08-21: @@ -404,14 +399,12 @@ relay 自身 ingress 失效时,reconcile 把它作为 child 换父并保留 su - [Media Capture Content Hints](https://www.w3.org/TR/mst-content-hint/) - [RTCRtpSender.replaceTrack](https://www.w3.org/TR/webrtc/#dom-rtcrtpsender-replacetrack) - [WebRTC Stats](https://www.w3.org/TR/webrtc-stats/) -- [TURN, RFC 8656](https://www.rfc-editor.org/rfc/rfc8656.html) - [WebRTC SVC](https://www.w3.org/TR/webrtc-svc/) - [Media Capabilities](https://www.w3.org/TR/media-capabilities/) - [WebCodecs](https://www.w3.org/TR/webcodecs/) - [WebRTC official samples](https://webrtc.github.io/samples/) -- [coturn TURN server documentation](https://github.com/coturn/coturn/blob/master/README.turnserver) +- [coturn server documentation](https://github.com/coturn/coturn/blob/master/README.turnserver) - [coturn example configuration](https://github.com/coturn/coturn/blob/master/examples/etc/turnserver.conf) -- [TURN REST API draft implemented by coturn](https://datatracker.ietf.org/doc/html/draft-uberti-behave-turn-rest-00) - [Cookies: HTTP State Management Mechanism draft (RFC6265bis)](https://datatracker.ietf.org/doc/draft-ietf-httpbis-rfc6265bis/) - [Node.js `crypto.createHmac()` and `crypto.timingSafeEqual()`](https://nodejs.org/api/crypto.html) - [W3C WAI: Labeling Controls](https://www.w3.org/WAI/tutorials/forms/labels/) diff --git a/docs/需求理解.md b/docs/需求理解.md index 4b03841..e5d113a 100644 --- a/docs/需求理解.md +++ b/docs/需求理解.md @@ -6,11 +6,11 @@ ### 当前路由不变量 -`PEER_ASSISTED_MEDIA=true` 是旗舰部署的单一总开关;每个房间独立保存 route revision、会话代次、失败状态和容量预算。系统优先使用 direct/peer UDP,普通 peer 连接保持 STUN-only。所有非服务器端点使用同一个服务端权威稳态 outbound media-copy capacity:默认 `2`,只允许静态配置 `1`、`2` 或 `3`;peer child 或 Host 的唯一 SFU publication 各消耗一份,上游接收不消耗该预算,selected TURN 只替换同一份 edge 的 transport,角色、浏览器、UA、设备或页面可见性不产生特例。 +`PEER_ASSISTED_MEDIA=true` 是旗舰部署的单一总开关;每个房间独立保存 route revision、会话代次、失败状态和容量预算。系统优先使用 direct/peer UDP,普通 peer 连接保持 STUN-only,唯一应用兜底是专用 LiveKit SFU/UDP。所有非服务器端点使用同一个服务端权威稳态 outbound media-copy capacity:默认 `2`,只允许静态配置 `1`、`2` 或 `3`;peer child 或 Host 的唯一 SFU publication 各消耗一份,上游接收不消耗该预算,角色、浏览器、UA、设备或页面可见性不产生特例。 构建一个面向熟人游戏场景的低延迟屏幕共享工具。分享者可以在私密房间内分享游戏、应用窗口或显示器画面及声音,多名朋友可以即时观看。中心服务负责房间、身份、确定性路由和建立连接;每名 Viewer 同时至多一个 active upstream,活动媒体图保持无环且可达源。切换绑定当前鉴权会话、分享代次和唯一 pending revision;可用旧边保留到 exact candidate child 解码出第一帧新画面,所有路线最终进入有效媒体、明确等待或有界失败。系统自动完成分配和恢复,用户不手动选择拓扑。当前发布面只包含 Web Host、Viewer 和 browser relay;Electron/原生 sender、capture helper 与 shared-encode executable 属于最低优先级的后续增强。观看端使用普通桌面或手机浏览器。 -SFU/TURN 的 publication、subscription、transport、server-resource accounting、临时 overlap 和具体 fallback 序列由 ADR-0005 的整体模型统一定义,不能从普通端点 child 数量推导。SFU 与 TURN 只能是有界兜底资源,不能把产品改成无界或 always-SFU 的整房转发。当前 PoC 的观看人数限制是实现事实,不是性能声明;任何扩容目标必须以真实资源、质量、深度和恢复矩阵另行验收。产品不承担公开直播或大规模观看。 +SFU 的 publication、subscription、server-resource accounting、临时 overlap 和 direct-to-SFU fallback 序列由 ADR-0005 的整体模型统一定义,不能从普通端点 child 数量推导。SFU 只能是有界兜底资源,不能把产品改成无界或 always-SFU 的整房转发。当前 PoC 的观看人数限制是实现事实,不是性能声明;任何扩容目标必须以真实资源、质量、深度和恢复矩阵另行验收。产品不承担公开直播或大规模观看。 ## 功能需求 @@ -19,32 +19,32 @@ SFU/TURN 的 publication、subscription、transport、server-resource accounting - 首屏双入口:分享入口通过站点访问后,主工作区必须同等突出“开始分享”和“加入房间”两个主要动作,并用清楚的视觉分隔表达发送与接收;持合法房间 grant 的观看者不需要知道站点访问密码。“加入房间”在当前舞台内就地展开房间码输入并提交,不先跳转到独立页面;直接访问 `/join` 仍提供同一输入能力。不增加营销首页、模式向导或拓扑选择。浏览器标签页使用与现有 `MonitorUp` 品牌语言一致的 favicon。 - 简单房间:分享者从当前空闲池随机获得一个 `1000` 至 `9999` 的四位房间码,房间码只负责定位,不是秘密或永久身份。Host 与 Viewer 标题附近都紧凑、显眼地显示房间号并提供只复制房间号的动作;Host 的主分享动作复制含独立 Viewer grant 的完整邀请链接。新房默认允许已通过站点访问的 Viewer 直接用房间号加入且不设房间密码;Host 可把 code-only 入口改为需要房间密码或完全关闭,不影响 grant 邀请。所有入口只授予该房间 Viewer,不能建房、发布、冒充 Host 或跨房。 - 免安装观看:桌面和手机观看者可直接通过浏览器链接进入、授权并播放,不依赖与分享端相同的桌面客户端。 -- 屏幕采集:分享者可主动选择显示器或应用窗口,开始、停止、临时同步暂停音视频和切换分享来源;暂停保留现有连接并禁用当前 capture stream 的全部媒体轨道。服务端按当前分享代次同步这一主动暂停状态,观看端在视频舞台显示“分享者已暂停”,不得从黑帧、track mute 或网络状态推断暂停;晚加入和信令重连使用权威快照,恢复、停止、新分享或会话替换清除旧提示。Host 本地预览必须直接使用 capture stream,支持独立全屏;页面失焦或隐藏时只暂停本地 video element,并明确提示“本地预览已暂停,分享仍在继续”,恢复焦点后继续预览,不得停止 capture、编码或上行。预览不得建立 viewer/媒体边或消耗 TURN、SFU、应用服务器 media egress;预览仍可能产生本地合成、缩放和 GPU 显示成本,不能把隐藏预览称为停止采集或编码。 +- 屏幕采集:分享者可主动选择显示器或应用窗口,开始、停止、临时同步暂停音视频和切换分享来源;暂停保留现有连接并禁用当前 capture stream 的全部媒体轨道。服务端按当前分享代次同步这一主动暂停状态,观看端在视频舞台显示“分享者已暂停”,不得从黑帧、track mute 或网络状态推断暂停;晚加入和信令重连使用权威快照,恢复、停止、新分享或会话替换清除旧提示。Host 本地预览必须直接使用 capture stream,支持独立全屏;页面失焦或隐藏时只暂停本地 video element,并明确提示“本地预览已暂停,分享仍在继续”,恢复焦点后继续预览,不得停止 capture、编码或上行。预览不得建立 viewer/媒体边或消耗 SFU、应用服务器 media egress;预览仍可能产生本地合成、缩放和 GPU 显示成本,不能把隐藏预览称为停止采集或编码。 - 游戏声音:Windows Chrome/Edge 的初次分享和切换来源都默认请求可用的标签页、窗口或系统音频;浏览器、系统、所选来源或用户没有返回音轨时必须明确提示,而不是静默无声或阻止 video-only 分享。 - 观看端本地音量:播放控制栏提供 `0%` 至 `100%` 音量条和静音按钮,只改变当前浏览器的媒体元素,不修改 Host 音轨、编码、房间状态或其他观看者。切流、换父和 ICE 恢复保留当前页面内的值;刷新后不持久化。静音保留上次非零音量,拖动到非零值自动取消静音。 - 本地昵称与房间成员:用户可设置一个只保存在当前浏览器同源 `localStorage` 的显示名,界面统一称为“昵称”;默认以只读文本显示,铅笔编辑按钮紧贴昵称,点击后才进入编辑。Viewer 顶部标题直接显示“{Host 名称} 的屏幕”,不得在下方重复一行 Host 名称。信令只在当前在线 socket 会话内保存和广播 NFC 规范化、最多 24 个 Unicode code points、拒绝控制/换行/双向覆盖符与孤立 surrogate 的纯文本标签,不写服务端持久存储或日志。空值使用通用角色 fallback。Web Host 与 Viewer 都必须看到全部当前在线 Viewer 的权威总数与 roster,不能拿最多两条 Host 媒体直连数冒充在线人数;presence 更新也不得创建媒体连接。展示身份默认只显示名称;仅当房内出现同名时追加当前房间 `peerId` 的至少六字符短后缀,并只在同名项后缀碰撞时有界延长。显示名、后缀和 `clientId` 都不是账号、授权身份或全局 UUID,重复名称允许且不得参与授权、路由或质量决策。Web Host 与 Viewer 通过同一 opt-in participant roster 看到 Host 名称;Native/executable sender 不在当前发布面,也不接收 Web roster。 -- 一对多观看:每个房间允许一名 Host 和最多 `20` 名 authenticated Viewer;房间 admission 与端点下游容量、SFU/TURN admission 分别配置,超额加入必须明确拒绝。Production 当前默认 8、显式接受 1 至 16 是部署事实;本轮 route release 把 source hard limit 提升到 20,并以一个 20-Viewer smoke 验收,不从 endpoint cap 推导房间人数。 -- 端点发送容量:所有非服务器端点使用同一个服务端权威稳态 outbound media-copy capacity,由唯一部署项 `ENDPOINT_MEDIA_COPY_CAPACITY` 配置,默认 `2` 且只接受 `1`、`2` 或 `3`;不读取旧键或保留第二套 tier。peer child 或 Host 的唯一 SFU publication 各消耗一份,上游接收不消耗该预算,committed selected TURN 只替换同一份 logical edge 的 transport。客户端只能声明不高于部署值的可用容量,角色、浏览器、UA、设备类型和页面可见性不产生不同上限。服务端在每次 `authenticated` 权威快照中返回最终 `endpointMediaCopyCapacity`;普通非 peer-assisted 模式也只授权稳定加入顺序中的至多 `C` 个 Host child,超额 Viewer 保持入房等待并在槽位释放后提升,信令不能绕过该集合。Browser Host 与 Viewer 严格校验该值并用它约束稳态 assignment;并行产生媒体的 candidate 必须持有唯一 `TransitionOverlapSlot`,active+pending 物理 copy 只可达到 `min(C + 1, 3)` 并在 commit/abort 后释放。`C=3` 满载时必须先收到 active bounded-gap retirement 再 prepare,绝不产生第四份 copy。SFU subscriber egress 与 TURN allocation 由独立 server admission 记账。当前不兼容房间合同以 `screener-v8` 原子升级;缺失/非法 cap、旧 wire 与 executable sender都在取得room/route authority前fail closed。 +- 一对多观看:每个房间允许一名 Host 和最多 `20` 名 authenticated Viewer;房间 admission 与端点下游容量、SFU admission 分别配置,超额加入必须明确拒绝。观看人数不从 endpoint cap 推导,20-Viewer smoke 只证明控制面与资源上界,不是任意网络质量声明。 +- 端点发送容量:所有非服务器端点使用同一个服务端权威稳态 outbound media-copy capacity,由唯一部署项 `ENDPOINT_MEDIA_COPY_CAPACITY` 配置,默认 `2` 且只接受 `1`、`2` 或 `3`;不读取旧键或保留第二套 tier。peer child 或 Host 的唯一 SFU publication 各消耗一份,上游接收不消耗该预算。客户端只能声明不高于部署值的可用容量,角色、浏览器、UA、设备类型和页面可见性不产生不同上限。服务端在每次 `authenticated` 权威快照中返回最终 `endpointMediaCopyCapacity`;普通非 peer-assisted 模式也只授权稳定加入顺序中的至多 `C` 个 Host child,超额 Viewer 保持入房等待并在槽位释放后提升,信令不能绕过该集合。Browser Host 与 Viewer 严格校验该值并用它约束稳态 assignment;并行产生媒体的 candidate 必须持有唯一 `TransitionOverlapSlot`,active+pending 物理 copy 只可达到 `min(C + 1, 3)` 并在 commit/abort 后释放。`C=3` 满载时必须先收到 active bounded-gap retirement 再 prepare,绝不产生第四份 copy。SFU subscriber egress 由独立 server admission 记账。当前不兼容房间合同以单一 release 原子升级;缺失/非法 cap、旧 wire 与 executable sender都在取得room/route authority前fail closed。 - 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,普通边不得预发 TURN candidate。所有恢复和候选授权必须绑定 authenticated room/endpoint session、share generation、base/pending route revision 与 candidate connection identity;成功即停止,失败、超时或绑定失效即撤销,全部可用路径耗尽后明确等待或失败。已接受的顺序是保持既有 logical edge,必要时 direct/STUN 后使用 exact selected TURN;只有 logical ingress 不可用时才使用当前 Host publication 的 SFU subscription。Host 无 direct first-level path 时优先一条可经 TURN 的 Host-SFU publication,而不是逐 Viewer 建 Host-TURN peer 边。凭据不得进入应用页面 URL、日志或服务端持久存储;TURN 凭据和 Screener route grant 不进入任何 URL,LiveKit join JWT 仅按协议进入不得记录的 WebSocket request target。HTTPS/WSS 继续独立使用 TLS/TCP。 +- NAT 与传输边界:STUN 必须可用,所有 ordinary peer `RTCPeerConnection` 只接收 STUN ICE servers。所有恢复和候选授权必须绑定 authenticated room/endpoint session、share generation、base/pending route revision 与 candidate connection identity;成功即停止,失败、超时或绑定失效即撤销,全部可用路径耗尽后明确等待或失败。已接受的完整顺序只有保持或尝试 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)三档推荐上限,三档与高级设置初始值统一采用 `balanced`(平衡)与 `automatic`(自动编码),`maintain-resolution`(清晰优先)、`maintain-framerate`(流畅优先)和 H.264/VP8 保留为显式选择。分享期间可直接换视频档位和 64/128/256 kbps 音频 sender ceiling,不重新选择来源或重建健康连接;视频 codec 首个可交付切换边界是在分享已显式暂停时执行有界重协商,未暂停热切换是后续复用同一事务的扩展。Peer-assisted relay 与已配置的 SFU 必须接收同一个严格校验的房间级权威设置,覆盖当前和以后建立的 sender;普通 P2P 信令结构保持不变。任何设置都只是浏览器上限和偏好,具体降级由浏览器决定;必须读回实际 sender 参数并通过 stats 观测结果、显示参数拒绝、改写和持续受限原因。不做自定义动态 FPS、综合评分或周期性全房间调档。 - 路径隔离双表示:direct/peer 继续使用每条 PeerConnection 独立的 stock WebRTC GCC,不把 viewer 反馈合成全房目标。每条 SFU path 从 `HIGH` ceiling 开始;优先使用 pinned LiveKit 的标准两层 screen-share simulcast,让内建 per-subscriber BWE 独立降层和恢复,Screener 不实现媒体层 selector。应用只诊断实际接收层、硬件编码、游戏性能、资源和上传;SFU 自动降层不改变拓扑。`LOW` 停编只是资源优化,active representation/layer 硬上限为二且不随 viewer 增长。当前 Web P2P simulcast 与 Web/LiveKit SVC 不能证明跨 direct/peer 共享编码或可移植硬件路径;全部标准路径失败后才考虑 custom/native dual encode。 - 路由失效输入:应用不推断 parent-wide 质量,也不计算综合分数。当前 authenticated child 只在 PeerConnection `failed/closed`,或非权威暂停时现有 periodic stats 在统一 deadline 内没有新 decoded frame 时,使自己的 exact edge 失效并唤醒同一个 reconcile。decoded progress 只保存 route-keyed `lastDecodedAt`,route/connection变化或新帧重置、暂停抑制,同一 edge只报告一次,不增加轮询器。该 operation 把失败的 `parent + transport` tuple 在当前 fact version 直接视为已尝试,先选择其他 direct parent;同一 parent 只有换用另一 transport 时才可在本次继续,新的外部事实才允许原 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;selected transport随session变化统一retire。应用只在上述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 | selected-turn | sfu` 语义阶段等分,不配置每个 candidate 的 magic timeout。阶段内的硬失败继续推进 cursor;silent ICE 到达阶段边界时跳过剩余同类 tuple,必须把后续阶段的派生份额留给 selected transport 与 SFU create/reuse。任何 strict-NAT guessed candidate 以后也只能处于 direct 阶段,不能延长总 deadline。 +- 断线恢复:临时网络变化先交给 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。任何 strict-NAT guessed candidate 以后也只能处于 direct 阶段,不能延长总 deadline。 - 可观测性:按目标 RTP transport 与不透明连接/ICE 代次关联 selected path、`iceGatheringState`/ICE/connection state、事件推导的建立时长、generation-baselined pair-change delta,以及 RTT、RTP 丢包率、抖动、码率、帧率、编码耗时和受限原因。候选与错误的 wire、日志和导出只使用[低服务器成本媒体路由](./research/low-server-media-routes.md)的脱敏字段和 code bucket,禁止 raw SDP、candidate、地址、URL、`errorText` 或网络标识。只有创建该 `RTCPeerConnection` 的本机可在折叠详情中显示浏览器对 selected local/remote candidate 实际暴露的地址与端口;该值不上传、不持久化、不用于地理/NAT 推断,缺失或被隐私保护时显示未知。 -- 渐进式诊断:分享者和观看者默认只看到房间、画面、核心状态和常用控制;Host 的 Viewer 卡片只把本机连接或当前接收证据称为“已连接”,其余状态保持中性的“线路分配中”或具体连接状态,不把 active assignment 当作连接成功。展开连接详情也只在当前媒体证据成立后显示媒体方式:`P2P` 使用绿色与直观图标,`SFU` 使用黄色的 `SFU fallback`;实际 relay 时另标 `TURN`,只有本地 relay protocol 可见时才附加 `/UDP` 等实际值。尚未连接时不预告 P2P、SFU 或未知 ICE,只保留中性状态。Host/Peer 上下游仍只在独立的权威拓扑视图中展示。传输协议、候选路径、RTT、视频/音频 RTP 区间丢包率、实际编解码器、音频区间码率/jitter 和视频编码耗时等技术详情由一个明确的“显示连接详情”开关展开。远端 Viewer evidence 的详情 shell 在同一 viewer/parent/connection/route identity 内保持稳定;非空字段从各自最后一次实际观测起精确保留五秒,`connecting`/`disconnected` 只降为非 fresh,`failed`/`closed`、identity/session/share/route 变化或非单调 sequence 则清空/换代。非 fresh evidence 不得继续证明“已连接”或被其他 current-state 消费者使用。音频 codec MIME、RTP clock、channels 与 fmtp 只能来自当前 RTP 对象引用的 `RTCCodecStats`,缺失即未知;`opus/48000/2` 不得写成 48 kHz 音源或实际立体声。隐藏诊断不能隐藏失败原因、音频缺失、持续画质受限或需要用户操作的告警。 -- 站点访问:生产部署必须配置独立的 `SITE_ACCESS_PASSWORD` 控制建房、Host 信令角色和所有 code-only Viewer 入口;只有本地开发/测试可显式关闭。非空值接受 8 至 128 个可见 ASCII bytes,不能复用 TURN、LiveKit、TLS 或其他部署 secret。`POST /api/site-access` 验证成功签发 12 小时、无服务端会话状态的 HttpOnly、`SameSite=Strict` site-access cookie。有效 room-scoped fragment grant 可绕过站点 gate 直接进入对应房间;其余 `/r/{code}` 和 `/join` 必须先持有效站点 cookie,之后才检查 `open | password | disabled` code-only policy。失败统一且不得泄露房间是否存在或 policy;建房 HTTP 只认该 cookie。已移除的旧环境变量、endpoint 和 cookie 不保留兼容。不引入账号、JWT、服务端 session Map 或通用 logout。 +- 渐进式诊断:分享者和观看者默认只看到房间、画面、核心状态和常用控制;Host 的 Viewer 卡片只把本机连接或当前接收证据称为“已连接”,其余状态保持中性的“线路分配中”或具体连接状态,不把 active assignment 当作连接成功。展开连接详情也只在当前媒体证据成立后显示媒体方式:direct peer使用绿色`P2P`,SFU使用黄色`SFU fallback`。尚未连接时不预告 P2P、SFU 或未知 ICE,只保留中性状态。Host/Peer 上下游仍只在独立的权威拓扑视图中展示。传输协议、候选路径、RTT、视频/音频 RTP 区间丢包率、实际编解码器、音频区间码率/jitter 和视频编码耗时等技术详情由一个明确的“显示连接详情”开关展开。远端 Viewer evidence 的详情 shell 在同一 viewer/parent/connection/route identity 内保持稳定;非空字段从各自最后一次实际观测起精确保留五秒,`connecting`/`disconnected` 只降为非 fresh,`failed`/`closed`、identity/session/share/route 变化或非单调 sequence 则清空/换代。非 fresh evidence 不得继续证明“已连接”或被其他 current-state 消费者使用。音频 codec MIME、RTP clock、channels 与 fmtp 只能来自当前 RTP 对象引用的 `RTCCodecStats`,缺失即未知;`opus/48000/2` 不得写成 48 kHz 音源或实际立体声。隐藏诊断不能隐藏失败原因、音频缺失、持续画质受限或需要用户操作的告警。 +- 站点访问:生产部署必须配置独立的 `SITE_ACCESS_PASSWORD` 控制建房、Host 信令角色和所有 code-only Viewer 入口;只有本地开发/测试可显式关闭。非空值接受 8 至 128 个可见 ASCII bytes,不能复用 LiveKit、TLS 或其他部署 secret。`POST /api/site-access` 验证成功签发 12 小时、无服务端会话状态的 HttpOnly、`SameSite=Strict` site-access cookie。有效 room-scoped fragment grant 可绕过站点 gate 直接进入对应房间;其余 `/r/{code}` 和 `/join` 必须先持有效站点 cookie,之后才检查 `open | password | disabled` code-only policy。失败统一且不得泄露房间是否存在或 policy;建房 HTTP 只认该 cookie。已移除的旧环境变量、endpoint 和 cookie 不保留兼容。不引入账号、JWT、服务端 session Map 或通用 logout。 - 房间级观看授权:新房间总是生成含 256-bit 随机量、限时且可撤销的 room-scoped Viewer grant;服务端内存只保存完整 grant 的 SHA-256 摘要。到期阻止以后鉴权/重连,但不为已鉴权 Viewer 设置踢出定时器;即时清退使用 rotate/revoke。浏览器从 URL fragment 严格读取 grant 后,只写该房间的 `sessionStorage` 并立即用 `history.replaceState` 清除 fragment;刷新和页面内重连可继续。code-only 入口独立采用 `open | password | disabled`,默认 `open` 且无房间密码;所有 code-only 尝试先通过站点访问。grant 不得进入 `localStorage`、cookie、query、WebSocket URL、Referrer、日志或错误。Host 可独立 rotate/revoke grant 或改变 code-only policy;授权变化必须先提交新的内存代次,再清除 Viewer grace/路由、终止旧代次信令和媒体并断开受影响 Viewer。 - 易失房间状态:服务器只在一个有界内存 RoomStore 中保存房间码、租期、Host token/Viewer grant 摘要、code-only policy、可选 48-byte 房间密码校验材料、参与者和控制状态。`ROOM_LEASE_SECONDS` 默认 `86400`;Host 正在分享时不回收,停止分享或 Host 断开后开始 dormant lease,同一 Host token 在期限内重新分享即续租,Viewer 活动不续租,过期后释放号码。应用进程重启时全部房间、租约、邀请和密码状态直接失效;不使用 SQLite、`ROOM_DATABASE_PATH`、schema migration、账号、用户表或服务器 session 表。 - 本地 Host 偏好:房间所有权继续使用随机 Host token,不使用 IP、UA、硬件信息或浏览器指纹。Host 浏览器在同源 `localStorage` 保存 raw Host token,以及昵称、code-only policy 和可选房间密码组成的创建偏好;服务器只在当前进程内保存 token 摘要和密码 verifier。旧房间不存在时,下一次显式分享创建新房间并原子应用本地偏好,同时生成新的四位房间号、Host token 和 Viewer grant。换设备、换浏览器或清理站点数据即不恢复。 ### P1 - 重要功能 -- 公网服务器一键部署包:在用户自有的公网服务器上一键部署当前 Web/信令应用、易失房间状态和所需 STUN/SFU/TURN/反向代理组件,复用唯一配置、secret、路由、健康检查和回滚合同。它不是托管服务,不能内置真实凭据或跳过域名、TLS、防火墙、端口、容量和回滚预检。 -- 本地自包含一键包:提供 Windows、macOS 和 Linux 可分发的桌面包,在分享者电脑上同时运行 Host、当前 Web/信令应用服务器和本地状态,朋友继续通过普通浏览器邀请观看。安装后启动不要求源码 checkout、Node 或任何外部 Screener、STUN、SFU、TURN、隧道或 relay 服务;它尽力使用本机可达入口和 direct P2P,但必须明确报告公网入口、可信 HTTPS/WSS、网关映射或 NAT 路径缺失,不能承诺任意 CGNAT/严格防火墙必通。两种分发包都排在当前功能、路由、NAT 和真实网络 TODO 完成之后。 +- 公网服务器一键部署包:在用户自有的公网服务器上一键部署当前 Web/信令应用、易失房间状态和所需 STUN/SFU/反向代理组件,复用唯一配置、secret、路由、健康检查和回滚合同。它不是托管服务,不能内置真实凭据或跳过域名、TLS、防火墙、端口、容量和回滚预检。 +- 本地自包含一键包:提供 Windows、macOS 和 Linux 可分发的桌面包,在分享者电脑上同时运行 Host、当前 Web/信令应用服务器和本地状态,朋友继续通过普通浏览器邀请观看。安装后启动不要求源码 checkout、Node 或任何外部 Screener、STUN、SFU、隧道或 relay 服务;它尽力使用本机可达入口和 direct P2P,但必须明确报告公网入口、可信 HTTPS/WSS、网关映射或 NAT 路径缺失,不能承诺任意 CGNAT/严格防火墙必通。两种分发包都排在当前功能、路由、NAT 和真实网络 TODO 完成之后。 - 独立房间入口:合法 `/r/{code}#v={grant}` 邀请直接进入对应房间;没有 grant 的 `/join` 或 `/r/{code}` 先显示明确标为“站点访问”的整站验证,再按房间 `open | password | disabled` policy 直接进入、要求房间密码或中性拒绝。有效 site-access cookie 只跳过第一步,两类密码不得复用或互相替代。Host 可设置、轮换或移除房间观看密码;密码接受 1 至 64 个可见 ASCII 字符,异步 scrypt 全局最多执行 2 个、等待 16 个派生。Host 可把房间密码保存在同源本地创建偏好中;它不得进入 URL、cookie、日志或服务端持久存储。不存在、错误密码、队满和正确密码后的满房都返回通用结果。 -- 统一自动路由:旗舰部署开启 `PEER_ASSISTED_MEDIA` 后,每个房间使用一个服务端 route controller,独占 committed graph、一个 pending child operation 和 event-driven reconcile。运行时只增加一个串行pump、一个operation deadline timer与typed resource lifecycle task,不维护另一份graph。operation持有candidate tuple list/cursor、一个current candidate/资源预留、fact version和一个总deadline。committed graph必须无环、source-reachable,且每名Viewer同时至多一个active upstream。加入、容量变化、节点离开、exact edge `failed/closed`,以及非暂停decoded-frame stall都只唤醒同一个reconcile;其余健康分支保持sticky。候选按当前身份、source reachability、无环、effective capacity与sender transition硬过滤,再按结果深度、剩余sender slots、稳定加入顺序和peer identity排序;server admission在执行当前cursor时独立reserve。candidate child首帧ready触发`settleReady(commitAdmissionSync)`,成功prepare `P`提交active `P`;失败释放资源并用同一monotonic allocator广播`R>P` rollback,cursor推进但总deadline不重置。bounded-gap先preflight server resource,再retire producer、按新graph重算未尝试suffix,并在切断healthy edge时追加一个普通restore candidate。relay自身ingress失效时作为child换父并保留subtree;relay disconnect/cap降低让overflow children逐个走同一事务,childless后删除。房间admission和无环约束最大深度,不设独立hard cap。普通peer始终STUN-only,SFU/TURN使用独立admission。 +- 统一自动路由:旗舰部署开启 `PEER_ASSISTED_MEDIA` 后,每个房间使用一个服务端 route controller,独占 committed graph、一个 pending child operation 和 event-driven reconcile。运行时只增加一个串行pump、一个operation deadline timer与typed resource lifecycle task,不维护另一份graph。operation持有candidate list/cursor、一个current candidate/资源预留、fact version和一个总deadline。committed graph必须无环、source-reachable,且每名Viewer同时至多一个active upstream。加入、容量变化、节点离开、exact edge `failed/closed`,以及非暂停decoded-frame stall都只唤醒同一个reconcile;其余健康分支保持sticky。候选按当前身份、source reachability、无环、effective capacity与sender transition硬过滤,再按结果深度、剩余sender slots、稳定加入顺序和peer identity排序;因此 Host root退出后新释放的Host槽位比已有root的更深槽位优先,但健康旧边不因周期性平衡而迁移。server admission在执行当前cursor时独立reserve。candidate child首帧ready触发`settleReady(commitAdmissionSync)`,成功prepare `P`提交active `P`;失败释放资源并用同一monotonic allocator广播`R>P` rollback,cursor推进但总deadline不重置。bounded-gap先preflight server resource,再retire producer、按新graph重算未尝试suffix,并在切断healthy edge时追加一个普通restore candidate。relay自身ingress失效时作为child换父并保留subtree;relay disconnect/cap降低让overflow children逐个走同一事务,childless后删除。房间admission和无环约束最大深度,不设独立hard cap。普通peer始终STUN-only,SFU使用独立admission。 - 观看端连接反馈:Viewer 舞台从鉴权到首帧使用一个由现有权威状态推导的稳定阶段反馈,至少区分等待分享、分配线路、建立 P2P、连接备用线路、等待首帧、浏览器阻止自动播放、恢复连接和有界失败。阶段不是进度百分比,也不产生 route authority;只有当前 video element 实际提交首帧后才显示正在播放。播放按钮只在 `play()` 因自动播放策略被拒绝时稳定出现,连接中不得忽隐忽现;明确失败后才提供一次受现有 controller 约束的重试入口。make-before-break 或仍有可见旧帧时保留画面并叠加恢复状态,不为显示反馈主动清空健康 media element。 - 质量控制扩展:推荐档位仍是默认入口;“分享高级设置”只允许 720p/1080p/1440p 分辨率 ceiling、15 至 60 的整数 FPS ceiling、2 至 12 Mbps 视频码率 ceiling、清晰/平衡/流畅三种偏好、`自动 | H.264 | VP8` 编码选择,以及离散的 `64 kbps 省流 | 128 kbps 音乐默认 | 256 kbps 极高音质` 屏幕音频 sender ceiling;初始值采用 `balanced`、`automatic` 与 128 kbps。视频档位和音频档位在直播中显式应用;音频变更只更新固定 Opus 路径的 sender ceiling,不是 codec 切换。所有当前及以后建立的 Web Host、Browser relay 与 SFU sender 都读取最新 desired 档位,并各自在 fresh sender parameters 上应用和读回 endpoint-local applied 值;实际码率仍由浏览器拥塞控制和 stats 决定。Peer 只在唯一明确的 active Opus audio 上做结构化、幂等的 Viewer answer `stereo=1;maxaveragebitrate=256000` 变换,使同一接收上限覆盖三档;非目标形态或 parser/writer 异常必须原样保留浏览器 answer,不得阻断可继续的 mono/video edge。不得用宽泛字符串/正则改 SDP,也不得改写浏览器协商的 FEC。SFU 将三档映射到 pinned SDK 的 music stereo preset 或显式 256 kbps ceiling,统一保留 `forceStereo = true`、`dtx = false` 与 pinned room default `red = true`。不得暴露任意码率、声道、采样率、audio codec、stereo、DTX、RED 或 FEC 控件;256 kbps 只是显式较高上限,不是无损、高保真或最低码率保证。实际质量持续低于所选上限时必须显示可解释的限制原因;在真实拥塞、编码器和接收端证据完成前,不把任一组合称为“完美平衡”。未来 microphone voice 若进入范围,必须是独立 track/处理链,不能把 AEC/降噪/DTX 套到电影或游戏音频。 - SFU 质量设置生命周期:直播中修改档位时,浏览器 sender 的 applied readback、LiveKit `LocalVideoTrack` 保存的 degradation preference、track restart options 与 `LocalTrackPublication` 的 republish options 必须作为同一次串行操作更新;任一步失败恢复旧 sender 与旧 SDK 保存态,旧 publication generation 不得回写当前状态。LiveKit full reconnect/republish 不得恢复初次发布的旧偏好。该修复不改变 codec、码率上限、层数或路由,也不能单独解释即时低帧。 @@ -54,26 +54,26 @@ SFU/TURN 的 publication、subscription、transport、server-resource accounting - 原生 shared encode relay:具备原生 relay 能力的非服务器端点同样受统一服务端权威下游容量约束,并应复用兼容编码输出而不再次编码。当前普通浏览器 relay 使用逐边重编码的标准 WebRTC 路径,不伪称 shared encode。 - SVC 重开条件:当前标准 Web/LiveKit SVC 已静态拒绝为 `no-go-web-svc-cross-path-hardware-contract`。sender `scalabilityMode` 没有 direct/peer receiver 对应的按层选择或跨 PeerConnection shared-encode 契约;requested/applied mode、Media Capabilities `powerEfficient` 和 RTCStats encoder 字段也不能证明没有静默软件 fallback。Pinned client 2.22.0 还会把 screen-share SVC 覆盖为三层 temporal-only `L1T3`,不满足低分辨率 base 与最多两层。只有未来 native sender 能明确选择硬件编码器、最多发布两层、将同一编码输出复用于 direct/peer/SFU、保持每条路径独立选择并通过真实游戏和亚秒延迟门槛时,才可用新的 capability decision 重开 SVC;不能把浏览器 API 支持或 UA 推断当成采用理由。 - 分包/分层多树实验:把编码包、对象或可独立解码层拆到多个 peer 路径以分散上传,只能在独立 spike 中验证。必须保持亚秒级,处理多父重组、排序、丢包恢复、关键帧、同步和节点退出;未通过门槛前不进入产品路由控制器。 -- SFU/TURN 中央兜底:产品部署可以提供有界 SFU 与可选 TURN 资源,但不要求用户选模式,也不得默认变成整房 always-SFU。只有 Host 拥有一个 authoritative publication;SFU-fed 与 peer-fed endpoint 使用同一个 provisional-child first-frame transaction,严格受限 endpoint 在没有成功 child candidate 时自然保持 leaf。TURN 是某条授权 logical edge 或 Host-SFU ingress 的 selected transport,不是拓扑节点。启用 LiveKit fallback 时必须使用专用且关闭 `room.auto_create` 的实例,并显式提供私有 `LIVEKIT_API_URL`、无默认值的正整数 `SFU_INGRESS_CAPACITY` 与 `SFU_EGRESS_CAPACITY`。单进程全局账本按 exact room/share/publication 保存一份 Host publication ingress,并在该 generation 下按 exact Viewer 保存 subscription egress;当前 publication 被后续 SFU Viewer 复用,每个 child candidate 只 reserve/commit 自己的 subscription。一份 publication 占一个 ingress,每份 reserved、committed 或 draining subscription 占一个 egress;离开 route 的 subscription 在整个 publication room 删除并确认 absent 前仍作为 draining 计费,同一 Viewer handle 再次启用不重复计数。新 generation 在发 token 前 reserve publication 与首个 subscription,并先通过 RoomService 创建对应 room;first-frame ready同步commit admission后才promote graph。publication replacement、abort、timeout、disconnect、share rollover、room stop/delete按资源差集或controller `dispose()`进入draining,只有DeleteRoom成功和absent readback才释放该generation的ingress/egress。启动时必须先拒绝外来room、清除全部遗留Screener room并确认namespace为空;Host信令离线但LiveKit participant仍存在时继续保留和计费,participant消失后有界回收。实际因deployment-wide admission失败的房间只进入一个waiter Set;TURN usage释放或LiveKit drain完成时一次性touch这些controller的external fact并走普通reconcile,不轮询。固定root数和普通endpoint child数不能代替准入;资源耗尽进入下一candidate、明确waiting或failure。多应用进程部署必须先引入共享原子owner。 +- SFU 中央兜底:产品部署提供有界 SFU 资源,但不要求用户选模式,也不得默认变成整房 always-SFU。只有 Host 拥有一个 authoritative publication;SFU-fed 与 peer-fed endpoint 使用同一个 provisional-child first-frame transaction,严格受限 endpoint 在没有成功 child candidate 时自然保持 leaf。启用 LiveKit fallback 时必须使用专用且关闭 `room.auto_create` 的实例,并显式提供私有 `LIVEKIT_API_URL`、无默认值的正整数 `SFU_INGRESS_CAPACITY` 与 `SFU_EGRESS_CAPACITY`。单进程全局账本按 exact room/share/publication 保存一份 Host publication ingress,并在该 generation 下按 exact Viewer 保存 subscription egress;当前 publication 被后续 SFU Viewer 复用,每个 child candidate 只 reserve/commit 自己的 subscription。一份 publication 占一个 ingress,每份 reserved、committed 或 draining subscription 占一个 egress;离开 route 的 subscription 在整个 publication room 删除并确认 absent 前仍作为 draining 计费,同一 Viewer handle 再次启用不重复计数。新 generation 在发 token 前 reserve publication 与首个 subscription,并先通过 RoomService 创建对应 room;first-frame ready同步commit admission后才promote graph。publication replacement、abort、timeout、disconnect、share rollover、room stop/delete按资源差集或controller `dispose()`进入draining,只有DeleteRoom成功和absent readback才释放该generation的ingress/egress。启动时必须先拒绝外来room、清除全部遗留Screener room并确认namespace为空;Host信令离线但LiveKit participant仍存在时继续保留和计费,participant消失后有界回收。实际因deployment-wide admission失败的房间只进入一个waiter Set;LiveKit drain完成时一次性touch这些controller的external fact并走普通reconcile,不轮询。固定root数和普通endpoint child数不能代替准入;资源耗尽进入明确waiting或failure。多应用进程部署必须先引入共享原子owner。 - 分享端桌面封装:用 Electron 固定 Chromium 版本、改善选源/热键/系统音频体验,同时保留 Web 观看端。 - Windows 应用音频:Windows 11 Native sender 使用本地 opaque window target 绑定 HWND、PID 与进程创建时间,只捕获所选游戏及其子进程的音频,避免把语音聊天或通知声一并分享;“全部系统声音”只能由分享者显式选择。普通 Windows 10 不具备当前 process-loopback API 门槛,必须明确标为未解决/不支持而不是伪兼容。浏览器无法证明或隔离任意进程音频时必须如实提示,原生捕获失败、无声或进程退出不得静默扩大为整机捕获。 -- 受限网络增强:短期鉴权 TURN 只有在完整、默认关闭的部署配置和当前 route controller 明确授权时可用;普通 peer PC 从创建起保持 STUN-only,不得向整个 participant、房间或所有 peer connections 预发 TURN candidates。凭据必须绑定 exact room/share/session/route/connection generation,短期有效、内存态、受配额约束且不进入 URL、日志、浏览器持久化或服务端持久存储。TURN 只替换授权 edge 的 transport;Host 无 direct first-level path 时可作为单条 Host-SFU ingress 的 transport。TURN 不得被描述为更高清的传输方式。 +- 受限网络边界:当前媒体只使用 ordinary direct/STUN UDP 与 LiveKit SFU/UDP;全 UDP 封锁、只允许标准 HTTPS 出站等网络在有界窗口后明确失败。不得用 TCP 探测、客户端声称的 NAT 类型或另一套应用 route candidate 猜测可达性。若真实矩阵证明该失败边界需要扩展,先比较 LiveKit 内建 ICE/TCP 或其内部安全 relay transport,并另行接受安全、端口、延迟和部署合同。 - Web 编码选择:分享高级设置提供 `自动 | H.264 | VP8`,默认自动使用浏览器/LiveKit 协商;首次分享前直接应用,分享中只在 Host 已显式暂停后允许切换。暂停切换与 route mutation 串行,使用 generation-fenced P2P/relay 重协商和 SFU republish;恢复播放后才以实际 codec 与 decoded-frame progress 验证,失败重新暂停并有界回滚。P2P/浏览器 relay 保留完整 repair 与其他 codec 回退列表,SFU 保持 `backupCodec = false`,不维护第二套应用编码链。实际 codec/profile、encoder implementation 与 stats 是结果真相;H.264 可选不等于浏览器必走硬编,AV1 仍不得仅凭压缩率推断默认启用。 ### P2 - 可选功能 - 运行时有效容量:部署的 steady cap `C` 仍只接受 `1`、`2` 或 `3` 且默认 `2`,不按角色、设备或 UA 分类。服务端把 authenticated endpoint 当前广告的 `0..C` 可用值 clamp 成唯一 `effectiveDownstreamCapacity`;`0` 表示当前不承担下游,不是另一套 release tier。controller 通过同一个 reconcile 迁移 deterministic overflow children。H.264、hardware encode 或 shared encode 可以降低编码成本或同观感码率,但每个 ordinary child 仍消耗一份网络副本。 - 原生 Windows 捕获辅助程序:显式 `native-window-h264` 模式使用同一 bound window target 的 WGC/D3D11 画面与 WASAPI process-tree PCM,并用 adapter-bound、hardware-only Media Foundation H.264 `42c01f` 直接进入现有 Go fanout;普通 Web Host 独立采用默认 `automatic`、可显式选择 H.264/VP8 的标准协商路径,不允许运行时静默改用软件编码、显示器画面或系统混音。一名普通 Chrome Viewer 已 decoded/rendered 203 帧并收到 500 Opus packets,同一 helper PID/adapter 的 `VideoEncode` 与 LUID 匹配。 -- Viewer 向电视输出:系统级屏幕镜像现在可直接使用;产品只复用平台 AirPlay/Cast,不自造投屏协议。截至 2026-08-21,Chromium 的 Remote Playback URL 路径和 WebKit 明确拒绝 wireless target 的 live `MediaStream` player 均未给当前 WebRTC `srcObject` 提供可用契约,因此不显示伪可用入口。只在浏览器实现明确支持并由实体 Apple TV 或 Chromecast/Google TV 证明当前 live 画面与音频后,才按通过的平台加入 Viewer 本地图标按钮。投屏不能改变 room、TURN/SFU/peer route、Host fanout 或服务端媒体分发;Google Cast receiver 需要可获取的媒体 URL,当前 P2P `MediaStream` 不满足该输入,因此不加入 Cast SDK/receiver。 -- 多地区 server-assisted ingress/egress 调度,以及可选 TURN 兼容层的按地区、运营商可达率。 +- Viewer 向电视输出:系统级屏幕镜像现在可直接使用;产品只复用平台 AirPlay/Cast,不自造投屏协议。截至 2026-08-21,Chromium 的 Remote Playback URL 路径和 WebKit 明确拒绝 wireless target 的 live `MediaStream` player 均未给当前 WebRTC `srcObject` 提供可用契约,因此不显示伪可用入口。只在浏览器实现明确支持并由实体 Apple TV 或 Chromecast/Google TV 证明当前 live 画面与音频后,才按通过的平台加入 Viewer 本地图标按钮。投屏不能改变 room、SFU/peer route、Host fanout 或服务端媒体分发;Google Cast receiver 需要可获取的媒体 URL,当前 P2P `MediaStream` 不满足该输入,因此不加入 Cast SDK/receiver。 +- 多地区 server-assisted ingress/egress 调度及 SFU 的按地区、运营商可达率。 ## 非功能需求 -- 性能:在 RTT 不高于 40 ms、丢包不高于 1% 的受控 P2P 路径中,玻璃到玻璃延迟目标为 p50 不高于 150 ms、p95 不高于 250 ms;区域 SFU/UDP root 路径的 p95 目标不高于 350 ms。可选 TURN 单独测量,不能借用该目标宣称通过。 +- 性能:在 RTT 不高于 40 ms、丢包不高于 1% 的受控 P2P 路径中,玻璃到玻璃延迟目标为 p50 不高于 150 ms、p95 不高于 250 ms;区域 SFU/UDP root 路径的 p95 目标不高于 350 ms。 - 启播:观看者请求观看后,除用户授权耗时外,3 秒内出现首帧;从请求开始到首帧期间持续显示真实、稳定且可访问的连接阶段,不用黑屏、`playing` 事件或 ICE `connected` 冒充已播放。 - 流畅性:选择 60 fps 档位时,不能在没有降档或提示的情况下连续 5 秒低于 50 个解码帧每秒。 -- 成本:direct/peer UDP 不经过应用服务器。SFU/TURN 的 ingress、egress、allocation 和并发上限由整体路由模型明确配置;验收逐 hop 记录 Host、peer、SFU、TURN 和供应商计费的真实 bytes,不得用普通端点 child 数或有效载荷近似代替中央资源准入。 -- 安全:应用控制面全程 HTTPS/WSS;site access、Host token 与 room-scoped Viewer grant 权限正交,任何一个 Viewer grant 只授予一个房间的观看角色。P2P/TURN 边在 WebRTC 端点间使用 DTLS-SRTP,普通 SFU 是逐跳 DTLS-SRTP;只有另行实现并验收应用层 E2EE 才能对 SFU operator 保持媒体端到端机密。仓库、服务端持久存储、日志和错误不得包含明文密码或 grant。已鉴权 Host 与 shipped clients 是当前私密房间中的可信媒体参与者;越权房间、角色和未授权 subscription 必须拒绝。旧 generation 不得重新取得 route authority,并在有界 drain 后由 exact-room 删除阻止再次加入。 +- 成本:direct/peer UDP 不经过应用服务器。SFU 的 ingress、egress 和并发上限由整体路由模型明确配置;验收逐 hop 记录 Host、peer、SFU 和供应商计费的真实 bytes,不得用普通端点 child 数或有效载荷近似代替中央资源准入。 +- 安全:应用控制面全程 HTTPS/WSS;site access、Host token 与 room-scoped Viewer grant 权限正交,任何一个 Viewer grant 只授予一个房间的观看角色。P2P 边在 WebRTC 端点间使用 DTLS-SRTP,普通 SFU 是逐跳 DTLS-SRTP;只有另行实现并验收应用层 E2EE 才能对 SFU operator 保持媒体端到端机密。仓库、服务端持久存储、日志和错误不得包含明文密码或 grant。已鉴权 Host 与 shipped clients 是当前私密房间中的可信媒体参与者;越权房间、角色和未授权 subscription 必须拒绝。旧 generation 不得重新取得 route authority,并在有界 drain 后由 exact-room 删除阻止再次加入。 - 隐私:P2P 会向房间内好友暴露网络地址,首版威胁模型仅覆盖可信熟人;若要求隐藏 IP,必须切换为服务器转发。 - 可维护性:代码、需求、调研、项目记忆、`AGENTS.md` 和 `.codex/` 均在 Git 中跟踪;开发配置不得依赖单一操作系统的绝对路径。 - 迁移完整性:项目拥有者明确宣布公开发布前,只维护当前内部产品合同,不承担 backward/forward compatibility。客户端、服务端和部署配置按同一 release 原子迁移;旧客户端在 authority 前拒绝,替代后立即删除旧配置、wire、parser、双写、translator、deprecated alias、migration shim 与仅服务旧版本的测试,不保留第二套架构。Git 历史承担旧实现追溯;公开发布后才按明确的新要求重新评估兼容策略。 @@ -84,8 +84,8 @@ SFU/TURN 的 publication、subscription、transport、server-resource accounting - 首版不包含远程键鼠控制、公开/大规模直播、录制、社区系统或完整语音聊天;大规模分享直接使用现有直播服务。 - `open` code-only 入口只省略房间密码,不省略站点访问;四位房间码只定位,不提供隐私保证、目录或防转发承诺。现有每房 Viewer 上限、全局/未鉴权连接上限、房间上限和中央 egress/admission cap 仍适用,任何访问方式都不能创建无界服务器 fanout。 -- 房间控制面是单进程、单实例、易失内存状态;不得持久保存房间身份、租约、Host/Viewer credential、房间密码 verifier、成员、`clientId`、`peerId`、SDP、ICE candidate、IP 地址、媒体或 TURN 凭据。进程重启是明确的全部房间失效边界,而不是恢复或迁移事件。 -- 20-Viewer admission 是产品边界,不承诺任意机器、档位、网络或跨洲路径都达到相同画质;endpoint cap、SFU/TURN admission 和明确 wait/failure 继续保护资源。 +- 房间控制面是单进程、单实例、易失内存状态;不得持久保存房间身份、租约、Host/Viewer credential、房间密码 verifier、成员、`clientId`、`peerId`、SDP、ICE candidate、IP 地址或媒体。进程重启是明确的全部房间失效边界,而不是恢复或迁移事件。 +- 20-Viewer admission 是产品边界,不承诺任意机器、档位、网络或跨洲路径都达到相同画质;endpoint cap、SFU admission 和明确 wait/failure 继续保护资源。 - 浏览器规范要求每次由用户主动选择分享源,权限不能永久保存;纯网页不能保证指定游戏窗口或始终获得系统音频。 - 1080p60 是能力目标,实际分辨率、帧率和硬件编码由浏览器、GPU、驱动、画面复杂度及网络共同决定。 - 旗舰 release 的正常房间都可使用同一自动 controller;部署未启用总开关的轻量实例才保持标准 WebRTC P2P。Peer relay 只复用标准 remote-track WebRTC,不加入 Encoded Transform/DataChannel/WebCodecs 媒体协议,也不自研 UDP、编解码器、拥塞控制、FEC/RTX、多树、转码、codec ladder 或综合评分器。独立 native shared encode sender 不在本实验内;跨模式迁移受 ADR-0005 的 exact attempt、candidate-child first frame 和 typed resource admission 共同约束。 @@ -108,17 +108,17 @@ SFU/TURN 的 publication、subscription、transport、server-resource accounting - Given 应用进程重启,When 旧 Host、Viewer 或 code-only 链接再次访问,Then 所有旧房间、租约、摘要、密码 verifier 和参与者均已丢弃并 fail closed;运行时不读取数据库、迁移旧 schema 或恢复旧号码。 - Given 部署切换了不兼容的当前协议,When 仍打开的旧页面连接新服务端,Then 单一 release/protocol mismatch 边界终止该 session、停止自动重连并明确提示刷新;不得通过旧 parser、双写或双协议继续假连接。 -### P2P、SFU 与可选 TURN 兜底 +### P2P 与 SFU 兜底 - Given 一个应用进程已经持有配置的监听地址,When 另一个进程以相同地址启动,Then 竞争进程必须在任何 LiveKit list/delete/create 前因 bind 失败退出。Given owner 已 bind 但 LiveKit startup reconciliation 尚未完成,Then HTTP 只返回 `503` 且 signaling 不接受连接;只有 foreign-room 检查、managed-room 清场和 empty readback 全部成功后才开放服务。 - Given 两端网络允许 UDP 打洞,When 观看者加入,Then `getStats()` 显示选中的候选对不是 `relay`,且服务器没有承载媒体流量。 -- Given ordinary peer edge 被创建或重建,When 检查其 ICE 配置,Then 只包含部署 STUN,不包含预发给 participant、房间或普通 peer 的 TURN candidate。 +- Given ordinary peer edge 被创建或重建,When 检查其 ICE 配置,Then 只包含部署 STUN。 - Given route candidate、signal、recovery 或 commit 的 authenticated room/endpoint session、share generation、base/pending revision 或 candidate connection identity 任一不匹配,When 旧消息或异步结果到达,Then 该操作 fail closed 且不能替换当前媒体;SFU publication generation 继续由 resource owner 单独验证。 - Given 新路线正在准备,When exact candidate child 尚未解码出第一帧新画面,Then 可用旧路线保持权威;首帧后只原子提交该 candidate,失败或统一 deadline 到期销毁候选并保留或恢复旧路线。 -- Given 所有已接受且已配置的媒体路径耗尽,When 有界连接窗口结束,Then 界面显示通用等待或连接失败,不静默黑屏、不长时间假连接,也不尝试未授权路径。SFU/TURN 的精确 path order 和 resource accounting 按 ADR-0005 验证。 +- Given 所有 direct 与 SFU 媒体路径耗尽,When 有界连接窗口结束,Then 界面显示通用等待或连接失败,不静默黑屏、不长时间假连接,也不尝试未授权路径。SFU 的精确 path order 和 resource accounting 按 ADR-0005 验证。 - Given 同一 committed graph 和候选集合以不同枚举顺序输入 controller,When 新 Viewer 加入或 exact failed edge 触发换父,Then 硬过滤结果相同,选择稳定落到最浅且分配后剩余容量最多的合格 parent,并以加入顺序和 peer identity 打破平局;健康 current edge 不因出现更优候选而迁移。Given 第一候选 ICE 或首帧失败,Then controller 先幂等清理该候选再尝试下一候选,且不会启动 all-pairs probe;若候选创建了受管 SFU room,其 server capacity 在删除并证明 room 不存在前仍计费。 -- Given owned endpoint-dependent NAT emulator 中 ordinary ICE 可重复失败,When isolated guessed-candidate harness 运行,Then 只有满足[低服务器成本媒体路由](./research/low-server-media-routes.md)的 candidate budget、双浏览器重复性、首帧 deadline、隐私和 cleanup gate 才可继续。门禁前 ordinary room 不接收 guessed candidate;门禁后也只允许一个人工批准的 operator-owned test room,在双方 endpoint 显式同意、default-off kill switch 以及 exact room/edge/ICE-generation/deadline/aggregate-budget 约束下运行 canary。任何失败都关闭该候选并保留现有 SFU/TURN fallback。 +- Given owned endpoint-dependent NAT emulator 中 ordinary ICE 可重复失败,When isolated guessed-candidate harness 运行,Then 只有满足[低服务器成本媒体路由](./research/low-server-media-routes.md)的 candidate budget、双浏览器重复性、首帧 deadline、隐私和 cleanup gate 才可继续。门禁前 ordinary room 不接收 guessed candidate;门禁后也只允许一个人工批准的 operator-owned test room,在双方 endpoint 显式同意、default-off kill switch 以及 exact room/edge/ICE-generation/deadline/aggregate-budget 约束下运行 canary。任何失败都关闭该候选并保留现有 SFU fallback。 ### 浏览器与移动端观看 @@ -129,28 +129,29 @@ SFU/TURN 的 publication、subscription、transport、server-resource accounting - Given 用户查看本机身份,When 尚未进入编辑状态,Then 界面显示“昵称”、当前昵称和紧贴其后的铅笔按钮,不显示默认可编辑输入框。Given 用户修改昵称后以 Host 或 Viewer 加入房间,Then 所有在线参与者看到同一份服务端权威 roster、对应房间标题;唯一名称只显示名称,重名项追加当前房间 `peerId` 短后缀。刷新后本机名称仍在,清除站点数据后恢复未命名默认值,服务器不存在显示名或成员的持久记录,展示身份不参与授权或路由。 - Given 手机从 Wi-Fi 切换到蜂窝网络或页面从后台恢复,When 原候选对失效,Then 客户端执行 ICE restart 或重建连接,并显示恢复进度而不是永久黑屏。 - Given Viewer 已鉴权但尚未提交首帧,When route 从 peer prepare 进入 SFU fallback 或等待媒体,Then 固定舞台依次显示当前真实阶段且不跳动、不显示虚假百分比;只有当前代 video frame 被 compositor 接收后才显示正在播放。Given `video.play()` 仅因 autoplay policy 被拒绝,Then 稳定显示一个播放动作;连接尚未完成时不显示忽隐忽现的播放按钮。Given 旧媒体仍可见且正在 MBB 恢复,Then 保留旧画面并叠加恢复状态。 -- Given Viewer 尝试把直播输出到电视,When 浏览器/系统提供已验证的 Remote Playback、AirPlay 或屏幕镜像能力,Then 该动作只改变本机输出设备,现有 upstream PeerConnection、TURN/SFU/peer route 和其他参与者保持不变。Given live `srcObject` 未经目标设备证明或 API 缺失,Then 产品不显示伪可用 Cast/AirPlay 按钮,也不创建自有 receiver 或转码服务器。 +- Given Viewer 尝试把直播输出到电视,When 浏览器/系统提供已验证的 Remote Playback、AirPlay 或屏幕镜像能力,Then 该动作只改变本机输出设备,现有 upstream PeerConnection、SFU/peer route 和其他参与者保持不变。Given live `srcObject` 未经目标设备证明或 API 缺失,Then 产品不显示伪可用 Cast/AirPlay 按钮,也不创建自有 receiver 或转码服务器。 ### 一对多与带宽保护 -- Given `ENDPOINT_MEDIA_COPY_CAPACITY=C`,其中 `C` 只能是 `1`、`2` 或 `3` 且默认 `2`,When Browser Host 或任一 Browser Viewer 建立、恢复、重鉴权或迁移媒体路径,Then peer child 与 Host publication 的稳态实际发送副本总数不超过 `C`,upstream receive 不计数,committed selected TURN 不重复计数,角色、浏览器和 UA 不改变 `C`;每个 `authenticated` 快照都携带同一个 `C`,并行候选只有持有 `TransitionOverlapSlot` 时才可达到 `min(C + 1, 3)`,并在 commit/abort 后释放。When 当前房间合同上线,Then server 与 Browser assets 在同一 `screener-v8` release 原子升级,缺失或越界的 `C`、旧 wire 与 executable sender 都在房间授权前得到明确 mismatch,且 `C=3` 满载 Host 在取得会产生第四份 endpoint media copy 的 publication/token/grant 前明确等待或失败。 +- Given `ENDPOINT_MEDIA_COPY_CAPACITY=C`,其中 `C` 只能是 `1`、`2` 或 `3` 且默认 `2`,When Browser Host 或任一 Browser Viewer 建立、恢复、重鉴权或迁移媒体路径,Then peer child 与 Host publication 的稳态实际发送副本总数不超过 `C`,upstream receive 不计数,角色、浏览器和 UA 不改变 `C`;每个 `authenticated` 快照都携带同一个 `C`,并行候选只有持有 `TransitionOverlapSlot` 时才可达到 `min(C + 1, 3)`,并在 commit/abort 后释放。When 当前房间合同上线,Then server 与 Browser assets 在同一 release 原子升级,缺失或越界的 `C`、旧 wire 与 executable sender 都在房间授权前得到明确 mismatch,且 `C=3` 满载 Host 在取得会产生第四份 endpoint media copy 的 publication/token 前明确等待或失败。 - Given peer-assisted 路径分别运行 1、5、10、20 名观看者,When 用户加入、容量释放、节点离开、重鉴权或当前 edge 失效,Then 服务端只为新 Viewer、等待 Viewer 或失去 parent 的 subtree root 选择一个 active parent;每名 Viewer 同时至多一个 active upstream,活动图无环,其余健康边保持 sticky。没有可用 parent 时明确等待或拒绝,不主动重平衡或引入常驻多源媒体。 -- Given 一名分享者和 20 名观看者,When 运行 bounded browser smoke,Then 全部 Viewer 取得明确媒体或 bounded wait/failure,非服务器端点不超过 `C`,并记录 route depth、首帧时间、实际 sender 数、应用服务器 CPU/内存以及 SFU/TURN 用量。该 smoke 验证 admission、控制面效能和资源上界,不单独证明任意公网画质或 30 分钟 endurance。 +- Given 一个 Host root 退出并释放 Host sender slot,When 新 Viewer 加入或一个失父 child 进入同一 reconcile,Then 候选按结果深度优先,选择 Host 而不是另一 root 的更深空槽;已健康挂在另一 root 下的旧 child 不被周期性搬动。Given 需要判断历史房间为何形成某一拓扑,Then 只使用脱敏、代次绑定的 route-decision snapshot,不从当前拓扑反推已经丢失的事件。 +- Given 一名分享者和 20 名观看者,When 运行 bounded browser smoke,Then 全部 Viewer 取得明确媒体或 bounded wait/failure,非服务器端点不超过 `C`,并记录 route depth、首帧时间、实际 sender 数、应用服务器 CPU/内存以及 SFU 用量。该 smoke 验证 admission、控制面效能和资源上界,不单独证明任意公网画质或 30 分钟 endurance。 - Given candidate 改变房间 admission 默认值或上限,When 在目标数量与代表性路径上运行规定时长并注入 relay departure、重连和一个弱路径,Then 每名观看者持续解码、每个非服务器端点不超过配置容量、深度与故障半径有界、未受影响 subtree 不迁移,并记录端点与中央服务的 CPU、GPU、内存、上行、出口、帧率、丢包率、冻结和恢复;任一门槛失败时不得切换默认值。 - Given peer-assisted host 鉴权或直播中连续修改质量设置,When 服务端校验其分辨率、FPS、码率和偏好,Then 只有 host 的完整有界设置被接受,last-wins 地应用到当前和以后建立的 relay child sender 以及启用的 SFU publisher;越界、缺字段和多余字段被拒绝,普通 P2P 的鉴权 wire 不新增该字段且不接受同步消息。 - Given 标准浏览器把收到的 remote track 直接加入下游 `RTCPeerConnection`,When 检查 relay 统计和 libwebrtc 路径,Then 明确记录新的 outbound encoder、编码耗时和 CPU/GPU 限制,不把它当作 encoded forwarding 或 shared encode 成果。 - Given ADR-0004 的任一硬门槛只有在引入 Encoded Transform/DataChannel/WebCodecs 媒体、自定义拥塞控制、FEC/RTX、多树、综合评分、转码、codec ladder、浏览器 RTP 注入 hack 或放宽 endpoint capacity 后才能满足,When 结束 bounded spike,Then 放弃并删除该浏览器 relay 实验路径,保留标准 P2P 与整体路由模型已接受的 fallback;后续 native shared encode sender 仍按独立 ADR 推进,但不能据此把失败的 relay 拓扑判为通过。 - Given 新观看者会使预计总上行超过安全预算,When 其请求加入,Then 系统只选择整体路由模型已授权且满足配置容量与资源准入的路径;没有合格路线时明确等待或拒绝,而不是创建超过配置容量的 ordinary edge 或持续堆积发送队列。各 transport 的拥塞控制适应该路径,诊断证据不改变拓扑或合成全房降档。 -- Given SFU 或 TURN 参与路线,When 计算准入、迁移 overlap 或失败恢复,Then 同时检查统一端点容量和独立的 server ingress/egress、SFU、TURN 资源预算;不得把普通端点 child 数当作 server cap,也不得借兜底形成无界整房 fanout。 +- Given SFU 参与路线,When 计算准入、迁移 overlap 或失败恢复,Then 同时检查统一端点容量和独立的 server ingress/egress、SFU 资源预算;不得把普通端点 child 数当作 server cap,也不得借兜底形成无界整房 fanout。 - Given 配置完整 LiveKit credential tuple,When `LIVEKIT_API_URL` 缺失或不是允许的 control-plane origin,或 `SFU_INGRESS_CAPACITY` / `SFU_EGRESS_CAPACITY` 缺失、非正整数或超出 JavaScript safe-integer 范围,Then 服务必须在启动时 fail closed。Given 配置有效,When startup listing 出现非 Screener room、遗留 room 无法删除、删除后仍存在,或 exact candidate room 无法显式创建,Then 应用不得签发 token 或接受可能重复计算的 SFU generation。Given 当前 reserved、committed 与 draining 的 ingress 或 egress 会越过任一部署容量,Then controller 必须在签发任何新 SFU token 前拒绝该 candidate,并保持旧 route 和计数不变。Given generation 被 commit、abort、timeout、stop、delete 或 Host participant 离线回收,When DeleteRoom 尚未成功或 absent readback 尚未完成,Then 它必须继续占用账本资源且旧 token 不能创建 room。 -- Given 配置 selected TURN,When 四项 tuple 不完整或 `SELECTED_EDGE_TURN_ALLOCATION_CAPACITY` 不是正 safe integer,Then 服务在启动时 fail closed。Given 配置有效,When exact peer-selected edge 或 Host-SFU ingress 申请 credential,Then 同一个 deployment ledger 必须先 reserve `1`,并让 reserved、committed、draining authorization 持续计费至 exact logical release;容量耗尽保持旧 route 并走现有有界候选、等待或失败。Given stale room/share/session/revision/connection identity、重复 release、停止分享、删房或 shutdown,Then 只能释放匹配 authorization,不能误扣其他 edge。 -- Given Host、relay parent 或 leaf 处于受限网络,When direct/peer 路径不足,Then 只执行 ADR-0005 授权的 publisher、subscriber、selected transport 和 fallback sequence;不得靠未授权的固定数量或静态 lease 假设猜测实现。 +- Given Host、relay parent 或 leaf 处于受限网络,When direct/peer 路径不足,Then 只执行 ADR-0005 授权的 SFU publisher/subscriber fallback;不得靠未授权的固定数量、静态 lease、TCP probe 或 claimed NAT type 猜测实现。 ### 质量和恢复 -- Given 用户选择任一推荐档位或高级质量参数,When 接收端实际分辨率、帧率或码率持续显著低于该目标,Then 诊断数据能区分采集约束、发送编码、带宽/拥塞、TURN 路径和接收端限制;产品不得只改变档位标签却继续隐藏实际降级。 +- Given 用户选择任一推荐档位或高级质量参数,When 接收端实际分辨率、帧率或码率持续显著低于该目标,Then 诊断数据能区分采集约束、发送编码、带宽/拥塞、当前 peer/SFU 路径和接收端限制;产品不得只改变档位标签却继续隐藏实际降级。 - Given 一个健康 SFU leaf 和一个受控弱 SFU leaf 都以 `HIGH` ceiling 订阅,When 内建 LiveKit BWE 运行,Then 弱 leaf 自动使用唯一共享 `LOW` 并在恢复后回到 `HIGH`,健康 leaf 不受影响,且 Screener 没有实现媒体层 selector;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 当前产品只诊断而不迁移。任何后续质量驱动机制必须先用标准/框架算法与论文证据定义 exact-child 的可比较收益、最小观测窗、hysteresis/cooldown、旧边首帧前保留和最大尝试预算;不得把一个 child 的弱路径推断为 parent-wide 质量、定期重排健康 subtree 或用不透明全房加权分数。 - 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 operation 及其 current candidate/预留、保留 active graph、让新 Viewer waiting,并且不因没有 decoded frame 触发恢复;resume 后从当前 graph 重新 reconcile。 - Given exactly-two publication 的实测 encoder/CPU/GPU 或上传成本超过游戏预算,或没有合格 hardware/power-efficient 路径,When 评估该候选,Then 保留健康路径并明确显示弱路径降级或不可用,不得退回全房间最低目标或静默软件编码;若 supported sender cohort 无法以常驻或按需方式提供共享 `LOW`,Then 质量验收失败,必须优化编码路径或明确标记不支持。 @@ -164,7 +165,7 @@ SFU/TURN 的 publication、subscription、transport、server-resource accounting - Given 浏览器未返回系统音轨,When 分享开始,Then 分享者在启播前看到“当前来源没有可共享音频”的提示。 - Given 分享者选择标签页或窗口,When 浏览器支持对应音频捕获,Then picker 提供该来源音频而不扩大为系统混音。Given 分享者选择整个屏幕,When 浏览器支持系统音频,Then picker 提供由用户决定的系统音频开关。应用同时请求 `windowAudio: "window"` 和 `systemAudio: "include"`,不根据 track 存在性伪造来源标签。 - Given 浏览器返回分享音轨,When 开始发布,Then 应用设置标准 `contentHint = "music"` 作为内容意图元数据;该值可读回,但不作为 Opus、码率、声道、采样率、FEC、DTX 或立体声的质量证据。 -- Given Web Host、浏览器 relay 或 selected-edge 建立或替换分享音轨,When 对实际音频 sender 应用发送参数,Then 从当次 `getParameters()` 派生所选 `encodings[0].maxBitrate = 64000 | 128000 | 256000`、调用 `setParameters()` 并立即读回,旧配置缺失时按 128000 处理;Given Web Viewer 为含唯一有效 audio/Opus payload 的 offer 生成 answer,Then 用结构化 SDP parser 只 upsert 该 payload 的 `stereo=1` 与 `maxaveragebitrate=256000`,并把同一个结果用于 `setLocalDescription()` 和 signaling。首次连接、ICE restart、同 parent rebuild 和 relay child 都适用;空 SDP、无/歧义 Opus、多个有效 audio、重复目标参数、畸形 fmtp 或 parse/write 异常必须按对象 identity 返回浏览器原 answer 并继续,不能发送部分修改或静默冒充 stereo/实际码率。只有实际 `setLocalDescription()` 失败才进入既有连接错误路径。`sprop-stereo` 与 `usedtx` 不新增,其他 fmtp/FEC/session/media 字段逐项保留。 +- Given Web Host 或浏览器 relay 建立或替换分享音轨,When 对实际音频 sender 应用发送参数,Then 从当次 `getParameters()` 派生所选 `encodings[0].maxBitrate = 64000 | 128000 | 256000`、调用 `setParameters()` 并立即读回,旧配置缺失时按 128000 处理;Given Web Viewer 为含唯一有效 audio/Opus payload 的 offer 生成 answer,Then 用结构化 SDP parser 只 upsert 该 payload 的 `stereo=1` 与 `maxaveragebitrate=256000`,并把同一个结果用于 `setLocalDescription()` 和 signaling。首次连接、ICE restart、同 parent rebuild 和 relay child 都适用;空 SDP、无/歧义 Opus、多个有效 audio、重复目标参数、畸形 fmtp 或 parse/write 异常必须按对象 identity 返回浏览器原 answer 并继续,不能发送部分修改或静默冒充 stereo/实际码率。只有实际 `setLocalDescription()` 失败才进入既有连接错误路径。`sprop-stereo` 与 `usedtx` 不新增,其他 fmtp/FEC/session/media 字段逐项保留。 - Given 通过 SFU 发布分享音轨,When 构建 LiveKit 2.22.0 publish options,Then 64 kbps 使用 `AudioPresets.musicStereo`、128 kbps 使用 `AudioPresets.musicHighQualityStereo`、256 kbps 使用同等结构的显式 sender ceiling,三档都设置 `forceStereo = true` 与 `dtx = false`,且不覆盖 pinned room default `red = true`;验收同时核对 SDK 选项、applied ceiling、当前 RTP codec/fmtp 与 stats 实际码率,不能从 preset 名称或 `opus/48000/2` 单独推断实际声道内容。 - Given 分享正在直播或暂停且 Host 选择新的 64/128/256 kbps 音频档位,When 当前 profile mutation 串行执行,Then 服务端以 last-wins 保存并广播最新 desired profile,每个当前 Host、relay 与 SFU audio sender 从 fresh `getParameters()` 派生 `setParameters()` 并立即 readback,过期 completion 不得覆盖新值;以后创建的 sender 使用最新 desired profile。单个端点应用失败时保留媒体与其旧 applied 值、显示可见告警并允许后续更新或重建重试,不得谎称全房已收敛。当前 applied readback 保持 endpoint-local;没有显式回执 wire 就不宣称跨端点原子提交。该操作不改变 Opus、route revision、media-binding generation 或 publication generation,也不创建第二个音频控制环。当前 screen-audio Host 基线是 Chrome/Edge;其他浏览器只有实际 sender readback 成功时才算支持。 - Given 分享已显式暂停且 Host 选择 `自动 | H.264 | VP8` 的不同值,When 执行首个视频 codec 切换闭环,Then 为该请求分配单调 codec-negotiation generation,并与 room route mutation 串行;事务开始时冻结 exact current route/media binding target,离开的 Viewer 从 target 移除,新加入者等待事务结束。暂停期间对 target 的 P2P/relay path 完成显式 offer/answer 重协商,并对 SFU 路径完成新的 publication generation/republish;选择 `automatic` 时以空 codec preference 恢复浏览器默认。暂停帧不能充当媒体证明:恢复 source 后,每名 still-current target 发送一个只属于 codec transaction 的 proof,精确绑定 room/share、Viewer session、assignment/connection/media-binding/publication generation、codec-negotiation generation、实际 inbound codec 与 decoded-frame baseline/delta;该 proof 不进入路由失效判断。若 leave、hard failure 或任何 exact binding 变化使 target 失效,服务端把它移出本事务并标为 `route-wait`,codec 事务在 deadline 内以剩余 target settle 后释放 route lock,controller 再用最终 committed preference 为仍在线 Viewer 恢复路径。只有服务端校验全部仍在 target 内的 proof 后才提交 desired codec。失败时立即重新暂停,按旧 preference 开启有界 rollback generation;rollback 必须证明旧 preference 已重新应用(P2P 空/有序 preference 或 SFU fresh publish options),并由新协商代次报告实际 inbound codec 与 decoded-frame progress,不要求 actual codec 字面等于历史值。回滚失败则保持暂停并显示明确失败,不允许 stale generation 恢复播放。零 current Viewer 或事务中全部离开时直接提交 future-edge desired codec,无需伪造 frame proof。`pause`、`replaceTrack()` 或仅调用 `setCodecPreferences()` 均不能冒充已完成切换;本 slice 是明确的 paused bounded-gap 事务,未暂停 MBB 热切换留作后续扩展。 @@ -176,14 +177,14 @@ SFU/TURN 的 publication、subscription、transport、server-resource accounting ### 仓库可移植性 - Given 在新的 Windows、macOS 或 Linux 工作副本中克隆仓库,When 打开项目,Then 需求、调研、项目记忆、`AGENTS.md` 与 `.codex/` 均存在且处于 Git 跟踪状态,不依赖原开发机器的绝对路径或密钥。 -- Given 用户在自有公网服务器运行一键部署包,When 域名、TLS、端口、secret、容量、存储和回滚预检通过,Then 同一版本的应用、STUN/SFU/TURN 与反向代理可重复启动并输出精确健康结果;任一必需条件失败时不得显示 ready、写入占位凭据或留下半配置服务。 +- Given 用户在自有公网服务器运行一键部署包,When 域名、TLS、端口、secret、容量、存储和回滚预检通过,Then 同一版本的应用、STUN/SFU 与反向代理可重复启动并输出精确健康结果;任一必需条件失败时不得显示 ready、写入占位凭据或留下半配置服务。 - Given 用户安装本地自包含一键包,When 在 Windows、macOS 或 Linux 启动分享,Then 包内 Host、当前应用服务器和受限本地状态使用同一协议与配置合同启动,用户无需源码、Node 或任何外部服务,Viewer 邀请仍由普通浏览器打开。Given 公网 HTTPS/WSS、信令入口、媒体 UDP、网关映射或 NAT 路径任一不可用,Then 预检必须如实显示受限或不可达,不得把 loopback/LAN 成功写成公网成功;可工作的 direct P2P 保留,失败进入明确终态。 ## 待确认事项 - 哪些分享端硬件、质量档位和网络条件能让 20-Viewer admission 全部持续取得目标画质,以及生产默认值应在 20 人 smoke 后设为多少。 - 项目许可证及是否计划闭源商用。 -- 首批用户主要位于香港、内地还是跨地区;该信息决定 TURN 部署位置和测试矩阵。 +- 首批用户主要位于香港、内地还是跨地区;该信息决定 SFU 部署位置和测试矩阵。 - iOS Safari 与 Android Chrome 作为 Viewer 的最低版本及是否要求平板适配。 基础 WebRTC 证据与难度评估见 `docs/research/webrtc-p2p-screen-sharing.md`;peer-assisted、shared encode 和浏览器限制见 `docs/research/peer-assisted-media.md`。