From 0af91c7c38c7c6ddbfbe2046548366ccc3305787 Mon Sep 17 00:00:00 2001 From: AAA <35992542+TNTcraftHIM@users.noreply.github.com> Date: Sat, 22 Aug 2026 21:40:46 +0800 Subject: [PATCH] docs(route): define local selection and fallback gates (#199) --- .../0005-automatic-hybrid-media-routing.md | 21 ++++++++++ docs/project-memory.md | 4 +- docs/research/advanced-peer-distribution.md | 27 +++++++++---- docs/research/low-server-media-routes.md | 39 ++++++++++++------- docs/status.md | 10 +++-- docs/todo.md | 14 ++++--- docs/方案设计.md | 21 ++++++---- docs/需求理解.md | 8 +++- 8 files changed, 103 insertions(+), 41 deletions(-) diff --git a/docs/adr/0005-automatic-hybrid-media-routing.md b/docs/adr/0005-automatic-hybrid-media-routing.md index 3e04aec..bcb8a0d 100644 --- a/docs/adr/0005-automatic-hybrid-media-routing.md +++ b/docs/adr/0005-automatic-hybrid-media-routing.md @@ -69,6 +69,20 @@ failure, and accepted path-health evidence may open a bounded reassignment. The controller does not continuously optimize the room, infer policy from a user agent, or aggregate unrelated paths into a room-wide quality score. +Parent selection is deterministic and local. The controller first filters on +current authority, source reachability, acyclicity, depth, sender reservations, +relay eligibility, exact-edge exclusion/cooldown, and server admission. It then +orders eligible parents lexicographically by the shallowest resulting depth, +the greatest remaining steady sender capacity, stable join order, and peer +identity. It prepares one candidate at a time. The candidate's standard ICE +checklist proves connectivity and current-generation RTP plus decoded frames +prove media; raw addresses, a claimed NAT class, geography, user agent, or a +weighted room-wide score never choose a parent. A failed candidate releases its +reservation before the next candidate is attempted. Join and hard-failure repair +accept the first candidate that reaches the media-usable floor; a soft-quality +move also needs the quality owner to prove recovery and improvement over the +still-healthy old edge. + ### Authorization and transition Every prepare, signal, recovery, commit, and rollback is bound to the exact @@ -171,6 +185,9 @@ Before a revised controller ships: signals, departure, rollback, and admission exhaustion; - route changes preserve unaffected branches and never commit before media proof; +- candidate lists are deterministic under input permutation, preserve a healthy + current edge, prefer the shallowest least-loaded eligible parent, and try only + the next eligible candidate after exact failure and idempotent cleanup; - quality tests distinguish child-scoped C+B reparent from parent-scoped corroborated drain, prove that same-edge C+B cannot drain a parent, require independent sibling edges for parent corroboration, include a Viewer that is @@ -227,6 +244,10 @@ Negative: - [TURN, RFC 8656](https://www.rfc-editor.org/rfc/rfc8656.html) - [RTP topologies, RFC 7667](https://www.rfc-editor.org/rfc/rfc7667.html) - [PIM-SM Join/Prune behavior, RFC 7761](https://www.rfc-editor.org/rfc/rfc7761.html) +- [ICE connectivity checks and candidate checklists, RFC 8445](https://www.rfc-editor.org/rfc/rfc8445.html) +- [NICE degree-bounded application-layer multicast](https://conferences.sigcomm.org/sigcomm/2002/papers/appmulti.pdf) +- [Overcast adaptive single-source distribution trees](https://www.usenix.org/conference/osdi-2000/overcast-reliable-multicasting-overlay-network) +- [Kubernetes cordon and drain](https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands) - [LiveKit selective subscription](https://docs.livekit.io/transport/media/subscribe/#selective-subscription) - [LiveKit track subscription permissions](https://docs.livekit.io/transport/media/publish/#track-permissions) - [Kubernetes controllers](https://kubernetes.io/docs/concepts/architecture/controller/) diff --git a/docs/project-memory.md b/docs/project-memory.md index abb066b..4837d6d 100644 --- a/docs/project-memory.md +++ b/docs/project-memory.md @@ -10,11 +10,13 @@ Last updated: 2026-08-22 - Every non-server endpoint uses one server-authoritative steady outbound media-copy capacity: default `2`, statically configurable as `1`, `2`, or `3`. A peer child or the Host publication consumes one slot; upstream receive is free, selected TURN replaces the same copy's transport, and SFU subscriber egress is accounted at the server. Browser role/UA/visibility does not create a separate tier, and clients cannot raise the deployment value. - The stable routing invariants are one authoritative upstream per Viewer, an acyclic active graph, exact room/session/share/revision/generation authorization, media-proven make-before-break, and bounded success or failure. - The accepted route model is one room controller with allocate/distribute, child reparent, and relay abdicate/drain operations. One Host publication may serve all SFU-fed Viewers; an SFU-fed Viewer may relay only with independent outbound proof; selected TURN changes an authorized edge transport and is not a topology node. A confirmed bad edge reparents only its child. A relay parent's own ingress failure first enters suspect/cordon and attempts local repair; only failed repair, independent downstream evidence, or explicit sender/resource failure drains its children. Host source failures use publication repair and child migration. Endpoint capacity, temporary overlap, SFU/TURN admission, exact fallback, media binding generations, and rollback are one bounded transaction model. +- Parent choice is sticky and deterministic: hard-filter current authority, reachability, cycles, depth, capacity, eligibility, exact-edge cooldown, and server admission; then order by shallowest result, remaining steady sender capacity, stable join order, and peer identity. Standard ICE and exact media proof test one candidate at a time. Raw IP, claimed NAT type, UA, geography, all-pairs probes, weighted scores, and periodic room rebalancing are not route inputs. - Ordinary peer ICE is STUN-only. Credentials, room secrets, candidate details, and diagnostic data remain private and narrowly scoped. - Screen audio offers live-switchable 64/128/256 kbps sender ceilings and defaults to 128. The existing last-wins quality wire owns desired state; each endpoint serially applies and reads back current Host/relay/SFU audio senders, exposes local failures, and gives future senders the latest desired value. Opus remains fixed, and without an applied-ack wire the Host does not claim room-wide atomic convergence. These are configuration ceilings, not fidelity claims. - Video codec selection remains `automatic | H.264 | VP8`. The accepted first switching boundary is a generation-fenced renegotiation/republish transaction while sharing is explicitly paused; unpaused hot switching is later reuse of that transaction, not an independent mechanism. - A source-checkout LAN one-command launcher is accepted as a Web development/deployment convenience. It reuses the current Node/config/access/storage contract and does not claim to be a packaged client, TLS provisioner, NAT traversal tool, or public deployment service. - Current UI, presence, route labels, and diagnostics describe observed state only; they do not create route authority. +- Viewer startup feedback remains on the stage until current-generation frame presentation is proven, distinguishes peer, fallback, media, autoplay, recovery, and failure phases, and keeps `Play` specific to autoplay rejection. The strict-NAT guessed-candidate idea remains an isolated emulator gate; only a pass may authorize one manually approved, kill-switched canary in an operator-owned test room with explicit consent from both endpoints and exact room/edge/generation/budget bounds. ## Current Source And Production @@ -28,7 +30,7 @@ Last updated: 2026-08-22 ## Current Priority 1. Keep this truth set and [the TODO ledger](./todo.md) concise and internally consistent. -2. Rebuild the accepted route runtime from the latest canonical `main`, while independently delivering the live audio-ceiling mutation and fresh-main LAN launcher. Follow with paused codec switching, evidence-led H.264 repair, release validation, deployment, and rollback evidence. +2. Rebuild and release the accepted route runtime first. Then release live audio and paused video-codec switching, follow with evidence-led H.264 diagnosis and the strict-NAT emulator gate, then connection feedback, the LAN launcher, and the remaining accepted roadmap. Parallel branches may finish earlier but do not change this merge/deployment order. ## Working Rules diff --git a/docs/research/advanced-peer-distribution.md b/docs/research/advanced-peer-distribution.md index 6a31d64..3fe0194 100644 --- a/docs/research/advanced-peer-distribution.md +++ b/docs/research/advanced-peer-distribution.md @@ -304,13 +304,21 @@ has an upstream, never handles a failed-path intent, and never selects a root by quality. An authenticated viewer can still lie about its relay capacity, so the exact-room canary and ordinary route-failure recovery remain required; the lie can cause at most this one local move before the candidate is no longer -unassigned. Broader experiments -may react to a hard media failure, a reviewed threshold-crossing event from -correlated path evidence, or a proven native capacity change, but never a -continuous optimizer. Do not use a weighted score, UA/device model, IP -geography, or one party's unverified report. Keep healthy assignments sticky, -move only one affected subtree, use separate enter and recovery thresholds plus -a cooldown, and disable proactive moves for the share after repeated rollback. +unassigned. Broader experiments may react to a hard media failure, a reviewed +threshold-crossing event from correlated path evidence, or a proven native +capacity change, but never a continuous optimizer. Standard ICE already owns +candidate-pair priority, pruning, connectivity checks, peer-reflexive discovery, +and nomination inside one `RTCPeerConnection`; the application should not build +a second ping mesh. Route selection first applies exact authority, reachability, +acyclicity, depth, sender-slot, eligibility, exclusion/cooldown, and server +admission filters, then uses a small lexicographic order: shallowest resulting +tree, most remaining steady sender slots, stable join order, and peer identity. +Only the selected provisional edge receives real connectivity and media proof; +failure releases it before the next candidate. Do not use a weighted score, +UA/device model, IP geography, claimed NAT type, or one party's unverified +report. Keep healthy assignments sticky, move only one affected subtree, use +separate enter and recovery thresholds plus a cooldown, and disable proactive +moves for the share after repeated rollback. Use make-before-break only when the new parent has a free ordinary downstream slot under the server-authoritative configured cap. A provisional child reserves @@ -424,7 +432,7 @@ failed-parent state. ## Sources And License Boundary -Sources checked on 2026-08-20 and 2026-08-21: +Sources checked on 2026-08-20, 2026-08-21, and 2026-08-22: - [WebRTC SVC](https://www.w3.org/TR/webrtc-svc/), [Encoded Transform](https://www.w3.org/TR/webrtc-encoded-transform/), @@ -441,6 +449,9 @@ Sources checked on 2026-08-20 and 2026-08-21: [room defaults](https://github.com/livekit/client-sdk-js/blob/v2.22.0/src/room/defaults.ts), and [state reconciliation](https://github.com/livekit/client-sdk-js/blob/v2.22.0/src/room/Room.ts) - pinned implementation behavior, not a universal timeout prescription. +- [Kubernetes cordon/drain](https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands) + - operational analogy for stopping new children before bounded evacuation; + no scheduler or disruption framework is copied. - [LiveKit server 1.13.5 participant quality aggregation](https://github.com/livekit/livekit/blob/v1.13.5/pkg/rtc/participant.go) and [connection scorer](https://github.com/livekit/livekit/blob/v1.13.5/pkg/sfu/connectionquality/scorer.go) - Apache-2.0; studied only, with no source copied. diff --git a/docs/research/low-server-media-routes.md b/docs/research/low-server-media-routes.md index 54b3346..b231dff 100644 --- a/docs/research/low-server-media-routes.md +++ b/docs/research/low-server-media-routes.md @@ -150,7 +150,7 @@ network changes, but their selected ICE path must be measured. | --- | --- | --- | | Endpoint-independent mapping with UDP | Full ICE can check host, server-reflexive, and peer-reflexive paths | Keep direct/peer UDP first | | One endpoint-dependent mapper | Coordinated checks can sometimes create a peer-reflexive path; success is not guaranteed | Exhaust the existing bounded restart/rebuild/alternate-parent steps | -| Both peer endpoints use endpoint-dependent mapping | A direct peer path is not reliable; RFC 8835 requires TURN support for this case | A public SFU/UDP or authorized TURN path may still work when outbound UDP reaches that server; exact route ownership is pending | +| Both peer endpoints use endpoint-dependent mapping | A direct peer path is not reliable; RFC 8835 requires TURN support for this case | A public SFU/UDP or authorized TURN path may still work when outbound UDP reaches that server; ADR-0005 owns exact edge and resource authority | | All outbound UDP is blocked | The UDP media ladder has no reachable candidate | End with a clear bounded failure | | Wi-Fi/cellular or address change | Old mappings and candidate pairs can become invalid | Use a new opaque generation and bounded ICE restart/rebuild, then re-run the same priority ladder | @@ -243,18 +243,27 @@ change or explicit reconnect; it does not support periodic probing, a carrier churn, keep the healthy SFU route and fix the generation/commit boundary before another rollout. -### Isolated Eight-Guess Candidate Spike +### Isolated Bounded Guessed-Candidate Spike -The earlier categorical rejection of every browser-side prediction experiment -was too broad. WebRTC does not expose raw UDP, but the WebRTC API does let the -application signal a remote ICE candidate and call `addIceCandidate()`. An owned -experiment can therefore inject at most eight nearby remote-port candidate -guesses in total across both endpoints for one edge generation and let the -browser ICE agents send authenticated connectivity checks. This is bounded port -prediction, not the birthday-paradox technique proposed for native peers with -hundreds of sockets and probes. +WebRTC does not expose raw UDP, but the WebRTC API lets the application signal a +remote ICE candidate and call `addIceCandidate()`. An owned experiment can +therefore inject a small, explicitly capped set of nearby +remote-port candidate guesses for one edge generation and let the browser ICE +agents send authenticated connectivity checks. The initial harness ceiling of +eight guesses across both endpoints is a safety fixture to bound abuse and ICE +work, not a product constant or evidence that eight is optimal. This is bounded +port prediction, not the birthday-paradox technique proposed for native peers +with hundreds of sockets and probes. -- Scope: zero production integration. Use an isolated test page or harness and +RFC 5780 behavior discovery is Experimental, requires a STUN server that +supports alternate addresses, observes only current behavior toward those test +destinations, and explicitly does not replace ICE. It may help an owned emulator +confirm that a mapping pattern exists, but route decisions must still use actual +operating connectivity and media evidence. A NAT label or two observed ports do +not authorize a guessed production path. + +- Current status: zero product or production integration and not yet eligible + for a production canary. Use an isolated test page or harness and a sequential endpoint-dependent NAT emulator fixture. Reuse existing candidate signaling and ICE generations; do not mutate SDP or create a second production protocol. @@ -271,8 +280,11 @@ hundreds of sockets and probes. connectivity-check packets may exceed eight because guesses pair with local candidates and ICE retransmits checks; the candidate budget is not a packet budget. Safari is measured separately before any product decision. Only an - emulator pass permits a paired, explicitly consented mobile canary; it is still - not production evidence. + emulator pass permits one manually approved canary in an operator-owned test + room. Both endpoints must explicitly consent, and the canary must be default-off + behind a kill switch and bound to one exact room, logical edge, ICE generation, + deadline, and aggregate candidate budget. It is still not general production + evidence. - Stop line: reject the candidate if it needs raw sockets, extra peer connections, browser-specific SDP rewriting, more than eight total guesses, unbounded retries, a longer black-screen deadline, or fails the two-engine @@ -611,6 +623,7 @@ Primary sources accessed on 2026-08-19, 2026-08-21, and 2026-08-22: standards and BCP under IETF Trust terms. - [UDP NAT behavior, RFC 4787](https://www.rfc-editor.org/rfc/rfc4787.html), [P2P across NATs, RFC 5128](https://www.rfc-editor.org/rfc/rfc5128.html), + [NAT behavior discovery, RFC 5780](https://www.rfc-editor.org/rfc/rfc5780.html), [PCP, RFC 6887](https://www.rfc-editor.org/rfc/rfc6887.html), and [CGN requirements, RFC 6888](https://www.rfc-editor.org/rfc/rfc6888.html) - mapping, filtering, prediction, simultaneous-open, gateway-control, and CGN diff --git a/docs/status.md b/docs/status.md index 3864eca..944b816 100644 --- a/docs/status.md +++ b/docs/status.md @@ -17,14 +17,16 @@ This is the current execution index. Git history owns completed timelines; [veri - The accepted endpoint direction is one steady outbound media-copy cap for every non-server endpoint: default `2`, static `1/2/3`, a peer child or Host publication consumes one slot, upstream receive is free, selected TURN replaces the same copy's transport, and no Browser/UA/visibility tier exists. - Automatic P2P-first routing, one active upstream, an acyclic graph, exact identity/generation authority, media-proven make-before-break, and bounded failure remain accepted. - The holistic assisted-route model is accepted: one controller performs allocation, child reparenting, and relay abdication/drain. One Host publication serves SFU subscriptions; TURN is an exact edge transport, not a topology node. Endpoint cap, transition overlap, media-binding generations, and independent SFU/TURN admission are governed by one bounded transaction and remain runtime validation work. +- Parent selection is deterministic rather than random or score-based: hard eligibility filters, then shallowest result, remaining sender capacity, stable join order, and peer identity; one provisional edge uses standard ICE and media proof at a time. Viewer connection feedback and the strict-NAT emulator gate are accepted follow-up work, not deployed behavior. - Live 64/128/256 audio-ceiling mutation, a source-checkout LAN one-command launcher, and paused-share video codec switching are accepted but not yet implemented on canonical `main`. H.264 startup blur/low-FPS reports remain an evidence-led diagnosis, not a presumed codec defect. ## Current Milestone -1. Rebuild the cap/accounting runtime from exact canonical `main`, preserving safe identity, media-proof, and rollback behavior. -2. In independent fresh-main branches, deliver live audio-ceiling mutation and the scoped LAN one-command launcher. -3. Add paused-share codec switching on the resulting generation/transition boundary, and reproduce the H.264 startup report before any codec-specific repair. -4. Validate cap `1/2/3`, child and parent quality ownership, constrained Host/SFU/TURN paths, codec/profile mutation, bounded resources, and rollback before deployment. +1. Rebuild and release route waves A-D from exact canonical `main`, including cap/accounting, bounded server admission, deterministic local parent selection, quality ownership, and one controller. +2. Release live audio-ceiling mutation and paused-share codec switching on the resulting generation/transition boundary. +3. Reproduce the H.264 startup report before any codec-specific repair, and build the strict-NAT emulator gate before any manually approved canary in an operator-owned test room. +4. Add first-frame-driven Viewer connection stages, then integrate the scoped LAN launcher and remaining accepted tools. +5. Validate and deploy each coherent slice in this order with its focused browser/network/resource and rollback gates. ## Active Boundaries diff --git a/docs/todo.md b/docs/todo.md index c778ba0..f67a1b3 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -7,12 +7,14 @@ Only items in **Now** are executable after their stated decision gate. A branch ## Now 1. **Route wave A: endpoint capacity and accounting.** From the latest canonical `main`, atomically bump the incompatible signaling wire and replace the Browser/role tiers with `ENDPOINT_MEDIA_COPY_CAPACITY=1|2|3`, default `2`. A peer child or Host publication consumes one steady sender slot, upstream receive is free, committed selected TURN replaces the same logical edge transport, and a media-producing candidate needs the one bounded overlap reservation. Reject an old client before room authority, reject a fourth Host copy when `C=3`, and keep parent-wide quality drain fail-closed until its accepted evidence owner exists. Do not merge the old configurable-cap worktree. -2. **Switch screen-audio ceilings live.** Keep Opus fixed and serialize 64/128/256 kbps desired profile updates across every current Host, relay, and SFU audio sender. Each endpoint uses fresh `getParameters()`/`setParameters()` readback, ignores stale completions, retains media plus its prior applied value on failure, and applies the latest desired value to future edges. Do not add a remote applied-ack protocol or claim room-wide atomic convergence in this slice. -3. **Rebuild the LAN one-command launcher.** From fresh canonical `main`, transplant only the useful launcher/tests/wrappers from `feat/local-oneclick-bundle`, replace its stale access variable with `SITE_ACCESS_PASSWORD`, keep local SQLite state out of Git, and verify startup, health, localhost Host, and LAN Viewer output on PowerShell and POSIX boundaries. This is a source-checkout launcher, not an installer or public deployment system. +2. **Route waves B-D: bounded fallback, selection, and controller convergence.** Replace the temporary fixed SFU subscriber guard with explicit deployment-wide SFU ingress/egress admission; then replace the temporary room-wide selected-lease guard with per-edge TURN allocation admission and exact accounting. Converge allocation, child reparent, and relay drain on one committed graph/controller. Parent candidates use hard eligibility filters followed by deterministic shallowest, remaining-capacity, join-order, and peer-identity ordering; standard ICE plus exact media proof tests one candidate at a time. Do not add raw-IP/NAT ranking, all-pairs probes, a weighted score, or periodic rebalancing. The temporary guards are safety boundaries, not accepted product limits, and must not be removed early. +3. **Switch screen-audio ceilings live.** Keep Opus fixed and serialize 64/128/256 kbps desired profile updates across every current Host, relay, and SFU audio sender. Each endpoint uses fresh `getParameters()`/`setParameters()` readback, ignores stale completions, retains media plus its prior applied value on failure, and applies the latest desired value to future edges. Do not add a remote applied-ack protocol or claim room-wide atomic convergence in this slice. 4. **Switch video codec while paused.** After the route/media-generation boundary is available, serialize the change with route mutation, freeze exact current route/media bindings, hold new joins, renegotiate P2P/relay and republish SFU while paused, then verify actual codec plus decoded frames on resume. A binding invalidated by leave or hard route failure exits this proof set and waits for routing to rebuild from the final committed preference after the bounded codec transaction settles. On failure pause again and perform bounded rollback; rollback proves the prior preference was reapplied plus a newly negotiated actual codec and decoded progress, not literal equality with the old actual codec. Zero targets update only the future-edge preference. Reset `automatic` with empty codec preferences. This is a paused bounded-gap transaction; unpaused make-before-break is a later extension. 5. **Diagnose the production H.264/blur report.** Reproduce exact release `769de201f7cc` against current `main` on the same Host/browser/driver/game and one wired direct Viewer, changing one variable at a time. Implement a codec-specific repair only if codec/encoder evidence identifies it; otherwise fix the proven capture, sender, BWE, or lifecycle cause. -6. **Route waves B-D: bounded fallback and controller convergence.** Replace the temporary fixed SFU subscriber guard with explicit deployment-wide SFU ingress/egress admission; then replace the temporary room-wide selected-lease guard with per-edge TURN allocation admission and exact accounting. Only after those resource owners exist, converge allocation, child reparent, and relay drain on one committed graph/controller. The temporary guards are safety boundaries, not accepted product limits, and must not be removed early. -7. **Validate and release.** Run focused state-machine tests, child-vs-parent quality ownership, cap `1/2/3` topology and recovery matrices, constrained Host/SFU/TURN paths, audio/codec mutation, independent review, build/full gates, bounded production preflight, deployment, postflight, and rollback verification. +6. **Gate the strict-NAT guessed-candidate experiment.** Build only the isolated endpoint-dependent NAT emulator and browser harness first. Standard ICE must fail in the owned fixture, the capped candidate set must pass the documented repeatability, deadline, privacy, and cleanup gates in stable Chrome and Firefox, and fallback must remain unchanged. Only then may one manually approved, operator-owned test room run a default-off, kill-switched canary with explicit consent from both endpoints and exact room/edge/generation/budget bounds; failure closes the candidate. Do not ship guessed candidates to ordinary rooms or infer NAT type from IP, UA, carrier, or two STUN observations. +7. **Make Viewer connection progress explicit.** Derive one stable stage from existing access, route, peer/SFU, media, autoplay, and bounded-failure state. Keep the overlay until a composited current-generation frame, show `Play` only for actual autoplay rejection, preserve a still-valid old frame during recovery, and expose retry only through the bounded controller. Measure the existing token-free SFU prewarm and route stages before adding another connection mechanism. +8. **Rebuild the LAN one-command launcher.** From fresh canonical `main`, transplant only the useful launcher/tests/wrappers from `feat/local-oneclick-bundle`, replace its stale access variable with `SITE_ACCESS_PASSWORD`, keep local SQLite state out of Git, and verify startup, health, localhost Host, and LAN Viewer output on PowerShell and POSIX boundaries. This is a source-checkout launcher, not an installer or public deployment system. +9. **Validate and release in priority order.** Review, merge, and deploy each coherent slice from canonical `main`: route first, then audio/codec mutation, then H.264/NAT evidence and any proven repair, then connection feedback and remaining tools. Run the focused state machine and browser/network matrix relevant to that slice, plus independent review, build/full gates, bounded production preflight, postflight, and rollback verification. A parallel branch may finish early but does not jump this rollout order. The existing route implementation is an input to these tasks. Preserve its verified generation, authorization, make-before-break, and media-proof invariants while migrating capacity and fallback accounting. The accepted model is the destination; each wave must remain safe and truthful on its own rather than installing a name-only controller or an unreserved overlap. @@ -23,7 +25,7 @@ These items are real product work, ordered after the current release boundary; t 1. **Native shared encode, Windows first.** Prove one hardware encoder output can serve two independent Viewer transports and the Host-SFU publication while each connection retains its own RTP/RTCP, pacing, encryption, feedback, and upload accounting. Productize only after actual access-unit identity, Viewer2/FIFO, heterogeneous feedback, game-resource, browser/hardware, A/V, packaging, and licensing gates pass. Web Host/relay remains standard per-`RTCPeerConnection` encoding because no portable shared-encoder contract exists. 2. **Mobile Viewer lifecycle.** Run Android Chrome and iOS Safari device matrices for autoplay gesture, foreground/background audio, foreground video recovery, lock/page reclamation, rotation, and Wi-Fi/cellular migration. Preserve the same media element and current reconnect path; do not promise background video composition or unsupported lock-screen behavior. 3. **Distributable sender.** Turn a proven Windows native path into a signed, installable, updateable sender before expanding capture/audio backends to macOS and Linux. A source checkout or evaluation ZIP is not a self-contained cross-platform client. -4. **Real network acceptance.** Measure standard ICE/STUN, IPv4/IPv6, independent STUN destinations, selected TURN, SFU ingress, and bounded failure across representative NATs and mobile networks. Do not implement browser port prediction, raw-IP ranking, or a room-wide quality score; parent choice uses exact edge evidence and the accepted route controller. +4. **Real network acceptance.** Measure standard ICE/STUN, IPv4/IPv6, independent STUN destinations, selected TURN, SFU ingress, and bounded failure across representative NATs and mobile networks. Browser port prediction remains experiment-only until its emulator and allowlisted-canary gates pass; raw-IP ranking and a room-wide quality score remain out of scope. Parent choice uses exact edge evidence and the accepted route controller. 5. **Platform output only when real.** Revisit AirPlay/Cast only when a target browser and physical receiver prove the live `MediaStream` contract. System mirroring remains external; no placeholder button, custom receiver, or server transcode is scheduled. ## Product Decisions Needed @@ -42,7 +44,7 @@ These decisions are not permission to continue old branches, and current deploym - Open PR #192 measures synthetic flash/tone timing; it is not an A/V correction feature. - The Native PR stack #16/#18/#22/#23/#25/#28 and related fanout branches are one research program, not six product TODOs. -- Cap3, resource, route-recovery, Viewer-MBB, signaling-blackhole, Host-generation, SFU-shaping, NAT, mobile, and endurance artifacts remain bounded evidence. Merge or retain them only when a current decision consumes them. Browser NAT/port prediction, raw-IP ranking, AirPlay/Cast receivers, and mobile background guarantees are not implied by the current implementation batch. +- Cap3, resource, route-recovery, Viewer-MBB, signaling-blackhole, Host-generation, SFU-shaping, NAT, mobile, and endurance artifacts remain bounded evidence. Merge or retain them only when a current decision consumes them. The isolated strict-NAT harness authorizes no product path until its gate passes; raw-IP ranking, AirPlay/Cast receivers, and mobile background guarantees are not implied by the current implementation batch. - Real heterogeneous-network SFU/TURN, mobile lifecycle, audio/A-V device, and endurance/resource acceptance are scheduled at the relevant release boundary. ## Candidate Handling diff --git a/docs/方案设计.md b/docs/方案设计.md index b753207..7f5e31a 100644 --- a/docs/方案设计.md +++ b/docs/方案设计.md @@ -297,12 +297,15 @@ Host 与 relay Viewer 复用同一个纯 presentation reducer 保存远端 `View - 访问入口:未鉴权根路径、`/join` 和 code-only `/r/{code}` 显示“站点访问”与“站点口令”,未知路径只显示“无法访问”。有效 `/r/{code}#v={grant}` 直接进入;其余 Viewer 通过 site access 后,public-watch 进入房间,private room 显示“加入房间”与“房间密码”。已有有效 site-access cookie 不重复显示整站验证;站点口令和房间密码不共享输入值或授权状态。无效、过期、错误密码或满员只显示通用拒绝,不泄露房间存在性或 policy。 - 分享者根页面:顶部显示“{分享者显示名} 的屏幕”和房间状态,并在标题旁显示可单独复制的紧凑房间号;Viewer 同一位置显示权威 Host 名称,不再在标题下重复。主区域保持稳定的 16:9 舞台。未发布、已停止或可恢复错误状态时,舞台内以同等尺寸和权重显示“开始分享”和“加入房间”,中间只用“或”与分隔线表达发送/接收;前者直接开始现有分享流程,后者作为 disclosure 在当前舞台内、紧邻触发按钮就地展开共享的房间码表单,提交后才导航到 `/r/{code}`。按钮同步 `aria-expanded`/`aria-controls`,展开后输入获得焦点,DOM 与焦点顺序保持触发器后接表单;窄屏允许表单换到动作行下方,不产生横向滚动。不增加首页、向导或拓扑选择。直播时舞台显示本地预览及独立全屏图标,底部保留停止、同步暂停音视频、质量档位和复制完整邀请链接;页面失焦仅暂停本地 video element,并在舞台提示“本地预览已暂停,分享仍在继续”;媒体暂停则禁用 capture stream 轨道,两者都保留现有连接。 - `/join`:保留为可直接访问的回退入口,并复用同一个房间码表单;通过 site access 后只输入正整数房间码并进入 `/r/{code}`。public-watch 直接通过,private room 缺失 grant 时显示当前房间的密码表单。 -- `/r/{code}` 观看者:private room 首次解析合法 fragment grant 后立即清理地址栏并直达;仅有 code 时先通过 site access,再按 public/private policy 进入。鉴权后主区域最大化远端视频,标题旁显示可复制的紧凑房间号,顶部保留连接状态,并提供本地音量、静音、全屏和重试控制;Host 主动暂停时复用视频舞台遮罩显示“分享者已暂停”,恢复后立即移除。 +- `/r/{code}` 观看者:private room 首次解析合法 fragment grant 后立即清理地址栏并直达;仅有 code 时先通过 site access,再按 public/private policy 进入。鉴权后主区域最大化远端视频,标题旁显示可复制的紧凑房间号,顶部保留连接状态,并提供本地音量、静音、全屏和重试控制;Host 主动暂停时复用视频舞台遮罩显示“分享者已暂停”,恢复后立即移除。视频舞台用单一派生 presentation reducer 合成 access、Host presence、route prepare/active、peer/SFU connection、remote track、首个 composited frame、autoplay rejection 和 bounded failure。支持时用 `requestVideoFrameCallback()` 确认第一帧已提交,旧浏览器回退到当前代 decoded-frame delta 与 video readiness;`playing`、track object 或 ICE `connected` 单独都不足以结束占位层。连接阶段占用固定布局且不显示虚假百分比,播放按钮只处理实际 autoplay rejection,失败重试只调用现有有界 controller 入口。 +- 首帧观察绑定当前 assignment/connection/media-binding generation 以及适用的 publication generation;`srcObject` 或 route identity 变化立即使旧回调和旧 decoded baseline 失效,迟到回调不能把新路线标成已播放。 - 移动端:视频保持稳定宽高比,控制与状态移到视频下方,任何文字和按钮不得溢出或遮挡。 两端默认显示紧凑在线 roster;Viewer 端复用同一权威 participant snapshot,只列出当前 Viewer(含本机)并在新 snapshot 到达时整体替换,停止分享仅移除已离线 Host,鉴权失效、房间关闭或页面卸载则清空。Host 与 Viewer 的本机显示名在界面中统一标为“昵称”,默认只读,铅笔按钮紧贴昵称并只切换当前控件的输入、保存和取消,唯一名称仅显示名称,重名时才追加 room-scoped `peerId` 短后缀。一个不持久化的“显示连接详情”复选框按页展开媒体方式、传输协议、候选路径、浏览器实际暴露的本地/远端候选地址、codec、RTT、RTP 丢包率、编解码耗时及逐边统计;地址与端口只从当前页面 `RTCPeerConnection.getStats()` 精确关联的 selected pair 读取,浏览器隐藏或缺失任一字段就不显示,且不进入质量证据、信令、服务端、日志或持久化。真实连接失败、音频缺失、持续画质受限和需要用户动作的告警始终可见。显示名/roster 不进入 access credential 或 SQLite。 -用户可见的非直播状态只使用“已停止分享”(分享端已结束当前发布)、“等待开始分享”(观看端等待发布)、“正在连接”与“正在恢复连接”。`room-closed` 显示“房间已关闭”或“房间已过期”,不得误写成“分享已结束”。房间寿命、媒体代次、换父和恢复等内部状态不得作为额外产品文案暴露。 +用户可见的非直播状态使用稳定、真实的阶段词:分享端使用“已停止分享”;观看端使用“等待开始分享”“正在分配线路”“正在建立 P2P”“正在连接备用线路”“正在接收画面”“需要点击播放”“正在恢复连接”或明确失败。阶段只描述当前用户可感知动作,不暴露房间寿命、媒体代次、parent identity 或内部迁移步骤。`room-closed` 显示“房间已关闭”或“房间已过期”,不得误写成“分享已结束”。 + +Token-free SFU standby prewarm 保持可选且不创建 participant、publication、subscription 或 route authority。它只提前加载 pinned SDK 并调用官方 `prepareConnection()`;是否缩短公网首帧由阶段时间戳验证,失败时继续既有冷路径。 按钮使用 `lucide-react` 图标并提供可访问名称和 tooltip。动态状态区域固定最小尺寸,避免连接状态变化导致布局跳动。320、375 与 390 CSS px 下两个首屏动作仍须同排等宽、文字不溢出且触控高度至少 44 px;展开详情不遮挡视频或引入横向滚动。 @@ -330,6 +333,8 @@ Host 与 relay Viewer 复用同一个纯 presentation reducer 保存远端 `View - 质量协议的 host 授权、严格字段与数值范围、last-wins、当前/未来 relay、已配置 SFU 和普通 P2P wire 兼容;sender 的四字段 requested/applied 读回、拒绝与持续受限提示;screen audio SDP helper 覆盖 CRLF、existing/missing fmtp、unknown key preservation、幂等、payload number、rejected/non-audio,以及 empty/no-Opus/ambiguous/parser 异常原对象 fallback,`ViewerPeer` 覆盖 initial/restart/rebuild 时同一结果的 local/signaling 一致性,SFU publisher 精确断言 high-quality stereo/forceStereo/DTX 且不覆盖 RED; - HTTP/WS 集成:nginx per-source site-access `limit_req` 与统一错误不泄露 secret,应用不建立 IP limiter 状态;Viewer 可在 Origin/容量门下 upgrade;Host 首条鉴权同时要求 upgrade-captured site access 与 Host token,Viewer 要么持 exact-room grant,要么先有 site access 再按 room policy;site-access cookie 不绕过 private grant/password,grant/password 不能建房、取得 Host 或跨房。覆盖 fragment/sessionStorage/replaceState、无 site access 的 public/password code-only 统一拒绝、通过 site access 后的 public/password 进入、不含明文于 URL/浏览器持久存储/SQLite/log、Native capability isolation,以及 rotate/revoke 后 grace/route 清理、旧 Viewer/上游媒体边关闭和 reconnect 拒绝; - 客户端策略与 stats 解析:会话替换不重连,ordinary ICE、LiveKit 与 selected-edge TURN protocol 分离,远端 TURN transport 保持未知; +- route planner property tests:候选输入乱序仍产生相同 target,健康边 sticky,硬过滤拒绝 stale/cycle/depth/cap/ineligible/cooldown/unadmitted parent,同深度优先剩余容量并稳定打破平局,失败候选清理后才尝试下一项; +- Viewer stage reducer:peer/SFU prepare、remote track 无首帧、首个 composited frame、autoplay rejection、恢复和 bounded failure 不互相冒充,动态状态不改变布局或 route authority; - TypeScript 类型检查、Vite production build 和仓库 hygiene。 人工矩阵: @@ -337,6 +342,7 @@ Host 与 relay Viewer 复用同一个纯 presentation reducer 保存远端 `View - 同机双标签页与同 LAN direct; - 真实跨网络、同房间 direct/peer UDP + SFU/UDP root; - CGNAT、双 NAT、移动热点和普通家宽下按 accepted route model 验证 direct/peer、server-assisted path、selected transport、恢复和 bounded failure;记录 allocation/relay port/RSS/CPU/latency 与 RX/TX/loss。人为封锁全部 UDP 时仍必须有界明确失败。旧 production release 留在独立实例作为回滚,不在同进程保留旧 transport; +- guessed-candidate 只先在 owned endpoint-dependent NAT emulator 中运行 ordinary-ICE failure 与 bounded-candidate A/B;没有通过双浏览器重复性、首帧 deadline、privacy 和 cleanup gate 时不得进入任何 production room,通过后也只在一个人工批准的 operator-owned test room 运行一次 canary,并要求双方 endpoint 显式同意、default-off kill switch 以及 exact room/edge/ICE-generation/deadline/aggregate-budget 约束; - Windows Chrome/Edge 的 tab/window/monitor 音轨有/无、同时语音通话和 video-only;一个可区分左右声道的 fixture 只验证 Web P2P 与 SFU 的 channel correctness、协商 fmtp 和 route switch 后持久 `MediaStream`,不做听感/性能标定;Windows 11 原生候选另测 game process tree、通知、无渲染流与进程重启,Windows 10 记录明确不支持/未解决; - Android Chrome 与 iOS Safari 的播放手势、旋转、前后台和网络切换; - 一名分享者、八名异构观看者连续 30 分钟,同时记录较低人数基线、实际上行、编码负载和首帧时间。真实 1:8 未完成前不得把默认上限写成性能承诺。 @@ -350,11 +356,10 @@ Host 与 relay Viewer 复用同一个纯 presentation reducer 保存远端 `View 2. 完成 RoomStore、建房 API、WebSocket 鉴权和定向信令。 3. 完成屏幕捕获、逐观看者 PeerConnection、观看播放和手动质量档。 4. 完成连接恢复和 stats 面板;随后以 STUN-only snapshot 替换旧全房 TURN credential/refresh wire。 -5. 现有 automatic peer/SFU controller 作为 legacy deployed input;按 ADR-0005 迁移其 server-assisted route semantics,并让 room `1` 仅作为历史 smoke fixture。 -6. 按 ADR-0005 的整体路由模型迁移 server-assisted transport;ordinary STUN-only、SFU 与 TURN 的配置和 rollout 保持独立边界。 -7. Site access、room-scoped Viewer grant 与 SQLite v3 已部署,当前 wire 为 `screener-v5`;以后不兼容的 wire/schema 迁移仍须原子切换并保留迁移前备份。用户名/roster 保持独立。 -8. Windows Native fixed-`HIGH` 保持可选且不改变 Web 默认;移动端保持 Viewer-only 且不单独分配 relay capacity。Viewer-to-TV 只采用已验证的平台本地输出,不改变媒体路由。 -9. 屏幕音频在一个原子 Web slice 中声明 `sdp-transform`/types direct dependencies,加入 answer helper 与 `ViewerPeer` wiring,并同步设置 SFU stereo options、focused tests 和 owning docs;高级设置只增加 64/128/256 kbps 离散 sender ceiling,不增加 Web Audio、第二表示或自定义降级算法。 +5. 从 canonical `main` 按 ADR-0005 依次落 route waves A-D:先统一 endpoint cap/accounting 与 wire,再建立 SFU/TURN resource admission,最后把 deterministic allocation、child reparent 和 parent drain 收到一个 controller;每个 wave 独立可回滚。 +6. 路由边界合入后依次落 live screen-audio ceiling 和 paused video-codec transaction;随后做 H.264 根因诊断与 strict-NAT emulator gate,门禁通过才允许 allowlisted canary。 +7. 加入 first-frame-driven Viewer connection stages,再整合 source-checkout LAN launcher。Site access、room-scoped Viewer grant 与 SQLite v3 的已部署合同保持不变。 +8. Windows Native fixed-`HIGH` 保持可选且不改变 Web 默认;shared encode、移动 lifecycle、可分发 sender 和平台输出按 TODO roadmap 后续推进。 任何一步只实现当前消费者需要的接口。不得为未来账号、第三种媒体拓扑或原生 sender 预建抽象层。 @@ -362,6 +367,8 @@ Host 与 relay Viewer 复用同一个纯 presentation reducer 保存远端 `View 服务端维护每个房间唯一的版本化 route owner。候选 parent 可以形成图,但 committed media graph 必须无环;Host 没有 upstream,每名 Viewer 同时只有零或一个 active upstream,且每个 active Viewer 必须沿 peer edges 到达 Host 或沿当前 authoritative Host publication 到达 source。加入、容量释放、节点离开、当前边失败和已接受的 path-health evidence 才能触发一次有界重选,未受影响分支保持 sticky。 +Parent selection 使用硬过滤加字典序,不建立综合质量评分器。先拒绝 stale session、不可 source-reachable、成环、超深、无 sender/overlap slot、relay-ineligible、位于 failed-edge cooldown 或 server resource 未准入的候选;对剩余候选依次比较结果深度、分配后剩余稳态 sender slots、稳定加入顺序与 peer identity。controller 每次只 reserve/prepare 一个候选,使用该 `RTCPeerConnection` 自带的 ICE checklist/connectivity checks,再以 current-generation RTP、live track 与 decoded frame 验证;初次加入或 hard failure 采用达到 media-usable floor 的首个候选,soft-quality reparent 还需 quality owner 证明候选达到 recovery threshold 且优于仍健康旧 edge。失败释放 reservation 后才取下一个。应用不做 all-pairs ping、raw-IP/NAT/地理排名、周期探测或全房重平衡。一个 exact edge 的近期失败只进入该 edge 的有界 exclusion/cooldown,不污染其他路径。 + 所有非服务器端点共享一个 server-authoritative steady outbound media-copy capacity:唯一配置 `ENDPOINT_MEDIA_COPY_CAPACITY` 默认 `2`,只接受 `1`、`2` 或 `3`,不兼容旧键。peer child 或 Host publication 各占一份,upstream receive 免费,committed selected TURN 只替换同一份 edge transport;SFU subscriber egress 与 TURN allocation 使用独立 server admission。客户端 capability 只能收紧该值,不能按角色、浏览器、UA、设备或页面可见性建立另一套 policy。代码通过一个 shared accounting guard 执行,不在 Host、Viewer、active、recovery 或 migration 分支重复 magic number;client/server/native wire 随该能力原子升级。首个迁移波次在独立 server admission 就绪前保留现有 SFU subscriber 与 room-wide selected-lease 安全护栏,但不把其固定值当作产品 policy;`C=3` 满载时必须在发出会产生第四份 Host media copy 的 token/grant 前明确等待或失败。 普通 Web relay 把收到的 remote track 加入独立 downstream `RTCPeerConnection`,因此可能逐跳解码和重编码;浏览器不保证跨连接 shared encode。每条 ordinary child 是独立网络副本,实际 encode time、CPU/GPU、bitrate、loss、frame rate、depth 和 recovery 必须测量。 diff --git a/docs/需求理解.md b/docs/需求理解.md index cfd2155..7b828c0 100644 --- a/docs/需求理解.md +++ b/docs/需求理解.md @@ -41,7 +41,8 @@ SFU/TURN 的 publication、subscription、transport、server-resource accounting - 本地一键启动:提供基于当前源码 checkout 的跨平台单命令 LAN 启动器,而不是安装包、云部署器或公网穿透工具。它复用唯一 Node 应用、当前 `SITE_ACCESS_PASSWORD`/SQLite/config 合同,自动选择可用端口并打印 localhost Host URL、LAN Viewer URL 和健康结果;Host 使用 secure-context 可接受的 localhost 或部署者提供的 HTTPS,LAN Viewer 可在已声明的本地 HTTP 边界访问。启动器不得伪造 TLS、防火墙、路由器端口映射、STUN、SFU 或 TURN 已配置,也不得复活已删除的环境变量;Node 24 与依赖安装仍是明确前置条件。 - 私密房间双入口:private room 接受现有 `/r/{code}#v={grant}` 合法邀请直接进入,或“站点访问 + 房间号 + 该房间观看密码”。没有 grant 的 `/join` 或 `/r/{code}` 先显示明确标为“站点访问”的整站验证,通过后才显示明确标为“加入房间”的当前房间密码输入;有效 site-access cookie 直接跳过第一步,但两类密码不得复用、代填或互相替代。Host 可设置、轮换或移除房间观看密码。密码接受 1 至 64 个可见 ASCII 字符;异步 scrypt 全局最多执行 2 个、等待 16 个派生。站点口令和房间密码都不得进入 URL/query/storage/log/SQLite;不存在、错误密码、队满和正确密码后的满房都返回通用结果。 -- 统一自动路由:旗舰部署开启 `PEER_ASSISTED_MEDIA` 后,所有正常房间使用同一个服务端 route controller,并各自保存 assignment、session、route revision、assignment generation、connection identity、media-binding generation 和容量状态。controller 只执行 allocate/distribute、child reparent 和 relay abdicate/drain;合格父节点可形成候选图,但 committed media graph 必须无环、每名 Viewer 同时至多一个 active upstream,且每个 active Viewer 必须沿 peer edges 到达 Host 或沿当前 authoritative Host publication 到达 source。加入、容量释放、节点离开、当前边失败或已接受的 path-health evidence 只开启一次有界重选;其余健康分支保持 sticky,不做常驻多源、周期评分或全房重平衡。迁移保留旧边,只有 exact current candidate 取得 live track、正 RTP 和 decoded-frame progress 后才原子 commit;失败、超时、代次变化或授权撤销销毁候选并保留或恢复旧权威。普通 peer 始终 STUN-only。SFU/TURN overlap 和资源准入由同一 typed ledger 与事务约束。该能力不增加 UI、用户选择、百分比发布或第二套路由器。 +- 统一自动路由:旗舰部署开启 `PEER_ASSISTED_MEDIA` 后,所有正常房间使用同一个服务端 route controller,并各自保存 assignment、session、route revision、assignment generation、connection identity、media-binding generation 和容量状态。controller 只执行 allocate/distribute、child reparent 和 relay abdicate/drain;合格父节点可形成候选图,但 committed media graph 必须无环、每名 Viewer 同时至多一个 active upstream,且每个 active Viewer 必须沿 peer edges 到达 Host 或沿当前 authoritative Host publication 到达 source。加入、容量释放、节点离开、当前边失败或已接受的 path-health evidence 只开启一次有界重选;其余健康分支保持 sticky,不做常驻多源、周期评分或全房重平衡。候选先按当前身份、source reachability、无环、深度、sender slot、relay eligibility、failed-edge/cooldown 和 server admission 做硬过滤,再按最小结果深度、最多剩余稳态 sender slots、稳定加入顺序和 peer identity 确定性排序;一次只准备一个候选,以浏览器 ICE 和当前代媒体证明决定是否采用,失败才尝试下一个。初次加入或 hard failure 接受达到媒体可用门槛的首个候选;soft-quality reparent 还必须由 quality owner 证明候选达到恢复门槛并优于仍健康的旧 edge,否则回滚后试下一个。raw IP、地理位置、UA、自报 NAT 类型、未与 current-generation media 相关的单边 RTT 或跨 edge 加权分数均不得进入排序。迁移保留旧边,只有 exact current candidate 取得 live track、正 RTP 和 decoded-frame progress 后才原子 commit;失败、超时、代次变化或授权撤销销毁候选并保留或恢复旧权威。普通 peer 始终 STUN-only。SFU/TURN overlap 和资源准入由同一 typed ledger 与事务约束。该能力不增加用户拓扑选择、百分比发布或第二套路由器。 +- 观看端连接反馈:Viewer 舞台从鉴权到首帧使用一个由现有权威状态推导的稳定阶段反馈,至少区分等待分享、分配线路、建立 P2P、连接备用线路、等待首帧、浏览器阻止自动播放、恢复连接和有界失败。阶段不是进度百分比,也不产生 route authority;只有当前 video element 实际提交首帧后才显示正在播放。播放按钮只在 `play()` 因自动播放策略被拒绝时稳定出现,连接中不得忽隐忽现;明确失败后才提供一次受现有 controller 约束的重试入口。make-before-break 或仍有可见旧帧时保留画面并叠加恢复状态,不为显示反馈主动清空健康 media element。 - 质量控制扩展:推荐档位仍是默认入口;“分享高级设置”只允许 720p/1080p/1440p 分辨率 ceiling、15 至 60 的整数 FPS ceiling、2 至 12 Mbps 视频码率 ceiling、清晰/平衡/流畅三种偏好、`自动 | H.264 | VP8` 编码选择,以及离散的 `64 kbps 省流 | 128 kbps 音乐默认 | 256 kbps 极高音质` 屏幕音频 sender ceiling;初始值采用 `balanced`、`automatic` 与 128 kbps。视频档位和音频档位在直播中显式应用;音频变更只更新固定 Opus 路径的 sender ceiling,不是 codec 切换。所有当前及以后建立的 Web Host、Browser relay 与 SFU sender 都读取最新 desired 档位,并各自在 fresh sender parameters 上应用和读回 endpoint-local applied 值;实际码率仍由浏览器拥塞控制和 stats 决定。Peer 只在唯一明确的 active Opus audio 上做结构化、幂等的 Viewer answer `stereo=1;maxaveragebitrate=256000` 变换,使同一接收上限覆盖三档;非目标形态或 parser/writer 异常必须原样保留浏览器 answer,不得阻断可继续的 mono/video edge。不得用宽泛字符串/正则改 SDP,也不得改写浏览器协商的 FEC。SFU 将三档映射到 pinned SDK 的 music stereo preset 或显式 256 kbps ceiling,统一保留 `forceStereo = true`、`dtx = false` 与 pinned room default `red = true`。不得暴露任意码率、声道、采样率、audio codec、stereo、DTX、RED 或 FEC 控件;256 kbps 只是显式较高上限,不是无损、高保真或最低码率保证。实际质量持续低于所选上限时必须显示可解释的限制原因;在真实拥塞、编码器和接收端证据完成前,不把任一组合称为“完美平衡”。未来 microphone voice 若进入范围,必须是独立 track/处理链,不能把 AEC/降噪/DTX 套到电影或游戏音频。 - SFU 质量设置生命周期:直播中修改档位时,浏览器 sender 的 applied readback、LiveKit `LocalVideoTrack` 保存的 degradation preference、track restart options 与 `LocalTrackPublication` 的 republish options 必须作为同一次串行操作更新;任一步失败恢复旧 sender 与旧 SDK 保存态,旧 publication generation 不得回写当前状态。LiveKit full reconnect/republish 不得恢复初次发布的旧偏好。该修复不改变 codec、码率上限、层数或路由,也不能单独解释即时低帧。 - 本地预览性能候选:在相同动态场景中 A/B 测量 Host 动态预览开/关的 CPU、GPU、encode time 和实际 FPS。只有稳定收益且捕获、编码和远端画面不受影响时,才允许直播开始后默认显示静止缩略图或隐藏动态预览并提供按需展开;未测前不宣称收益。 @@ -66,7 +67,7 @@ SFU/TURN 的 publication、subscription、transport、server-resource accounting ## 非功能需求 - 性能:在 RTT 不高于 40 ms、丢包不高于 1% 的受控 P2P 路径中,玻璃到玻璃延迟目标为 p50 不高于 150 ms、p95 不高于 250 ms;区域 SFU/UDP root 路径的 p95 目标不高于 350 ms。可选 TURN 单独测量,不能借用该目标宣称通过。 -- 启播:观看者请求观看后,除用户授权耗时外,3 秒内出现首帧。 +- 启播:观看者请求观看后,除用户授权耗时外,3 秒内出现首帧;从请求开始到首帧期间持续显示真实、稳定且可访问的连接阶段,不用黑屏、`playing` 事件或 ICE `connected` 冒充已播放。 - 流畅性:选择 60 fps 档位时,不能在没有降档或提示的情况下连续 5 秒低于 50 个解码帧每秒。 - 成本:direct/peer UDP 不经过应用服务器。SFU/TURN 的 ingress、egress、allocation 和并发上限由整体路由模型明确配置;验收逐 hop 记录 Host、peer、SFU、TURN 和供应商计费的真实 bytes,不得用普通端点 child 数或有效载荷近似代替中央资源准入。 - 安全:应用控制面全程 HTTPS/WSS;site access、Host token 与 room-scoped Viewer grant 权限正交,任何一个 Viewer grant 只授予一个房间的观看角色。P2P/TURN 边在 WebRTC 端点间使用 DTLS-SRTP,普通 SFU 是逐跳 DTLS-SRTP;只有另行实现并验收应用层 E2EE 才能对 SFU operator 保持媒体端到端机密。仓库、数据库、日志和错误不得包含明文密码或 grant。 @@ -112,6 +113,8 @@ SFU/TURN 的 publication、subscription、transport、server-resource accounting - Given route candidate、signal、recovery 或 commit 的 room/share/session/revision/assignment/connection/media-binding identity,及适用时 publication generation,任一不匹配,When 旧消息或异步结果到达,Then 该操作 fail closed 且不能替换当前媒体。 - Given 新路线正在准备,When 尚未同时证明当前代 live track、正 RTP 与 decoded-frame progress,Then 旧路线保持权威;证明后只原子提交 exact candidate,失败或超时销毁候选并保留或恢复旧路线。 - Given 所有已接受且已配置的媒体路径耗尽,When 有界连接窗口结束,Then 界面显示通用等待或连接失败,不静默黑屏、不长时间假连接,也不尝试未授权路径。SFU/TURN 的精确 path order 和 resource accounting 按 ADR-0005 验证。 +- Given 同一 committed graph 和候选集合以不同枚举顺序输入 controller,When 新 Viewer 加入或 exact failed edge 触发换父,Then 硬过滤结果相同,选择稳定落到最浅且分配后剩余容量最多的合格 parent,并以加入顺序和 peer identity 打破平局;健康 current edge 不因出现更优候选而迁移。Given 第一候选 ICE 或媒体证明失败,Then 它的 reservation 先幂等释放,之后才尝试下一候选,且不会启动 all-pairs probe。 +- Given owned endpoint-dependent NAT emulator 中 ordinary ICE 可重复失败,When isolated guessed-candidate harness 运行,Then 只有满足[低服务器成本媒体路由](./research/low-server-media-routes.md)的 candidate budget、双浏览器重复性、首帧 deadline、隐私和 cleanup gate 才可继续。门禁前 ordinary room 不接收 guessed candidate;门禁后也只允许一个人工批准的 operator-owned test room,在双方 endpoint 显式同意、default-off kill switch 以及 exact room/edge/ICE-generation/deadline/aggregate-budget 约束下运行 canary。任何失败都关闭该候选并保留现有 SFU/TURN fallback。 ### 浏览器与移动端观看 @@ -121,6 +124,7 @@ SFU/TURN 的 publication、subscription、transport、server-resource accounting - Given Viewer 已通过浏览器要求的用户手势开始可听播放,When 页面切到后台且浏览器仍保留该页面,Then 同一个媒体元素、信令和 WebRTC 连接继续承载音频,返回前台后沿当前直播恢复画面而无需重新加入;后台视频合成或逐帧回调增长不作为连续播放承诺。Given iOS 锁屏、系统回收页面或后台期间产生新的媒体连接,Then 以当前真机行为为准,恢复时继续使用已有重连和显式播放入口。 - Given 用户查看本机身份,When 尚未进入编辑状态,Then 界面显示“昵称”、当前昵称和紧贴其后的铅笔按钮,不显示默认可编辑输入框。Given 用户修改昵称后以 Host 或 Viewer 加入房间,Then 所有在线参与者看到同一份服务端权威 roster、对应房间标题;唯一名称只显示名称,重名项追加当前房间 `peerId` 短后缀。刷新后本机名称仍在,清除站点数据后恢复未命名默认值,数据库中不存在显示名或成员记录,展示身份不参与授权或路由。 - Given 手机从 Wi-Fi 切换到蜂窝网络或页面从后台恢复,When 原候选对失效,Then 客户端执行 ICE restart 或重建连接,并显示恢复进度而不是永久黑屏。 +- Given Viewer 已鉴权但尚未提交首帧,When route 从 peer prepare 进入 SFU fallback 或等待媒体,Then 固定舞台依次显示当前真实阶段且不跳动、不显示虚假百分比;只有当前代 video frame 被 compositor 接收后才显示正在播放。Given `video.play()` 仅因 autoplay policy 被拒绝,Then 稳定显示一个播放动作;连接尚未完成时不显示忽隐忽现的播放按钮。Given 旧媒体仍可见且正在 MBB 恢复,Then 保留旧画面并叠加恢复状态。 - Given Viewer 尝试把直播输出到电视,When 浏览器/系统提供已验证的 Remote Playback、AirPlay 或屏幕镜像能力,Then 该动作只改变本机输出设备,现有 upstream PeerConnection、TURN/SFU/peer route 和其他参与者保持不变。Given live `srcObject` 未经目标设备证明或 API 缺失,Then 产品不显示伪可用 Cast/AirPlay 按钮,也不创建自有 receiver 或转码服务器。 ### 一对多与带宽保护