From 83ce08302f01ec9caf8b3bd713450485531909a3 Mon Sep 17 00:00:00 2001 From: AAA <35992542+TNTcraftHIM@users.noreply.github.com> Date: Thu, 20 Aug 2026 17:15:08 +0800 Subject: [PATCH] docs(media): restore selected-edge transport policy (#71) * docs(deploy): record routing and TURN canaries * docs(media): restore selected-edge transport policy --- AGENTS.md | 6 +- docs/README.md | 6 +- docs/adr/0001-p2p-first-media-topology.md | 18 +- .../0005-automatic-hybrid-media-routing.md | 176 +++++----- .../0006-fixed-high-native-sender-canary.md | 6 +- docs/deployment.md | 171 ++++++---- docs/project-memory.md | 24 +- docs/research/advanced-peer-distribution.md | 6 +- docs/research/browser-screen-audio-quality.md | 4 +- docs/research/built-in-peer-ice-turn.md | 315 +++++------------- docs/research/low-server-media-routes.md | 112 ++++--- docs/research/native-shared-encode-sender.md | 6 +- docs/research/peer-assisted-media.md | 47 +-- docs/research/webrtc-p2p-screen-sharing.md | 32 +- docs/status.md | 34 +- docs/方案设计.md | 95 +++--- docs/需求理解.md | 18 +- 17 files changed, 499 insertions(+), 577 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8abf755..e77ed8d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,9 +4,9 @@ - Build private, low-latency game screen sharing for one broadcaster and a small group of trusted friends. - This is not a public or large-scale streaming product; users can use OBS/Twitch-class services for that workload. -- Keep media distributed and automatic. Use centralized services for rooms, authentication, signaling, STUN, observability, and bounded SFU-root capacity. TURN is an optional authenticated built-in ICE compatibility transport for explicit exact-room canaries, disabled unless its complete tuple is configured; it is not a media topology. +- Keep media distributed and automatic. Use centralized services for rooms, authentication, signaling, STUN, observability, and bounded SFU-root capacity. TURN is an optional authenticated selected-edge compatibility transport, disabled unless explicitly configured; it is not a media topology. - Let viewers join from a normal desktop or mobile browser without installing the sender application. -- STUN is required. In an explicitly allowlisted canary, each capable Web peer connection may start with STUN plus short-lived authenticated TURN/UDP under `iceTransportPolicy: "all"`; standard ICE preference keeps direct candidates first and may select relay without a second application transport state machine. A failed peer connection still follows one restart, one same-parent rebuild, one alternate peer, then an SFU/UDP virtual parent feeding at most two roots, followed by clear bounded failure. Non-allowlisted rooms and clients without the capability remain STUN-only. HTTPS/WSS remains TLS/TCP and is independent of this media-transport policy. +- STUN is required. The target ladder is direct/peer UDP, then an SFU/UDP virtual parent feeding at most two roots, then optional authenticated selected-edge TURN, followed by a clear bounded failure. Ordinary peer edges must not receive TURN candidates by default. HTTPS/WSS remains TLS/TCP and is independent of this media-transport policy. - Every non-server endpoint has at most two active downstream media edges; an upstream receive edge does not consume this upload budget. Browser relays remain stricter at one downstream edge until their re-encode/resource gates pass. Central media normally serves at most two roots; extra server-fed exceptional viewers require an explicit deployment egress/admission cap and must never create unbounded fanout. - Treat a web client as the initial delivery target. A packaged desktop sender or native capture helper is a later optimization, not an assumed requirement. - Do not silently change the product into an always-SFU conferencing system. An SFU is a virtual parent for one or two necessary roots whose peer descendants remain distributed; record any broader topology change as an ADR with measured justification. @@ -62,8 +62,6 @@ - Prefer the smallest proven extension point; do not add speculative frameworks, hooks, or abstractions without a current consumer. - Add automated tests in proportion to behavioral risk. Block the current phase on P0 security, privacy, irreversible data, authorization, generation, bounded-resource, and rollback failures, plus P1 failures of the milestone's core user path. Record or cover P2/P3 rare environments, diagnostic detail, and minor UI edges cheaply without turning them into canary blockers unless they can cause P0 harm. Use high-repeat race runs only for a reproduced concurrency failure. For realtime media changes, also document the manual network and browser matrix used. - Batch validation by risk and phase: run narrow checks while iterating, and run expensive full suites, browser matrices, endurance tests, or deployment checks only at an acceptance boundary or when a relevant change invalidates prior evidence. Documentation-only or unrelated changes do not invalidate media-path evidence. -- A dedicated deployment subagent owns each staged production deployment and verification while root/mainline continues; root supplies the exact merged SHA, artifact hash, rollback target, and acceptance list, and the deployer must not expand scope. -- Immutable production releases must not share hard-linked dependencies or other permission-mutated files with a rollback release. Copy or reflink an independent tree, verify zero shared regular-file inodes, and only then change ownership or switch the release symlink. - Use comments for non-obvious rationale, invariants, protocol constraints, and workarounds; do not narrate obvious code. - Use WebRTC statistics and reproducible measurements for latency, bitrate, candidate type, packet loss, encode time, and quality limitations. Do not claim performance from assumptions alone. - Keep repository scripts and hooks cross-platform, deterministic, fast, and runnable in CI. A hook must call a tracked script rather than hide project logic in machine-local configuration. diff --git a/docs/README.md b/docs/README.md index 713229b..0d7568c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,8 +8,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): SFU-root/peer distribution, built-in Peer ICE TURN, hop-level accounting, privacy-safe ICE evidence, and the bounded transport canary. -- [Built-in peer ICE TURN candidate](./research/built-in-peer-ice-turn.md): implemented default-off direct-first TURN fallback, short-lived participant-session bearer, refresh ownership, idle-allocation cost, and deployment gates. +- [Low-server-cost media routes](./research/low-server-media-routes.md): SFU-root/peer 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. - [Advanced peer distribution](./research/advanced-peer-distribution.md): multi-tree/SVC, encoded-object relay, native RTP forwarding, FEC/network-coding, MoQ, and measurable go/no-go 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. @@ -20,7 +20,7 @@ - [ADR-0001](./adr/0001-p2p-first-media-topology.md): accepted P2P-first media topology and its consequences. - [ADR-0002](./adr/0002-persistent-protected-rooms.md): Host admission, room-scoped private Viewer grants, explicit public-watch, rotation, and minimal SQLite persistence. - [ADR-0004](./adr/0004-peer-assisted-media-experiment.md): proposed, experiment-only standard-WebRTC peer-assisted spike with hard fanout and failure gates. -- [ADR-0005](./adr/0005-automatic-hybrid-media-routing.md): accepted direct/peer UDP -> SFU-root -> optional exceptional-edge TURN direction, with the current default-off controller and migration gates. +- [ADR-0005](./adr/0005-automatic-hybrid-media-routing.md): accepted direct/peer UDP -> SFU-root -> optional exceptional-edge TURN direction, with the room-`1` exact smoke and broader migration gates. - [ADR-0006](./adr/0006-fixed-high-native-sender-canary.md): proposed fixed-`HIGH` native canary, current no-go result, and staged revalidation boundary. - [ADR-0007](./adr/0007-demand-driven-dual-representation-quality.md): accepted path-isolated `HIGH + at most one LOW` policy, optional idle-layer stop, evidence contract, and SVC boundary. diff --git a/docs/adr/0001-p2p-first-media-topology.md b/docs/adr/0001-p2p-first-media-topology.md index 60d404a..a7d6992 100644 --- a/docs/adr/0001-p2p-first-media-topology.md +++ b/docs/adr/0001-p2p-first-media-topology.md @@ -10,11 +10,11 @@ peer-assisted media, and an enabled SFU fallback is a standing requirement and is governed by ADR-0005. Historical ADR-0003's explicit whole-room SFU mode is rejected/superseded. -Proposed ADR-0004 and the default-off ADR-0005 implementation do not yet -supersede this production baseline. ADR-0005 now accepts the target direct/peer -UDP -> SFU-root UDP -> optional exceptional-edge TURN ladder. Its config/wire -migration now exists only in the repository candidate; isolated exact-room -acceptance gates must still pass before production topology changes. +ADR-0004's bounded peer topology and ADR-0005's default-off automatic controller +now run only in the exact-room production smoke. ADR-0005 accepts the target +direct/peer UDP -> SFU-root UDP -> optional authenticated selected-edge TURN +ladder. Ordinary peer ICE remains STUN-only; the selected-edge config/wire is +not implemented or deployed and still requires isolated acceptance gates. ## Context @@ -27,17 +27,17 @@ Use separate control and media planes: - A small HTTPS/WSS service owns identity, rooms, invitations, presence, and WebRTC signaling. - A viewer can join through a numeric room code on desktop or mobile without installing the sharing client. The viewer link has no separate token or fragment; the host publication token remains internal. A deployment may place one site-wide password gate in front of both hosting and viewing; access policy remains control-plane state and does not change the media topology. ADR-0002 owns the current room-ID and lifetime policy. - ICE attempts a direct UDP path for every current broadcaster-viewer edge, using STUN to discover candidates. A later accepted peer-assisted topology must apply the same rule independently to each assigned parent-child edge. -- For the deployed MVP, only pairs that cannot connect directly use authenticated TURN. Its current production contract requires STUN plus TURN over UDP and TCP; TURN/TLS is an optional restrictive-network enhancement. ADR-0005 owns the accepted future change to optional, controller-selected exceptional-edge TURN. -- Candidate selection is independent per pair. A room may simultaneously contain direct and relayed viewers without moving working peers onto the server. +- Ordinary peer connections use STUN-only ICE. After direct/peer UDP and the bounded SFU/UDP virtual-parent path fail, ADR-0005 permits an optional authenticated TURN attempt for only the controller-selected exceptional edge. TURN/TCP or TURN/TLS remains a separately gated restrictive-network enhancement. +- Candidate and route selection is independent per edge. A room may simultaneously contain direct, SFU-root, and one selected relayed edge without moving healthy peers onto the server. - The deployed MVP broadcaster creates one peer connection per viewer. Rooms default to eight viewers and deployments may configure a limit from 1 through 16. Eight is an admission default, not a validated media-performance promise. A newer product target caps host media fanout at two; the current implementation does not satisfy that target above two viewers, and Proposed ADR-0004 owns the isolated experiment rather than silently changing this accepted baseline. -- An SFU is not the default whole-room path. Closed PR #12's explicit whole-room mode is superseded by merged PR #17's default-off automatic fallback. Its current code selects SFU only after peer recovery fails and remains unconfigured in production. ADR-0005 accepts SFU roots as the primary central fallback after direct/peer UDP while retaining distributed descendants; rollout remains gated. +- An SFU is not the default whole-room path. Closed PR #12's explicit whole-room mode is superseded by merged PR #17's automatic fallback. Production enables only a room-`1` exact smoke; current code selects SFU after peer recovery, but retained media and broad rollout remain unverified. ADR-0005 owns the bounded-root target. ## Consequences Positive: - Direct sessions consume almost no server media bandwidth and normally take the shortest network path. -- TURN cost is paid only for failed direct pairs. +- TURN cost is paid only for a controller-selected edge after its lower-cost UDP paths fail. - The initial product can be built with standard browser WebRTC and a small backend. - Friends can watch from a phone or unmanaged desktop through a link instead of installing a TeamSpeak-like full client. diff --git a/docs/adr/0005-automatic-hybrid-media-routing.md b/docs/adr/0005-automatic-hybrid-media-routing.md index 44a11e7..4d566c2 100644 --- a/docs/adr/0005-automatic-hybrid-media-routing.md +++ b/docs/adr/0005-automatic-hybrid-media-routing.md @@ -11,31 +11,30 @@ prohibited silent migration was an incorrect interpretation and is superseded by this decision. Simplicity constrains the controller and user experience; it does not require users to choose a media topology. -The ordered preference remains: +The ordered preference is: -1. a direct-selected P2P edge, with standard ICE allowed to select authenticated - TURN/UDP inside that same peer connection only in an explicit canary; +1. a direct-selected P2P/peer UDP edge using STUN-only ICE; 2. peer-assisted forwarding through an alternate eligible parent; 3. an SFU virtual parent supplied by the flagship deployment; and -4. bounded waiting or failure when no route can satisfy the budgets. +4. optional authenticated TURN for one controller-selected exceptional edge; and +5. bounded waiting or failure when no route can satisfy the budgets. -STUN is required. A complete default-off Peer ICE TURN tuple lets only capable -Web sessions in exact allowlisted rooms create peer connections with STUN plus -short-lived authenticated TURN/UDP and `iceTransportPolicy: "all"`. Standard -ICE type priority prefers direct candidates and can nominate relay without an -application route transition. Non-allowlisted rooms and clients without the -capability remain STUN-only. After peer recovery and alternate-parent options -are exhausted, the controller assigns one or two SFU roots. HTTPS/WSS remains -TLS/TCP and is outside this media policy. Every non-server endpoint has at most two active +STUN is required and ordinary peer connections remain STUN-only. After peer +recovery and alternate-parent options are exhausted, the controller assigns one +or two SFU roots. Only after the SFU/UDP path also fails may the same controller +authorize one short-lived TURN attempt for the current exceptional edge. TURN +credentials are not participant-wide ICE configuration and are never added to +ordinary peer connections by default. HTTPS/WSS remains TLS/TCP and is outside +this media policy. Every non-server endpoint has at most two active downstream media edges; its upstream receive edge does not consume that upload budget. The current browser relay remains stricter at one child until its re-encode and resource gates pass. TURN and SFU occupy different layers. A selected TURN candidate continuously -relays media for that ICE edge; it is not a handshake helper or a topology -decision. LiveKit TURN cannot rescue an unavailable SFU. Independent coturn may -transport an authorized participant's ordinary peer connection around a NAT -failure without changing the assigned peer topology. An SFU is a virtual topology parent and may feed only one +relays media for its authorized ICE edge; it is not a handshake helper or a +topology decision. LiveKit TURN cannot rescue an unavailable SFU. Independent +coturn may transport only the controller-selected peer edge after the SFU/UDP +attempt has failed. An SFU is a virtual topology parent and may feed only one or two necessary roots; after media reaches a reliable root, the existing bounded peer subtree remains the preferred distribution path. A zero-descendant SFU root is allowed only when no relay root satisfies @@ -71,22 +70,23 @@ The flagship deployment target supplies SFU capacity with a complete LiveKit endpoint/key/secret tuple. There is no user-facing topology selector and no process-wide `MEDIA_MODE` union. A deployment without that tuple ends at peer assistance and bounded waiting/failure and is not the final flagship route -configuration. Optional Peer ICE TURN is a separate exact-room deployment -tuple and is never a topology choice. +configuration. Optional selected-edge TURN is a separate default-off deployment +tuple and remains a controller-owned transport attempt, never a topology choice. ## Implementation Status Merged PR #17 implements this controller on top of merged PR #13, and PR #20 adds the bounded standby prewarm below. Production enables them only for persistent room `1`; other rooms stay ordinary P2P, and production remains -STUN-only. The current source adds a default-off Peer ICE TURN canary but does -not deploy it or modify coturn. +STUN-only. The current source still contains the rejected default-off built-in +TURN canary, but it is not deployed and must be removed or replaced before any +future TURN canary. The current runtime removes the old all-room coturn contract. Production -requires STUN and authenticated ICE snapshots contain only STUN servers. Source -now has a capability-gated optional TURN group plus expiry in `iceConfig`, and -strict `refresh-ice`/`ice-config` messages. They have no effect unless the new -complete tuple and exact-room allowlist both match. The tracked +requires STUN and authenticated ICE snapshots contain only STUN servers in +production. The participant-wide TURN group, capability, and refresh messages +in source belong only to the rejected candidate and are not the accepted target. +The tracked LiveKit sample explicitly sets `tcp_port: 0`, disables TCP fallback, supplies the deployment-owned STUN server, and configures no TURN service. The SFU controller still activates only after a peer edge exhausts recovery. Public participant entry has @@ -96,8 +96,8 @@ permanent compatibility branch or advertised current transport. The repository also requires a strict, non-empty `PEER_ASSISTED_ROOM_IDS` deployment allowlist whenever `PEER_ASSISTED_MEDIA=true`. Only exact listed room -IDs enter the controller or receive optional LiveKit standby, grants, or Peer -ICE TURN configuration. All other rooms retain the current ordinary P2P +IDs enter the controller or receive optional LiveKit standby, grants, or a +future selected-edge TURN attempt. All other rooms retain the current ordinary P2P authentication shape, STUN-only ICE, and signaling/quality/lifecycle behavior. Missing, blank, malformed, or duplicate entries fail startup; there is no configuration state that enables all rooms. The boundary is a temporary deployment-only validation gate with no browser selector, percentage @@ -111,12 +111,13 @@ descendants is still a root under the same authorization and total cap. The server currently retries a failed edge through the deterministic peer topology; only an exhausted peer route can request an SFU branch root. The target keeps healthy routes sticky but may select SFU directly when admission has no eligible -peer path. Optional Peer ICE TURN is not controller-selected: a capable Web -participant in the exact canary receives a short-lived participant-session -bearer and every bounded peer PC uses the same `all` configuration. Coturn -validates HMAC and expiry, not room, edge, parent, revision, or connection -generation, and the holder may reuse the bearer until expiry. LiveKit -publisher/subscriber ICE remains a separate participant-wide domain. +peer path. Optional TURN is controller-selected only after that SFU/UDP attempt +fails. The authorization must bind the current room/share generation, viewer and +parent sessions, route revision, and replaced/new connection identities; stale, +replayed, participant-wide, or unselected attempts fail closed. Coturn still +validates only HMAC and expiry, so application generation checks, short TTL, +one-use state, fanout, and quotas bound the bearer. LiveKit publisher/subscriber +ICE remains a separate participant-wide domain. Every viewer starts with zero relay capacity for each authenticated session. A peer-assisted Web client explicitly advertises either zero or one downstream @@ -217,23 +218,21 @@ Minimal protocol additions: - `route-ready { revision, phase: "prepare" | "active" }`; - `route-failed { revision, phase, connectionId }`; and - `refresh-sfu { revision }`; -- optional Web authentication capability `peerIceTurn: true`; -- `refresh-ice {}` from the current capable authenticated session; and -- `ice-config { iceConfig }` with the replacement short-lived config. +- a future controller-selected TURN grant for exactly one current edge; and +- a matching parent/child rebuild bound to the old and new connection identity. -Native v2 does not advertise the optional capability and therefore retains its -strict STUN-only authenticated shape; the server never sends it `ice-config`. -Pinned LiveKit embedded TURN remains participant-wide ICE configuration for a -LiveKit publisher or subscriber and is not the ordinary Peer ICE bearer. +Native v2 retains its strict STUN-only authenticated shape. Pinned LiveKit +embedded TURN remains participant-wide ICE configuration for a LiveKit +publisher or subscriber and is not the selected peer-edge grant. -The ordinary authenticated `iceConfig` contains `iceServers` populated from -`STUN_URLS`. Only an opted-in exact-room Web session receives one additional -TURN group and `turnCredentialsExpiresAt`. Its signaling client is the sole -refresh-timer owner, requests two minutes early, and updates existing PCs with -`setConfiguration()` without proactively restarting them. The server reuses -the current in-memory session grant until that window, then rotates it. A -signaling reconnect obtains a new participant-session bearer. No credential is -placed in URL, log, browser persistence, room data, or SQLite. +The ordinary authenticated `iceConfig` contains only `iceServers` populated +from `STUN_URLS`. The rejected built-in candidate still exists in source behind +a complete default-off tuple, but its first production canary failed before the +forced-relay phase and it is not an accepted deployment path. The replacement +selected-edge slice must remove participant-wide issuance and refresh state; +only its controller-selected rebuild may receive a short-lived relay-only +configuration. No credential may enter a URL, log, browser persistence, room +data, or SQLite. Authentication carries the current participant assignment and room revision, plus the non-secret standby URL when fallback is configured. It never carries a @@ -260,12 +259,10 @@ complete LiveKit tuple. by a newer connection generation cancels it; expiry enters the existing staged recovery at one ICE restart. It does not reuse or change the existing three-second recovery deadline. The 15-second boundary is unit-tested - but remains subject to mobile-network measurement. In an enabled exact-room - canary, the peer connection has already gathered - direct and relay candidates and standard ICE may select relay during these - attempts. When admission has no eligible peer path, or recovery is exhausted, - select the SFU root plan. Stock LiveKit participant ICE remains a distinct - domain. + but remains subject to mobile-network measurement. The peer connection is + STUN-only throughout these attempts. When admission has no eligible peer path, + or recovery is exhausted, select the SFU root plan. Stock LiveKit participant + ICE remains a distinct domain. 3. The server computes revision `R+1` without mutating the active topology. It sends a prepare plan only to the host and required fallback roots. 4. When fallback is configured, host and viewer clients have already made one @@ -292,10 +289,11 @@ complete LiveKit tuple. route. A failure after commit creates a new rollback/failure revision; stale acknowledgements cannot revive the abandoned plan. -8. An active SFU transport may request one fresh short-lived grant. If that - retry fails, or token issuance itself fails, the server creates a newer peer - baseline revision and disables SFU for the rest of the current share. Stop or - a new sharing generation clears that one-shot circuit breaker. +8. An active SFU transport may request one fresh short-lived grant. Current code + creates a newer peer baseline and disables SFU if that retry or token issuance + fails. The accepted replacement inserts one selected-edge TURN attempt before + terminal failure; it is not implemented yet. Stop or a new sharing generation + clears the one-shot circuit breaker. Production logs on 2026-08-20 observed two root participants for about 4.6 seconds and two short Host participants (about 0.46 and 0.27 seconds), all ending @@ -405,39 +403,37 @@ Sub-second failure recovery is a target after failure detection. The current plane needs a small 100-200 ms liveness/queue signal or an equivalent native transport event. This signal must be measured before its interval is fixed. -## Built-In Peer ICE TURN And SFU Migration +## Selected-Edge TURN After SFU -The accepted product direction supersedes the earlier dual-TURN shadow -candidate as the preferred central comparison. Healthy direct/peer UDP stays -distributed. When no such path can satisfy admission or recovery, one SFU -publication feeds one or two roots, which keep their bounded peer descendants. -Production still ends in bounded failure and has no TURN config. Current source -adds the default-off exact-room built-in Peer ICE tuple and wire described -above. It is participant-session bearer issuance, not assignment-level coturn -enforcement. LiveKit participant-wide embedded/external TURN is likewise not -configured by current production and remains a separate ICE domain. +Healthy direct/peer UDP stays distributed. When no such path can satisfy +admission or recovery, one SFU publication feeds one or two roots, which keep +their bounded peer descendants. Only an edge that also cannot use SFU/UDP may +receive a controller-selected authenticated TURN attempt. Production currently +ends in bounded failure and has no application TURN config. The default-off +built-in participant-wide candidate in source is rejected after its first +direct acceptance failed; it must not be enabled or treated as the target. +LiveKit participant-wide embedded/external TURN remains a separate ICE domain. The bounded cost model, privacy-safe ICE fields, and exact-room A/B sequence live in [Low-Server-Cost Media Routes](../research/low-server-media-routes.md). -For the normal-root baseline with no exceptional server-fed viewer, one equal -representation of bitrate `B` and `R` roots gives TURN seed host upload `R*B` -and central ingress plus egress `2R*B`; one SFU publisher gives host upload `B` -and central ingress plus egress `(R+1)*B`. Mixed representations use measured -`B_pub` and `sum(B_i)`. If `E` separately admitted exceptional viewers receive -bitrate `B_exc,j`, SFU egress adds `sum(B_exc,j)`; every TURN leg additionally -adds its own ingress and egress. Every host NIC, TURN ingress/egress, SFU -ingress/egress, root NIC, and exception NIC hop remains real traffic. +For one SFU publisher at bitrate `B_pub` and `R` roots at measured bitrates +`B_i`, host upload is `B_pub` and central ingress plus egress is +`B_pub + sum(B_i)`. If `E` separately admitted exceptional viewers receive +bitrate `B_exc,j`, SFU egress adds `sum(B_exc,j)`. Each selected TURN edge adds +its own measured ingress and egress; it is never multiplied across ordinary +peer PCs. Every host NIC, TURN ingress/egress, SFU ingress/egress, root NIC, and +exception NIC hop remains real traffic. Migration is gated, not optional design debate. Production has entered a shared-IP exact-room smoke while the old release remains a separate rollback -instance. Room `1` must still verify direct-selected peer, relay-selected peer, -SFU/UDP, and bounded failure with all UDP blocked; the current process never -restores the old all-room TURN wire. The matrix covers CGNAT, double NAT, mobile hotspot, +instance. Room `1` must still verify direct/peer UDP, SFU/UDP, one explicitly +selected relay edge, and bounded failure with all transports exhausted; the +current process never restores the old all-room TURN wire. The matrix covers CGNAT, double NAT, mobile hotspot, ordinary home networks, root departure, reconnect, SFU unavailable, and rollback. It records CPU seconds/GiB, NIC bytes/pps, RSS, host upload, p95/p99 forwarding latency, loss/recovery, final quality, host edges at most two, SFU roots at most two, and -unchanged healthy subtrees. Non-allowlisted and capability-absent sessions stay -STUN-only; only after that gate may the controller broaden beyond room `1`. +unchanged healthy subtrees. Every ordinary connection stays STUN-only; only +after that gate may the controller broaden beyond room `1`. A peer root re-publishing its received stream to the SFU while also feeding peer descendants remains a separate bounded candidate. The current browser @@ -469,8 +465,9 @@ record which boundary is actually configured and the UI must not claim E2EE. 5. Integrate host/viewer first-frame switching while retaining relay children. 6. Extend the tracked `1/3/5/8` benchmark with automatic fallback, peer/SFU UDP, bounded UDP-blocked failure, source/profile/pause/stop, and server-egress - measurements; separately measure idle TURN allocation/RSS/ports and selected - relay bandwidth. Only after those pass, add the 20-viewer gate before changing + measurements. After the SFU path passes, add one selected-edge TURN attempt + and measure allocation/RSS/ports and relay bandwidth. Only after those pass, + add the 20-viewer gate before changing the room default. ## Acceptance Gates @@ -494,8 +491,8 @@ record which boundary is actually configured and the UI must not claim E2EE. - In one process, non-allowlisted rooms preserve ordinary P2P route fields, directed signaling, quality rejection, stop/reconnect/delete semantics, and remain isolated from allowlisted peer/SFU state and receive STUN-only ICE. - Capability-absent clients, including Native v2, also remain STUN-only. Only a - capable session in an allowlisted room may receive TURN credentials and expiry. + Native v2 and every ordinary peer PC also remain STUN-only. Only a current + controller-selected exceptional edge may receive a short-lived TURN grant. - LiveKit/SFU UDP must pass CGNAT, double-NAT, hotspot, home-network, loss, rollback, and SFU-unavailable gates. Blocked UDP fails clearly within a bounded window even when TURN/UDP is enabled. Media TCP remains a separate @@ -508,9 +505,9 @@ Positive: - Users do not select or understand a topology. - Host fanout remains bounded while server media egress is paid only for fallback roots and separately admitted exceptional viewers. -- The ordinary peer path remains direct-first under standard ICE; current - production has no TURN media cost, and source confines the optional relay - candidate to an exact-room capability gate. +- The ordinary peer path remains direct-first and STUN-only; current production + has no TURN media cost, and a future relay attempt is confined to one + controller-selected exceptional edge after SFU/UDP. - Revisioned prepare/commit isolates stale asynchronous results without a continuous optimizer. @@ -525,9 +522,8 @@ Negative: An unconfigured client pays neither cost. - A media liveness signal and versioned cross-client transition expand the state space and require real failure-injection tests. -- Enabling built-in Peer ICE TURN creates unselected allocations for configured - peer connections; relay ports, coturn memory, refresh traffic, quotas, and any - selected media bandwidth must be measured on the 1-GiB host. +- A selected TURN edge adds relay ports, coturn memory, refresh traffic, and + two media NIC legs; quotas and the 1-GiB host budget must be measured. ## Relationship To Existing ADRs diff --git a/docs/adr/0006-fixed-high-native-sender-canary.md b/docs/adr/0006-fixed-high-native-sender-canary.md index 4b347f5..9a56254 100644 --- a/docs/adr/0006-fixed-high-native-sender-canary.md +++ b/docs/adr/0006-fixed-high-native-sender-canary.md @@ -125,9 +125,9 @@ stage while synchronously retaining a bounded final-negative snapshot: 5. Add viewer 3, prove no third edge and a host-side waiting observation while the viewer keeps its existing waiting state, then close viewer 1 and prove FIFO promotion plus decoding/rendering. -6. Keep this candidate STUN-only and capability-absent. Built-in Peer ICE TURN - is a Web exact-room canary; any Native opt-in requires its own bounded gate - rather than widening ordinary Native ICE or blocking the direct-path canary. +6. Keep this candidate STUN-only. Ordinary Native ICE never receives TURN + candidates; a future selected-edge attempt requires its own generation-bound + gate after SFU/UDP rather than widening or blocking the direct-path canary. Only after these stages pass may separate performance, quality, loss, reconnect, browser, audio/A-V-sync, packaging, and license gates begin. Do not diff --git a/docs/deployment.md b/docs/deployment.md index 5c46d75..1da2a9a 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -4,23 +4,27 @@ Last verified against upstream documentation: 2026-08-20. This section documents the repository's UDP-only deployment candidate: one Node.js process provides the built Web client, room API, and WebSocket signaling -behind Caddy or nginx; coturn runs in `stun-only` mode for address discovery; -LiveKit supplies bounded SFU-root capacity. Normal media remains distributed -through direct or peer edges whenever those paths work. +behind Caddy or nginx; application ICE advertises only STUN by default; LiveKit +supplies bounded SFU-root capacity. Normal media remains distributed through +direct or peer edges whenever those paths work. A deployment may additionally provide one single-node LiveKit process as the current controller's automatic final media fallback. This capacity is dormant unless the complete `LIVEKIT_URL`/key/secret tuple is configured. It does not replace the P2P path, the peer-assisted experiment, or required STUN discovery. LiveKit remains -ICE/UDP only. Source also contains a separate default-off built-in Peer ICE -TURN/UDP canary; production and the tracked coturn example remain STUN-only. +ICE/UDP only. Source still contains a rejected default-off built-in Peer ICE +TURN/UDP candidate. Its first production canary was no-go and fully rolled +back; it must not be configured. Production advertises STUN-only ICE and the tracked coturn example remains +`stun-only`. The shared host still retains its older authenticated-relay daemon +configuration and firewall range, but the application advertises no credential. -Production `d4bc421828c4` runs this candidate for exact room `1` on the existing -shared public IP. Immediate rollback to `05f98d10ecd1` uses the current v2 +Production `31bee238bc1e` runs this candidate for exact room `1` on the existing +shared public IP. Immediate rollback to `7fea60ef6f2` uses the current v2 database and environment. Rolling back the admission-policy cutover requires `89e6d7649169` plus the environment backup recorded below; only deeper -pre-access `9610032fc5f5` may restore matching v1 state. Coturn relay remains -rollback-only. This is a bounded smoke, not broad-rollout acceptance. +pre-access `9610032fc5f5` may restore matching v1 state. Application TURN +issuance is disabled and the failed canary backup is inactive. This is a +bounded smoke, not broad-rollout acceptance. ## Topology and prerequisites @@ -31,7 +35,7 @@ self-hosted STUN listener owns UDP 3478. ```text browser -- HTTPS/WSS --> Caddy or nginx :443 --> Node.js :8787 browser <------------ DTLS-SRTP P2P ------------> browser -browser -- STUN binding/UDP --> coturn stun-only :3478 +browser -- STUN binding/UDP --> coturn :3478 (application advertises STUN only) browser <---------- DTLS-SRTP/UDP ----------> LiveKit :7882 ``` @@ -57,11 +61,22 @@ copy-on-write reflink is acceptable only after an inode audit confirms that the old and new regular-file sets have zero shared inodes. Do not recursively change permissions until that check passes. +Transport archives must preserve UTF-8 entry names. Prefer the POSIX release +archive path and verify the extracted manifest before switching. A Windows ZIP +used for the 2026-08-20 route-rescue release altered two non-runtime Chinese +documentation names on Linux. Runtime artifacts were unaffected and the +immutable upload remains evidence, but do not reuse that packaging method. + In a strict-shell deployment, expected service states are data, not command failures. Do not call `systemctl is-active` bare under `set -e`/`ERR`: an intentionally stopped service returns a nonzero status and can trigger a false rollback. Read `ActiveState` with `systemctl show`, compare the returned string explicitly, and keep stop, symlink switch, start, health check, and rollback as separate steps. +Preflight every inspection dependency before taking a backup or changing a +service. If a host lacks the `sqlite3` CLI, use the pinned Node runtime's +`node:sqlite` API or stop before the first write. After a start, poll health for +a short bounded window instead of treating one request during startup as a +failed release; retain the last failure while still enforcing the deadline. Run `npm start` under a service supervisor that injects the environment, restarts on failure, and applies bounded logs. For a simple untracked environment file, @@ -86,18 +101,17 @@ direct/peer UDP first, then an SFU virtual parent feeding normally one or two roots, whose peer descendants continue carrying media. Multiple exceptional viewers that cannot attach behind a healthy root may consume additional server egress only under a separate explicit cap. -The repository requires STUN. It optionally accepts a complete -`PEER_ICE_TURN_*` tuple and issues short-lived coturn REST bearer credentials -only to capability-enabled Web sessions in an exact allowlisted room. Their -peer connections use `iceTransportPolicy: "all"`, so standard ICE candidate -priority prefers direct paths and can select TURN/UDP within the same PC. A PC -failure still owns the existing restart, rebuild, alternate-peer, then SFU -ladder. LiveKit participants receive only revision-bound `sfu-config` URL/token -messages and negotiate within LiveKit's separate ICE domain. +The repository requires STUN and ordinary peer connections receive STUN-only +ICE. The accepted ladder is direct/peer UDP, then the revision-bound SFU/UDP +virtual parent, then optional authenticated TURN for one controller-selected +exceptional edge. 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 not yet implemented or deployed. -`PEER_ASSISTED_ROOM_IDS` is also the Peer ICE TURN boundary. An unlisted room or -a client without the explicit capability gets STUN-only ICE and keeps ordinary -P2P routing. Run candidate rooms on an isolated instance/hostname and keep +`PEER_ASSISTED_ROOM_IDS` remains the topology/SFU canary boundary. Listed and +unlisted ordinary peer connections both get STUN-only ICE; only a future +selected-edge implementation may grant TURN to the controller's then-current +edge. Run candidate rooms 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. @@ -108,13 +122,33 @@ UDP 7882, Peer ICE TURN is disabled, 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. -The application-side TURN slice does not change coturn or production by itself. -TURN REST authentication proves only HMAC and expiry to coturn; the temporary -credential is an App-authorized participant-session bearer and may be reused by -its holder until expiry. Coturn cannot verify a room, route, edge, or connection -generation. Never describe it as a cryptographic selected-edge restriction. +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. +The 2026-08-20 exact-room canary used source `a11a73dfa79d`, inactive release +`/opt/screener/releases/a11a73dfa79d-r4`, artifact SHA-256 +`C954185869A3A15CCCE642AB72A4CF90770476C117D61182D7728280C30A036F`, and +backup `/opt/screener/backups/turn-a11-20260820T065933Z`. The candidate +switch completed at 14:59:33 +08. Local authenticated coturn allocation and +application/deployment gates passed, but the real direct Host plus Pion Viewer +did not establish its peer connection. The forced-relay test was therefore not +run. Issuance was removed first, then coturn and its firewall rules returned to +the pre-canary authenticated-relay baseline: TCP/UDP 3478 and UDP 49152-49251. +Exact `7fea60ef6f2ad14a9ac1c23a89a523d91bbb97e4` was restored and advertises no +TURN credential. The final lock-free check at 15:12:40 +08 found active +services, zero allocations, and matching health, configuration, firewall, and +SQLite state. +No canary test process, browser profile, or transient firewall rule remained. +The retained release is inactive. This is a canary no-go, not TURN deployment +or media evidence. Two earlier attempts changed no durable state: one stopped +before backup/write because `sqlite3` was absent, and one rolled back after a +single health request landed in the normal startup window. The preflight and +bounded-poll rules above are the retained fixes. + ## Production application environment Keep the real values in the process secret store or an untracked, access-restricted @@ -154,25 +188,13 @@ LIVEKIT_API_SECRET= MAX_SFU_ROOTS_PER_ROOM=2 ``` -To make built-in Peer ICE TURN available to capability-enabled Web clients in -the same exact rooms, provision authenticated coturn separately and add the -complete independent tuple: - -```dotenv -PEER_ICE_TURN_URLS=turn:turn.example.com:3478?transport=udp -PEER_ICE_TURN_SHARED_SECRET= -PEER_ICE_TURN_CREDENTIAL_TTL_SECONDS=600 -``` - -All three values must be present together, require -`PEER_ASSISTED_MEDIA=true`, and accept a TTL from 300 through 1800 seconds. -The first canary accepts exactly one explicit `turn:` UDP URI with -`transport=udp`. The secret -must not reuse Host admission or LiveKit credentials. Omitting the tuple keeps -authenticated ICE snapshots and every peer STUN-only. The Web client refreshes two minutes -before expiry with a 30-second skew floor; the server reuses a current session's -grant until that window and rate-limits refresh requests to one per five seconds. -Credentials never enter URLs, logs, browser persistence, room rows, or SQLite. +Do not configure the rejected `PEER_ICE_TURN_*` participant-wide tuple. A future +selected-edge release must introduce a distinct complete default-off tuple in +the same coherent change as its controller consumer, strict schema and rollback +tests; deployment documentation will name it only after that change lands. It +must accept one explicit TURN/UDP URI, use an independent secret and bounded TTL, +and issue credentials only for a current one-use route attempt. Credentials +never enter URLs, logs, browser persistence, room rows, or SQLite. `PEER_ASSISTED_ROOM_IDS` is the required deployment canary boundary whenever `PEER_ASSISTED_MEDIA=true`. Its non-empty value @@ -182,8 +204,8 @@ leading zeroes, and malformed IDs fail startup. A non-empty allowlist requires `PEER_ASSISTED_MEDIA=true`. Only listed rooms receive peer-assisted authentication, routing, room quality state, or optional LiveKit fallback; every other room keeps ordinary P2P route fields and signaling behavior. The -optional Peer ICE TURN snapshot is confined to capable sessions in listed -rooms; unlisted rooms remain STUN-only. Omitting or blanking the variable fails startup rather +future selected-edge TURN attempt is confined to a current controller edge in a +listed room; every ordinary peer connection remains STUN-only. Omitting or blanking the variable fails startup rather than enabling every room. There is no browser control, percentage rollout, or all-room fail-open. @@ -215,13 +237,14 @@ the application, and verify that its authenticated message contains `mediaMode: "peer-assisted"` while a second non-allowlisted room contains none of `mediaMode`, `qualitySettings`, `routeRevision`, `routeAssignment`, or `sfuStandbyUrl`. Exercise join, offer and -answer, stop, reconnect, and room deletion in both rooms. With the optional -tuple configured, only the capable Web session in the listed room receives a -TURN group and expiry; the other room and Native-shaped clients remain -STUN-only. Roll back by removing the complete Peer ICE TURN tuple before -restoring coturn `stun-only`, or direct traffic to the unchanged old -release. Do not treat disabling `PEER_ASSISTED_MEDIA` alone as rollback: remove -its dependent TURN tuple and exact-room allowlist together, and no removed old +answer, stop, reconnect, and room deletion in both rooms. Ordinary Web and +Native-shaped clients remain STUN-only. With a future 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 before restoring the exact recorded pre-canary coturn/firewall baseline, or direct +traffic to the unchanged old release. 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. After acceptance, retire or replace the temporary exact-room gate in a separate coherent change; never clear the value to trigger an implicit all-room rollout. @@ -353,6 +376,31 @@ or cgroup high/max/OOM events; host available memory was 373,469,184 bytes. No quality/reparent log or real-room trigger appeared, so this verifies the deployed code and containment, not reparenting behavior. +Viewer presence plus deterministic relay-admission rescue switched from +`2026-08-20T15:33:21.720345870+08:00` to health-ready at +`2026-08-20T15:33:22.282829810+08:00`; its gate finished at +`2026-08-20T15:33:23.051172737+08:00` on exact +`31bee238bc1e901e823f34e50737e202dc4b04bf`. The 889,710-byte immutable upload +is `/opt/screener/uploads/screener-31bee238bc1e-20260820T071907Z.zip` with +SHA-256 `C759A33A50EC3E272F07BCC00F2043CA0042446078472CAFA66CBD406EC925D9`, +and the active release is `/opt/screener/releases/31bee238bc1e`. +The gate passed 28 test files/421 tests and both builds. Restart-to-health took +560.607 ms; 20 ms probing observed 437.137 ms unavailable. External health, +the asset hash, three neutral entry routes, SQLite integrity/version 2/four +rooms including room `1`, and the exact revision passed. Screener, LiveKit, +coturn, and nginx remained active with zero automatic restarts; only Screener +had the one planned restart. Environment, nginx, LiveKit, coturn, firewall, +listeners, exact-room/SFU caps, and disabled application TURN tuple were +unchanged. +At the `2026-08-20T15:39:07.082219441+08:00` audit, Screener used 38,031,360 +bytes with zero cgroup low/high/max/OOM events. The pre-cutover rollback is exact +`7fea60ef6f2ad14a9ac1c23a89a523d91bbb97e4` with +`/opt/screener/backups/31bee238bc1e-precutover-20260820T073321Z`. +No temporary symlink, deployment process, browser profile, or held deployment +lock remained; the immutable upload, active release, and backups are retained. +No real room triggered admission rescue, so this verifies deployment and +rollback readiness, not the new route transition or media quality. + Enabling persistence does not migrate rooms that existed only in memory. The deployment restart invalidates those temporary links; the first subsequently created persistent room receives ID `1`. @@ -365,10 +413,11 @@ 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 replacement names `PEER_ICE_TURN_URLS`, +The rejected candidate names `PEER_ICE_TURN_URLS`, `PEER_ICE_TURN_SHARED_SECRET`, and -`PEER_ICE_TURN_CREDENTIAL_TTL_SECONDS` form a distinct complete tuple; the old -keys above remain rejected and are not aliases. +`PEER_ICE_TURN_CREDENTIAL_TTL_SECONDS` must remain absent from production. They +are not aliases for the future selected-edge tuple; partial presence must fail +closed until the source removal/migration lands. ## HTTPS and WSS ingress @@ -460,6 +509,14 @@ canary pass silently. ## Self-hosted STUN +This subsection describes the current temporary STUN-only deployment template, +not the optional selected-edge target 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, while Screener advertises no TURN credential and +the final canary audit found zero allocations. Authenticated selected-edge +fallback remains unimplemented and default-off; its rollout stays blocked on +retained SFU/UDP media and one forced-relay edge acceptance. + Copy [`deploy/coturn/turnserver.conf.example`](../deploy/coturn/turnserver.conf.example) to an untracked service-owned location and use the tracked [`coturn.service.example`](../deploy/systemd/coturn.service.example). The config diff --git a/docs/project-memory.md b/docs/project-memory.md index 514dcde..76b27e9 100644 --- a/docs/project-memory.md +++ b/docs/project-memory.md @@ -7,7 +7,7 @@ Last updated: 2026-08-20 - Build low-latency game sharing for one broadcaster and trusted friends; public or large broadcasts belong on OBS/Twitch-class services. - Viewers join from normal desktop/mobile browsers. A later native sender may share encoding and improve capture/audio without changing the Web viewer requirement. - Use a small central service for access, rooms, signaling, deterministic topology, STUN, observability, and bounded SFU-root capacity. -- Minimize server bandwidth. Peer ICE is direct-first; an exact-room, default-off tuple adds expiring authenticated TURN/UDP to capable Web PCs. Failure tries restart, rebuild, alternate peer, SFU, then bounded failure. TURN is transport, not topology. +- Minimize server bandwidth. Ordinary peer ICE is STUN-only. Failure tries restart, rebuild, alternate peer and bounded SFU/UDP roots; only then may the controller authorize short-lived TURN for one selected exceptional edge before clear failure. TURN is transport, not topology. - Non-server nodes have at most two downstream edges; browser relays stay at one until resource gates pass. SFU normally feeds one or two roots that retain peer descendants; separately capped server edges may serve exceptional viewers that cannot attach behind a healthy root. - Two-tree packet/layer striping may reduce endpoint upload toward one stream bitrate, but needs a bounded multi-parent, loss, sync, churn, and latency experiment; it is not in the current full-stream chains. - Direct/peer keeps per-PC stock GCC. SFU paths start at a `HIGH` ceiling and share at most one `LOW`; next test LiveKit exactly-two built-in BWE on zero-descendant leaves. If it passes, no app media selector is built. Explicit quality, manual activation, then custom/native are later fallbacks; always-on `LOW` needs resource gates. @@ -25,13 +25,13 @@ Last updated: 2026-08-20 - Keep experiments bounded: the standard representation sequence is below; native RTP relay, encoded-object striping, and FEC remain separate. - ADR-0006 stays no-go: the second product run reached first WebCodecs output, then zero Go frame/RTP and no Viewer; cleanup passed 5/5. A separate offline Media Foundation fixture proved one RTX 4070 SUPER H.264 hardware path over 360/360 frames, but its valid Constrained Baseline SPS `42c01f` is outside Pion's default exact fmtp set; Pion/Viewer interop remains unproven. Native v2 stays memory-only and leaves SQLite/Web unchanged. - Mobile Web Host is unsupported; feature-detect and fail clearly. Mobile Viewer stays leaf-only. After Windows native, gate Android 14+; iOS waits for stable iOS 27 ScreenCaptureKit. -- ADR-0005 accepts direct-first Peer ICE and bounded SFU roots. Its default-off exact-room slice is undeployed; production/coturn stay STUN-only. PR #12/selected-edge superseded. +- ADR-0005 accepts direct/peer UDP, bounded SFU roots, then optional selected-edge TURN. The built-in participant-wide candidate failed its real direct acceptance before forced relay and rolled back completely; it is rejected as the product path. Production advertises STUN-only ICE. - Keep the controller exact-room only: room `1` is the STUN/SFU smoke. Preserve sticky progressing P2P, mobile leaves and break-before-make. Recovery spends one attempt per layer (ICE restart, same-parent rebuild, alternate peer, then SFU), never three identical retries; active SFU gets one fresh grant before Peer failback. - C+B quality reparenting is edge-local and cooldown-bound; no score, timer, or global parent penalty. - Flagship media is UDP; HTTPS/WSS stays TLS/TCP; the old release remains rollback-only. - Treat settings as ceilings and degradation as unclassified. Use correlated Host A+B/Viewer C and one-variable evidence; never force AV1, infer by UA, or create a composite score. - Production reports poor film audio and self-echo when system capture includes voice software. Diagnose audio A/B/C and sync; Web cannot isolate arbitrary processes and `maxBitrate` is not quality-up. A Windows 11 native candidate defaults to game-process-tree audio and never widens silently; Windows 10 remains unresolved/unsupported. See `docs/research/browser-screen-audio-quality.md`. -- Candidate UI has Viewer-local names and an opt-in Web Host roster without media changes. Host names, Viewer roster, endpoint details, and RTP loss remain pending. +- Viewer-local names and the opt-in Web Host roster are deployed without media changes. Host self-name, endpoint details, and RTP loss remain pending. - Do not add scene detection, dynamic-FPS control, or forced AV1 without negotiation, encode, game, CPU/GPU, and sender evidence. - ADR-0002 access is deployed; names/presence are session-only with no account, member, session, or roster table. - Deferred architecture audit: `docs/maintenance.md`. @@ -39,19 +39,19 @@ Last updated: 2026-08-20 ## Current Implementation - The repository is one npm package using Node.js 24, React, TypeScript, Vite, native WebRTC, `ws`, Zod, Vitest, and separate coturn. -- Production `d4bc421828c4b74f55195723aace290ffc0e5f9d` deployed at 2026-08-20 11:41:22 +08 to `https://share.bonfire.icu`. Persistent room `1` alone enables peer/SFU routing and C+B-correlated local quality reparenting; unlisted rooms remain ordinary P2P. Immediate rollback is `05f98d10ecd1` with the current v2 DB/env. -- Production ICE is STUN-only. Source adds expiring STUN+TURN only for capable Web sessions in exact rooms; other rooms/Native stay STUN-only. Room `1` may feed two LiveKit roots with peer descendants; real relay/SFU media and game quality remain unverified. +- Production `31bee238bc1e901e823f34e50737e202dc4b04bf` deployed at 2026-08-20 15:33:21 +08 to `https://share.bonfire.icu`. Room `1` alone enables peer/SFU routing, C+B local reparenting, and admission rescue; unlisted rooms remain ordinary P2P. Immediate rollback is `7fea60ef6f2a` with the current v2 DB/env. +- Production ICE is STUN-only. Source still contains the default-off rejected built-in TURN candidate, but no room or client receives it. Room `1` may feed two LiveKit roots with peer descendants; selected-edge TURN is not implemented. Real rescue/relay/SFU media and game quality remain unverified. - Production requires an independent `HOST_ADMISSION_PASSWORD` only for creation/Host role. Default private fragment grants and explicit public-watch authorize Viewers; four anonymous Chrome routes stayed neutral until authorization. There are no accounts/JWT/session rows; SQLite v2 still contains all four rooms, including room `1`. -- Production uses nginx, Node.js 24.19.0, coturn 4.17.2 for STUN/rollback, and pinned LiveKit 1.13.5 on UDP 7882 with TCP fallback disabled. Screener/LiveKit `NRestarts` remain 0/0; health and `index-YdNLg2E8.js` return 200. They used 39,198,720/79,269,888 bytes with zero cgroup high/max/OOM events at 11:44:15. No real room triggered quality reparenting. Public TCP 7880/7881 stays blocked; LiveKit keeps its 192/256 MiB cgroup boundary and restart disabled. -- Old all-room TURN artifacts remain absent. The independent, default-off, undeployed `PEER_ICE_TURN_*` tuple is not an alias. Admission rollback requires `89e6d7649169` plus its environment backup; only pre-access `9610032` pairs with v1 SQLite. +- Production uses nginx, Node.js 24.19.0, coturn 4.17.2, and pinned LiveKit 1.13.5 on UDP 7882 with TCP fallback disabled. The application TURN tuple is absent; coturn retains the old authenticated-relay config and TCP/UDP 3478 plus UDP 49152-49251 rules but receives no advertised credential. All services are active with zero automatic restarts; Screener used 38,031,360 bytes with zero cgroup events at 15:39:07. +- Old all-room application TURN wire/config remains absent. Canary release `a11a73dfa79d-r4` is inactive after rollback to `7fea60e`; final health/config/firewall/SQLite passed with zero allocations. - ADR-0005 is configured with exact `PEER_ASSISTED_ROOM_IDS=1`; unlisted rooms use ordinary P2P. `screener-v2` is the only deployed signaling literal; there is no v1 parser or translator. -- Production logs show two root participants and two short Host participants with no service restart or retained track. Their timing is consistent with, but does not directly prove, one fresh-grant retry and Peer failback. The Host exposes only the local current-revision failure stage (`connect|source|video-publish|sender-config|audio-publish|transport`) and uploads no raw error or endpoint data. Browser relays still re-encode; native capture/hardware remains outside validated production behavior. -- The retained v1 backup is the only schema rollback path. A failed artifact attempt exposed unsafe hard-linked dependency reuse; the successful immutable release has zero shared regular-file inodes with rollback, and future releases preserve that boundary. +- Production logs once showed two roots and two short Host participants without a retained track. Timing cannot prove retry/failback. Host exposes only its local current-revision failure stage and uploads no raw error or endpoint data; browser relays still re-encode. +- The retained v1 backup is the only schema rollback path. Immutable releases share no regular-file inodes with rollback. - Chrome 151/LiveKit localhost A/B cut failure-to-active/render from 1.481/2.257 seconds to 0.200/0.320; 31 new frames and 25 ms sampling kept host edges at two. It is headless synthetic 720p30 and includes SDK/network prewarm, not public-network evidence. - Chrome 151 synthetic topology/quality-control runs kept fanout 2/1 and all viewers decoding; one relay close recovered in 5.32 seconds. This is control evidence only. - Room `1` publishes `HIGH+LOW` with Dynacast off and subscriber `HIGH` ceilings, and now has bounded local quality reparenting. Both remain unverified on real media; test zero-child leaves and calibrated C+B loss before broad rollout. -- C+B uses three hard-bad pairs, one-use samples and guarded intent/cooldown. Source-only admission rescue moves the oldest childless zero-capacity Host leaf below an unassigned one-slot relay in one revision; focused/typecheck pass, real media unverified. -- Undeployed Web name/presence leaves Native wire/media unchanged and separates Viewer roster from Host diagnostics; focused checks pass. +- C+B uses three hard-bad pairs, one-use samples and guarded intent/cooldown. Deployed admission rescue moves the oldest childless zero-capacity Host leaf below an unassigned one-slot relay in one revision; no score/timer/global rebalance. Deployment passed, but no real room triggered it. +- Deployed Web name/presence leaves Native wire/media unchanged and separates Viewer roster from Host diagnostics. ## Provisional Quality Targets @@ -66,7 +66,7 @@ These are measurement gates, not performance claims. - Sustainable count by hardware, quality, network, and route: finish instrumented `1/3/5/8`, then pass a 20-viewer matrix before changing the accepted target default to 20. - Whether ADR-0004 passes fanout, re-encoding, depth-four latency, reparenting, silent-partition, and mobile-leaf gates. -- Whether Peer ICE TURN passes direct/forced-relay, mobile, expiry/reconnect, 1/3/5/8 allocation/resource, 1/2 relay-bandwidth, and 1-GiB gates. Coturn deployment/ports remain unselected; media TCP is out of scope. +- Whether one controller-selected edge can pass generation-bound parent/child rebuild, forced relay, expiry/failure, mobile, 1-GiB resource and relay-bandwidth gates after SFU/UDP fails. The rejected built-in canary left no application tuple, advertised credential, transient rule, or allocation; the old coturn relay config and ports remain. Media TCP is out of scope. - Exact mobile lifecycle behavior and whether Windows per-application audio is required for the first release. - Project license and distribution model, which determines whether GPL/AGPL sources can move beyond study-only use. - Initial deployment regions and expected mainland China, Hong Kong, and overseas network mix. diff --git a/docs/research/advanced-peer-distribution.md b/docs/research/advanced-peer-distribution.md index c190ada..9bbfd3d 100644 --- a/docs/research/advanced-peer-distribution.md +++ b/docs/research/advanced-peer-distribution.md @@ -312,8 +312,8 @@ optimizer, temporary fanout above budget, self-reported geography or device quality, or cannot beat the unchanged route. The earlier admission-rescue case remains valid, while this quality slice moves only the affected Viewer-rooted subtree and excludes only its current failed parent through the existing -peer-first, then SFU, then explicit-failure path. Healthy TURN-to-direct -optimization remains future work. +peer-first, then SFU path. A future selected-edge TURN attempt follows SFU +failure; healthy paths never enter relay optimization. ## Staged Connection Recovery Evidence @@ -362,7 +362,7 @@ The enabled SFU is normally a virtual parent for at most `R=2` roots, which retain bounded peer descendants. A viewer that cannot attach behind any healthy root may be separately admitted only under the explicit `E`/central-egress cap; this is an exceptional compatibility budget, not unbounded whole-room fanout. -The SFU/UDP and optional built-in Peer ICE TURN transport accounting lives in +The SFU/UDP and optional selected-edge TURN transport accounting lives in [Low-Server-Cost Media Routes](./low-server-media-routes.md). ADR-0005's current controller remains failure-only, while its accepted target also handles admission with no eligible peer path; neither changes the sticky local diff --git a/docs/research/browser-screen-audio-quality.md b/docs/research/browser-screen-audio-quality.md index 3cf244e..17e70de 100644 --- a/docs/research/browser-screen-audio-quality.md +++ b/docs/research/browser-screen-audio-quality.md @@ -187,8 +187,8 @@ candidate on current Windows 11 with game parent and child audio, an independent voice process, notifications, no render stream, and process restart. Windows 10 records the explicit unsupported/unresolved result rather than a fake fallback. Direct is the primary route; one browser-relay and one -SFU-root audio check cover route preservation, while TURN needs only a -connectivity smoke. Correlate capture settings, negotiated codec/derived fmtp, +SFU-root audio check cover route preservation, while selected-edge TURN needs +only one post-SFU connectivity smoke. Correlate capture settings, negotiated codec/derived fmtp, actual outbound/inbound bitrate, loss, jitter, concealment, jitter buffer, and A/V playout timing using a distinguishable stereo fixture plus game/film audio. diff --git a/docs/research/built-in-peer-ice-turn.md b/docs/research/built-in-peer-ice-turn.md index c2c9515..c93aca2 100644 --- a/docs/research/built-in-peer-ice-turn.md +++ b/docs/research/built-in-peer-ice-turn.md @@ -1,255 +1,122 @@ # Built-In Peer ICE TURN Candidate - Research date: 2026-08-20 -- Scope: optional authenticated TURN on ordinary peer `RTCPeerConnection` - values in one exact-room canary -- Status: default-off source candidate implemented; no coturn rollout or deployment +- Scope: authenticated TURN on every ordinary peer `RTCPeerConnection` in an + exact-room canary +- Status: rejected after bounded production canary; historical evidence only -## Result +## Decision -The smallest standard-WebRTC fallback is to give each participant a short-lived -STUN-plus-TURN `RTCConfiguration` when it authenticates. Each peer connection -then gathers host, server-reflexive, and relayed candidates with -`iceTransportPolicy: "all"`. ICE prefers a working direct candidate pair and -can select the relay pair without an application-owned transport transition. -Only after that peer connection fails does Screener need its existing bounded -recovery ladder: one ICE restart, one same-parent rebuild, one alternate peer, -then the SFU virtual parent. - -This is substantially smaller than an application-selected edge grant. The -owning requirements and ADR now accept it only behind the existing exact-room -allowlist and a complete default-off deployment tuple. It does not add another -router state machine or broaden TURN to non-allowlisted rooms. - -## Current-Code Boundary - -At `f806689896db9719509b0e0695d741daa6ecb364`: - -- `authenticated.iceConfig` is STUN-only. Its schema accepts neither TURN URLs - nor usernames, credentials, or expiry. -- `HostPeer`, `ViewerPeer`, and `ViewerRelay` already support - `RTCPeerConnection.setConfiguration()` through `updateIceConfig()`. -- Host and Viewer authentication handlers already replace the retained ICE - config and update every live peer after signaling reconnect. -- `ViewerPeer` owns the 15-second initial checking deadline, one ICE restart, - and one same-parent rebuild. `HybridMediaRouter` then owns alternate-parent - selection and SFU preparation. -- `TURN_URLS`, `TURN_SHARED_SECRET`, and - `TURN_CREDENTIAL_TTL_SECONDS` are rejected removed configuration. They must - not be reused for a new contract. -- The tracked coturn service is `stun-only`, UDP-only, and has no auth secret, - allocation quota, or relay port range. - -The built-in candidate therefore needs no new router state, route revision, -assignment, parent/viewer synchronization, or connection-ID protocol. - -## Standards Behavior - -RFC 8445 defines candidate gathering from configured STUN and TURN servers. A -TURN Allocate request creates a relayed candidate and consumes a relay address -even when that candidate is never selected. Its recommended type preferences -are host `126`, peer-reflexive `110`, server-reflexive `100`, and relay `0`, so -the ordinary checklist prefers direct candidates over a relay candidate. -Actual browser nomination and timing still require a canary measurement. - -The WebRTC specification says that replacing `iceServers` with -`setConfiguration()` takes effect at the next gathering phase; an application -that needs it immediately performs an ICE restart. Consequently, a credential -refresh should update healthy peer connections without restarting them. Their -next existing recovery attempt will gather with the fresh credentials. - -Coturn implements the TURN REST credential convention: +Do not deploy or continue the built-in participant-wide candidate. Ordinary +peer connections remain STUN-only. The accepted ladder is: ```text -username = : -credential = base64(HMAC-SHA1(shared-secret, username)) +direct/peer UDP -> SFU/UDP virtual parent -> selected-edge TURN -> failure ``` -The browser receives only the temporary username and credential. The shared -secret remains in the application and coturn service configuration. Credential -expiry prevents a new allocation with that credential; it is not immediate -revocation of an allocation that already exists. Allocation quotas and -application admission remain necessary cost boundaries. +TURN remains a compatibility transport, not a topology. Only the existing media +controller may select one exceptional edge after direct/peer UDP and SFU/UDP +have failed. No room, participant, or ordinary peer connection receives TURN +candidates by default. -## Minimal Config And Wire +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. -Use new names that describe the actual all-peer-ICE behavior and cannot be -confused with the deleted all-room contract: +## Production Canary -```dotenv -PEER_ICE_TURN_URLS=turn:turn.example.com:3478?transport=udp -PEER_ICE_TURN_SHARED_SECRET= -PEER_ICE_TURN_CREDENTIAL_TTL_SECONDS=600 -``` +The first exact-room canary used source `a11a73dfa79d`, immutable release +`a11a73dfa79d-r4`, and artifact SHA-256 +`C954185869A3A15CCCE642AB72A4CF90770476C117D61182D7728280C30A036F`. +The candidate switch completed at 2026-08-20 14:59:33 +08. A local coturn REST +allocation passed without retaining or printing its credential, and the +application health, artifact, route, admission, SQLite, service, and local TURN +configuration gates passed. -The tuple is optional and default-off, but partial configuration fails startup. -The shared secret is at least 32 independent bytes and must differ from Host -admission and LiveKit credentials. The first canary accepts exactly one explicit -TURN UDP URI without embedded username, password, fragment, or unrelated query -parameters. It issues TURN credentials only for explicitly allowlisted -peer-assisted rooms; every other room retains STUN-only ICE. +The real direct Host plus Pion Viewer acceptance path did not establish its peer +connection. The stop line prevented the forced-relay test, so the run proved +neither direct-with-TURN behavior nor relayed media. It was fully rolled back. -The initial authenticated message remains the primary config response: +Application issuance was disabled before coturn and its firewall rules returned +to the recorded pre-canary authenticated-relay baseline: TCP/UDP 3478 plus UDP +49152-49251. Exact production `7fea60ef6f2ad14a9ac1c23a89a523d91bbb97e4` +was restored and advertised no TURN credential. At the final lock-free check at +2026-08-20 15:12:40 +08, services were active, health/configuration/firewall/ +SQLite matched baseline, and coturn reported zero allocations. The inactive +candidate release and `/opt/screener/backups/turn-a11-20260820T065933Z` remain +deployment evidence. Production later advanced independently to `31bee238`. -```ts -type PeerIceConfig = { - iceServers: Array< - | { urls: string | string[] } - | { - urls: string | string[]; - username: string; - credential: string; - } - >; - turnCredentialsExpiresAt?: string; -}; -``` +No candidate, credential, address, or raw signaling value is retained here. -Add exactly one refresh request and response: +## Why Built-In Issuance Was Rejected -```ts -type RefreshIce = { type: "refresh-ice" }; -type IceConfigUpdated = { type: "ice-config"; iceConfig: PeerIceConfig }; -``` +RFC 8445 gives relay candidates a lower recommended type preference than host, +peer-reflexive, and server-reflexive candidates, so a healthy direct path is +normally preferred. That does not make participant-wide TURN free: -`refresh-ice` has no room, participant, edge, or expiry supplied by the client. -The current authenticated WebSocket session is the sole authority. The server -rejects it unless that session opted into the capability and belongs to an -exact allowlisted room with the tuple enabled. Its initial authenticated -snapshot remains STUN-only otherwise. +- gathering a relay candidate sends a TURN Allocate request and consumes an + allocation, relay port, memory, quota, and refresh traffic even when direct is + selected; +- coturn REST credentials authenticate an expiry-tagged bearer, not the room, + peer edge, route revision, parent, or connection generation; +- one participant credential can be reused by its holder until expiry; +- `setConfiguration()` affects future gathering and does not itself move an + existing connection, so refresh/recovery still needs lifecycle ownership; and +- placing TURN on the initial peer PC allows it before the bounded SFU/UDP root, + contrary to the accepted cost and topology order. -The participant session, not the media router, owns issuance of the bearer. -`SocketState` retains only its latest in-memory config, expiry, and refresh -window. The opaque username tag is derived from current room, peer, role, and -session identity with domain-separated HMAC; none of those values appears in -the username. No credential, username, or tag enters a URL, log, SQLite, room -row, or browser persistence. +The candidate was smaller in code because it delegated selection to standard +ICE, but it widened credential scope and idle server cost. Its failed direct +acceptance supplied no compensating runtime evidence. -`SignalingClient` is the one refresh-timer owner. It requests a refresh two -minutes before a ten-minute expiry, cancels that timer on disconnect, and gets -a fresh config through normal reauthentication after reconnect. Early or -duplicate requests in the same issuance window reuse the current grant rather -than minting unbounded credentials; the server also enforces a five-second -per-session request interval. A 30-second client timer floor prevents clock -skew from creating an immediate response loop. Pages handle `ice-config` -through the same retained config and `updateIceConfig()` path already used by -`authenticated`. +## Selected-Edge Boundary -Ten minutes with a two-minute lead is a canary value, not a security optimum. -The accepted parser should keep a narrow five-to-thirty-minute range and the -deployment must keep application and coturn clocks synchronized. +A correct replacement is not a renamed participant refresh API. It must be one +coherent controller-owned change with a current consumer: -## Scope Limit +- a complete default-off deployment tuple with an independent secret, one + explicit TURN/UDP URI, bounded TTL, quota, bandwidth, and relay-port limits; +- issuance only after the current edge exhausts direct/peer UDP and SFU/UDP; +- one in-memory attempt bound to room/share generation, viewer and parent + sessions, route revision, the replaced connection, and a server-generated new + connection identity; +- coordinated parent and child rebuild for exactly that edge, using relay-only + ICE only on the new connection; +- revalidation at grant, rebuild, ready, failure, timeout, route change, + disconnect, and room stop/delete boundaries; +- one-use consumption and explicit terminal failure, with no refresh timer, + participant-wide credential, SQLite row, URL, log, browser persistence, raw + candidate, or second router; and +- Native and every ordinary Web peer connection remaining STUN-only. -TURN REST authentication does not cryptographically bind a credential to a -remote peer, route revision, or one `RTCPeerConnection`. Coturn validates the -expiry, HMAC, realm, and quotas. The simplest honest scope is therefore one -opaque credential per authenticated participant session, reused by that -participant's bounded peer connections. +Coturn still cannot enforce the application edge identity. The application +guards provide selected-edge authorization; short expiry and coturn quotas bound +bearer reuse and resource cost. Do not claim cryptographic edge binding. -Per-edge cryptographic or application binding requires separate grant/rebuild -state and cannot be claimed by this built-in design. A client that receives the -bearer can reuse it until expiry; coturn cannot tell which room, edge, or peer -connection caused an allocation. App-side issuance is still bounded by short -expiry, exact-room authorization, the current WebSocket session, endpoint -fanout, and coturn quotas. +The exact protocol and environment names belong in the implementation PR, after +all parent roles (`HostPeer` and relay parents), the Viewer child, and +`HybridMediaRouter` share one reviewed generation contract. A config-only or +issuer-only partial slice is forbidden because it would create credentials with +no safe current consumer. -## Resource Bound +## Acceptance Boundary -An `N`-viewer peer tree has `N` peer connections. If both endpoints gather one -TURN allocation for one BUNDLE/RTCP-mux component and one TURN URI, the ideal -upper bound is about `2N`, or 16 allocations for eight viewers. Multiple local -interfaces, address families, TURN URIs, or browser gathering choices can make -the real count higher. This must be measured rather than encoded as a promise. +Before production enablement, one isolated canary must prove, in order: -An unselected allocation still consumes a relay port, coturn memory, quota, and -small Allocate/Refresh/connectivity-check traffic. It does not carry continuous -media. If one `B`-bit/s media edge selects TURN, the server sees roughly `B` -ingress plus `B` egress, before headers and retransmission. At the current -8-Mbit/s recommendation that is about 16 Mbit/s of useful-payload NIC traffic; -at the 12-Mbit/s ceiling it is about 24 Mbit/s. +1. ordinary direct/peer PCs contain no TURN candidates; +2. a real SFU/UDP root reaches a decoded/rendered frame under the existing caps; +3. after that edge's SFU/UDP failure, exactly one authorized parent/child pair + rebuilds and selects relay; +4. stale/replayed/wrong-session/wrong-revision/wrong-connection grants fail; +5. success stops further escalation, while expiry, coturn failure, disconnect, + and all-UDP-blocked cases terminate within the bounded window; +6. one and two selected relay edges stay within measured allocation, port, RSS, + CPU, RX/TX, loss, latency, and 1-GiB host limits; and +7. Android Chrome and iOS Safari complete the representative home, hotspot, and + carrier matrix before broader rollout. -For the first exact-room test, one TURN UDP URI, a 64-port relay range, and -reviewed starting quotas such as four allocations per username and 32 total -allocations give a bounded failure instead of an allocation storm. These are -test starting points, not production capacity claims. Measure coturn RSS, CPU, -allocation count, occupied relay ports, RX/TX bytes, packets per second, and -connection latency on the actual 1-GiB host before accepting them. Coturn does -not encode media, but that fact alone does not prove its memory or network -budget. - -## Coturn Deployment And Rollback - -The existing coturn process could be the first canary owner instead of adding a -second daemon. Remove `stun-only`, retain UDP listener 3478, and enable -`use-auth-secret`, an explicit realm, the untracked static auth secret, a small -relay range, and allocation/bandwidth quotas. Keep `no-tcp` and `no-tls` for the -first UDP-only measurement. Verify that unauthenticated STUN Binding still -works and that an authenticated temporary allocation succeeds before the -application issues any credentials. - -This source slice deliberately does not change or deploy coturn. A later -deployment acceptance boundary follows this order: - -1. Open only the reviewed UDP relay range and deploy authenticated coturn. -2. Verify STUN and a temporary TURN allocation without printing credentials. -3. Deploy the application tuple and exact-room issuance. - -Rollback reverses the ownership: - -1. Disable application issuance first. -2. Restore coturn `stun-only` and close the relay range. - -Existing relay allocations are intentionally interrupted by this rollback. -Healthy direct connections remain independent. TURN/UDP can work around NATs -that cannot form a peer pair while still reaching the server over UDP; it does -not help a network that blocks all UDP. TURN/TCP or TURN/TLS, especially on -TCP 443, needs its own port, certificate, shared-IP, and mobile-network gate. - -## Failure Surface Comparison - -| Boundary | Built-in participant ICE | Application-selected edge | -| --- | --- | --- | -| Core owner | signaling session and client refresh timer | router plus both edge endpoints | -| Route state | unchanged | attempt generation and old/new connection binding | -| New runtime wire | refresh request and config response | grant, parent rebuild, readiness/failure | -| Direct preference | standard ICE priority | application first exhausts a STUN-only PC | -| Idle server cost | allocations for configured peer connections | none until selection | -| Credential scope | participant session | application edge attempt | -| Main failure risks | allocation count, refresh/clock skew, unreachable TURN delaying ICE | stale grants, split endpoint state, rebuild timeout, revision races | -| Estimated core TypeScript | about 250-400 lines | about 600-1,000 lines | - -The estimates are review-planning ranges, not delivery commitments. Tests and -documentation are additional. - -## Validation Layers - -P0/P1 automated gates: - -- default-off and strict complete-tuple parsing; -- TURN URI and TTL bounds plus secret independence; -- exact-room/session-only issuance and refresh rate limiting; -- deterministic expiry/HMAC vectors without snapshotting real credentials; -- no credential in URLs, logs, persistence, errors, or non-allowlisted rooms; -- authenticated refresh updates Host, Viewer, and relay peer configs; -- stale/replaced sessions cannot refresh; -- a next ICE restart uses refreshed config while a healthy PC is not restarted; -- current restart/rebuild/alternate-peer/SFU order remains unchanged after a - peer connection reports failure. - -Acceptance canary: - -- Chrome/Edge direct selection with TURN configured and relay candidates - present; -- forced direct failure selecting TURN without an application route change; -- unreachable coturn while a direct pair remains possible; -- expired credential, refresh, reconnect, and coturn restart; -- `1/3/5/8` direct-selected allocation/RSS/CPU/latency comparison; -- `1/2` forced-relay media bandwidth, packet loss, latency, and recovery; -- Android Chrome and iOS Safari on home, hotspot, carrier, and UDP-blocked - networks, with UDP-blocked failure expected in the first UDP-only slice. - -No broad rollout or 1-GiB capacity claim follows from unit tests. +TURN/TCP or TURN/TLS is a separate media-transport decision. HTTPS/WSS remains +TLS/TCP and does not satisfy these media gates. ## Sources diff --git a/docs/research/low-server-media-routes.md b/docs/research/low-server-media-routes.md index 03b5d54..7946704 100644 --- a/docs/research/low-server-media-routes.md +++ b/docs/research/low-server-media-routes.md @@ -3,10 +3,10 @@ - Research date: 2026-08-19 - Scope: one broadcaster, at most eight trusted viewers, low latency, and host media fanout at most two -- Status: ADR-0005 accepts standard direct-first Peer ICE with optional - authenticated TURN/UDP only for capable Web sessions in exact-room canaries, - then alternate peer and bounded SFU/UDP roots; production remains STUN-only, - and public transport behavior plus every non-browser data plane is unverified +- Status: ADR-0005 accepts STUN-only direct/peer UDP, bounded SFU/UDP roots, + then optional authenticated TURN/UDP for one controller-selected exceptional + edge; production remains STUN-only, and public transport behavior plus every + non-browser data plane is unverified ## Current Route Ladder @@ -34,9 +34,9 @@ The smallest current plan is: Any browser-spike failure other than isolated relay re-encoding closes that browser-relay route. It does not cancel the separate native sender plan. Closed PR #12's explicit whole-room SFU mode is superseded. ADR-0005 and merged PR #17 -own the default-off automatic cross-mode controller. Its code is deployed but -inactive because production has no peer-assist flag or LiveKit tuple. Corrected -localhost Chrome/LiveKit functional recovery passed, while public +own the automatic cross-mode controller. Production enables it only for room `1` +with a complete LiveKit tuple; participant entry was observed, but retained +media was not. Corrected localhost Chrome/LiveKit functional recovery passed, while public transport, quality, load, and browser validation remain open. ## Token-Free SFU Standby Prewarm @@ -90,26 +90,28 @@ useful last-hop delivery is approximately `N*B`. Ignoring protocol overhead: The table's `R*B` and `D*B` rows are equal-representation screening cases with `E=0`. -With mixed root representations, peer host/seed-TURN traffic is `sum(B_i)` and +With mixed root representations, direct peer-root traffic is `sum(B_i)` and descendant upload is `sum(B_edge)`. SFU host upload and ingress are `B_pub`, the sum of all actively published representations; if simulcast `HIGH` and `LOW` both remain active, that may be `B_HIGH+B_LOW`, not one root bitrate. The SFU-root row, not full-room SFU, is the retained fallback shape. For equal representations, `B_pub = B` and `sum(B_i) = R*B`. Publisher-to-SFU and every -SFU-to-root subscriber transport are independent ICE connections and may -select deployment-supported ICE/UDP, ICE/TCP, or TURN/TLS; peer descendants -may also select TURN. If the publisher leg uses TURN, retain host upload +SFU-to-root subscriber transport are independent ICE connections and may use a +separately deployed LiveKit transport. Ordinary descendants are STUN-only; only +one controller-selected exceptional edge may later use independent coturn. If a +publisher leg separately uses LiveKit TURN, retain host upload `B_pub`, TURN ingress `B_pub`, TURN egress `B_pub`, and SFU ingress `B_pub` as distinct interface/service traffic. A relayed root leg likewise adds TURN -ingress and egress `B_i`; a relayed descendant edge adds both directions for -that edge's actual bitrate. They are one logical useful payload copy but real +ingress and egress `B_i`; a selected-edge relay adds both directions for that +edge's actual bitrate. They are one logical useful payload copy but real physical hops, so NIC, service, and billing counters must never be folded. If `E>0`, SFU root/exception egress is `sum(B_i) + sum(B_exc,j)` and central SFU traffic is `B_pub + sum(B_i) + sum(B_exc,j)`. Peer descendant upload remains -`sum(B_edge)`. Any TURN-relayed root, exception, or descendant leg adds TURN -ingress and egress equal to that leg's measured bitrate. The separately capped +`sum(B_edge)`. Any separately TURN-relayed LiveKit root/exception leg or +controller-selected peer edge adds TURN ingress and egress equal to that leg's +measured bitrate. The separately capped exceptions are therefore never hidden inside the normal `R<=2` root budget or the equal-representation formulas. @@ -122,9 +124,9 @@ it cannot remove the network copy delivered to each viewer. ## Transport And Topology Layers -TURN is a per-edge ICE transport. Each host-root or peer-peer P2P edge and each -SFU publisher/subscriber connection can independently select direct or relay -candidates without changing the rest of the room. An SFU is a topology node. +TURN is a per-edge ICE transport, but ordinary host-root and peer-peer PCs gather +only STUN candidates. The controller may authorize one failed edge to rebuild +relay-only after SFU/UDP; LiveKit participant transport remains separate. An SFU is a topology node. Screener uses it as a virtual parent for one or two roots, not as an automatic all-viewer fanout service. After media reaches a root, the deterministic sticky subtree, host/root fanout @@ -158,51 +160,51 @@ old connection or ICE-restart history from being attributed to a new route. Use opaque generations only. This is a bounded diagnostic manifest, not a backend telemetry schema or controller input. -## Built-In Peer ICE TURN And SFU/UDP +## SFU/UDP And Selected-Edge TURN -The accepted target gives each bounded peer `RTCPeerConnection` standard ICE. -By default it has STUN only. In a complete default-off exact-room canary, a -capable Web participant gets STUN plus short-lived authenticated TURN/UDP under -`iceTransportPolicy: "all"`; standard candidate priority prefers direct and may -nominate relay inside the same PC. PC failure then follows one restart, one -same-parent rebuild, one alternate peer, SFU/UDP roots, and bounded failure. -The SFU normally emits only one or two root copies, and roots keep their peer -descendants. Endpoint and central egress caps remain unchanged. +Every ordinary bounded peer `RTCPeerConnection` uses STUN-only ICE. Failure +follows one restart, one same-parent rebuild, one alternate peer, and one or two +SFU/UDP roots whose bounded descendants remain distributed. Only an edge that +also cannot use SFU/UDP may receive one controller-selected authenticated +TURN/UDP rebuild before bounded failure. Endpoint and central egress caps remain +unchanged. STUN/ICE discovers and checks paths. RFC 8656 TURN allocates a relayed address and continuously carries media when selected; it is not a handshake helper or -topology. Configuring it can also create unselected allocations. Exact-room, -complete-tuple, and Web capability gates prevent a process-wide rollout, while -standard ICE priority leaves working direct paths selected. +topology. Ordinary PCs do not gather relay candidates, so healthy paths consume +no idle allocation. Exact-room, complete-tuple, current-generation and one-use +controller gates prevent participant-wide or process-wide rollout. -The application issues an opaque coturn REST bearer only from the current -authenticated participant session. Coturn validates its HMAC and expiry, not -the originating room, peer edge, revision, parent, or connection generation; -the holder can reuse it until expiry. This is App-side authorization plus TTL, -fanout, and quota containment, not cryptographic selected-edge enforcement. -The detailed wire, refresh, allocation, and rollback analysis is in +Coturn validates a REST bearer's HMAC and expiry, not the originating room, +peer edge, revision, parent, or connection generation. Application issuance and +both endpoint rebuilds therefore must revalidate the controller's current edge +identity and consume the attempt on success, failure, expiry, or generation +change. This is application selected-edge enforcement plus TTL, fanout, and +quota containment, not coturn-side cryptographic edge binding. The failed +built-in alternative and retained standards evidence are in [Built-In Peer ICE TURN Candidate](./built-in-peer-ice-turn.md). Pinned LiveKit 1.13.5 can advertise authenticated TURN to LiveKit participants, but that separate ICE domain covers publisher/subscriber connections to the -SFU, not ordinary peer PCs. Screener's built-in Peer ICE uses independent -coturn configuration and credentials. LiveKit TURN cannot rescue an unavailable -SFU; ordinary coturn may carry the already-assigned peer topology but does not -choose or create it. +SFU, not ordinary peer PCs. Selected-edge coturn uses independent configuration +and credentials. LiveKit TURN cannot rescue an unavailable SFU; the application +controller selects whether independent coturn may rebuild one failed peer edge. -Production remains STUN-only. Current source accepts only the new complete -`PEER_ICE_TURN_*` tuple, not the removed all-room keys, and uses capability- -gated `iceConfig` expiry plus `refresh-ice`/`ice-config`. Non-allowlisted rooms -and Native-shaped clients remain STUN-only. The tracked coturn example is UDP +Production remains STUN-only. Current source still contains the rejected +default-off `PEER_ICE_TURN_*` participant-wide candidate; it must not be +configured and will be removed or replaced with its selected-edge consumer. +Every ordinary room, Web peer and Native-shaped client 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. Coturn 4.17.2 documents `stun-only` as ignoring TURN requests and provides -`no-tcp` and `no-tls`; it marks `no-dtls` deprecated, so the tracked candidate -does not use that switch. Coturn remains the production STUN owner without a -TURN allocation surface; the application source slice alone does not change or -deploy it. +`no-tcp` and `no-tls`; it marks `no-dtls` deprecated, so the tracked temporary +template does not use that switch. The shared production host instead retains its old +authenticated-relay daemon and TCP/UDP 3478 plus UDP 49152-49251 rules. The +application advertises no TURN credential, and the post-canary audit found zero +allocations. This baseline is neither the accepted selected-edge rollout nor +proof that relay media works. No public port is selected by this decision. LiveKit documents ICE/UDP mux as optional and its pinned sample recommends a multi-port UDP mux range at least @@ -235,12 +237,12 @@ One bounded exact-room gate owns rollout evidence: 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. -2. Cover representative consumer networks first with Peer ICE TURN disabled, - then with the complete exact-room tuple. Verify direct-selected peer, - forced relay-selected peer, and SFU/UDP separately. Unlisted rooms and - capability-absent clients must remain STUN-only. -3. Measure idle allocation count/relay ports/RSS/CPU/latency at 1/3/5/8 - participants and selected relay RX/TX/loss/latency at one and two edges. +2. Cover representative consumer networks with STUN-only direct/peer first, + then SFU/UDP. Only after both gates pass, force one controller-selected edge + through TURN. 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 root departure, reconnect, SFU unavailable, and rollback. Endpoint downstream edges stay at most two, normal SFU roots at most two, separately @@ -250,7 +252,7 @@ One bounded exact-room gate owns rollout evidence: generations; never upload raw SDP, candidate/address/IP, credentials, or device identifiers. -Ordinary TURN retains endpoint-to-endpoint DTLS-SRTP. Ordinary SFU transport +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 Screener later implements application E2EE and key distribution. That accepted tradeoff remains visible in deployment and UI claims. diff --git a/docs/research/native-shared-encode-sender.md b/docs/research/native-shared-encode-sender.md index f3afef1..d406a86 100644 --- a/docs/research/native-shared-encode-sender.md +++ b/docs/research/native-shared-encode-sender.md @@ -429,9 +429,9 @@ diversity, lifecycle, and sustained CPU/GPU, memory, latency, quality, and upload measurements. A third host edge, custom RTP/SRTP, or a custom congestion-control framework remains out of scope. The #28 minimum-of-two policy is also out of scope for product code; retaining its -evidence does not retain its policy. Built-in exact-room Peer ICE TURN -supersedes selected-edge as a separate Web source canary; Native stays STUN-only -unless its own bounded capability and transport gate is accepted. +evidence does not retain its policy. Ordinary Web and Native peer ICE stays +STUN-only; optional selected-edge TURN follows the shared controller only after +SFU/UDP fails and needs its own bounded transport gate. ## Primary Sources And License Boundary diff --git a/docs/research/peer-assisted-media.md b/docs/research/peer-assisted-media.md index ff2b0df..055c0ed 100644 --- a/docs/research/peer-assisted-media.md +++ b/docs/research/peer-assisted-media.md @@ -2,9 +2,9 @@ - Research date: 2026-08-19 - Scope: one game-screen broadcaster, at most eight trusted viewers, host media fanout at most two -- Status: evidence for the bounded ADR-0004 experiment; the stacked ADR-0005 - Draft adds binary relay capability and automatic optional-SFU routing but is - not a production topology decision +- Status: historical evidence for the bounded ADR-0004 experiment; accepted + ADR-0005 now owns automatic peer/SFU routing, with only room `1` enabled as a + production smoke and retained SFU media still unverified ## Conclusion @@ -18,7 +18,8 @@ experiment uses only standard WebRTC media: 3. the signaling server assigns two balanced chains deterministically. This path reuses WebRTC capture, codec negotiation, RTP, NACK/PLI/RTX, congestion -control, jitter buffering, ICE, STUN, TURN, DTLS-SRTP, and browser rendering. Its +control, jitter buffering, ICE, STUN, DTLS-SRTP, and browser rendering. A later +controller-selected exceptional edge may also use TURN. Its cost is unavoidable in ordinary browsers: every relay decodes and re-encodes the screen stream. The first spike measures whether that cost is acceptable; it does not hide it or claim shared encoding. @@ -27,8 +28,9 @@ The accepted flagship preference is `direct/peer UDP -> SFU-root fallback -> optional exceptional-edge TURN -> bounded failure`. Direct P2P remains the simplest path for one or two viewers. The experiment assigns the third and later viewers to peers automatically. ADR-0005 makes SFU capacity part of the flagship -target while retaining peer descendants; its current default-off controller is -still failure-only and unconfigured in production. +target while retaining peer descendants. Production currently enables only a +room-`1` exact smoke; participant entry was observed, but retained SFU media and +broad rollout remain unverified. ## What Browsers Can Share @@ -158,8 +160,8 @@ Sources: The signaling server remains the topology authority. The experiment uses two sticky, balanced chains selected by one breadth-first walk. Here, one media edge -means one downstream `RTCPeerConnection` carrying the shared stream; selecting -TURN for that connection does not change the edge count. +means one downstream `RTCPeerConnection` carrying the shared stream; a later +controller-selected TURN rebuild does not change the edge count. - the host has capacity for at most two children and every viewer for at most one; @@ -180,13 +182,14 @@ than something hidden by a scheduler. The controlled relay cohort uses foreground desktop Chrome/Edge; mobile leaf checks join after their intended parent so the spike does not pretend to solve background mobile relay policy. -Per-edge ICE remains independent. An edge may be direct or may use authenticated -TURN, so peer assistance reduces normal server media traffic but cannot promise -zero server traffic in restrictive networks. +Per-edge ICE remains independent. Ordinary peer edges are STUN-only; after +direct/peer UDP and SFU/UDP fail, a controller-selected exceptional edge may use +authenticated TURN. Peer assistance therefore reduces normal server media +traffic but cannot promise zero server traffic in restrictive networks. ## Implemented Bounded Quality Coordination -The current Draft coordinates one strict `QualitySettings` object across the +The current implementation coordinates one strict `QualitySettings` object across the peer-assisted tree without adaptation logic. It accepts only 720p/1080p/1440p, integer 15-60 fps, integer 2-12 Mbps, and the three standard degradation preferences; missing, extra, or out-of-range fields fail schema validation. @@ -282,22 +285,25 @@ slot: Peer assistance distributes traffic; it does not eliminate it. Relay eligibility must therefore be visible and voluntary in any production design. The standalone ADR-0004 spike has no runtime capability flag and relies on -controlled join order. The accepted ADR-0005 direction's current default-off controller starts every viewer as a leaf, -then accepts an explicit per-session capacity of zero or one; its Web client +controlled join order. The accepted ADR-0005 controller starts every viewer as +a leaf, then accepts an explicit per-session capacity of zero or one; production +scopes it to room `1` while broader rollout remains gated. Its Web client reports detected mobile/iPad clients as leaves and desktop-class browsers as one-child relays. That conservative heuristic is still unverified on the real mobile matrix and is not a substitute for a future voluntary relay policy. ## Bounded Spike And Gates -The experiment is disabled by default through `PEER_ASSISTED_MEDIA=false` and -cannot be enabled above eight viewers. It uses the existing standard WebRTC +The feature remains default-off in configuration through +`PEER_ASSISTED_MEDIA=false`; production enables only the exact room-`1` smoke. +It cannot be enabled above eight viewers and uses the existing standard WebRTC screen stream, current desktop Chrome/Edge as relay nodes, current Android Chrome and iOS Safari as required leaf checks, one child per viewer, and at most eight viewers. It may carry the existing screen-audio track when the browser provides one. SVC/simulcast, custom encoded transport, FEC changes, multi-tree striping, transcoding, -background mobile relay, and automatic SFU migration are excluded. +background mobile relay, and the later ADR-0005 SFU controller are outside this +historical browser-relay gate. Run 1, 3, 5, and 8 viewers for 30 minutes across the three recommended ceiling combinations, plus any advanced combination proposed for production, under @@ -351,9 +357,10 @@ per-edge upload, and cannot rescue other failed browser-relay gates. A rejected relay experiment keeps standard P2P plus explicit user-operated or central SFU fallbacks. -Passing these gates proves only that a second design phase is justified. It -does not accept peer-assisted media for production. Production adoption would -need a new ADR, voluntary relay policy, and broader audio/A-V verification. The +Passing these historical gates proved only that a second design phase was +justified. ADR-0005 subsequently accepted the bounded controller and production +now has a room-`1` smoke, but retained SFU media, voluntary relay policy, and +broader audio/A-V verification remain open. The native host shared-encode sender is a separate planned phase regardless of this experiment's result and is not implemented here. diff --git a/docs/research/webrtc-p2p-screen-sharing.md b/docs/research/webrtc-p2p-screen-sharing.md index ead5fc0..62c44ea 100644 --- a/docs/research/webrtc-p2p-screen-sharing.md +++ b/docs/research/webrtc-p2p-screen-sharing.md @@ -3,7 +3,7 @@ - 调研日期:2026-08-18 - 移动端采集能力复核:2026-08-19 - 目标场景:一名玩家向少量熟人私密分享,观看者可用手机/桌面浏览器加入,低延迟,尽量不消耗媒体服务器带宽 -- 结论状态:本文记录已部署 PoC 的 P2P/coturn 基线。ADR-0005 与[低服务器成本媒体路由](./low-server-media-routes.md)已取代本文早期“每条 peer edge 必带 TURN”的旗舰建议;生产代码尚未迁移 +- 结论状态:本文记录已部署 PoC 的 P2P/coturn 基线。ADR-0005 与[低服务器成本媒体路由](./low-server-media-routes.md)已取代本文早期“每条 peer edge 必带 TURN”的旗舰建议;room `1` 已部署 peer/SFU controller smoke,ordinary ICE 仍为 STUN-only,selected-edge TURN 尚未实现 ## 结论 @@ -15,9 +15,9 @@ | | +---- ICE + STUN 尝试直接 UDP WebRTC ----------+ | - +---- 仅直连失败的观看者 ---- TURN relay -------+ + +---- direct/peer UDP -> SFU root -> selected TURN edge -旗舰目标顺序:一至两名观看者 direct P2P -> 通过门槛后的 peer-assisted -> SFU virtual parent 服务必要 roots;当前生产仍未配置 peer assistance/LiveKit。 +旗舰目标顺序:direct/peer UDP -> SFU virtual parent 服务必要 roots -> optional selected-edge TURN -> 明确失败;生产只为 room `1` 开启未验收 retained media 的 exact smoke。 系统必须无感完成拓扑分配、恢复和必要迁移;大规模公开分享仍直接使用现有直播服务。 ``` @@ -54,7 +54,7 @@ - STUN 让客户端发现公网映射并产生 server-reflexive candidate。它不承载媒体,也不能保证穿过所有 NAT。 - ICE 测试 host、server-reflexive、peer-reflexive 和 relay candidates,并选择可工作的候选对。Trickle ICE 可以减少建连等待。 - TURN 在无法直连时转发完整媒体流,是 NAT 组件中真正产生高带宽成本的部分。 -- 当前 release `769de201f7cc` 的生产候选集合是 STUN、TURN/UDP 和 TURN/TCP,可选再加入 TURN/TLS;这是部署事实,不是新旗舰目标。ADR-0005 的迁移目标是 ordinary peer ICE/UDP -> SFU/UDP roots -> 可选兼容 transport -> bounded failure。ICE 仍可能交错或并发检查候选,应用不手写严格串行计时器;媒体 TCP 在丢包时可能产生队头阻塞。 +- 当前 production release `31bee238bc1e` 只向 ordinary peer 提供 STUN;共享主机保留旧 authenticated coturn relay 配置,但应用不签发凭据且最终审计为零 allocation。ADR-0005 的目标是 direct/peer UDP -> SFU/UDP roots -> optional selected-edge TURN -> bounded failure。每个 PeerConnection 只检查实际配置的候选;媒体 TCP 仍是独立验收项,在丢包时可能产生队头阻塞。 不存在适用于所有用户的权威“P2P 直连率”。CGNAT、endpoint-dependent mapping、校园/企业防火墙、移动网络、IPv6 和地区运营商都会改变结果。首版必须通过 `getStats()` 统计自己的 `host/srflx/prflx/relay` 比例,而不是引用未经验证的行业百分比。 @@ -62,11 +62,11 @@ ICE 是按分享者与每一名观看者的网络组合独立选路,而不是整个房间只做一次连接。一个家庭宽带观看者可能成功 UDP 打洞,另一个处于 CGNAT、对称 NAT、校园网、企业代理或蜂窝网络的观看者却没有可用直连候选。因此同一房间天然可能出现不同结果。 -已部署 PoC 的处理方式是同时提供有效 TURN 凭据和多种传输;旗舰迁移后,下列 2/3 项只描述现行基线: +早期 PoC 曾同时提供 TURN 凭据和多种传输;当前/目标边界如下: 1. 将 direct UDP 设为最高优先级,成功者保持零媒体服务器路径。 -2. 当前生产同时提供 TURN/UDP 与 TURN/TCP candidates;目标 ordinary peer edge 不再默认收到它们,UDP 失败时优先使用 SFU root,可选 coturn 只由新 per-edge grant/wire 授予异常连接。 -3. 通过统计确认最终选中的 candidate pair;LiveKit participant TURN 与 ordinary peer coturn 分别记录,不能从应用计时顺序推断路径。 +2. 当前生产 ordinary peer ICE 为 STUN-only;UDP peer 失败时优先使用 SFU root,可选 coturn 只由新 selected-edge grant/rebuild 授予异常连接。 +3. 通过统计确认最终选中的 candidate pair;LiveKit participant TURN 与 selected-edge coturn 分别记录,不能从应用计时顺序推断路径。 4. 网络切换或候选对失效时执行 ICE restart,超时后重建该 peer connection。 5. 在 UI 和诊断中区分“直连”“服务器中继”“正在恢复”和明确失败原因。 @@ -113,18 +113,18 @@ IETF 对 mesh/SFU 的拓扑说明见 [RFC 7667](https://www.rfc-editor.org/rfc/r ## 推荐的拓扑策略 -已部署 MVP 基线: +当前生产基线: -- 小范围观看者默认 P2P,每人一条独立连接。 +- 未命中 exact allowlist 的小范围观看者默认使用 STUN-only P2P,每人一条独立连接;room `1` 启用 bounded peer-assisted/SFU controller smoke。 - 当前 PoC 默认允许八名观看者,部署者可配置 1 至 16,超额连接会被明确拒绝。该数值只控制接入,不代表 1:8 已通过性能验收;必须收集可用上行、实际发送码率、`qualityLimitationReason`、编码耗时和发送队列来确定真实可持续人数。 -- 每条链路独立使用 ICE;只有失败的链路走 TURN。 -- 若一开始就有多条 `relay`,应提示服务器带宽正在增加。 +- 每条 ordinary peer 链路独立使用 STUN-only ICE;当前应用不向任何 peer connection 下发 TURN credential。 +- room `1` 在 peer recovery 与 alternate parent 耗尽后可准备最多两个 SFU/UDP roots;participant entry 已观察到,但 retained media 尚未验收。 - 桌面和手机观看者使用同一个 Web 播放端;分享者不要求朋友安装完整客户端。 -新的产品目标要求分享端 fanout 永远不超过二。Proposed ADR-0004 用一个可整体删除的实验验证第三名及后续 viewer 能否由客户端转发;`main` 中已合并 PR #17 按 ADR-0005 提供默认关闭、failure-only 的 SFU-root 控制器。旗舰目标要求 SFU 只服务必要 roots 并保留 peer descendants,但配置迁移与真实门槛尚未通过。已关闭 PR #12 的显式整房 SFU 模式已被取代,不再作为当前方案;自动路由通过真实门槛前仍不可部署,超过小房间上限时仍建议使用外部直播服务。观察项包括: +产品目标要求分享端 fanout 永远不超过二。ADR-0004 的可删除实验验证第三名及后续 viewer 由客户端转发;ADR-0005 的自动 controller 已在 room `1` 受限部署,SFU 只服务必要 roots 并保留 peer descendants。retained SFU media、移动端矩阵和 broad rollout 仍未通过门槛;已关闭 PR #12 的显式整房 SFU 模式不再是当前方案。超过小房间上限时仍建议使用外部直播服务。观察项包括: - 正常工作负载持续超过实测可承载的 P2P 人数。 -- 第一名或多名观看者已使用 TURN;这只进入成本观测,不直接触发自动迁移。 +- 当前 edge 已耗尽 peer recovery、alternate parent 与 SFU/UDP;未来只有该 edge 可进入 selected-edge TURN 决策。 - 分享者上行安全余量不足。 - 分享者因 CPU/encoder 限制降质。 - 产品开始要求隐藏好友之间的 IP。 @@ -238,7 +238,7 @@ W3C TAG 的 capability URL 指南指出 URL 仍会出现在地址栏、历史、 来源(访问于 2026-08-19):[RFC 3986 section 3.5](https://www.rfc-editor.org/rfc/rfc3986.html#section-3.5)、[WHATWG WebSockets](https://websockets.spec.whatwg.org/#the-websocket-interface)、[RFC 6455](https://www.rfc-editor.org/rfc/rfc6455.html)、[RFC 6750 section 2.3](https://www.rfc-editor.org/rfc/rfc6750.html#section-2.3)、[W3C Referrer Policy](https://www.w3.org/TR/referrer-policy/)、[W3C TAG Capability URLs](https://www.w3.org/TR/capability-urls/)、[HTML Web Storage](https://html.spec.whatwg.org/multipage/webstorage.html)、[Web Cryptography Level 2](https://www.w3.org/TR/WebCryptoAPI/)、[Node.js Crypto](https://nodejs.org/api/crypto.html)、[SQLite STRICT Tables](https://www.sqlite.org/stricttables.html)、[SQLite ALTER TABLE](https://www.sqlite.org/lang_altertable.html) 与 [OWASP Logging Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html)。 -WebRTC 媒体本身使用 DTLS-SRTP 加密;但 direct P2P 仍会让这组可信好友看到彼此网络地址,若需要隐藏 IP,必须允许强制 TURN,这会增加服务器带宽。 +WebRTC 媒体本身使用 DTLS-SRTP 加密,但 direct P2P 仍可能让这组可信好友看到彼此网络地址。若房间政策要求完全隐藏 endpoint IP,ordinary direct/peer 不能满足,应使用明确的中央媒体路径或拒绝该连接;selected-edge TURN 仅是 SFU/UDP 失败后的兼容 transport,不是全房隐私开关。 ## 编解码策略 @@ -291,7 +291,7 @@ WebRTC 标准没有承诺固定毫秒延迟。工程目标必须带网络条件 | [Tailchat Meeting](https://github.com/msgbyte/tailchat-meeting) | React 捕获生命周期与会议产品交互参考 | Apache-2.0,但媒体基于 mediasoup/SFU,不能作为当前 P2P 拓扑底座 | [ScreenShare.ts](https://github.com/msgbyte/tailchat-meeting/blob/master/app/src/features/ScreenShare.ts)、[media.ts](https://github.com/msgbyte/tailchat-meeting/blob/master/packages/sdk/src/client/media.ts) | | [WebRTC samples](https://github.com/webrtc/samples) | 官方浏览器 API 最小示例 | BSD 风格。用于理解 API,不是产品框架 | [getDisplayMedia](https://github.com/webrtc/samples/tree/gh-pages/src/content/getusermedia/getdisplaymedia)、[peer connection examples](https://github.com/webrtc/samples/tree/gh-pages/src/content/peerconnection) | | [PeerJS](https://github.com/peers/peerjs) | 快速 P2P 原型与简单信令抽象 | MIT。原型快,但产品最终可能需要直接控制 RTCPeerConnection 和统计 | [PeerJS server](https://github.com/peers/peerjs-server) | -| [coturn](https://github.com/coturn/coturn) | 当前生产 STUN/TURN 与未来可选 ordinary-peer fallback | BSD-3-Clause。当前 release 必需,旗舰目标按异常 edge 可选 | [turnserver 文档](https://github.com/coturn/coturn/blob/master/README.turnserver)、[Docker](https://github.com/coturn/coturn/blob/master/docker/coturn/README.md) | +| [coturn](https://github.com/coturn/coturn) | 当前 STUN 服务与未来 selected-edge TURN | BSD-3-Clause。STUN 当前必需,TURN 仅按异常 edge 可选 | [turnserver 文档](https://github.com/coturn/coturn/blob/master/README.turnserver)、[Docker](https://github.com/coturn/coturn/blob/master/docker/coturn/README.md) | | [Peer Calls](https://github.com/peer-calls/peer-calls) | 同一应用中的 mesh/SFU 双模式 | Apache-2.0;维护速度较慢,适合参考而非首选底座 | [mesh.go](https://github.com/peer-calls/peer-calls/blob/master/server/mesh.go)、[sfu.go](https://github.com/peer-calls/peer-calls/blob/master/server/sfu.go)、[iceauth.go](https://github.com/peer-calls/peer-calls/blob/master/server/iceauth.go) | | [Broadcast Box](https://github.com/Glimesh/broadcast-box) | 未来专用一对多 SFU,WHIP 推流/WHEP 播放 | MIT。比会议型 SFU 更贴近单路广播 | [Broadcast.tsx](https://github.com/Glimesh/broadcast-box/blob/main/web/src/components/broadcast/Broadcast.tsx)、[simple watcher](https://github.com/Glimesh/broadcast-box/blob/main/examples/simple-watcher.html) | | [LiveKit](https://github.com/livekit/livekit) | 生产级区域 SFU、SDK、内置 TURN、鉴权 | Apache-2.0。未来需要稳定 SFU 时的首选完整底座 | [屏幕共享](https://docs.livekit.io/transport/media/screenshare/)、[turn.go](https://github.com/livekit/livekit/blob/master/pkg/service/turn.go) | @@ -348,7 +348,7 @@ WebRTC 标准没有承诺固定毫秒延迟。工程目标必须带网络条件 1. 首版坚持 P2P-first,但明确只服务小房间;默认接入上限为八名、可配置 1 至 16,真实 1:8 测量完成前不把它写成性能承诺。 2. Web 先行,目标 Windows Chrome/Edge;把 1080p60 写成 best effort,同时提供降档。 -3. 保留当前 STUN-only coturn 部署直到 ADR-0005 迁移 gate 通过。旗舰默认验证 direct/peer UDP 与 SFU/UDP roots;可选 Web exact-room canary 从 PeerConnection 创建时下发短期 authenticated TURN candidate,由标准 ICE direct-first/relay fallback 选路。未命中房间及 Native 仍为 STUN-only;coturn 只验证 credential 与 expiry,不能验证 room 或 edge。 +3. 保留当前 STUN-only 应用配置直到 ADR-0005 迁移 gate 通过。旗舰先验证 direct/peer UDP 与 SFU/UDP roots;只有 controller 选中的异常 edge 才在两者失败后以新连接使用短期 authenticated TURN。普通 Web/Native PeerConnection 始终 STUN-only;coturn 只验证 credential 与 expiry,room/edge/revision 必须由应用重验。 4. 观看端优先做成免安装响应式 Web;分享端先 Web 验证,再按捕获/音频实测升级 Electron。 5. 产品代码优先直接使用浏览器 WebRTC API;借鉴 MiroTalk BRO 和 Screego,不在许可证未定前直接 fork GPL/AGPL 代码。 6. 分享端 hard fanout 为二;一至两名 viewer 走直接 P2P,第三名及以后只通过 ADR-0004 的 bounded peer-assisted spike 验证,不得隐藏回退为更多 host 连接。 diff --git a/docs/status.md b/docs/status.md index ba8d1d9..9e0c1f9 100644 --- a/docs/status.md +++ b/docs/status.md @@ -4,15 +4,14 @@ Last updated: 2026-08-20 ## Phase -`https://share.bonfire.icu` runs `d4bc421828c4b74f55195723aace290ffc0e5f9d` -since 2026-08-20 11:41:22 +08 with neutral anonymous entry, initial-connect -recovery, and local quality reparenting. +`https://share.bonfire.icu` runs `31bee238bc1e901e823f34e50737e202dc4b04bf` +since 2026-08-20 15:33:21 +08 with Viewer presence, initial-connect recovery, +local quality reparenting, and deterministic relay-admission rescue. Ordinary ICE is STUN-only process-wide; persistent room `1` alone enables the automatic peer/SFU-UDP controller with at most two SFU roots. The old -`05f98d10ecd1` is the immediate rollback with the current v2 DB/env. Crossing -the admission cutover requires `89e6d7649169` plus its recorded old environment; -only deeper pre-access `9610032` may restore v1 state. Source `main` at `a11a73d` -also contains a no-go Native v2 candidate, but production does not run it. +`7fea60ef6f2a` is the immediate rollback with the current v2 DB/env; older +schema rollback boundaries are in `docs/deployment.md`. Native remains no-go +and is not part of production. ## Current Snapshot @@ -24,30 +23,29 @@ also contains a no-go Native v2 candidate, but production does not run it. - Fallback prewarm is token-free and limited to room `1`; mobile/iPad viewers are leaves and healthy edges stay sticky. - After an answer, a generation-bound 15s initial-connect deadline enters ICE restart; success/replacement/disposal cancels it. It is deployed but not mobile-verified. - Room `1` can publish exactly `HIGH+LOW` with Dynacast/backup codec off and subscriber `HIGH` ceilings; it has no retained real media frame. -- Room `1` C+B reparenting remains uncalibrated; source-only admission rescue promotes an unassigned relay over one zero-capacity Host leaf without scores or periodic optimization. -- Source-only roster; Native wire/media unchanged. +- Room `1` C+B reparenting remains uncalibrated. Deployed admission rescue promotes an unassigned one-slot relay over the oldest childless zero-capacity Host leaf, with no scores or periodic optimization. +- Viewer names/true Host roster are deployed; Native wire/media remains unchanged. ## Verified Evidence -- `d4bc421` passed 25 files/378 tests, typecheck and builds. The 760ms switch preserved 200 health/`index-YdNLg2E8.js`, SQLite v2/four rooms/room `1`, 0/0 service restarts, and unchanged environment/nginx/LiveKit hashes. Artifact SHA-256: `3F8CF25F8D989CBDF38DBBCAC4A261C349E44B72D274A944B877540056171A08`. -- The `05f98d1` access cutover passed 25 files/369 tests; four anonymous Chrome routes stayed neutral, the current key returned 200, and former/incorrect keys returned 401. Exact rollback and the contained assertion-output incident are in `docs/deployment.md`. -- At 11:44:15 Screener/LiveKit used 39,198,720/79,269,888 bytes; cgroup high/max/OOM and error journals were zero. No real room triggered reparenting, so this proves deployment only. +- `31bee238` passed 28 files/421 tests and both builds. Health returned in 560.607 ms; observed downtime was 437.137 ms. Health/assets/routes, SQLite v2/four rooms/room `1`, and unchanged config/network gates passed. No real room triggered admission rescue. +- At 15:39:07 Screener used 38,031,360 bytes with zero cgroup events. All four services were active with zero automatic restarts; only Screener had the planned restart. - Chrome 151 synthetic `1/3/5/8` and 720p30 quality-change runs kept 2/1 fanout and decoding; slowest first frame was 1.05 seconds and one relay close recovered in 5.32 seconds. This is control evidence only. - A mobile-network room-1 run produced two root participants for about 4.6 seconds and two short Host participants (about 0.46/0.27 seconds), all client-requested leaves, with zero service restarts and no retained track. This is consistent with the code's one fresh-grant retry and Peer failback, but logs cannot prove those transitions. The deployed Host now shows only the local failure stage. - Host A+B and authenticated P2P Viewer C are sanitized, generation-bound, read-only, and fail closed for stale, ambiguous, or SFU-fed evidence; no raw media metadata is retained. - SVC is `no-go-web-svc-cross-path-hardware-contract`: no direct/peer selection, cross-PC shared encode, or portable hardware proof; pinned screen share is `L1T3`. No browser run was warranted. -- Source TURN tests pass; production/coturn stay STUN-only and untested. +- The `a11a73d` built-in TURN canary is rejected: local allocation passed, but direct Host/Pion Viewer failed before forced relay. Full rollback restored STUN-only client ICE and zero allocations. - Native stays no-go at WebCodecs -> Go RTP. MF proved RTX H.264 hardware (360/360, p95 11.575 ms); `42c01f` misses Pion's default fmtp, so Viewer interop is unproven. -- Access focused tests pass protocol/config/HTTP/storage/RoomStore/SQLite/signaling, including commit-first teardown, persistence-failure continuity, relay/SFU retirement, grant bounds, and v1 migration rollback. +- Access protocol/config/HTTP/storage/SQLite/signaling focused tests pass, including commit-first teardown and v1 rollback. ## Unverified Boundaries -- Room 1 proved LiveKit participant entry but not a retained SFU Viewer frame, exact retry/failback transition, selected UDP pair, edge cap, quality or resource deltas. Former game-share load/blur remains unclassified. +- Room 1 proved LiveKit participant entry but not a retained SFU Viewer frame, exact retry/failback transition, selected UDP pair, admission-rescue transition, edge cap, quality or resource deltas. Former game-share load/blur remains unclassified. - Android Chrome/iOS Safari Viewer leaves remain unverified and conservatively leaf-only. Mobile Web Host is unsupported; native senders are planned only. - Silent partitions can wait 30 to 60 seconds for heartbeat detection before the default 5-second grace; this remains unverified. - Real audio and heterogeneous clients remain unverified. Production reports poor film audio and voice-call self-echo under system capture; there is no app audio ceiling or Web process isolation. Diagnose A/B/C and sync, then gate Windows 11 game-process audio; Windows 10 stays unresolved without system fallback. - The former release reportedly sustained bandwidth-limited blur on a capable LAN; Host refresh recovered while Viewer refresh did not. The new deployment has not yet reproduced or cleared it. -- ADR-0006 stays no-go: its runtime gap is WebCodecs output -> Go frame/RTP; the unrun bridge-generation probe rechecks signal/media/final. Viewer, two-edge/FIFO, and Native TURN remain unverified. +- ADR-0006 stays no-go at WebCodecs output -> Go frame/RTP. Viewer and two-edge/FIFO remain unverified. - Browser fanout is host two/viewer one; any accepted endpoint relay stays capped at two downstream edges. - PR #49/#50 BWE is unverified/default-off: test zero-child leaves, then root impact. No local per-leaf UDP shaper; resume with Linux `tc` or public canary, never CDP. Web P2P/SVC shortcuts remain no-go. - The corrected standby smoke is localhost/headless/video-only; public DNS/TLS reuse, transport, audio, shaping, load, mobile, endurance, and sub-25 ms overlap remain open. @@ -65,7 +63,9 @@ Next gate exactly-two/Dynacast-off BWE on a zero-child SFU leaf, then root evacuation. Native needs separate authorization; its next run gates Go RTP on the first bridge send. Audio A/B/C may proceed without displacing P0. -Peer ICE TURN supersedes selected-edge but remains source-only pending an isolated coturn canary. +Ordinary Peer ICE stays STUN-only. Remove participant-wide issuance; after +SFU/UDP fails, bind one selected parent/child rebuild to current generations and +run one forced-relay canary. Do not repeat the built-in direct canary. ADR-0004 still requires a full-resolution 30-minute `1/3/5/8` network, resource, quality, latency, recovery, and browser/mobile-leaf matrix. A separate diff --git a/docs/方案设计.md b/docs/方案设计.md index 55ab497..94d99b4 100644 --- a/docs/方案设计.md +++ b/docs/方案设计.md @@ -2,7 +2,7 @@ ## 2026-08-18 14:45 -状态:首个 PoC 基线及其已接受扩展的当前设计。Production 仍使用 STUN-only ordinary ICE,并只为 room `1` 启用 exact-room peer-assisted 与 LiveKit SFU/UDP;current source 新增默认关闭的 exact-room built-in Peer ICE TURN/UDP canary,但未修改 coturn 或部署。旧 release 与 coturn relay 仅保留为隔离回滚路径,不是同进程兼容分支。 +状态:首个 PoC 基线及其已接受扩展的当前设计。Production 仍使用 STUN-only ordinary ICE,并只为 room `1` 启用 exact-room peer-assisted 与 LiveKit SFU/UDP。默认关闭的 built-in Peer ICE TURN canary 已失败并回滚,不再是产品方向;下一候选只能在 SFU/UDP 后由 controller 选择单条异常 edge。旧 release 与 coturn relay 仅保留为隔离回滚路径,不是同进程兼容分支。 ## 目标与边界 @@ -30,7 +30,7 @@ | 可选房间持久化 | Node.js 内置 `node:sqlite` | 单文件、无额外依赖;只保存房间 ID、Host token 摘要与 nullable Viewer grant 摘要 | | 运行时校验 | Zod | WebSocket 和 HTTP 输入不可信,需要与 TypeScript 类型一致的运行时边界 | | 静态文件 | `sirv` | 生产环境由同一 Node 进程提供前端构建结果,避免引入完整 Web 框架 | -| 地址发现/可选兼容 | coturn 4.17.2 或更新补丁版本 | tracked/production 保持 `stun-only`;独立部署验收后,exact-room canary 才可启用 authenticated TURN REST、UDP relay range 与 quotas,不启用媒体 TCP | +| 地址发现/鉴权回退 | coturn 4.17.2 或更新补丁版本 | 当前 tracked template 与 production 应用只下发 STUN,但共享机保留旧 authenticated-relay daemon 配置/端口规则且不下发 credential;未来只为 controller 选中的异常 edge 签发短期 TURN,先验收 TURN REST、UDP relay range 与 quotas,不启用媒体 TCP | | 测试 | Vitest | 覆盖纯逻辑和 Node 集成测试;真实媒体能力另做浏览器/网络矩阵 | | TLS 入口 | Caddy 或 nginx,由部署者选择 | 统一 HTTPS/WSS;不把证书管理写进应用进程 | @@ -229,39 +229,34 @@ fixed-`HIGH` -> 现有 WebRTC/一至两名 Web Viewer 的 P2 窄实验。硬件 ## 媒体传输配置 -当前仓库候选已经删除旧全房 TURN contract。`ServerConfig` 接受 `STUN_URLS` -以及独立的新 tuple:`PEER_ICE_TURN_URLS`、 -`PEER_ICE_TURN_SHARED_SECRET`、 -`PEER_ICE_TURN_CREDENTIAL_TTL_SECONDS`。新 tuple 全空/缺失即默认关闭,部分 -配置、超过一个或非 UDP TURN URI、少于 32 或多于 128 visible ASCII bytes 的 secret、 -300 至 1800 秒以外的 TTL、secret 复用或未开启 exact-room peer assistance 都 -启动失败。旧 `TURN_URLS`、`TURN_SHARED_SECRET`、 -`TURN_CREDENTIAL_TTL_SECONDS` 仍 fail-fast,绝不作为 alias。生产缺失 STUN 也 -启动失败。 +当前仓库已经删除旧全房 TURN contract,`ServerConfig` 必须配置 `STUN_URLS`。 +Source 仍含 `PEER_ICE_TURN_*`、participant capability、`refresh-ice` 与 +`ice-config` 的 default-off built-in canary;其真实 direct acceptance 失败并完整 +回滚,因此这些字段不得配置、部署或作为 accepted target。后续 selected-edge +实现应以新的完整 default-off tuple 替换这组 participant-wide contract,部分配置、 +非法 UDP TURN URI、弱或复用 secret、越界 TTL 仍必须启动失败。旧 `TURN_URLS`、 +`TURN_SHARED_SECRET`、`TURN_CREDENTIAL_TTL_SECONDS` 继续 fail-fast,绝不作为 alias。 -Web authenticate 可选声明严格 capability `{ peerIceTurn: true }`。只有当前 -session 命中 exact allowlist 且新 tuple 完整时,`authenticated.iceConfig` 才在 -STUN group 后增加一个带 temporary username/credential 的 TURN/UDP group 与 -`turnCredentialsExpiresAt`;其他 Web 房间和未声明 capability 的 Native v2 均 -保持原 STUN-only shape。`refresh-ice` 不接受 room、edge、revision 或 expiry, -服务端只按当前 authenticated socket 授权;窗口前复用同一 config,进入到期前 -两分钟后签发新 bearer 并发送 `ice-config`,每个 session 的请求最短间隔五秒。 -客户端 `SignalingClient` 是唯一 timer owner,并用 30 秒最小延迟避免时钟偏差造成 -立即循环,断线取消;Host/Viewer 页面只更新 retained config 并对 existing PCs 调 -`setConfiguration()`,不主动 restart,后续新 PC 或现有 staged recovery 自然使用 -fresh config。凭据不进入 URL、日志、browser storage、room row 或 SQLite。 +普通 direct/peer `RTCPeerConnection` 的 authenticated snapshot 只含 STUN。 +Controller 先完成一次 ICE restart、一次同 parent rebuild、一个 alternate peer,随后 +准备最多两个 SFU/UDP roots;只有该 edge 仍无法通过 SFU/UDP 时,才生成一次 +selected-edge TURN attempt。该 attempt 必须同时绑定 room/share generation、viewer +与 parent session、route revision、replaced connection 和 server-generated next +connection identity;parent 与 child 只为该新 PC 协同重建,Viewer 使用 relay-only +配置。任何代次变化、超时、成功或失败都消费内存态 grant;普通 PC、其他 edge、 +Native v2 与 authenticated snapshot 继续 STUN-only。凭据不进入 URL、日志、browser +storage、room row 或 SQLite。 -普通 direct/peer `RTCPeerConnection` 与 LiveKit participant 是两个明确的 -ICE 域。普通边只消费 `authenticated.iceConfig` 并显式使用 -`iceTransportPolicy: "all"`,由标准 candidate priority direct-first;SFU publisher/subscriber -只消费 revision-bound `sfu-config {url, token}`,再由 LiveKit SDK 与服务端 -协商自己的 candidates。tracked LiveKit 配置只开放 UDP mux,显式使用 +普通 peer、LiveKit participant 与 selected-edge coturn 是三个明确 ICE 域。SFU +publisher/subscriber 只消费 revision-bound `sfu-config {url, token}`,再由 LiveKit +SDK 与服务端协商 candidates。tracked LiveKit 配置只开放 UDP mux,显式使用 `tcp_port: 0`、`allow_tcp_fallback: false` 和部署自托管 `stun_servers`,不配置 -external TURN 或 embedded TURN。Node 应用不能替代部署 -验证来证明该 UDP listener 从公网可达;自托管 `stun_servers` 也避免 pinned -LiveKit 1.13.5 继承默认公共 Google STUN。 +external TURN 或 embedded TURN。Node 应用不能替代部署验证来证明该 UDP listener +从公网可达;自托管 `stun_servers` 也避免 pinned LiveKit 1.13.5 继承默认公共 +Google STUN。 -当前可执行梯级是 `direct/peer UDP -> SFU ICE/UDP -> failure`。Viewer 的 +当前可执行梯级是 `direct/peer UDP -> SFU ICE/UDP -> failure`;accepted target 在 +SFU/UDP 后增加一次 selected-edge TURN,再失败则明确终止。Viewer 的 现有两次有界 ICE restart/rebuild 后报告 route failure;router 先尝试局部 reparent,再在 root budget 内准备 SFU,最终没有可行 route 时返回明确的 `PEER_NOT_FOUND`,不长时间维持伪连接。HTTPS/WSS 始终使用 TLS/TCP,不 @@ -269,17 +264,15 @@ reparent,再在 root budget 内准备 SFU,最终没有可行 route 时返回 旧 release 需要 coturn TURN/UDP+TCP,目前仅作为独立回滚实例保留;current production 不根据 room allowlist 保留旧 release TURN 兼容分支。 -`PEER_ASSISTED_ROOM_IDS` 同时灰度 topology/SFU route 与可选 Peer ICE TURN; -未命中房间及 capability-absent 客户端始终 STUN-only。因此验证与回滚必须使用 +`PEER_ASSISTED_ROOM_IDS` 灰度 topology/SFU route;未命中房间始终 STUN-only,且 +普通 peer edge 即使命中也不能默认收到 TURN candidates。因此验证与回滚必须使用 隔离 candidate/旧实例,不能假设同进程未命中房间仍有旧 transport。 -Peer ICE TURN 是 built-in WebRTC candidate,不增加 selected-edge router wire。 -应用只向当前 exact-room participant session 授权下发短期 bearer;username 的 -opaque tag 不暴露 room/peer/session,但 coturn 只验证 HMAC 与 expiry,无法验证 -room、edge、parent、route revision 或 connection generation,也不能阻止持有者 -在 TTL 内复用。依靠短 TTL、session 授权、endpoint fanout 与 coturn allocation/ -bandwidth quota 限界,不宣称密码学 edge binding。LiveKit participant-wide TURN -仍是独立 ICE 域,不能冒充 ordinary Peer ICE。 +Selected-edge TURN 复用 `HybridMediaRouter` 的 assignment/revision owner,不新增 +第二个路由器或周期优化器。Coturn 只能验证 HMAC 与 expiry,不能自行验证 room、 +edge 或 generation;应用层必须在签发、parent/child rebuild、ready/failure 与清理 +边界逐次重验,并用短 TTL、one-use attempt、endpoint fanout 与 allocation/bandwidth +quota 限界。LiveKit participant-wide TURN 是独立 ICE 域,不能冒充 selected edge。 SFU 正常只作为 virtual parent 向一至两个必要 roots 发布;可靠 root 收到 媒体后继续服务 sticky descendants。host/native root 最多两条下游媒体边, @@ -331,8 +324,8 @@ hop 按[低服务器成本媒体路由](./research/low-server-media-routes.md) - 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/production 自托管 coturn 仍必须启用 `stun-only` 及 `no-tcp`/`no-tls`;当前源码 slice 不修改它。未来 exact-room 部署验收可在隔离配置中移除 `stun-only`,启用 `use-auth-secret`、独立 realm/secret、小 relay UDP range 与 allocation/bandwidth quotas,并先验证 STUN 与临时 credential;仍不启用匿名 TURN、TCP 或 TLS。coturn 4.17.2 已弃用 `no-dtls`,tracked 配置不使用它。 -- 分享者直连会向可信观看者暴露网络地址。当前 candidate 不提供 force-relay 产品分支;即使配置 Peer ICE TURN,`all` policy 仍由标准 ICE 优先直连。UI 和后台不得显示 bearer 或把 relay 宣称为匿名/密码学 edge 隔离。 +- tracked `stun-only` 及 `no-tcp`/`no-tls` 配置只是当前回滚/验收前模板,不是最终产品目标。当前共享机的应用 TURN tuple 已关闭且只下发 STUN,daemon/防火墙仍是旧 authenticated-relay baseline(TCP/UDP 3478 与 UDP 49152-49251),没有 credential 下发,最后审计 allocation 为零。Selected-edge 验收必须使用独立 realm/secret、小 relay UDP range 与 allocation/bandwidth quotas,并先验证 STUN、SFU/UDP exhaustion 与单条短期 credential;不启用匿名 TURN 或媒体 TLS/TCP。coturn 4.17.2 已弃用 `no-dtls`,tracked 临时配置不使用它。 +- 分享者直连会向可信观看者暴露网络地址。普通 peer PC 不配置 TURN;只有 controller 选择的异常 edge 可在重建时 relay-only。UI 和后台不得显示 bearer 或把 relay 宣称为匿名或 coturn 侧密码学 edge 隔离。 ## 验证计划 @@ -341,18 +334,18 @@ hop 按[低服务器成本媒体路由](./research/low-server-media-routes.md) - HostAdmission 的 production 必配、8 至 128 visible-ASCII bytes/独立 secret、local-dev 显式关闭、12 小时签名 cookie、过期、篡改、cookie 属性、旧 `ACCESS_PASSWORD` fail-fast 和无服务端 session 状态; - RoomStore 的随机临时房间、TTL、Host token/Viewer grant 摘要验证、private/public policy、grant 到期/跨房拒绝、rotate/revoke 强撤销、停止分享等待、Viewer 上限和超额拒绝; - SQLite 房间库从 `1` 自增、重启恢复、v1 -> v2 单事务 locked-private 迁移、nullable 32-byte CHECK、畸形值 fail-closed、rotate/revoke 写失败原子性和数据最小化;数据库路径无 Host admission 时启动失败; -- STUN/TURN UDP URL 严格校验、生产 STUN 必需、新 Peer ICE tuple 完整性/TTL/secret 独立性、issuer HMAC/expiry/opaque tag、`refresh-ice` 当前 session 授权/窗口复用与 `ice-config` strict schema,以及未命中房间和 Native-shaped capability-absent session 的 STUN-only isolation;LiveKit `sfu-config` 保持独立边界; +- STUN/TURN UDP URL 严格校验、生产 STUN 必需、selected-edge tuple 完整性/TTL/secret 独立性、issuer HMAC/expiry/opaque tag、当前 route/session/revision/old-new connection generation 与 one-use rebuild guard,以及 ordinary peer、未命中房间和 Native 的 STUN-only isolation;LiveKit `sfu-config` 保持独立边界; - 单一 `screener-v2` 信令 literal、v1 旧页面 mismatch 的一次性刷新终态,以及协议非法 JSON、未知消息、错误方向、跨房间目标和 payload 限制; - 质量协议的 host 授权、严格字段与数值范围、last-wins、当前/未来 relay、已配置 SFU 和普通 P2P wire 兼容;sender 的四字段 requested/applied 读回、拒绝与持续受限提示; - HTTP/WS 集成:nginx per-source admission `limit_req` 与统一错误不泄露 secret,应用不建立 IP limiter 状态;Viewer 无 Host cookie 可在 Origin/容量门下 upgrade;Host 首条鉴权同时要求 upgrade-captured admission 与 Host token,Viewer 只按 room policy;Host cookie 不绕过 private grant,grant 不能建房/Host/跨房。覆盖 private fragment/sessionStorage/replaceState、独立无 grant 访问、不含 query/Referrer/log、public code-only、public-to-private 一次性 grant,以及 rotate/revoke 后 grace/route 清理、旧 Viewer/上游媒体边关闭和 reconnect 拒绝; -- 客户端策略与 stats 解析:会话替换不重连,本地 TURN protocol 与 ICE protocol 分离,远端 TURN transport 保持未知; +- 客户端策略与 stats 解析:会话替换不重连,ordinary ICE、LiveKit 与 selected-edge TURN protocol 分离,远端 TURN transport 保持未知; - TypeScript 类型检查、Vite production build 和仓库 hygiene。 人工矩阵: - 同机双标签页与同 LAN direct; - 真实跨网络、同房间 direct/peer UDP + SFU/UDP root; -- CGNAT、双 NAT、移动热点和普通家宽下使用隔离 candidate,分别验证 TURN 未配置时的 direct/peer 与 SFU/UDP、TURN 配置后 direct-selected 与 forced relay-selected peer;记录 1/3/5/8 participants 的空闲 allocation/relay port/RSS/CPU/latency,以及 1/2 relayed edges 的 RX/TX/loss/latency。人为封锁全部 UDP 时仍必须有界明确失败。旧 production release 留在独立实例作为回滚,不在同进程保留旧 transport; +- CGNAT、双 NAT、移动热点和普通家宽下使用隔离 candidate,先验证 STUN-only direct/peer 与 SFU/UDP,再只对 controller 选中的单条失败 edge 验证 forced relay;记录 selected attempt 的 allocation/relay port/RSS/CPU/latency 与 RX/TX/loss。人为封锁全部 UDP 时仍必须有界明确失败。旧 production release 留在独立实例作为回滚,不在同进程保留旧 transport; - Windows Chrome/Edge 的 tab/window/monitor 音轨有/无、同时语音通话和 video-only;Windows 11 原生候选另测 game process tree、通知、无渲染流与进程重启,Windows 10 记录明确不支持/未解决; - Android Chrome 与 iOS Safari 的播放手势、旋转、前后台和网络切换; - 仅在 Windows native/deploy P1 完成后,Android 14/API 34+ 原生 sender 才运行一至两名 Web Viewer 真机 gate:整屏、授权拒绝与撤销、旋转、锁屏停止、Wi-Fi/蜂窝切换、持续热降频、硬件 codec 读回/实测、目标 UID 已知/未知的 audio capture policy、secure/protected content、A/V sync 与资源释放;单应用窗口只在 Android 14 QPR2+ 测试; @@ -369,7 +362,7 @@ hop 按[低服务器成本媒体路由](./research/low-server-media-routes.md) 3. 完成屏幕捕获、逐观看者 PeerConnection、观看播放和手动质量档。 4. 完成连接恢复和 stats 面板;随后以 STUN-only snapshot 替换旧全房 TURN credential/refresh wire。 5. 完成 automatic peer/SFU controller 与 exact-room canary 边界。 -6. 在同一 exact-room 边界增加默认关闭的 built-in Peer ICE TURN tuple、capability、短期 bearer 与单一 refresh owner;用有界网络矩阵验收 direct-selected peer、relay-selected peer、SFU/UDP、资源成本和明确失败,coturn/production rollout 保持独立验收边界。 +6. 在 SFU/UDP retained-media gate 后增加默认关闭的 selected-edge TURN tuple、revision-bound one-use grant 与 parent/child rebuild;用有界网络矩阵验收 ordinary STUN-only、SFU/UDP、单条 forced relay、资源成本和明确失败,coturn/production rollout 保持独立验收边界。 7. 在媒体主线阶段验收后,用一个原子 PR 迁移 Host admission、`screener-v2`、room-scoped Viewer grant 和 SQLite v2;同一部署切换客户端/服务端/环境和数据库备份,随后删除 v1 配置、wire 与测试。用户名/roster 保持独立。 8. P1 继续完成 Windows native fixed-`HIGH` staged gate 与 deployment;移动 Host 不阻断或先于它。 9. P2 只在上述门槛后运行 Android 14/API 34+ fixed-`HIGH` 原生 sender spike;失败即停,不扩展为跨平台 framework。 @@ -379,7 +372,9 @@ hop 按[低服务器成本媒体路由](./research/low-server-media-routes.md) ## 2026-08-19 Peer-Assisted 实验边界 -状态:Proposed / Experiment Only。该扩展不改变上面的已部署 PoC,也不表示正式采用。 +历史状态(2026-08-19):Proposed / Experiment Only。后续 ADR-0005 已接受 +bounded controller,且 production 已启用 room `1` exact smoke;下列内容保留为 +最初实验门槛,不代表当前 rollout 状态。 部署设置 `PEER_ASSISTED_MEDIA=true` 时必须同时提供非空的 `PEER_ASSISTED_ROOM_IDS` 精确限制实验房间。该值只接受逗号分隔、符合现有 @@ -387,10 +382,10 @@ room code 规则且不重复的 ID;缺失、空值或非法配置都启动失 fail-open。未命中房间不得进入 `HybridMediaRouter`、房间质量同步或 LiveKit 兜底,route 字段、定向信令、停止、 重连和删除保持普通 P2P 行为;未命中房间的 ordinary ICE 始终 STUN-only, -不接收可选 Peer ICE TURN config。命中房间仍要求完整 tuple 与 Web capability, -Native v2 保持 STUN-only。该边界不增加 UI、用户选择或百分比框架。 +不接收 TURN config。命中房间的 ordinary peer PC 与 Native v2 同样保持 STUN-only; +只有 controller-selected edge 可接收一次短期 grant。该边界不增加 UI、用户选择或百分比框架。 -产品硬目标是任一非服务器节点任何时刻最多两条下游媒体边。系统按一个或两个观看者直接 P2P、更多观看者 peer-assisted、部署 SFU 中央兜底的顺序自动分配和恢复,分享者与观看者不选择拓扑。默认媒体 transport 只使用 UDP;SFU 作为 virtual parent 正常只送一至两个 roots,roots 之后仍优先沿受限 peer descendants 扩散;多个无法挂到健康 root 的极端 viewers 只可使用另设上限的中央 edge。Production 已把普通 ICE 收敛为 STUN-only、删除旧全房 TURN wire并只为 room `1` 开启 failure-only controller;current source 的 optional built-in Peer ICE TURN 默认关闭,只给 exact-room capable Web sessions,以标准 ICE direct-first/relay fallback,不改变拓扑。exact-room 公网媒体、coturn allocation/resource 和 forced relay 仍未验收,旧 release/coturn 仅作回滚。ADR-0004/0005 的 broad-rollout 验收尚未完成。 +产品硬目标是任一非服务器节点任何时刻最多两条下游媒体边。系统按一个或两个观看者直接 P2P、更多观看者 peer-assisted、部署 SFU 中央兜底、最后 selected-edge TURN 的顺序自动分配和恢复,分享者与观看者不选择拓扑。默认媒体 transport 只使用 UDP;SFU 作为 virtual parent 正常只送一至两个 roots,roots 之后仍优先沿受限 peer descendants 扩散;多个无法挂到健康 root 的极端 viewers 只可使用另设上限的中央 edge。Production 已把普通 ICE 收敛为 STUN-only、删除旧全房 TURN wire并只为 room `1` 开启 failure-only controller;source 中失败的 optional built-in Peer ICE TURN 仍默认关闭且不得部署,replacement selected-edge 尚未实现。exact-room 公网 SFU media、selected relay 与资源仍未验收,旧 release/coturn 仅作回滚。ADR-0004/0005 的 broad-rollout 验收尚未完成。 恢复目标按证据和成本逐级升级:短暂 `disconnected` 先观察同代 bytes/stats,`failed` 才执行一次 ICE restart;仍失败时重建一次相同 parent,再尝试不同的合格 peer parent,peer 候选耗尽后才申请 SFU。每个动作最多一次且成功即停,不实现“三次相同重试”。初始 `connecting`/`checking` 的软提示和硬窗口尚未实现;硬窗口必须先用移动网络实测确定,不能直接复用现有 3 秒恢复常量。已实现的 SFU active 失败只刷新一次短期 grant,第二次失败回到新 peer baseline。Host 只保留当前 route revision 的 `connect | source | video-publish | sender-config | audio-publish | transport` 枚举阶段作为本地事件提示,不上传原始错误、URL、token、candidate 或地址。 diff --git a/docs/需求理解.md b/docs/需求理解.md index f7f7231..51b54ea 100644 --- a/docs/需求理解.md +++ b/docs/需求理解.md @@ -4,7 +4,7 @@ ## 需求概述 -构建一个面向熟人游戏场景的低延迟屏幕共享工具。分享者可以在私密房间内分享游戏、应用窗口或显示器画面及声音,多名朋友可以即时观看。系统采用类似 Photon Host Mode 的产品抽象:中心服务负责房间、身份、确定性拓扑和建立连接,媒体优先级依次为小规模直接 P2P、peer-assisted 转发、部署提供的 SFU virtual parent;STUN 协助打洞,SFU 只向一至两个必要 roots 送流,roots 继续分布式服务其 peer descendants。TURN 是部署可选、默认关闭并只在 exact-room canary 下提供的 Web peer ICE transport 兼容层,不是独立拓扑;标准 ICE 仍优先直连候选。系统必须自动、无感地完成分配、故障恢复和必要的路线迁移,分享者与观看者不手动选择拓扑。分享端 Web 优先并允许后续 Electron/原生增强;观看端必须可通过桌面或手机浏览器打开邀请链接,不绑定专用客户端。 +构建一个面向熟人游戏场景的低延迟屏幕共享工具。分享者可以在私密房间内分享游戏、应用窗口或显示器画面及声音,多名朋友可以即时观看。系统采用类似 Photon Host Mode 的产品抽象:中心服务负责房间、身份、确定性拓扑和建立连接,媒体优先级依次为小规模 direct/peer UDP、部署提供的 SFU/UDP virtual parent、可选的 authenticated selected-edge TURN,最后明确失败;SFU 只向一至两个必要 roots 送流,roots 继续分布式服务其 peer descendants。TURN 是默认关闭的异常边 transport,不是拓扑,也不得默认加入普通 peer edge 的 candidates。系统必须自动、无感地完成分配、故障恢复和必要的路线迁移,分享者与观看者不手动选择拓扑。分享端 Web 优先并允许后续 Electron/原生增强;观看端必须可通过桌面或手机浏览器打开邀请链接,不绑定专用客户端。 当前 PoC 默认允许一名分享者和最多八名观看者,部署者可在 1 至 16 之间配置上限,超额观看者会被明确拒绝。已接受的自动分布式拓扑目标把默认值和可配置上限提高到 20,但只能与通过 20 人资源、质量、深度和恢复门禁的路由 release 原子启用;在此之前不得只改常量,让普通逐观看者 P2P 建立 20 条 Host 边。产品仍不承担公开直播或大规模观看,相关需求交给 OBS/Twitch 等成熟直播服务。 @@ -22,7 +22,7 @@ - [ ] 一对多观看:当前安全默认上限为 8、可配置范围为 1 至 16;自动分布式拓扑验收后,同一 release 将默认值和上限改为 20。当前 PoC 的逐观看者连接是测量基线,不再是超过两名观看者时的目标拓扑;未通过 20 人门禁时继续明确拒绝超额加入。 - [ ] 端点 fanout 硬上限:任一非服务器端点任何时刻最多维护两条 active downstream media edges,包括加入、断线、换父和恢复过程;上游接收不计入这项上传预算。当前 browser relay 因逐跳重编码仍保守限制为一个 child,只有资源矩阵通过后才可升到二。第三名及后续观看者由系统自动分配给通过验收的 peer-assisted 拓扑,不能隐藏地回退为 Host 第三条连接。没有可行 peer path 时,SFU 作为主要中央兜底 virtual parent 正常向一至两个必要 roots 送流;媒体到达 root 后其余可行观看者仍优先沿受限 peer descendants 扩散。多个无法挂到健康 root 的极端 viewer 可成为额外 server-fed 例外,但必须受显式中央 egress/admission 上限约束,不能无界整房 fanout。TURN 只作为可选的极端网络 transport 兼容层,不定义房间拓扑。 - [ ] P2P 优先:每条逻辑媒体边的 ICE 必须先尝试直接 UDP 路径,服务器不得默认中转可直连的媒体。一个或两个观看者优先直接连接分享端;更多观看者的 peer-assisted 行为须先通过 ADR-0004 的实验门槛。 -- [ ] NAT 与传输兜底:STUN 必须可用。默认情况下以及所有未命中 allowlist 的房间只接收 STUN ICE servers。仅当完整、独立的 `PEER_ICE_TURN_*` tuple 与 exact room 同时启用时,声明 `peerIceTurn` capability 的 Web session 才在每条 bounded peer `RTCPeerConnection` 创建时收到短期 authenticated TURN/UDP,并固定 `iceTransportPolicy: "all"`,由标准 ICE candidate priority 自动 direct-first/relay fallback;Native v2 未声明 capability,必须继续获得严格 STUN-only wire。PC 失败后仍按一次 ICE restart、一次同 parent rebuild、一个 alternate peer、再 SFU virtual parent 的顺序恢复,任一步成功即停止。`SignalingClient` 是唯一续期 owner,提前两分钟请求;服务端只认当前 authenticated session、在窗口前复用同一 bearer,凭据不进入 URL、日志、浏览器持久化或 SQLite。TURN REST bearer 只能由应用按 room/role/peer/session 授权下发,coturn 本身只验证 HMAC 与 expiry,无法验证 room、edge、revision、connection generation,也不能阻止持有者在 TTL 内复用。当前 slice 只支持 TURN/UDP,不配置 LiveKit TURN、媒体 ICE/TCP、TURN/TCP 或 TURN/TLS;HTTPS/WSS 仍使用 TLS/TCP。 +- [ ] NAT 与传输兜底:STUN 必须可用,所有 ordinary peer `RTCPeerConnection` 只接收 STUN ICE servers。PC 失败后按一次 ICE restart、一次同 parent rebuild、一个 alternate peer、再 SFU/UDP virtual parent 的顺序恢复,任一步成功即停止;只有该 edge 的 SFU/UDP 也失败后,controller 才可授权一次短期 authenticated selected-edge TURN/UDP rebuild。授权必须绑定当前 room/share generation、viewer/parent session、route revision 与 old/new connection identity,成功、失败、超时或代次变化即消费;不得按 participant、房间或普通 peer PC 下发,Native v2 保持 STUN-only。凭据不进入 URL、日志、浏览器持久化或 SQLite。Coturn 只验证 HMAC 与 expiry,应用必须重验 edge 代次并用短 TTL、one-use state 与 quota 限界。媒体 ICE/TCP、TURN/TCP 或 TURN/TLS 另行验收;HTTPS/WSS 仍使用 TLS/TCP。 - [ ] 质量控制:保留 1080p60(8 Mbps)、1080p30(5 Mbps)、720p30(3 Mbps)三档推荐上限,默认采用清晰优先;分享期间可直接换档或应用有界高级设置,不重新选择来源或重建健康连接。Peer-assisted relay 与已配置的 SFU 必须接收同一个严格校验的房间级权威设置,覆盖当前和以后建立的 sender;普通 P2P 信令结构保持不变。任何设置都只是浏览器上限和偏好,必须读回实际 sender 参数并显示参数拒绝、改写和持续受限原因。不做自定义动态 FPS、综合评分或周期性全房间调档。 - [ ] 路径隔离双表示:direct/peer 继续使用每条 PeerConnection 独立的 stock WebRTC GCC,不把任何 viewer 反馈合成全房目标。每条 SFU path 从 `HIGH` ceiling 开始;优先用 pinned LiveKit 2.22.0/1.13.5 的标准两层 screen-share simulcast,在 Dynacast off 下常驻 exactly `HIGH+LOW`,让内建 per-subscriber BWE 独立降层和恢复。若该 gate 通过,Screener 不实现媒体层 selector。首 gate 使用 zero-descendant SFU leaf;root-with-children 的自动 downshift 与有界迁移是 default-on 前独立硬门。只有内建选择不满足门槛时才用标准 subscriber quality 显式限制,常驻成本不合格时再测标准 sender 手动启停。`LOW` 停编只是资源优化;active representation/layer 硬上限为二,禁止按 viewer 增长。必须实测实际接收层、硬件编码证据、游戏 FPS/p1 low、CPU/GPU、区间 encode cost、上传和分层 bytes。当前 Web P2P simulcast 缺少 receiver 选 RID 和跨 PeerConnection shared-encode 契约;当前 Web/LiveKit SVC 也缺少 direct/peer 独立选层与可移植硬件保证,且 pinned screen share 强制三层 temporal-only `L1T3`,二者按现契约静态 no-go。全部标准路径失败后才考虑 custom/native dual encode。没有合格 hardware/power-efficient 路径或额外表示破坏游戏/上传预算时 fail closed:保护健康路径并明确显示弱路径降级或不可用。 - [ ] 路径级质量状态:应用的 `HIGH`/`FALLBACK` 非对称窗口只决定拓扑资格和诊断,不命令正常的媒体层切换,也不计算综合分数。SFU BWE downshift 先进入 `suspect`;多个连续窗口的 sender/viewer 关联证据确认 `FALLBACK` 后,应用不得稳定保留该 parent,并把 downstream capacity 降为 0。计划或显式质量 fallback 仅是内建路径失败后的候选:已有 children 时先在 connection/assignment generation guard 下全部迁走,失败或代次过期则保持原 assignment/quality。Autonomous downshift 无法 packet-level 预先迁移,default-on 前必须验收 descendants 的短暂影响。只有更长的稳定恢复窗口和 cooldown 后才能重新声明 relay capacity。Viewer 请求必须鉴权、限频、去重,并由 sender 的 GCC/Transport-CC、实际码率、RTT、丢包、重传、编码限制、协商 codec/profile/parameters 与 viewer 解码、丢帧、冻结证据交叉验证;不能仅凭自报生效,也不得上传 raw SDP、candidate 地址或原始设备/网络标识。 @@ -35,9 +35,9 @@ ### P1 - 重要功能 -- [ ] 私密房间双入口:在不引入账号、密码找回或通用服务端 session 的前提下,private room 同时接受“房间号 + 该房间观看密码”和现有 `/r/{code}#v={grant}` 私密邀请;有效 grant 直接进入目标房间,没有 grant 的 `/join` 或 `/r/{code}` 只显示中性的房间密码输入。Host 可设置或轮换房间观看密码,并可选择复制仅含房间号的入口或默认附带 room-scoped access token 的完整邀请。房间密码、grant 与 verifier 的权限都只能是该房间 Viewer,不得进入 URL query、`localStorage`、日志或 SQLite 明文,也不能取得 Host admission、建房、发布或跨房权限。失败响应不区分房间不存在与密码错误,并复用有界 admission 限制;确切 verifier/KDF 参数必须在目标 1 GiB 部署机基准后选择。该项排在当前自动路由、TURN 兜底和 Windows Native 主线之后,不得提前改写生产能力声明。 +- [ ] 私密房间双入口:在不引入账号、密码找回或通用服务端 session 的前提下,private room 同时接受“房间号 + 该房间观看密码”和现有 `/r/{code}#v={grant}` 私密邀请;有效 grant 直接进入目标房间,没有 grant 的 `/join` 或 `/r/{code}` 只显示中性的房间密码输入。Host 可设置或轮换房间观看密码,并可选择复制仅含房间号的入口或默认附带 room-scoped access token 的完整邀请。房间密码、grant 与 verifier 的权限都只能是该房间 Viewer,不得进入 URL query、`localStorage`、日志或 SQLite 明文,也不能取得 Host admission、建房、发布或跨房权限。失败响应不区分房间不存在与密码错误,并复用有界 admission 限制;确切 verifier/KDF 参数必须在目标 1 GiB 部署机基准后选择。该项排在当前自动路由、selected-edge TURN 兜底和 Windows Native 主线之后,不得提前改写生产能力声明。 -- [ ] 实验路由按房间灰度:部署开启 `PEER_ASSISTED_MEDIA` 时必须同时提供非空、严格的房间 ID allowlist,只有指定房间进入 peer-assisted、已配置的 SFU 路径与可选 Peer ICE TURN;缺失、空值、非法或重复配置启动即失败,绝不 fail-open 为全房。未命中房间继续使用当前 ordinary P2P 拓扑及其质量拒绝、停止、重连和删除语义,并保持 STUN-only。Peer ICE TURN 还要求完整新 tuple 与显式 Web capability;无 capability 的 Native/旧客户端同样 STUN-only。旧 production release 与新 candidate 必须用隔离实例验证和回滚,不能在产品代码建立长期双 transport 分支。exact-room 只服务本阶段验证;验收后以独立 coherent change 退役灰度边界,不能靠清空变量暗中放量。该能力不增加 UI、用户选择、百分比发布或第二套路由器。 +- [ ] 实验路由按房间灰度:部署开启 `PEER_ASSISTED_MEDIA` 时必须同时提供非空、严格的房间 ID allowlist,只有指定房间进入 peer-assisted 与已配置的 SFU 路径;缺失、空值、非法或重复配置启动即失败,绝不 fail-open 为全房。未命中房间继续使用当前 ordinary P2P 拓扑及其质量拒绝、停止、重连和删除语义,并保持 STUN-only。未来 selected-edge TURN 只能复用同一 controller 的当前 assignment/session/revision/connection generation,不得借 allowlist 向普通 peer edge 预发 TURN candidates。旧 production release 与新 candidate 必须用隔离实例验证和回滚,不能在产品代码建立长期双 transport 分支。exact-room 只服务本阶段验证;验收后以独立 coherent change 退役灰度边界,不能靠清空变量暗中放量。该能力不增加 UI、用户选择、百分比发布或第二套路由器。 - [ ] 质量控制扩展:推荐档位仍是默认入口;折叠高级区只允许 720p/1080p/1440p 分辨率 ceiling、15 至 60 的整数 FPS ceiling、2 至 12 Mbps 视频码率 ceiling,以及清晰/平衡/流畅三种偏好。直播中显式应用后才生效,避免滑杆产生连续控制流量。当前 Web 发送端不提供音频声道、采样率、Opus 码率、stereo、DTX 或 FEC 控件:display capture 未把声道和采样率定义为可移植控制,标准音频 `maxBitrate` 只是可能导致更低质量或停播的发送上限,其他项依赖 codec/fmtp 协商而没有可移植 sender setter。除非真实音频矩阵证明用户问题、标准 API 能应用并读回且实际结果可独立观测,否则不得为这些控件新增应用层 SDP munging 或用默认值制造伪控制。实际质量持续低于所选上限时必须显示可解释的限制原因;在真实拥塞、编码器和接收端证据完成前,不把任一组合称为“完美平衡”。 - [ ] 本地预览性能候选:在相同动态场景中 A/B 测量 Host 动态预览开/关的 CPU、GPU、encode time 和实际 FPS。只有稳定收益且捕获、编码和远端画面不受影响时,才允许直播开始后默认显示静止缩略图或隐藏动态预览并提供按需展开;未测前不宣称收益。 @@ -49,8 +49,8 @@ - [ ] SFU-root 中央兜底:产品部署目标必须提供经 canary 验收的单节点 SFU 资源;系统在 direct/peer UDP 不可用或明确拓扑预算无法接纳时自动把它作为主要中央兜底,不要求用户选模式。它只向总计一至两个必要 roots 出口;root 优先承担受限 peer descendants,没有可靠 relay root 时必要 viewer 只作为同一总上限内的零 descendants root。已关闭 PR #12 的显式整房 SFU 模式已被取代。Production room `1` 已开启 failure-only exact-room smoke,但尚无 retained SFU media;通过 ADR-0005 的公网媒体、资源和恢复门禁后才能扩大到默认梯级。 - [ ] 分享端桌面封装:用 Electron 固定 Chromium 版本、改善选源/热键/系统音频体验,同时保留 Web 观看端。 - [ ] Windows 应用音频:Windows 11 原生候选默认仅捕获所选游戏及其子进程的音频,避免把语音聊天或通知声一并分享;“全部系统声音”只能由分享者显式选择。普通 Windows 10 不具备当前 process-loopback API 门槛,必须明确标为未解决/不支持而不是伪兼容。浏览器无法证明或隔离任意进程音频时必须如实提示,原生捕获失败、无声或进程退出不得静默扩大为整机捕获。 -- [ ] 连接自检:在开始分享前验证信令、STUN 和 SFU ICE/UDP 可达性,并给出不暴露地址或凭据的诊断结果。启用 Peer ICE TURN canary 时只报告脱敏的配置/可达性与 selected candidate type,不输出 URL、username、credential、地址或原始 candidate;媒体 TCP 不在当前配置面。 -- [ ] 受限网络增强:短期鉴权 TURN 只能由完整、默认关闭的 `PEER_ICE_TURN_*` tuple 与 exact-room allowlist 启用;capable Web peer PC 从创建起使用 `all` 配置,由标准 ICE direct-first 选路,未命中房间和 Native 保持 STUN-only。媒体 TCP 仍需独立 canary;两者都不能被描述为更高清的传输方式。 +- [ ] 连接自检:在开始分享前验证信令、STUN 和 SFU ICE/UDP 可达性,并给出不暴露地址或凭据的诊断结果。启用 selected-edge TURN canary 时只报告脱敏的授权阶段与 selected candidate type,不输出 URL、username、credential、地址或原始 candidate;媒体 TCP 不在当前配置面。 +- [ ] 受限网络增强:短期鉴权 TURN 只能由完整、默认关闭的部署 tuple 与 exact-room controller 启用,并且只能在 direct/peer UDP 与 SFU/UDP 均已按有界梯级失败后,向 controller 选中的当前异常 edge 一次性授权。普通 peer PC 从创建起保持 STUN-only;不得向整个 participant、房间或所有 peer connections 预发 TURN candidates。凭据必须绑定当前授权 session 与 route/connection generation、短期有效、内存态、一次性消费且不进入 URL、日志、浏览器持久化或 SQLite。媒体 TCP 仍需独立 canary;两者都不能被描述为更高清的传输方式。 - [ ] 运行时编解码选择:先保留浏览器默认协商;只有端点能力相交、Media Capabilities 对目标分辨率/FPS/码率报告 supported/smooth/power-efficient,且实际 outbound codec、区间 encode time、游戏 FPS、CPU/GPU 与 sender 数量共同通过时,才调整 H.264、VP8、VP9 或 AV1 优先级。AV1 不得因压缩率推断而默认启用,任何候选都不得静默落入高负载软件编码。 - [ ] 诊断页面:用户可导出不含敏感地址和凭据的连接诊断信息。 @@ -106,9 +106,9 @@ - Given 两端网络允许 UDP 打洞,When 观看者加入,Then `getStats()` 显示选中的候选对不是 `relay`,且服务器没有承载媒体流量。 - Given direct/peer UDP 无法建立但 LiveKit SFU/UDP 可达,When 观看者加入或现有 edge 恢复耗尽,Then 系统只把必要 root 切到 SFU,保留可行 peer descendants 和未受影响路径,无需用户选择模式。 - Given 未配置 TURN 且所有媒体 UDP 路径均不可用,When 有界连接窗口耗尽,Then 界面快速显示通用连接失败,不静默黑屏、不长时间假连接,也不自动尝试媒体 TCP。 -- Given 未配置完整 `PEER_ICE_TURN_*` tuple、房间未命中 exact allowlist 或客户端未声明 capability,When 该会话鉴权或请求 `refresh-ice`,Then authenticated snapshot 必须保持 STUN-only,续期请求 fail closed;不得把 TURN credential 发给 Native v2、普通房间或未鉴权会话。 -- Given exact-room Web session 已获临时 TURN bearer,When expiry 进入提前两分钟窗口,Then client 只发送无参数 `refresh-ice`,服务端用当前 session 授权并轮换;窗口前重复请求复用同一 config,现有 PC 仅 `setConfiguration` 而不主动 restart,新 PC 与后续既有恢复使用 fresh config。Given bearer 已下发,Then 明确接受 coturn 无法验证实际 room/edge 且持有者可在 TTL 内复用,依靠短 TTL、应用授权、fanout 和 coturn quota 限界。 -- Given 部署者为 LiveKit 启用了 embedded/external TURN,When publisher 或 subscriber 的 SFU/UDP 路径耗尽,Then 只把它作为 LiveKit participant-to-SFU 兼容 transport 验收;该 ICE 域与 Screener ordinary Peer ICE TURN 分开配置、鉴权和验收。 +- Given 未配置完整 selected-edge TURN tuple、房间未命中 exact allowlist、当前 route/connection generation 不匹配或该 edge 未经过 controller 选择,When 会话鉴权、peer PC 创建或旧消息重放,Then authenticated snapshot 与 ordinary peer PC 必须保持 STUN-only,服务端不得签发或接受该 TURN grant;不得把 credential 发给 Native v2、普通房间或未鉴权会话。 +- Given 当前异常 edge 已耗尽 direct/peer UDP 与 SFU/UDP 且 controller 授权一次 selected-edge TURN attempt,When parent 与 child 以同一 session/revision/old-and-new connection generation 重建,Then 只有该新 PC 使用 relay-only 短期 credential;成功即停止,失败或过期即消费并明确失败,不得把 bearer 复用于其他 edge、后续 PC 或整个 participant。 +- Given 部署者为 LiveKit 启用了 embedded/external TURN,When publisher 或 subscriber 的 SFU/UDP 路径耗尽,Then 只把它作为 LiveKit participant-to-SFU 兼容 transport 单独验收;它不是 Screener selected peer-edge grant,也不得改变 controller 的默认梯级。 - Given 部署者另行启用并验收媒体 TCP/TLS 兼容模式,When UDP 被封锁,Then 对应路径建立或返回可诊断失败;未启用该能力不阻塞面向消费网络的旗舰验收,HTTPS/WSS 仍正常使用 TLS/TCP。 - Given 同一房间中一名观看者可以 direct/peer UDP、另一名只能通过 SFU root 接收,When 两者同时观看,Then 前者保持分布式 P2P,后者使用 SFU,双方互不迁移且 host/SFU root 上限不变。