diff --git a/AGENTS.md b/AGENTS.md index 83a0ad2..f508514 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,90 +1,47 @@ # Project Instructions -## Durable Truth Before Action +## 1. Decision And Durable Truth -- Do not turn a discussion, correction, example, review suggestion, experiment, checkbox, or agent idea directly into implementation. First restate the intended outcome and its source rationale, reconcile it against the whole current product model and any conflicting evidence, and challenge assumptions that are inconsistent or unnecessarily complex. -- Before implementation work or implementation sub-agents begin, update every affected owning truth document: current requirement and design, accepted ADR or research conclusion when applicable, `docs/project-memory.md`, `docs/status.md`, and this file when a global rule changed. Replace conflicting current text in place, then create a Git-tracked checkpoint so the decision survives compaction and handoff. -- If the semantic decision is still disputed, record a truth hold and freeze only dependent work. Do not encode a guess as accepted truth. Code, merge, deployment, and cleanup may start only after the nonvolatile truth set is internally consistent. -- Merge the accepted truth checkpoint before integrating dependent candidates. Rebase or rebuild each retained candidate from that main exactly once; resolve conflicts from main's owning truth and transplant only approved scoped code, tests, and new facts. Never let an older branch's `AGENTS.md`, requirements, ADRs, memory, status, or deployment snapshot overwrite newer truth. -- After an audit or integration boundary, restore the canonical repository root to a clean, up-to-date `main` before starting new implementation. Do not leave `main` checked out in an auxiliary worktree. Preserve overlapping user changes explicitly, then create every new branch and worktree from the exact canonical `main` commit; an auxiliary worktree is never the source of current truth. -- Cleanup is last. Remove a worktree or branch only after the semantic decision, integration, and main-equivalence/open-reference checks are complete; never delete first to simplify a decision or later reintroduce an old branch wholesale. +- Treat discussions, examples, review suggestions, experiments, checkboxes, and agent ideas as inputs, not implementation authority. Restate the intended outcome and rationale, reconcile it against the whole product model and evidence, and challenge contradictions or needless complexity. +- Before dependent implementation or implementation agents start, update every affected owning requirement, design, ADR or research conclusion, plus `docs/project-memory.md` and `docs/status.md` when their snapshots change. Checkpoint the consistent truth in Git; chat is not durable truth. +- If semantics remain disputed, record the hold in `docs/todo.md` and freeze only dependent work. Do not encode a guess as accepted truth. +- **Current-truth ("Dongpo pork") rule:** after a correction, current docs, UI, code, comments, configuration, and PR copy state only the accepted behavior and rationale that still constrains it. Remove rejected alternatives and explanations of their removal; Git history owns that history. +- Queue new observations in their owner and continue the active milestone unless the user requests immediate investigation or the evidence reveals a P0 blocker. -## Temporary TODO Audit Hold +## 2. Safety And Privacy -- `docs/todo-audit-hold.md` is the execution quarantine while the 2026-08-22 truth audit is open. An unchecked box, old branch, agent proposal, experiment, review suggestion, or post-boundary follow-up is not an authorized TODO unless that ledger marks it `CONFIRMED` or the user explicitly reauthorizes it after the audit boundary. -- `AUDIT-HOLD` means preserve and inspect, not delete, revert, continue, merge, deploy, or use as current product truth. `EVIDENCE-ONLY` results may inform a later decision but must not become a release gate by themselves. -- The older Browser1 and global-SFU-root-two assumptions are disputed and must not drive implementation during the audit. The currently confirmed endpoint-cap intent is one server-authoritative value for every non-server endpoint: default `2`, configurable as `1`, `2`, or `3`, with upstream receive excluded. The exact SFU publication/subscription model is still under holistic routing review. -- Execute this audit in order: first reconcile TODO provenance and current truth owners; second produce the archive/worktree/branch/folder disposition schedule; third merge the audit-guard truth, restore canonical `main`, and execute only approved cleanup; fourth stop and reconcile that organized state and the TODO classifications with the user. Later phases must not run ahead merely because parallel capacity is available. -- Only after that checkpoint is confirmed, produce the holistic routing model as a held proposal without implementation, then stop again for route-semantics confirmation. Do not create or resume remaining-TODO implementation worktrees before the accepted owning truth is merged into canonical `main`; only then create high-confidence worktrees from that exact newer SHA. +- Never commit credentials, TURN shared secrets, TLS private keys, access tokens, or real user data. Commit only placeholder configuration and document local secret injection. +- Production TURN credentials are short-lived and narrowly authorized; rooms use authentication or unguessable expiring invitations. Do not log or persist private media-path identifiers beyond their accepted owner. +- Block the current phase on P0 security, privacy, authorization, irreversible-data, generation, bounded-resource, or rollback failures and on P1 failures of its core path. -## Product Direction +## 3. Product Contract -- 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 fallback media resources. Current production includes optional selected-edge TURN, but its retained role and placement are under the routing truth audit; it is not by itself a topology. -- Let viewers join from a normal desktop or mobile browser without installing the sender application. -- STUN is required and direct/peer UDP remains first. Ordinary peer edges must not receive TURN candidates by default. The exact SFU/TURN fallback order and accounting are held for the holistic routing audit; every accepted path must terminate in a clear bounded success or failure. HTTPS/WSS remains TLS/TCP and is independent of this media-transport policy. -- Every non-server endpoint has one server-authoritative ordinary downstream capacity: default `2`, configurable as `1`, `2`, or `3`. An upstream receive edge does not consume it, Browser role/UA/visibility does not change it, and a client advertisement cannot exceed the deployment value. SFU and TURN are bounded fallback resources, but publication, subscription, selected transport, temporary overlap, and server-resource accounting remain frozen for the holistic routing audit rather than being inferred from ordinary-child counts. -- 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. Any accepted server-assisted path must preserve the small-room, distributed-media goal unless an ADR with measured justification changes it. +- Build private, low-latency game screen sharing for one broadcaster and a small group of trusted friends, not a public or large-scale streaming service. +- A normal desktop or mobile browser is the initial Viewer target. A packaged sender or native capture helper is a later optimization. +- Keep media distributed and automatic. Central services own rooms, authentication, signaling, STUN, observability, and bounded fallback resources; do not silently make the product always-SFU. +- STUN and direct/peer UDP remain first. Ordinary peer edges receive no TURN candidates by default, and every accepted path ends in bounded success or clear failure. HTTPS/WSS transport is independent of media transport. +- Every non-server endpoint uses one server-authoritative ordinary downstream cap: default `2`, configurable as `1`, `2`, or `3`. Upstream receive is free; Browser role, UA, visibility, and client advertisement do not create another tier. +- SFU/TURN publication, subscription, selected transport, overlap, and server-resource accounting remain held in the current routing TODO until their holistic model is accepted. -## Repository Knowledge +## 4. Canonical Repository And Integration -- At the start of substantial work, read `docs/project-memory.md`, `docs/status.md`, and the relevant requirement, ADR, and research documents. -- Treat the Git-tracked repository as the persistent source of truth. Before dependent work continues, write every accepted requirement or priority, design change, durable research conclusion or retained candidate, actionable TODO or blocker, and material status change to its owning repository document; chat alone is not a durable record. -- Maintain one minimal truth set: current snapshots in `docs/project-memory.md` and `docs/status.md`, current requirements and design in their specifications, decisions in `docs/adr/`, evidence in `docs/research/`, and the timeline in Git history. Replace stale facts in place and do not duplicate detailed content into this file. -- Checkpoint that truth set at material phase boundaries, after accepted decisions change, at branch/PR or agent handoff, and before likely context compaction or a long pause. After compaction or resumption, re-read the snapshots and inspect Git state before continuing. -- Keep `docs/README.md` current when documentation is added, moved, or superseded. +- Keep the canonical repository root on a clean, current `main` at audit and integration boundaries. Create branches and worktrees from that exact commit; an auxiliary worktree or old branch is never a truth source. +- Merge an accepted truth checkpoint before integrating dependent candidates. Rebase or rebuild each retained candidate from that exact `main` once, preserve main's owning truth on conflicts, and transplant only approved scoped code, tests, and new facts. +- Cleanup is last. Preserve user work and verify integration, open references, branch equivalence, and link/reparse safety before removing a worktree or branch. +- Follow `CONTRIBUTING.md` for branch, research, validation, PR, merge, cleanup, and local-hook details. -## Research Before Changes +## 5. Truth Map -- Before non-trivial design, implementation, or bug fixing, inspect the current repository and research current official documentation and established reference implementations. -- Add community issue discussions or recent papers when they materially improve the answer. Prefer primary sources, record access dates, and distinguish verified facts from inference. -- Store durable findings and links under `docs/research/`; update or supersede stale conclusions instead of accumulating contradictory advice. -- Check licenses before copying code. GPL/AGPL sources are study-only until the project license and distribution model are decided. +- Start substantial work by reading `docs/project-memory.md`, `docs/status.md`, `docs/todo.md`, the relevant requirement/design, ADR, and research documents, then inspect the current branch and Git state. +- Current requirements and design live in `docs/需求理解.md` and `docs/方案设计.md`; decisions live in `docs/adr/`; evidence lives in `docs/research/`. +- `docs/project-memory.md` is the durable product snapshot, `docs/status.md` is the current execution/deployment index, and `docs/todo.md` is the only current work ledger. Git history owns the completed timeline. +- Give each fact one owner, replace stale text in place, keep status as an index rather than an archive, and update `docs/README.md` when documents move or change status. -## Git Workflow +## 6. Engineering Defaults -- Work on a short-lived branch for each coherent change. Use `feat/`, `fix/`, `docs/`, `refactor/`, `test/`, `chore/`, or `spike/` prefixes. -- Treat GitHub Actions as an acceptance-boundary resource: feature branches and pull requests use local gates and do not trigger workflows; run Actions only for `main` integration, releases, or explicit manual dispatch. Do not retry zero-step or infrastructure failures. -- Keep work-in-progress and checkpoint branches local. Do not push a branch merely for backup, agent handoff, or intermediate review; after local validation and review, push it once to open the final pull request. Direct changes to `main` require an explicit exception. -- Default to squash-merging each coherent, non-stacked pull request so `main` receives one meaningful `feat`, `fix`, `docs`, or other conventional commit. Use a merge commit only for an explicitly stacked dependency whose parent ancestry must remain intact; do not use ordinary merge by habit. -- Delete a remote branch only after verifying that its pull request is merged, its current head still equals the merged pull request head, and no open pull request uses it as a head or base. Keep branches required by an active stacked pull request chain. -- Once a merged branch's worktree is verified clean, remove the auxiliary worktree and local branch rather than accumulating completed branches. Before any local worktree or directory removal, resolve the exact absolute target and enumerate contained reparse points, junctions, and symlinks without following them. Verify every link target and handle the link itself with a non-following operation first; never run recursive deletion while an unresolved reparse point remains. -- Keep unrelated work out of a branch. Update tests, docs, ADRs, project memory, and status in the same pull request when their source facts change. -- Before the first push, a local branch may be rebased onto the latest `main` to keep the pull request focused. Follow `CONTRIBUTING.md` and the pull request template; never rewrite shared history unless the user explicitly requests it. - -## Context Hygiene - -- Keep this root file concise and stable; put specialized rules in the closest relevant directory only when that code exists. -- Never store raw transcripts, large logs, generated summaries, temporary plans, or facts that can be cheaply rediscovered in always-loaded memory. -- When a fact changes, update it in place and remove conflicting text in the same change. Do not preserve obsolete guidance merely for history. -- Remove rejected alternatives from active UI, code, comments, configuration, PR copy, and current docs. Describe current behavior by what it does; retain rationale only when it still constrains a live decision. - -## Security - -- Never commit credentials, TURN shared secrets, TLS private keys, access tokens, or real user data. -- Commit example configuration with placeholder values and document how local secrets are supplied. -- TURN credentials must be short-lived in production. Rooms must be authenticated or protected by unguessable, expiring invitations. - -## Cross-Platform Work - -- Support development from Windows, macOS, and Linux. Avoid absolute paths and OS-specific path separators in code and configuration. -- Prefer repository scripts or package-manager commands that behave consistently across platforms. -- Text files use LF unless `.gitattributes` explicitly requires otherwise. -- Use ASCII filenames for new files unless a user-facing language or an established project convention requires Unicode. - -## Engineering Workflow - -- Apply Occam's razor: choose the simplest design that satisfies the verified requirement, and stop there. Avoid over-engineering for hypothetical scale or future features. -- Treat new observations and follow-up ideas as queued input: record them in the owning requirement, design, research, or status document, then continue the active milestone. Interrupt current work only when the user explicitly requests immediate investigation or the new evidence reveals a P0 blocker. -- Keep changes scoped and preserve unrelated user work. -- 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. -- Use independent review for routing or protocol state machines, security/privacy boundaries, large features, and milestone integration. Narrow changes should normally close with focused tests plus the relevant typecheck/build; do not start a separate reviewer for every small feature. -- Treat `docs/status.md` as a current index, not an archive. When it approaches its budget, remove completed or stale facts that Git history already owns; move still-relevant detail to the owning requirement, ADR, research, or deployment document and leave a short link or index entry. Create a scoped second-level status document only when no existing owner fits; never micro-compress prose merely to satisfy the byte gate. -- 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. -- 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. -- Before finishing a material task, inspect `git status`, report untracked project artifacts, and stage files created for the requested work unless the user asks not to stage them. +- Apply Occam's razor: implement the smallest complete design with a current consumer, keep unrelated work out, preserve user changes, and avoid speculative frameworks or hooks. +- Research non-trivial design, implementation, and bug fixes from current primary sources. Record durable findings and license boundaries under `docs/research/`. +- Add tests and independent review in proportion to risk. Batch expensive browser, network, endurance, and deployment checks at acceptance boundaries; use reproducible WebRTC measurements for performance claims. +- Keep comments limited to non-obvious rationale and invariants. Keep scripts and hooks deterministic, fast, cross-platform, and CI-runnable. +- Support Windows, macOS, and Linux; avoid absolute paths and OS-specific separators. Text files use LF, and new filenames use ASCII unless an established user-facing convention requires otherwise. +- Before finishing material work, inspect Git status, report untracked project artifacts, and stage requested files unless told otherwise. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9dd1a15..fbd5de0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,7 +7,7 @@ 3. For non-trivial design, implementation, or bug fixing, inspect the repository and research current primary sources before changing code. Record durable findings under `docs/research/`. 4. Reconcile the requested outcome against the whole current product model and conflicting evidence. Before implementation, update every affected owning requirement/design/ADR/research document and current memory/status, replace stale current text, and checkpoint that nonvolatile truth in Git. If semantics remain disputed, record a hold and stop dependent implementation. 5. Implement the smallest complete change from that checkpoint. Add tests and concise rationale comments in proportion to risk, and keep operational/current-truth updates in the same coherent pull request when implementation changes source facts. -6. Run `sh scripts/check-project-state.sh` on macOS/Linux/Git Bash or `./scripts/check-project-state.ps1` on PowerShell, plus `npm run check` and any relevant manual browser/network checks. Record this local evidence in the pull request; GitHub Actions are reserved for `main` integration, releases, and explicit manual runs. +6. Run `sh scripts/check-project-state.sh` on macOS/Linux/Git Bash or `./scripts/check-project-state.ps1` on PowerShell, plus `npm run check` and any relevant manual browser/network checks. Record this local evidence in the pull request; feature branches and pull requests use local gates, while GitHub Actions are reserved for `main` integration, releases, and explicit manual runs. Do not retry zero-step or infrastructure failures. 7. Merge an accepted truth checkpoint before dependent candidates. Rebase or rebuild a retained candidate from that main once, keeping main's owning truth on conflicts and transplanting only approved scoped code, tests, and new facts. Make focused commits using `type(scope): summary` where practical, then push the branch. Create any new implementation branch or parallel worktree from that exact canonical `main` commit, never from an older candidate or auxiliary worktree. 8. Open a pull request using the repository template. Resolve review comments and required local checks before merging. @@ -23,6 +23,7 @@ Clean up worktrees and branches only after semantic review and integration are c - Use current community issues and operational reports to identify real compatibility failures, but label anecdotal evidence as such. - Include a date and direct URLs. Separate verified facts, measurements, assumptions, and recommendations. - Review licenses before copying implementation code. A useful reference is not automatically a compatible dependency. +- Until the project license and distribution model are decided, treat GPL/AGPL implementation sources as study-only. ## Pull Request Scope diff --git a/docs/README.md b/docs/README.md index 43ce175..8c4bf17 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,12 +1,11 @@ # Project Documentation - [需求理解](./需求理解.md): current product scope, priorities, constraints, and acceptance criteria. -- [方案设计](./方案设计.md): current implementation design; routing capacity and fallback accounting are temporarily frozen by the truth-audit hold. -- [Project memory](./project-memory.md): durable decisions and unresolved questions to read at the start of future work. +- [方案设计](./方案设计.md): current implementation design; unresolved routing semantics are tracked in the current TODO ledger. +- [Project memory](./project-memory.md): durable current product and source snapshot to read at the start of future work. - [Current status](./status.md): bounded current/index snapshot of production, active milestones, decisions, blockers, and links to detail. - [Verification status](./verification-status.md): demand-loaded ledger of current cross-cutting evidence, open proof boundaries, and expensive-test applicability. -- [TODO execution audit hold](./todo-audit-hold.md): temporary quarantine for disputed post-boundary work, experiments, and stale assumptions while the 2026-08-22 truth audit is open. -- [Workspace disposition](./workspace-disposition.md): demand-loaded worktree, branch, folder, junction, and archive schedule for restoring one canonical `main` root. +- [Current TODO](./todo.md): unresolved work, active truth holds, and decisions that still require user confirmation. - [Deployment](./deployment.md): exact current/rollback production configuration, release operations, and transport evidence boundaries. - [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. @@ -23,8 +22,8 @@ - [Agent context governance research](./research/agent-context-governance.md): official Codex, Claude Code, Hermes Agent, and GitHub practices adopted by this repository. - [ADR-0001](./adr/0001-p2p-first-media-topology.md): historical P2P-first baseline; later access and automatic-routing decisions supersede its stale current-state details. - [ADR-0002](./adr/0002-persistent-protected-rooms.md): site access, 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 automatic P2P-first direction and generation safety; capacity and fallback accounting are on truth-audit hold. +- [ADR-0004](./adr/0004-peer-assisted-media-experiment.md): historical standard-WebRTC peer-assisted experiment and reusable evidence. +- [ADR-0005](./adr/0005-automatic-hybrid-media-routing.md): accepted automatic P2P-first direction and generation safety; unresolved capacity and fallback accounting remain held in the current TODO. - [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. - [ADR-0008](./adr/0008-window-scoped-audio-capture.md): browser window-audio hint and the Windows WASAPI process-loopback boundary. diff --git a/docs/adr/0001-p2p-first-media-topology.md b/docs/adr/0001-p2p-first-media-topology.md index 12d1562..9528c26 100644 --- a/docs/adr/0001-p2p-first-media-topology.md +++ b/docs/adr/0001-p2p-first-media-topology.md @@ -1,70 +1,39 @@ # ADR-0001: P2P-First Media Topology -- Status: Accepted Historical MVP Baseline; Current Routing Details Superseded By ADR-0002/0005 And Audit Hold +- Status: Historical MVP baseline; superseded for current routing by ADR-0005 - Date: 2026-08-18 -The P2P-first product intent remains useful. Current access grants, deployed automatic routing, endpoint capacity, SFU publication/subscription, selected TURN, and fallback accounting must not be inferred from this dated baseline; use ADR-0002 and the [TODO audit hold](../todo-audit-hold.md) until ADR-0005 is reconciled. +## Durable Rationale -Current clarification, 2026-08-19: the MVP remains unchanged, but statements -below that exclude automatic topology migration do not describe the product -target. Automatic, viewer-transparent routing through direct P2P, -peer-assisted media, and an enabled SFU fallback is a standing requirement and -is governed by ADR-0005. +Screener serves one broadcaster and a small group of trusted friends. Low +latency, private access, browser viewing, and low server media cost matter more +than public-broadcast scale. Direct WebRTC therefore remains the preferred +media path, while centralized media is fallback infrastructure rather than the +default topology. -Historical ADR-0003's explicit whole-room SFU mode is rejected/superseded. -ADR-0004's bounded peer topology and ADR-0005's automatic controller are -process-enabled for all normal rooms when configured; room `1` is only the -historical 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. Production has a complete selected- -edge tuple, and an exact-source Host-to-SFU selected TURN/UDP canary passed; -initial-ingress, peer-selected, external-cohort, and performance gates remain. +The control and media planes are separate. HTTPS/WSS owns identity, rooms, +invitations, presence, and signaling; WebRTC owns encrypted media. STUN is +required for ordinary peer discovery. Every attempted route must end in bounded +success or a clear failure, because direct connectivity is not universal across +NAT, CGNAT, firewalls, and changing networks. -## Context +## Historical MVP Decision -The product targets one game broadcaster and a small group of trusted friends. Low glass-to-glass latency, private access, no-install browser viewing, and low server bandwidth cost matter more than large-room scalability. Public broadcasting is deliberately delegated to existing OBS/Twitch-class services. NATs, CGNAT, restrictive firewalls, variable publisher upload, and browser capture limitations prevent a direct-only design from being reliable for every user; partial room reachability is not acceptable behavior. +The first MVP used one broadcaster-to-Viewer peer connection per Viewer and did +not implement automatic topology migration. That implementation proved the +browser-first product surface but did not establish a sustainable fanout, +fallback order, or server-resource policy. -## Decision +The MVP explicitly excluded always-SFU conferencing, public-broadcast scaling, +MCU transcoding, and a custom Parsec-like transport. Those workloads either +contradict the private-room cost model or duplicate mature WebRTC capabilities. -Use separate control and media planes: +## Current Ownership -- 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. -- Ordinary peer connections use STUN-only ICE. After direct/peer UDP and the bounded SFU/UDP virtual-parent path fail, ADR-0005 permits one optional authenticated TURN/UDP attempt for only the controller-selected exceptional edge, followed by bounded failure. -- 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. The automatic controller is enabled for all configured normal rooms; local SFU/UDP media and active Host-ingress selected TURN/UDP function pass, while public-room and performance evidence remain open. ADR-0005 owns the bounded-root target. +[ADR-0005](./0005-automatic-hybrid-media-routing.md) owns current routing +invariants and unresolved assisted-route boundaries. [ADR-0004](./0004-peer-assisted-media-experiment.md) +owns only historical peer-assisted evidence. Access and room lifetime are owned +by [ADR-0002](./0002-persistent-protected-rooms.md). -## Consequences - -Positive: - -- Direct sessions consume almost no server media bandwidth and normally take the shortest network path. -- 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. - -Negative: - -- Broadcaster upload scales approximately as `viewer_count * stream_bitrate`. -- Browser APIs do not guarantee that several peer connections share a single hardware encode. -- Direct peers learn one another's network addresses; this is acceptable only for the initial trusted-friends threat model. -- If the site-wide password is disabled, the numeric room code is the sole viewing capability and does not provide a strong privacy guarantee. -- A room-level P2P-to-SFU migration adds state, keyframe, and reconnection complexity and is not part of the first prototype. This implementation boundary does not remove the product requirement for automatic, viewer-transparent fallback. -- TURN over TCP can suffer head-of-line blocking, and its `turn:` client-to-server transport is not TLS-wrapped. The WebRTC media remains protected by DTLS-SRTP independently of that TURN transport. - -## Rejected For The MVP - -- Always-SFU: operationally stable but makes the server carry all viewer egress, contrary to the main cost constraint. -- Public-broadcast scaling: Twitch/OBS-class services already solve this separate workload and it would distort the private-room design. -- MCU/transcoding: unnecessary CPU/GPU work and added latency for a single screen stream. -- Peer-assisted relay tree for the MVP: adds churn handling, extra latency, trust problems, and browser re-encoding or a custom packet-forwarding protocol. Proposed ADR-0004 permits only a bounded, removable experiment; production use remains rejected until a later ADR passes measured gates. -- Custom Parsec-like transport: duplicates capture, codec, congestion-control, NAT, and security work already provided by WebRTC. - -## Revisit Triggers - -- More than 20% of successful viewer connections use TURN in representative telemetry. -- The publisher is CPU-limited or cannot sustain the configured aggregate upload for representative rooms. -- Product requirements exceed the measured sustainable P2P room envelope. -- Protecting peer IP addresses becomes a product requirement. +This historical record does not authorize a capacity, SFU/TURN order, topology, +runtime migration, or release gate. diff --git a/docs/adr/0004-peer-assisted-media-experiment.md b/docs/adr/0004-peer-assisted-media-experiment.md index e02598d..fa447e3 100644 --- a/docs/adr/0004-peer-assisted-media-experiment.md +++ b/docs/adr/0004-peer-assisted-media-experiment.md @@ -1,224 +1,118 @@ # ADR-0004: Peer-Assisted Media Experiment -- Status: Proposed - Historical Experiment (rollout superseded by ADR-0005) +- Status: Historical experiment - Date: 2026-08-19 - -> Historical cap2/cap3 measurements remain evidence only. References below to a current Browser1 policy, global SFU-root-two limit, or accepted accounting are frozen by the [TODO audit hold](../todo-audit-hold.md) and must not authorize implementation or a release gate. +- Last updated: 2026-08-22 ## Context -ADR-0001 accepted standard browser WebRTC P2P for the MVP and rejected a viewer -relay tree because it adds churn, latency, and either browser re-encoding or a -custom packet-forwarding protocol. Real multi-viewer use later degraded severely. -The product now also has a hard target that a broadcaster must never carry more -than two outgoing media edges. +ADR-0001 accepted browser WebRTC P2P for the MVP. Multi-viewer measurements then +showed that sending one Host connection per Viewer does not fit the product's +small, bounded upload target. The repository therefore explored whether an +ordinary browser Viewer could relay the received screen stream to another +Viewer. -Closed PR #12's explicit whole-room SFU proposal under ADR-0003 is historical -and superseded. Merged PR #17 instead supplies ADR-0005's default-off automatic -optional-SFU fallback. Making that fallback the normal path would still move -every viewer's egress cost to the server. TeamSpeak-style shared encoding -reduces host encode work but still sends one network copy to every viewer, so it -cannot meet the new fanout target alone. +This ADR records that experiment and its reusable evidence. It does not set the +current endpoint capacity, SFU topology, TURN placement, fallback sequence, or +resource accounting. ADR-0005 owns current routing invariants. -The research in `docs/research/peer-assisted-media.md` finds no browser API that -can move one encoded frame between `RTCRtpSender` pipelines. Standard browser -track relay decodes and re-encodes at each hop. The first experiment accepts -that measurable cost because avoiding it requires a custom encoded-frame media -plane. +## Experiment -This ADR does not accept peer-assisted media for production. It proposes one -bounded experiment that must either meet explicit gates or be removed. -Historical cap2/cap3 loopback measurements under this ADR are experimental -evidence only. ADR-0005 owns the current release policy: Host at most two, -ordinary Browser Viewer at most one, with server-authoritative role clamps. +The signaling server assigned a deterministic, bounded directed acyclic graph. +Each Viewer had at most one active upstream. Join, capacity release, endpoint +departure, and current-edge failure were the only reassignment triggers; +healthy unaffected branches remained sticky. The experiment intentionally +excluded continuous route scoring, periodic rebalance, multiple simultaneous +upstreams, and user-selected topology. -## Proposed Experiment +Each browser relay rendered the received `MediaStream` and added its remote +tracks to a separate downstream `RTCPeerConnection`. This is standard WebRTC, +not packet forwarding or encoded-frame reuse. The browser decodes and normally +re-encodes at each relay hop, and each downstream connection owns separate +packetization, pacing, congestion control, encryption, and upload traffic. -Preserve the product priority in this order: +The experiment shared one strictly validated room quality object across current +and future senders. Values were bounded to the existing resolution, frame-rate, +bitrate, degradation-preference, codec, and screen-audio choices. Sender +mutation was serialized and read back from the browser. Requested settings were +treated as ceilings and preferences, never as proof of achieved media quality. -1. direct P2P for one or two viewers; -2. peer-assisted forwarding for later viewers when this experiment is active - and every required capability is present; and -3. the ADR-0005 SFU virtual parent as the flagship central fallback while - retaining bounded peer descendants. +Ordinary peer connections remained STUN-only. A TURN candidate would alter the +transport of an authorized edge, not the graph semantics or downstream edge +count. -This bounded experiment implements only the first two levels. That scope must -not be read as requiring users to select a topology or rejecting automatic -cross-mode fallback as a product goal. +## Evidence retained -The experiment is gated by `PEER_ASSISTED_MEDIA`, which defaults to `false`. -When enabled, it applies to every room. `MAX_VIEWERS_PER_ROOM` remains one -shared admission limit: it defaults to eight and accepts explicit values from -one through sixteen. The upper admission bound is not a media-capacity or -performance claim. The removed `PEER_ASSISTED_ROOM_IDS` allowlist fails startup; -there is no second rollout mode, UI selector, percentage rollout, or route score. +The experiment established these durable facts: -The signaling server assigned a sticky, bounded DAG with a deterministic -breadth-first walk. This historical experiment gave the host and each ordinary -Web viewer capacity for at most two children; it is not the current release -policy. Candidate parents were ordered by depth and -server-issued join sequence. Joining a viewer does not move existing assignments except for -ADR-0005's bounded admission rescue: an unassigned relay may replace -the oldest childless zero-capacity Host leaf when both Host slots are full. If a -parent leaves, only its orphaned subtree root is assigned to the first available -slot; the root's descendants stay attached. No RTT, bandwidth, CPU, geography, -capability, or quality scoring is added. +- standard browser track relay incurs a separate downstream sender and cannot + be described as zero-copy or shared encode; +- an upstream receive edge is distinct from downstream upload work; +- endpoint capacity must be enforced by the server and revalidated during + reconnect, replacement, and stale-signal handling; +- one active Viewer upstream plus an acyclic committed graph is a tractable + safety invariant; +- route replacement must be bound to exact sessions, revision, share + generation, assignment generation, and connection identity; +- ICE `connected` is not media proof; positive RTP, decoded-frame progress, and + a live current-generation track are required before switching; and +- loopback tests can prove graph and signaling invariants but cannot establish + real-device encoding cost, target-network latency, or production capacity. -For this ADR, one media edge is one downstream `RTCPeerConnection` carrying the -shared stream. Using TURN for that connection does not alter the edge count. The -experiment's two-child endpoint limit was a hard invariant across join, -reconnect, reparent, and recovery paths. If the deterministic topology had no connected -eligible parent, the viewer remains admitted but waits without media until a -slot becomes reachable; it must not create a third host connection. -Each logical edge still uses its own ICE process. In the repository candidate, -ordinary edges receive only STUN and route exhaustion proceeds to the bounded -SFU virtual parent or clear failure; the old production TURN behavior is not a -same-process compatibility branch. +Historical capacity-two and capacity-three runs remain measurement evidence. +They do not authorize a current release tier or a browser-specific policy. -Every edge uses the existing standard WebRTC media path. A viewer receives the -remote `MediaStream`, renders it, and sends those remote tracks through one new -downstream `RTCPeerConnection`. The browser therefore decodes and re-encodes at -every relay hop. Existing audio follows the same stream when capture provides -it; the experiment does not invent a separate audio protocol. +## Experiment gates -The authoritative peer-assisted wire value is one exact `QualitySettings` -object, not a profile ID. It accepts only 720p/1080p/1440p, integer 15-60 fps, -integer 2-12 Mbps, and clarity/balanced/fluid preference, with missing, extra, -or out-of-range fields rejected. The three presets remain UI recommendations, -not protocol states. The server keeps the latest object in bounded per-room -memory only; it is not persisted. Authentication carries it, the host reasserts -its local value before reconciling children, and live changes reach online -viewers without an acknowledgement protocol. +A browser relay path is viable only if representative real-device tests record: -`HostPeer`, the current and future viewer relay sender, and an enabled SFU -publisher consume that same last-wins setting. Initial sender setup, successful -track replacement, and live changes use the same serialized mutation path and -read back requested/applied bitrate, frame rate, scale, and preference. A -rejected mutation rolls back or fails closed through the existing route -controller. The ordinary P2P authenticated wire remains unchanged and rejects -room-setting messages. These are sender ceilings and preferences, not proof of -achieved resolution, bitrate, frame rate, or resource cost. +- end-to-end and per-hop latency; +- capture, encode, decode, and render frame rates; +- CPU, GPU, encoder count, and `qualityLimitationReason`; +- endpoint upload, packet loss, retransmission, and recovery; +- topology depth and the failure radius of a relay departure; +- audio continuity and synchronization when screen audio exists; and +- deterministic bounded recovery without stale edges or unauthorized signals. -Encoded Transform, DataChannel media, WebCodecs rendering, dummy-sender byte -replacement, custom congestion control, codec ladders, multiple trees, mobile -background relay, and cross-mode SFU migration are excluded from this spike. -They are not rescue work if standard track relay fails; a separate automatic -route-controller ADR owns cross-mode fallback. +The path fails closed if it requires a custom RTP injection layer, a new +congestion controller, hidden software encoding, unbounded fanout, or a quality +claim unsupported by measurements. -## Shared Encoding Boundary +## Current interpretation -The historical browser spike may encode once for each of the host's one or two seed -connections because browsers do not guarantee cross-connection encoder reuse. -Each viewer relay also performs one downstream encode per child, currently up -to two. This is tolerated only for the experiment and must be measured honestly. +Peer relay remains a useful distributed-media candidate and deployed source +contains portions of the experiment. Those portions must be evaluated against +ADR-0005's current invariants rather than carried forward wholesale. -A separate planned packaged/native sender must use custom libwebrtc encoder -proxies that share one encoded output while retaining independent standard -WebRTC packetizers for the host's edges. That work requires its own ADR and -measurements regardless of this browser relay experiment's result. It reduces -host encoding work but does not remove the upload copy for each outgoing edge. -It is not implemented or abstracted in advance by this spike. Standard browser -viewing remains required. +The current accepted endpoint rule is one server-authoritative downstream +capacity for every non-server endpoint, default `2` and statically configurable +as `1`, `2`, or `3`; upstream receive is free and role or user agent does not +create an exception. The separate SFU/TURN publication, subscription, transport, +server-resource, and migration-overlap model is still pending. -## Acceptance Gate - -Test 1, 3, 5, and 8 viewers for 30 minutes across the three recommended ceiling -combinations, plus any advanced combination proposed for production, with -controlled per-edge RTT at or below 40 ms and loss at or below 1%. Current -Chrome and Edge form the controlled relay cohort; Android Chrome and iOS Safari -remain compatibility observations. ADR-0005 currently gives every ordinary Web -Viewer one downstream edge without UA or visibility detection. The retained -two-edge Browser runs are historical experiments; a future higher tier requires -this resource/quality gate and cannot be inferred from device class. Mobile -resource behavior remains unverified, but does not define a separate route -class. The gate remains an eight-viewer experiment. A separate -sixteen-viewer loopback may prove only admission, bounded topology, and decode -function; neither result can authorize a 20-viewer default. That later release -requires a separate 20-viewer matrix or bounded central exceptions. - -The proposal advances only if every condition holds: - -- host and viewer media fanout are never greater than two, including relay loss - and reparenting, and a third child is never admitted; -- assignments are reproducible from depth and server join order, without a - composite score, live optimization loop, or proactive rebalancing; -- every relay's expected outbound RTP encoder is measured through encode time, - CPU/GPU load, and `qualityLimitationReason`; no result calls this shared or - zero-copy forwarding; -- excluding explicit TURN paths, the application server carries no media; -- host and relay upload remain within 20% of `childCount * observedBitrate`; -- authentication and live setting changes leave every current and future child - sender targeting the same latest room setting without altering the P2P wire; -- first picture is at most 3 seconds, a 60 fps run does not remain below 50 - decoded fps for more than 5 seconds, and depth-three p95 glass-to-glass - latency is at most 350 ms; -- relay delta encode time per frame stays at or below 16.7 ms on the reference - cohort and CPU limitation does not persist for more than 5 seconds; -- loss of either first-level relay that the server observes immediately first - receives the default 5-second disconnect grace; if it does not reconnect, - the same deterministic assignment rule produces a new decodable picture - within the following 3 seconds (about 8 seconds total under defaults); and -- the common codec path works on the controlled Web relay cohort. - -Android Chrome and iOS Safari remain non-blocking compatibility observations; -their behavior does not introduce a device-specific relay-capacity policy. - -The about-8-second gate covers a controlled page close that the server observes -immediately. A silent network partition depends on the default 30-second -heartbeat and can take 30 to 60 seconds to detect before the same 5-second -grace; that path is a separate, currently unverified measurement. - -Fixing an ordinary implementation bug inside the bounded spike is allowed. If -meeting a gate requires Encoded Transform/DataChannel/WebCodecs media, custom -congestion control, FEC/RTX changes, multiple trees, relay scoring, transcoding, -a codec ladder, relaxed host fanout, or a browser-specific RTP injection hack, -stop. Change this ADR to Rejected and delete the experimental browser-relay -runtime path and dependencies. That result keeps standard P2P plus enabled -user-operated or central SFU fallbacks for the later automatic controller. It -does not cancel the separate planned -native shared-encode sender, and that sender cannot be used to mark an otherwise -failed browser-relay topology as passing. - -Passing the gate did not change this ADR to Accepted. It permitted a separate ADR -to propose a production design, including broader game-audio/A-V verification, -automatic bounded relay policy, and privacy disclosure. The native shared-encode sender -is the independent planned work described above, not a reward for passing this -gate. - -## Consequences If The Spike Proceeds +## Consequences Positive: -- The host upload and connection count become bounded without assigning normal - viewer egress to an SFU. -- ICE, TURN, DTLS-SRTP, RTP recovery, congestion control, codec negotiation, - jitter buffering, and browser rendering remain provided by WebRTC. -- Deterministic assignment is small enough to inspect and reproduce. +- the browser path can extend a distributed graph using standard WebRTC; +- exact route generations and media-proven switching are reusable controller + invariants; and +- the experiment supplies concrete resource and failure measurements. Negative: -- Every relay decodes and re-encodes, adding CPU/GPU load, latency, and - generational quality loss. -- Every relay adds a network hop and becomes an availability dependency for its - descendants. -- Relay upload, battery, background suspension, and peer IP exposure affect - viewers, not just the broadcaster. -- The capacity-two DAG still adds repeated encode and up to three hops for eight - viewers, so the latency target remains uncertain until measured. +- every browser relay hop may add decode, encode, latency, and upload cost; +- browser APIs do not guarantee cross-`RTCPeerConnection` shared encoding; and +- experimental capacity results cannot substitute for current policy or a real + browser and network matrix. -## Relationship To Existing ADRs +## Relationship to other ADRs -- ADR-0001 remains the accepted production baseline. This proposal narrows its - peer-tree rejection only enough to run an isolated experiment. -- ADR-0003/PR #12 is retained only as the rejected/superseded explicit - whole-room SFU history. ADR-0005 and merged PR #17 own the default-off - automatic optional-SFU fallback; this ADR does not accept that route for - production. -- ADR-0007 owns path-isolated quality representations and topology eligibility. - Its accepted product target does not add SVC, multiple representations, or - adaptive switching to this bounded browser-relay experiment. -- If a later ADR accepts peer-assisted media, it must state exactly which parts - of ADR-0001 and ADR-0005 it supersedes. +- ADR-0001 remains the direct browser P2P baseline. +- ADR-0005 owns current automatic routing and capacity invariants. +- ADR-0007 owns path-quality evidence and representation behavior. +- A future native shared-encode sender or relay requires its own decision and + evidence; this experiment does not pre-approve it. ## References @@ -230,5 +124,4 @@ Sources were checked on 2026-08-19: - [WebRTC Data Channels, RFC 8831](https://www.rfc-editor.org/rfc/rfc8831.html) - [libwebrtc video send stream](https://webrtc.googlesource.com/src/+/refs/heads/main/video/video_send_stream_impl.cc) - [libwebrtc video encoder factory](https://webrtc.googlesource.com/src/+/refs/heads/main/api/video_codecs/video_encoder_factory.h) -- [TeamSpeak single-encoding explanation](https://community.teamspeak.com/t/ts6-beta-community-update-insights/58116/208) - [SplitStream, SOSP 2003](https://www.microsoft.com/en-us/research/publication/splitstream-high-bandwidth-multicast-in-a-cooperative-environment/) diff --git a/docs/adr/0005-automatic-hybrid-media-routing.md b/docs/adr/0005-automatic-hybrid-media-routing.md index fab5528..91b9dd4 100644 --- a/docs/adr/0005-automatic-hybrid-media-routing.md +++ b/docs/adr/0005-automatic-hybrid-media-routing.md @@ -1,763 +1,180 @@ # ADR-0005: Automatic Hybrid Media Routing -- Status: Accepted P2P-First Automatic Direction; Capacity And Fallback Accounting On Truth-Audit Hold -- Date: 2026-08-19 - -> The all-room automatic direction, single authoritative Viewer upstream, exact generation authorization, bounded failure, and media-proven transition principles remain accepted. Capacity, SFU publication/subscription, selected TURN, fallback ordering, and temporary resource accounting are disputed inputs rather than implementation authority until the [TODO audit hold](../todo-audit-hold.md) is resolved and this ADR is updated in place. +- Status: Accepted invariants; assisted-route resource model pending +- Date: 2026-08-20 +- Last updated: 2026-08-22 ## Context -The product has always required route selection and recovery to be automatic -and invisible to the broadcaster and viewers. Earlier repository wording that -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 is: - -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. 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 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. The current release policy gives the Host at most two active -downstream physical media edges and an ordinary Browser Viewer at most one; an -upstream receive edge does not consume that upload budget. Active and -provisional peer children, selected-edge overlays, and the Host SFU publication -all count. The server derives the effective limit from the authenticated role, -so an old or malicious Viewer advertising two or three cannot bypass one. A -deployment may tighten Host/Viewer below `2/1`, but cannot raise either limit. -There is no UA or visibility policy. - -TURN and SFU occupy different layers. A selected TURN candidate continuously -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 -capability, depth, path, and recovery gates; it still counts against the same -one-or-two-root room budget. - -ADR-0004 implements automatic peer assignment but not cross-mode fallback. -Closed PR #12 proposed a mutually exclusive process-wide `p2p|sfu` mode; this -ADR and merged PR #17 supersede that model because it cannot satisfy the route -priority or minimize server egress. - -## Accepted Target And Bounded Migration - -Treat the SFU as a virtual parent for selected fallback roots, not as an -all-room replacement. Keep the existing peer-assisted topology as the active -baseline and add one in-memory `MediaRouteController` per room. - -Eligible parent relationships form a mesh-like candidate graph, but active -media remains a loop-free graph with one upstream per Viewer. Join, capacity, -disconnect, and current-edge failure are bounded reselection triggers. A -transition may overlap old and prepared edges only within the existing edge -budgets and must commit from current-generation media proof; steady multi-source -striping is not part of the media path. - -```text -normal: host -> root A -> ... - host -> root B -> ... - -fallback: host -> remaining peer root -> ... - host -> SFU -> selected fallback roots -> existing descendants -``` - -Only one or two fallback roots subscribe directly to the SFU, and each can -continue to feed its bounded peer descendants. If no reliable relay root -exists, a necessary viewer may be an SFU root with no descendants under that -same total; this is the last central-fanout boundary, not a default whole-room -topology. The cost model is owned by the linked low-server research. - -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 selected-edge TURN is a separate 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. The process flag enables the controller -for every normal room; room `1` is retained only as a historical smoke fixture, -and production ordinary peer connections remain STUN-only. The current source -no longer contains the rejected participant-wide -TURN config, issuer, capability, refresh wire, or client propagation. Stale -`PEER_ICE_TURN_*` keys fail startup even when blank. The source candidate now -implements a complete selected-edge tuple and one relay-only rebuild for either -the initial pending Host publication or an active Host/SFU route; production -configures the tuple. A bounded exact-Web-source canary against the production -LiveKit/coturn tuple proves the active Host/SFU relay route; initial Host ingress -and `peer-selected` relay media remain open. -The wire has two explicit edge kinds: `peer-selected` for the last-mile failed -peer edge and `host-sfu-ingress` for a restricted Host-to-SFU retry. Ordinary -Peer ICE remains STUN-only. Each room admits at most one `peer-selected` lease -across its negotiating and answered states; `host-sfu-ingress` is an independent -source transport attempt and does not consume that last-mile admission slot. A -negotiating lease is a selected-edge migration attempt. After answer it becomes -the exact edge's long-lived active transport authority, not a room-wide -migration lock. - -The current runtime removes the old all-room coturn contract. Production -requires STUN and authenticated ICE snapshots contain only STUN servers in -production. 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 -been observed, but public-room retained media and route admission remain unverified. The old -`769de201f7cc` release and coturn relay are isolated rollback resources, not a -permanent compatibility branch or advertised current transport. - -`PEER_ASSISTED_MEDIA=true` is now the only rollout switch. It enables the -controller, LiveKit standby, grants, and selected-edge attempts for every normal -room, while each room keeps independent route state and the existing root/fanout -limits. `PEER_ASSISTED_ROOM_IDS` is retired and fails startup if supplied during -migration; room `1` is a historical smoke fixture, not a feature gate. A process -with the flag disabled remains ordinary P2P. - -The implementation keeps the LiveKit dependency dormant unless the complete URL, API key, -and API secret tuple is present together with `PEER_ASSISTED_MEDIA=true`. It -issues short-lived room-, role-, peer-, and publication-generation-bound grants, -and only selected branch roots may subscribe. A necessary viewer with no -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 TURN is controller-selected only after that SFU/UDP attempt -fails. The authorization binds the room/share generation, viewer and parent -sessions, grant revision, and replaced/new connection identities. After answer, -the signed grant revision stays immutable while the controller tracks the current -route revision separately. An unrelated route revision may carry the exact edge -only while its sessions, topology, share generation, and parent downstream -budget remain current; carry authority must precede the active route update. -Stale, replayed, participant-wide, over-budget, 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. -The current Browser client advertises one downstream edge. The unchanged wire -continues to parse `0 | 1 | 2 | 3` only as a future capability envelope; it is -not current route authority. The controller clamps all Browser Viewer -advertisements to one before topology assignment, including old-client values -two and three. `MAX_PEER_RELAY_DOWNSTREAM_EDGES` defaults to two and accepts one -or two as a deployment tightening input: effective Host capacity is -`min(config, 2)` and effective Browser Viewer capacity is `min(config, 1)`. -Withdrawing capacity does not proactively migrate an otherwise healthy existing -edge. There is no UA-, device-, or visibility-based branch. - -The source now implements one narrow admission exception to sticky assignment. -On an active peer-only route with no SFU publication or pending prepare, a -connected, childless Viewer that has no upstream or failed-parent history and -advertises relay capacity may replace the oldest connected, childless, -zero-capacity Host child when both Host slots are full. That leaf becomes one -child of the new relay. The synchronous change preserves Host fanout at two and -Browser Viewer fanout at one, advances one route revision, and clears -both changed upstream connection generations. Host-side reconciliation closes -the stale child edge before starting the replacement. No healthy routed -candidate, quality score, timer, global parent penalty, or periodic rebalance is -part of this admission rescue. - -The repository quality-reparent candidate adds one bounded parent-to-server -evidence message and no second route owner. Signaling authenticates and -forwards Viewer C under its existing session, connection, revision, parent, -sequence, two-second rate and 2 KiB guards. The current parent then answers only -from its current outbound `HostPeer` sample: a positive `packetsSent` delta is -required. Its existing proof union carries `sending` for current liveness, -`sender-limited` only for an exact `qualityLimitationReason` of `cpu` or -`bandwidth`, or `remote-loss` only when the same interval has at least 100 sent -packets and RTCP-reported remote loss divided by sent packets is at least 30%. -`sending` is diagnostic and cannot advance a bad streak. The 2 KiB answer -echoes the exact C sequence; signaling -accepts it once, within five seconds, from the current parent session and same -child generation. A parent consumes a stats sample only once per child and -current connection, using the existing finite monotonic stats timestamp; its -DOMHighResTimeStamp-derived window is rounded before the one-to-five-second -bound is checked. Viewer C may wait pending, but the router advances the streak -only when B independently reports one of the two hard sender predicates and C -is either severe or relatively degraded. Severe remains freeze, zero decode, -or high loss. Relative degradation applies only below a Viewer relay parent: -the parent's own most recent C must already be correlated, current for its -session, route and connection, no older than five seconds, and the child's FPS -must be below two-thirds of that actual inbound FPS. It never compares C with -the room's configured FPS ceiling. Both inputs are WebRTC stats gathered by -stock browsers at separate endpoints; parent remote loss originates in RTCP. -This is cross-endpoint corroboration, not cryptographic independence or defense -against colluding authenticated participants. Raw stats, addresses, candidates, -SDP, URLs and a composite score remain out of wire. -The router may create the existing Viewer route intent; -`PeerRelayTopology.reassignViewer` and `MediaRouteController` continue to own -the subtree change, revision, generation, prepare, commit and rollback. - -Targeted tests cover the revision controller, protocol authorization, relay -capacity and deterministic admission rescue, ordered break-before-make client -transitions, stale asynchronous work, server-restart -resynchronization, one-shot credential recovery, peer failback, and optional -standby warming. The 2026-08-22 `gate:sfu-root-invariants` preflight hard-checks -that controller state and token allowlists accept no more than two central -roots, one active Host SFU publication consumes one of the Host's two outbound -media edges, and a committed zero-descendant root remains selected across -reauthentication. A separate client transition check keeps its active subscriber -when a newer same-kind assignment still has no children. The benchmark evaluator -also rejects a third root or third Host media edge. This in-process evidence -starts neither LiveKit nor a browser and therefore proves no packet flow, -bandwidth adaptation, or resource cost. Chrome 151/LiveKit 1.13.5 localhost runs -established route -transition, resumed decoding, and the two-edge bound. Their elapsed times are -diagnostics only and do not satisfy the regional recovery target. Public -transport/audio/load/browser checks remain pending. The automatic controller -and its later bounded transition slices are deployed, while the fixed -accounting policy in this checkpoint is under truth-audit hold and real packet, -recovery, resource, and broad-browser acceptance remains unverified. - -## State And Wire - -The server holds one room revision with a participant assignment for every -retained member, plus at most one pending room plan: - -```ts -type Upstream = - | { kind: "none" } - | { kind: "peer"; peerId: string } - | { kind: "sfu" }; - -interface ParticipantRoute { - upstream: Upstream; - childPeerIds: string[]; -} - -interface RoomMediaRoute { - revision: number; - assignments: Map; - sfu: { - publicationGeneration: string | null; - rootPeerIds: string[]; - }; -} -``` - -The server mapping is authoritative. Each participant receives only its own -assignment for that room revision; it never receives or edits the complete -mapping. The explicit SFU publication generation prevents an old and a new -publisher from being counted as one logical edge during commit or rollback. -The Web execution layer independently truncates each received assignment to -the current Host-two or Viewer-one physical-child limit before creating peer -connections. This is defense in depth for a regressed or mismatched server; -the wire's future capacity envelope is not client execution permission. - -Minimal protocol additions: - -- optional `sfuStandbyUrl` in a peer-assisted authenticated snapshot, only when - the server has complete LiveKit fallback configuration; -- `relay-capacity { downstreamEdges: 0 | 1 | 2 | 3 }` from an authenticated - peer-assisted viewer; the range is a future wire envelope while current - server policy clamps an ordinary Browser Viewer to one; -- `route-update { revision, phase: "prepare" | "active", assignment }`; -- `sfu-config { revision, url, token }`; -- `route-ready { revision, phase: "prepare" | "active" }`; -- `route-failed { revision, phase, connectionId }`; and -- `refresh-sfu { revision }`; -- a 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 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 only `iceServers` populated -from `STUN_URLS`. The rejected participant-wide tuple, issuer, capability, -refresh messages, and client propagation are removed, and stale -`PEER_ICE_TURN_*` keys fail startup. The selected-edge source slice atomically -adds its controller consumer and one-use rebuild; only that 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 -standby JWT. An authenticated snapshot is the sole authority allowed to replace -a higher client revision after the signaling server restarts; the client first -retires media owned by the old revision. Existing peer signaling keeps its -connection generation and assigned-edge authorization. The server -derives the failed edge from the authenticated session, active revision, and -connection ID instead of accepting a client-supplied parent or arbitrary reason. -Late, duplicate, or stale revision messages have no effect. -When the process flag is enabled, every normal room receives the hybrid snapshot -and its own controller state. A process with the flag disabled receives the -ordinary P2P snapshot. The distinction is process configuration, never a room -ID allowlist. - -## Transition - -1. Keep direct/peer UDP roots while those routes work. -2. A failed edge performs one ICE restart, one same-parent connection rebuild, - and then one alternate eligible peer-parent attempt; it does not repeat an - identical action three times. A short `disconnected` state first uses - generation-bound bytes/stats evidence. After the Viewer successfully sends - its answer, an initial `new` or `connecting` connection now has a separate, - conservative 15-second hard deadline. `connected`, disposal, or replacement - 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. 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. - The Viewer recovery control follows the authoritative active route: a peer - edge requests the existing peer recovery, while an SFU assignment restarts - Screener signaling so reauthentication reasserts the same route and the - existing authoritative resync rebuilds the subscriber from fresh config. It - neither reports a connection as healthy nor directly requests SFU-to-peer - migration. -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 - best-effort, token-free `prepareConnection(url)` call after their authoritative - authenticated snapshot. This only warms the SDK/DNS/TLS path and is not a - participant, media edge, or ready signal. Required participants then establish - actual LiveKit connections with - `autoSubscribe:false`, but the host does not publish and roots do not - subscribe yet. -5. After every required participant acknowledges prepare, the server commits - the active revision. The host synchronously closes the selected direct root - before publishing to the SFU. Thus: - - `directHostChildren + activeSfuPublicationGenerations <= 2`. - - `activeSfuPublicationGenerations` is zero or one. A new generation cannot - publish until the replaced direct edge and any old SFU generation are - inactive. - -6. Selected fallback roots explicitly subscribe. On their first new video - track, they atomically replace their upstream stream, acknowledge active, - and feed the existing downstream relay without rebuilding descendants. -7. A prepare timeout increments the revision and restores the unchanged active - route. A failure after commit creates a new rollback/failure revision; stale - acknowledgements cannot revive the abandoned plan. - -8. An active Viewer SFU root may request one fresh short-lived grant. If its next - failure, or the initial SFU prepare, is exhausted, the router grants at most - one relay-only rebuild of that Viewer's original failed peer edge. A - Host-to-SFU route may instead receive one `host-sfu-ingress` relay-only grant - for an initial pending publication or an active route. The pending retry is - bound to the exact revision, publication/share generations, expected Host - session and a server-generated new connection ID. Prepare-ready consumes the - attempt; retry failure reports that new ID and restores the unchanged peer - baseline. Stop, session replacement, topology revision or a new sharing - generation also clears it. - -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 -with client-requested leave. Services did not restart and no track publication -was retained. That timing is consistent with the bounded fresh-grant retry and -Peer failback state machine, but the logs do not prove those transitions or the -publisher stage. The client now retains only a local enum -(`connect`, `source`, `video-publish`, `sender-config`, `audio-publish`, or -`transport`) for the current route revision and reports the failure as an event; -it never uploads the raw error, endpoint, token, candidate, or address. - -An activated SFU-root route is sticky until a discrete route event requires a -change or the current share stops. A new sharing generation starts from the -cheapest available UDP route. The controller does not continuously rebalance -healthy media. The bounded quality candidate changes only the Viewer-rooted -subtree whose current parent-to-child edge produced three consecutive hard-bad -windows. One confirmed child remains edge-local. Two-distinct-child -corroboration is retained only for a future accepted Viewer-capacity tier; the -current ordinary Browser Viewer1 release makes it unreachable and does not -authorize a second downstream child. Two distinct confirmed -children under the same Viewer parent session and share generation within five -seconds temporarily make that parent relay-ineligible (effective capacity zero) -for 30 seconds; its advertised capacity is unchanged. The triggering edge still -follows its own intent through one peer-to-peer make-before-break attempt. -Severe and relative-FPS quality evidence do not enter SFU or TURN; a real -`route-failed` remains the separate owner of the ordinary failure ladder. - -The authenticated standby is not a transport: it has no grant and never joins a -room. During route prepare the current implementation warms only an unpublishing/unsubscribed -transport. Its media transition remains break-before-make: the host releases the -replaced peer or SFU media edge before activating the new one. The accepted -one-root healthy SFU probe and the peer-quality slice below are explicit bounded -exceptions: they retain old media until current-generation receive/decode proof. - -## Standby Prewarm Result - -Pinned `livekit-client` 2.22.0 implements token-free self-hosted -`Room.prepareConnection(url)` as a best-effort HTTP `HEAD` to the corresponding -HTTP(S) origin. The SDK catches failure internally. It performs no `connect`, so -it neither creates a LiveKit participant nor publishes/subscribes media. The -client dynamically imports the SDK once, attempts each advertised URL once, and -drops work made stale before import by a newer authenticated snapshot. Failure -silently preserves the existing cold path; there is no timer or retry loop. - -One bounded cold/standby A/B comparison used Chrome 151, LiveKit 1.13.5, -headless synthetic 1280x720/30 video, three viewers, and localhost. The harness -physically closed the same leaf's inbound peer edge, observed it resume under a -second peer, then physically closed that new edge and reported its real -connection ID and active revision. The resulting plan had two selected SFU -roots. - -| Failure report to | Cold route controller | Authenticated standby | -| --- | ---: | ---: | -| SFU prepare | not retained | 5 ms | -| SFU active | 1,481 ms | 200 ms | -| Same-leaf rendered frame | 2,257 ms | 319.7 ms | - -The standby path had already downloaded and parsed the dynamically imported SDK -and had called its token-free `HEAD`, so this A/B does not isolate SDK startup -from DNS/TLS/HTTP preparation. It created no participant or media edge. The same -leaf decoded 31 SFU frames and produced 31 new frame callbacks before the -after-run completed. Both roots and the host SFU transport were connected; 25 ms -sampling observed a host media-edge peak of two. This is a local go for retaining -the optimization; the roughly 86% improvement must not be extrapolated to a -public network. - -## Triggers And Budgets - -The first controller reacts only to discrete events: - -- no eligible peer slot or the accepted maximum depth; -- exhausted ICE/rebuild for an assigned edge; -- relay participant departure; -- unsupported route capability; -- a hard bounded-send-queue overflow on a future encoded-object route; or -- three consecutive, current-identity correlated Viewer C/parent B windows in - which B reports `cpu`/`bandwidth` sender limitation or at least 100 sent - packets with RTCP-reported remote loss divided by sent packets at least 30%, - and C either has the existing severe freeze/zero-decode/high-loss predicate - or, below a Viewer relay parent, receives less than two-thirds of that - parent's fresh correlated inbound FPS. - -It does not combine RTT, bitrate, geography, or a synthetic health score; the -exact sender-limitation enum is a predicate, not an encoder score. Either side -alone is diagnostic-only. C may be stored before B arrives, but only one -unchanged severe or relative pair advances its own streak. Healthy or ambiguous -correlated windows clear it. A Viewer or parent session, -connection ID, route revision or parent identity change also resets it, as does -an evidence gap over five seconds. Severe and relative-FPS evidence each open at -most one peer-to-peer make-before-break attempt. The candidate is either a Viewer -whose active upstream is peer or SFU, or the Host; its session/share/revision -must be current and its effective downstream budget must have one strict spare -slot. A Viewer candidate holds one provisional child `HostPeer` outside its -active child IDs/map; the Host owns an equivalent provisional child separately -from its active peers, SFU publication, and selected overlay, while Host2 remains -the physical-edge ceiling. The old active edge and its connection ID remain authoritative -while the candidate uses a separate exact identity. The Viewer sends prepare-ready only after positive current-candidate -RTP and decoded-frame progress plus a live video track; only then does the server -commit the planned topology and promote the same provisional PC plus candidate -connection ID. A failure after prepare-ready retains that exact identity across -repeated matching active updates, where the candidate parent stays fail-closed -without originating `route-failed`; the target Viewer's probe owns recovery. -Rollback clears the identity. An unrelated authoritative route failure first -aborts the soft probe and then immediately resumes its ordinary ladder. Failure, -timeout, stale identity, or no eligible peer keeps the old active edge and does -not start SFU, TURN, an error, or another migration. A started attempt creates -one 30-second room migration budget. Viewer/session churn -cannot bypass that budget; removal, disconnect, authorization or generation -change clears per-edge evidence and intent state, while room stop/delete clears -the room budget. A quality-created intent retains its Viewer/parent sessions, -connection, revision and parent guard through prepare and commit. A successful -relative-FPS move holds its -old parent for 30 seconds, or until that parent session changes, to avoid a soft -bounce. A real `route-failed` is stronger evidence: it releases that soft hold, -hard-excludes the current failed parent and may immediately reuse the old -playable parent. Other successful quality moves release their temporary -exclusion. This slice migrates only the triggering child; make-before-break for -other children of a quarantined relay remains separate. The W3C stats -definitions establish the counter meanings and their WebRTC 1.0 example uses -30% loss as a likely culprit; they do not -prescribe Screener's route policy. Therefore 50%, two-thirds FPS, 100 packets, -three windows, five seconds and 30 seconds are conservative candidate constants pending -production calibration, not claimed optimums. Measurements may change them -only through a reviewed change. -ADR-0007's `HIGH`/`FALLBACK` quality state is separate and does not become a -topology trigger or a room-wide health score. - -`MAX_SFU_ROOTS_PER_ROOM` currently bounds server egress at one or two roots. -The target retains two as the normal distributed limit. If several exceptional -viewers cannot attach behind any healthy root, a later reviewed config may -admit additional direct server-fed edges under a separate explicit egress cap; -it never creates a third endpoint edge or unbounded whole-room fanout. - -Sub-second media recovery remains a target after media failure detection. The -deployed browser application challenge detects a silently partitioned signaling -WebSocket on a bounded 5-second baseline plus two exact 2-second misses, then -reauthenticates on a new socket without asking ordinary P2P or duplicate -same-revision SFU media to close and without reporting `route-failed`. Existing -`peer-selected` TURN authority remains session-bound and follows its bounded -recovery path after session replacement. Control-plane recovery is not media -evidence and does not replace a measured media liveness or native transport -event for sub-second route recovery. - -## Selected-Edge TURN After SFU - -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 -configures that selected-edge attempt but still ends in bounded failure if it -cannot connect. Source has no -participant-wide TURN path: the rejected candidate is removed and its stale -environment keys fail startup. The selected-edge implementation uses -a short coturn REST bearer and one server-generated connection identity per explicit -edge kind. `peer-selected` binds the failed parent/Viewer pair; -`host-sfu-ingress` binds the room, host session, SFU publication generation, and -the old/new connection identities. The Host client applies that grant only to a -relay-only LiveKit publisher `RTCConfiguration`, including during the initial -pending prepare. Pinned LiveKit client 2.22.0 accepts `rtcConfig` on -`Room.connect`; its engine clones that override before creating the publisher -PeerConnection and does not replace explicitly supplied ICE servers. This path -is configured in production, and its active Host-ingress relay function is now -proven; the initial-ingress and `peer-selected` variants remain open. -LiveKit participant-wide embedded/external TURN remains a separate ICE domain. - -On 2026-08-21 the standalone `gate:turn-udp-allocation` check issued a credential -through the production parser/issuer and received one UDP relay candidate. That -check proves UDP allocation only, not media, recovery, quality, or performance. -One subsequent bounded Chrome 151 canary ran the exact deployed Web source in a local -Screener room against the production LiveKit/coturn tuple. A temporary DEV-only -hook forced the active SFU publisher to fail once and was removed afterward. The -application then issued `host-sfu-ingress`, connected a relay-policy publisher -whose only ICE server was TURN, selected a succeeded/nominated pair with -`relayProtocol=udp`, sent video bytes and frames, and kept Viewer decode/render -counters advancing. Ordinary peer PCs remained STUN-only, Host outbound media -edges peaked at one, and explicit stop returned them to zero. This proves the -active functional transition only; it is not latency, quality, capacity, expiry, -mobile, or resource evidence. Chrome reported `candidateType=prflx` for the same -local candidate; the standards-bounded interpretation and raw-counter summary -are retained in the linked research rather than normalized away. - -### Deferred Optimization And Deployment Boundary - -Current deployment status is explicit: - -- A room in which the Host and every possible root are restricted may require - several server-fed exceptional edges. Any such extension requires a per-room - selected-relay and central-egress admission cap; it must wait or fail at that - cap rather than become unbounded server fanout. -- Bounded healthy SFU-to-peer reselection is accepted below. The one-root slice - and its existing baseline parent are deployed in exact `6ccb516a`; browser, - media, and production-route evidence remain unverified. Relay-capacity - transition `0 -> 1` and multi-root healthy reselection are not implemented. - -#### Bounded Healthy SFU Reselection - -Production release `6634cb9` was observed on 2026-08-21 to retain an SFU route -after network recovery or a short page refresh. A same-tab refresh retains the -room-scoped `clientId`, while the five-second disconnect grace retains peer and -route identity, so reauthentication legitimately receives the sticky SFU -assignment. Grace expiry removes that root and lets a later join start from the -peer baseline. - -Healthy reselection is event-driven and room-bounded. It must not poll -`navigator.connection`, run a periodic route score, or globally rebalance a -healthy room. Only these discrete events open one opportunity: - -1. An active SFU root receives an authoritative SFU assignment on a new - signaling session or reports LiveKit recovery, followed by two bounded stats - windows with positive current-generation RTP and decoded-frame progress. -2. A connected Viewer advertises relay capacity transitioning from zero to one. - -One room owns at most one soft peer migration attempt. A healthy or quality -probe and a negotiating `peer-selected` lease are serialized. An answered exact -edge lease is active transport and may coexist with an unrelated soft migration; -each route change first revalidates its share, endpoint sessions, topology and -parent downstream budget, then sends the carry grant before active route -authority. A soft probe may overlap active media only when `Host peer children + -active SFU publication + probe <= 2` and `browser relay children + probe <= 1`. -Without a free slot the opportunity is consumed and the active route is left -unchanged. The probe is bound to the current share/publication, active and -pending revisions, root, parent and Host sessions, and one pinned connection -ID. Signaling from any other pair or generation fails closed. - -During prepare the Viewer keeps rendering SFU while the existing peer parent -creates one provisional edge. ICE `connected` is insufficient. The Viewer -requires a live video track, positive inbound RTP and positive decoded frames, -then revalidates all of that immediately before promotion. The controller -commits that root/subtree atomically and retires SFU only after proof. A probe -failure sent before commit aborts without adding failed-parent state; if the -proven edge fails after server commit, the existing active-edge recovery path -runs rather than leaving server and Viewer on different routes. - -Timeout, stale generation, participant/session replacement, room stop or -deletion aborts the provisional route. Abort keeps the active SFU assignment -and descendants, deletes the provisional connection identity, and does not -consume an ordinary route-failure intent. The Viewer disposes the provisional -peer connection and buffered signals before accepting rollback. Active-SFU loss -first aborts the probe instead of being swallowed. If the active Viewer -subscriber or Host publisher reports a real pending-revision `prepare` -failure, only the exact current SFU owner may abort. Ordinary transport uses a -null connection; selected Host ingress must match its activated connection -identity. The rollback active revision then drives the existing one-refresh -recovery. Success and abort share the room's 30-second migration cooldown. -If one otherwise-current healthy event arrives during that cooldown, the room -retains only the latest root/session/revision/share/publication tuple. At the -cooldown boundary it reasserts the same authoritative SFU assignment only when -that tuple and the one-root topology are still current. The Viewer must then -produce two new positive RTP/decoded-frame windows before sending another -ready event; the pre-cooldown proof never authorizes a delayed peer probe. - -The implemented source slice deliberately handles exactly one active SFU root -and only its parent retained in the deterministic peer baseline. The accepted -capacity `0 -> 1` trigger and deterministic selection among multiple SFU roots -remain follow-ups. No periodic polling trigger, global score, new dependency, -ordinary-peer TURN grant, or second route controller was added. The single -cooldown-bound timer above is an event continuation and is cleared on session, -room-stop, or room-delete changes; a changed route or generation discards it at -the boundary. - -The bounded cost model and privacy-safe ICE fields -live in [Low-Server-Cost Media Routes](../research/low-server-media-routes.md). -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 -historical shared-IP room-`1` smoke while the old release remains a separate -rollback instance. The same direct/peer UDP, SFU/UDP, selected relay edge, and -bounded failure gates apply to every room in the flagship process; 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. Every ordinary connection stays STUN-only; room -scope does not change that rule. - -A peer root re-publishing its received stream to the SFU while also feeding -peer descendants remains a separate bounded candidate. The current browser -path would decode and re-encode, and it adds publication ownership and load; -it is not silently added to this migration. No composite score, continuous -optimizer, or geographic/UA inference is introduced. - -## Security And Privacy - -The standby URL is an origin, not a credential. LiveKit credentials remain -short-lived, room-bound, role-bound, and memory-only. -Only the host may publish screen tracks. Subscription permissions allow only -the current fallback-root identities, including any zero-descendant roots. -Ordinary SFU WebRTC transport encryption terminates at the SFU, so its operator -can access media. LiveKit supports application E2EE in which its server cannot -access media content, but signaling/API data remains visible and Screener has -not implemented the required key distribution. The comparison must record which -boundary is actually configured and the UI must not claim E2EE. - -## Implementation Order - -1. Reuse only the LiveKit dependencies, token issuance, deployment templates, - and thin publisher/subscriber boundary developed in closed PR #12. Do not - restore its mutually exclusive protocol or page branches. -2. Split publisher lifecycle into connect, activate, deactivate, and profile - update; make subscriber connections selective and inactive until assigned. -3. Add versioned route state and pure invariant/property tests. -4. Implement server prepare/commit/abort and strict authorization. -5. Integrate host/viewer first-frame switching while retaining relay children. -6. Land the bounded selected-edge function before treating performance evidence - as a prerequisite. Retain `gate:peer-topology-loopback` for decoded-media, - route-consistency, and two-edge correctness only. Collect peer/SFU UDP, - selected-edge, bounded UDP-blocked failure, resource, bandwidth, egress, and - recovery performance on representative target devices or production-like - networks. Only after those pass, add the 20-viewer gate before changing the - room default. - -## Acceptance Gates - -- Across prepare, commit, rollback, reconnect, reauthentication, reconciliation, - and stale-message sequences, Host physical media edges stay at or below - `min(config, 2)` and ordinary Browser Viewer edges stay at or below - `min(config, 1)`. Host SFU publication and provisional/selected overlays count; - upstream receive does not. Config and client advertisements cannot raise the - release limits. -- Only necessary roots or explicitly admitted exceptional viewers receive SFU - media and the configured central egress budget is never exceeded. -- A reliable SFU root continues to serve bounded peer descendants. Normal - central fanout stays at one or two roots; only multiple exceptional viewers - that cannot attach to a healthy root may consume separately capped direct - server edges. -- Prepare failure leaves the old active route unchanged. -- First new decodable picture arrives within one second after a route failure is - detected in the reference regional network. Localhost loopback timing is not - a pass/fail substitute for this target. -- Existing descendants, source selection, quality profile, pause, audio, and - persistent-room stop/restart semantics survive the transition. -- No LiveKit configuration preserves the existing P2P/peer behavior and wire, - but is not the final flagship deployment target. -- In a process with the controller flag disabled, ordinary rooms preserve the - ordinary P2P route fields, directed signaling, quality rejection, - stop/reconnect/delete semantics, and STUN-only ICE. With the flag enabled, - every room gets isolated per-room peer/SFU state under the same limits. Native - v2 and every ordinary peer PC 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 does not fall back to TCP. - TURN availability never becomes a quality claim. +Screener serves one broadcaster and a small group of trusted viewers. Direct +browser WebRTC gives the desired latency and avoids central media cost, but a +single Host cannot safely fan out to every Viewer and some networks cannot +establish a usable peer path. + +The product therefore needs one automatic route controller. Users do not choose +or understand the topology. The controller may use peers and bounded +server-assisted resources, but it must keep the ordinary media graph +distributed, deterministic, authorized, and recoverable. + +This ADR records the accepted invariants and the assisted-route boundary that +still needs one holistic decision. + +## Decision + +### Endpoint capacity + +Every non-server endpoint uses the same server-authoritative downstream media +capacity: + +- the deployment default is `2`; +- the only accepted static values are `1`, `2`, and `3`; +- an upstream receive edge does not consume this downstream budget; +- role, browser, user agent, device class, and page visibility do not create a + different release tier; +- a client advertisement may reduce its usable capacity but cannot exceed the + deployment value; and +- one named configuration value and one shared implementation boundary own the + policy. Route code must not repeat literal policy numbers. + +This capacity applies to ordinary downstream endpoint edges. It does not by +itself define how SFU publication, SFU subscription, TURN transport, server +egress, or temporary migration overlap are accounted. + +### Active topology + +The server owns one versioned route assignment per room. At every committed +revision: + +- the Host has no upstream; +- each Viewer has exactly zero or one active upstream; +- the active media graph is acyclic; +- every ordinary parent stays within the authoritative endpoint capacity; +- healthy unaffected branches remain sticky; and +- admission or recovery ends in a valid assignment, an explicit wait, or a + bounded failure. + +Direct or peer UDP remains the first media choice. Ordinary peer +`RTCPeerConnection` instances receive STUN candidates only; TURN candidates are +not distributed to ordinary peer edges by default. HTTPS and WSS continue to +use TLS/TCP independently of media transport. + +Routing is event-driven. Join, capacity release, endpoint departure, current-edge +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. + +### Authorization and transition + +Every prepare, signal, recovery, commit, and rollback is bound to the exact +room, share generation, endpoint sessions, route revision, assignment +generation, connection identity, and publication generation when one exists. +A stale or mismatched asynchronous result fails closed and cannot revive an old +edge. + +A route replacement uses make-before-break only when the still-to-be-defined +resource model admits the overlap. The old route remains authoritative until +the new route proves current-generation media with a live track, positive RTP +progress, and decoded-frame progress. ICE `connected` alone is insufficient. +Commit promotes that exact candidate atomically; timeout, failure, stale +identity, or revoked authority destroys it and keeps or restores the previous +valid route. + +Recovery attempts are finite, generation-bound, and stop after success. The +controller must not turn a rare failure into an unbounded retry loop, an +unbounded server fanout, or a hidden third route. + +## Pending Assisted-Route Decision + +The following items must be designed together before dependent implementation +or deployment: + +- who publishes to an SFU and from which topology level; +- which endpoints subscribe and whether one publication can serve roots or + network-restricted leaves at different levels; +- where selected TURN is permitted and whether it transports a peer edge, an + SFU ingress, or another explicitly authorized edge; +- how endpoint capacity, server ingress/egress admission, SFU resources, TURN + resources, and provisional overlap are counted; +- the exact fallback and recovery sequence when the Host, a relay parent, or a + leaf is network-restricted; and +- how relay downgrade, sibling migration, and make-before-break remain bounded + for static capacities `1`, `2`, and `3`. + +SFU and TURN are bounded fallback resources, not authorization for an always-SFU +room or unbounded central fanout. Conversely, ordinary endpoint child count +must not be copied onto a server as an implicit server cap. The accepted model +must state both endpoint and deployment resource bounds explicitly and justify +them with measurements. + +## Current Production Divergence + +Production release `9461e20` predates this revision. Its exact behavior is owned +by the deployment document. Generation guards, scoped authorization, +media-proven transitions, and bounded recovery remain reusable only where they +satisfy this ADR. + +That release remains a deployment fact and rollback reference. It must not be +merged wholesale into a new route implementation. After the pending model is +accepted, retained code and tests are transplanted from current `main` by +invariant; obsolete policy is replaced at its owner. + +## Acceptance Boundary + +Before a revised controller ships: + +- property tests cover capacity `1`, `2`, and `3`, one active upstream, + acyclic assignments, stale generations, duplicate signals, departure, + rollback, and admission exhaustion; +- route changes preserve unaffected branches and never commit before media + proof; +- real-browser tests cover direct peer media, peer relay, server-assisted media, + selected transport when configured, failure, and recovery; +- measured endpoint upload and server ingress/egress prove the accepted + accounting under normal and migration overlap; and +- every exhausted path reaches a clear bounded wait or failure without leaking + credentials, candidates, addresses, or raw errors. + +Loopback timing and synthetic signaling tests may validate invariants, but they +do not prove target-network latency, quality, capacity, or interoperability. + +## Security and privacy + +Media authority is room-, role-, session-, share-, revision-, and +connection-bound. Server-assisted credentials are short-lived, scoped, +memory-only, and never placed in URLs, logs, browser persistence, or SQLite. +Ordinary SFU transport terminates DTLS-SRTP at the SFU; the product must not +claim application E2EE unless key distribution and real media evidence exist. ## Consequences 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 and STUN-only; current production - has no TURN media cost, and a configured relay attempt is confined to one - controller-selected exceptional edge after SFU/UDP. -- Revisioned prepare/commit isolates stale asynchronous results without a - continuous optimizer. +- one capacity rule replaces role and browser policy forks; +- route safety is expressed through graph, identity, and media invariants; +- the unresolved server-assisted cases stay visible without being guessed into + runtime behavior. Negative: -- Strict two-edge migration can include a bounded interruption. -- LiveKit becomes an operational dependency for the flagship target and can - inspect ordinary SFU media. -- A configured peer-assisted client downloads/parses the current build's roughly - 137.5 kB gzip (531 kB minified) LiveKit chunk and sends one `HEAD` even if it - never needs SFU, shifting that small one-time client/static-egress cost earlier. - 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. -- 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. +- the deployed controller intentionally diverges until the assisted-route model + is accepted and migrated; +- make-before-break cannot be finalized independently of resource accounting; + and +- real SFU/TURN and target-network evidence is still required. -## Relationship To Existing ADRs +## Relationship to other ADRs -- This decision corrects the automatic-migration interpretation in ADR-0001; - room `1` is historical smoke evidence, while the source controller is - process-enabled for all normal rooms. -- ADR-0004 remains the bounded full-stream browser-relay experiment. -- This decision and merged PR #17 supersede ADR-0003/closed PR #12's - process-wide explicit media mode with default-off automatic hybrid fallback. - ADR-0003 remains historical rejected/superseded context. -- Native shared encoding and two-tree striped distribution remain orthogonal - data-plane experiments under separate decisions. -- ADR-0007 owns per-path quality fallback and its two-representation budget; - this route controller does not aggregate viewer quality into a shared target. +- ADR-0001 owns the original browser P2P baseline. +- ADR-0004 is historical evidence about browser peer relay and re-encoding; it + does not set current capacity or server-assisted policy. +- ADR-0007 owns path-quality evidence and representation behavior. Quality may + affect route eligibility but does not become a second route controller. ## References +- [WebRTC](https://w3c.github.io/webrtc-pc/) +- [WebRTC statistics](https://www.w3.org/TR/webrtc-stats/) +- [WebRTC transports, RFC 8835](https://www.rfc-editor.org/rfc/rfc8835.html) +- [TURN, RFC 8656](https://www.rfc-editor.org/rfc/rfc8656.html) - [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) -- [LiveKit client 2.22.0 `prepareConnection` source](https://github.com/livekit/client-sdk-js/blob/v2.22.0/src/room/Room.ts) -- [LiveKit client 2.22.0 connection options](https://github.com/livekit/client-sdk-js/blob/v2.22.0/src/options.ts) -- [LiveKit client 2.22.0 RTC configuration construction](https://github.com/livekit/client-sdk-js/blob/v2.22.0/src/room/RTCEngine.ts) -- [LiveKit JavaScript client usage](https://github.com/livekit/client-sdk-js#usage) -- [LiveKit end-to-end encryption](https://docs.livekit.io/transport/encryption/) -- [TURN, RFC 8656](https://www.rfc-editor.org/rfc/rfc8656.html) -- [WebRTC transports, RFC 8835](https://www.rfc-editor.org/rfc/rfc8835.html) -- [LiveKit ports and firewall](https://docs.livekit.io/transport/self-hosting/ports-firewall/) -- [LiveKit 1.13.5 configuration](https://github.com/livekit/livekit/blob/v1.13.5/config-sample.yaml) -- [WebRTC statistics](https://www.w3.org/TR/webrtc-stats/) -- [WebRTC](https://w3c.github.io/webrtc-pc/) diff --git a/docs/deployment.md b/docs/deployment.md index de195bb..c6246fc 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -2,13 +2,9 @@ Last verified against upstream documentation: 2026-08-21. -> Truth-audit boundary: this page records the exact `9461e20` deployment and -> its currently accepted configuration syntax. Browser1, fixed root counts, -> Host-publication accounting, room-wide selected-lease limits, and the exact -> fallback order are deployed/source facts under the -> [TODO audit hold](./todo-audit-hold.md), not authority for the next routing -> model. Do not change or redeploy them until that model is accepted in its -> owning truth and merged to canonical `main`. +This page records exact release `9461e20` deployment behavior and syntax. Current +product direction and pending migrations are owned by [project memory](./project-memory.md) +and [the TODO ledger](./todo.md). This section documents the repository's UDP-only deployment candidate: one Node.js process provides the built Web client, room API, and WebSocket signaling diff --git a/docs/maintenance.md b/docs/maintenance.md index cbbfa36..3249fad 100644 --- a/docs/maintenance.md +++ b/docs/maintenance.md @@ -2,123 +2,63 @@ ## 目标 -仓库本身是跨 session、跨设备、跨 agent 的唯一持久事实源。聊天记录可以很长,但任何会影响后续工作的决定都不能只存在于聊天中;同时,常驻上下文不能退化成聊天流水账。 +仓库是跨 session、设备和 agent 的持久事实源。会影响后续工作的结论必须先进入其权威文档;聊天、agent 摘要、旧分支和临时计划都不能替代仓库真相。 -所有维护遵守奥卡姆剃刀:选择能完整满足已验证需求的最简单方案。没有当前消费者、实测瓶颈或重复问题时,不增加抽象、服务、拓扑、hook 或“以后可能用到”的框架。 +常驻上下文只保留每次工作都需要的边界和导航。细节按需读取,完成历史交给 Git。选择满足已验证需求的最简单方案,不为假设性规模、没有消费者的能力或一次性过程增加常驻规则。 + +“东坡肉规则”适用于所有 current truth:纠正一个方案后,当前文档、代码、UI、配置和 PR 文案只描述现行结论,以及仍约束该结论的理由。被否定方案和“为什么删掉它”的解释一并移除,由 Git 历史保存。 ## 事实分层 -| 内容 | 文件 | 维护方式 | +| 内容 | 权威归属 | 维护方式 | | --- | --- | --- | -| 每次都必须遵守的规则 | `AGENTS.md` | 保持短、明确、无冲突,最多 200 行 | -| 当前产品事实与长期决定 | `docs/project-memory.md` | 就地更新,最多 200 行且不超过 16 KB | -| 当前生产、活跃里程碑、阻塞项及明细索引 | `docs/status.md` | 只保留 current/index,最多 120 行且不超过 12 KB | -| 跨模块的当前验证证据与待验证边界 | `docs/verification-status.md` | 按需读取、就地更新;不是完成记录或发布流水账 | -| 当前可验收需求 | `docs/需求理解.md` | 修改现行规范;变更历史交给 Git | -| 架构取舍与原因 | `docs/adr/` | 状态化管理;新决定 supersede 旧 ADR | -| 可复核的证据、保留候选与验收门槛 | `docs/research/` | 标注日期、来源和适用边界,过时结论要更新或标记 | -| 已完成工作的时间线 | commit、PR、issue | 不复制进常驻 memory | +| 每次都必须遵守的硬约束和导航 | `AGENTS.md` | 保持短、单义、稳定;不复制流程手册或临时状态 | +| 分支、研究、验证、PR、合并和清理流程 | `CONTRIBUTING.md` | 维护可执行的日常流程 | +| 当前产品事实和长期决定 | `docs/project-memory.md` | 就地更新,最多 200 行且不超过 16 KB | +| 当前生产、里程碑、阻塞项和明细索引 | `docs/status.md` | 只保留 current/index,最多 120 行且不超过 12 KB | +| 当前未决、已授权或待用户决策的工作 | `docs/todo.md` | 不是完成记录;不把 checkbox、实验或 agent 建议自动升级为 TODO | +| 当前需求与设计 | `docs/需求理解.md`、`docs/方案设计.md` | 维护一个现行版本,变更历史交给 Git | +| 架构取舍 | `docs/adr/` | 明确状态;新决定替代冲突旧决定 | +| 可复核证据和研究结论 | `docs/research/` | 标日期、来源、推断、适用边界和许可证 | +| 当前验证与部署边界 | `docs/verification-status.md`、`docs/deployment.md` | 按需读取,不保存发布流水账 | +| 已完成工作的时间线 | commit、PR、issue | 不复制进常驻 memory 或 status | -每个事实只设一个权威归属,顶层文件只索引,不复制深层正文。一次检查点的最小真相集只包含后续执行无法安全重建的内容:已接受且仍生效的要求/约束、当前实现与证据边界、下一项可执行工作、真实阻塞或未决项,以及需要继续工作的分支/PR。能从代码、测试、Git 或权威资料廉价恢复的细节不进入常驻快照。 +每个事实只有一个 owner。顶层文件提供链接和当前结论,不复制深层正文;`docs/README.md` 维护完整文档索引。 -## 长 Session 检查点 +## 顶级指令卫生 -在以下时机更新仓库快照: +[官方调研](./research/agent-context-governance.md)的共同结论是:顶层指令应是短而稳定的地图,流程和专业细节按需加载,并定期删除冲突或陈旧规则。因此本仓库的 `AGENTS.md` 目标是约 50 行,而不是把任何公开上限当作填充目标。模块特有规则只有在对应代码存在且每次读取确有必要时,才放到最近目录的 `AGENTS.md`;流程说明进入 `CONTRIBUTING.md`,当前工作进入 `docs/todo.md`,证据与理由进入其 owner。 -- 用户确认或改变产品范围、优先级、约束或工作习惯后,在依赖该结论的设计或实现继续前; -- 一个重要设计、调研结论或验收门槛被接受、推翻或被实测结果修正后; -- 从调研、设计、实现、验证中的一个实质阶段进入下一阶段时; -- 切换分支、提交/更新 PR、agent 交接或任务交接前; -- 长任务可能自动压缩、即将暂停或离开当前 session 前。 +## 真相更新顺序 -检查点应立即修改事实的权威归属文件;只有当前全局快照确实改变时才同步修改 `project-memory.md` 或 `status.md`。不要等到 session 末尾才补写,也不要为每个文件、函数、工具调用或未形成结论的探索建立检查点。 +1. 把讨论、纠正、示例、review 建议、实验或 agent 发现视为输入,先复述目标和源头理由。 +2. 对照完整产品模型、当前代码、测试和证据,指出冲突、不必要复杂度和仍未确定的语义。 +3. 就地更新受影响的 requirement、design、ADR 或 research owner;仅在快照确实变化时同步 memory/status,未决执行项进入 TODO。 +4. 删除冲突旧文本和被否定方案的残留,检查各 current 文档内部一致。 +5. 形成 Git-tracked truth checkpoint,再开始依赖该结论的实现或派发。 -讨论、纠正、示例、review 建议、实验结论、checkbox 或 agent 推导都只是输入,不能直接触发实现。先复述目标和源头理由,放回完整产品模型检查矛盾与不必要复杂度,再更新所有受影响的 owning requirement/design/ADR/research 以及确实变化的 memory/status/global rule,并形成 Git-tracked checkpoint。只有 nonvolatile truth 自洽后才能开始实现或派发实现;语义未决时先建立 truth hold,只冻结依赖项。 +语义仍未决定时,在 TODO 记录 hold,只冻结依赖项。接受的真相先进入 canonical `main`;保留候选再从该精确提交 rebase 或重建一次,只移植获批的 scoped code、tests 和 new facts。旧分支的 truth 文档不得覆盖当前 owner。具体 Git 和 worktree 操作遵循 `CONTRIBUTING.md`。 -集成必须先让 accepted truth checkpoint 进入 main,再逐个把保留候选基于该 main 做一次 rebase 或重建。冲突以 main 的 owning truth 为底,只移植已批准的 scoped code/tests/new facts;旧分支中的 `AGENTS.md`、requirements、ADR、memory、status、deployment snapshot 不得覆盖新真相。语义裁决和集成完成后才进入 worktree/branch 清理,并继续执行 PR 等价、开放引用和 link/reparse 安全检查。 +## 检查点与恢复 -审计或集成阶段结束后,先显式保全仍需保留的用户修改,再让 canonical repository root 回到 clean、最新的 `main`;辅助 worktree 不得长期占用 `main`。后续每个实现分支和并行 worktree 都必须从该 canonical `main` 的精确提交创建,不能从旧候选、旧快照或另一个辅助 worktree 继续开枝散叶。只有这一步完成后,才恢复功能实现并行。 +在需求或优先级被接受、设计或研究结论改变、实质阶段切换、分支/PR/agent 交接、可能 compaction 或长暂停前更新事实 owner。不要为每个函数、工具调用或未形成结论的探索建立检查点。 -当真相审计与功能设计同时存在时,严格按以下依赖顺序推进:先整理 TODO 来源和全部 current truth owner,再形成文档归档及 worktree/branch/folder 处置排期;随后合入审计护栏、恢复 canonical `main` 并只执行已批准的工作区清理;然后才从该 main 做整体设计建模并把它保持为 held proposal。并行化只能加速当前阶段,不能绕过前置真相或提前执行功能实现、候选合并或部署。 +检查点只保存以后仍需要的结论、当前状态、下一步和真实阻塞。不要保存原始对话、完整日志、工具输出、临时路径、重复代码结构、可廉价重查的事实或过程性 TODO 流水账。 -完成 canonical root/workspace 整理和整体设计 held proposal 后,必须先向用户报告唯一 `main` SHA、根目录状态、保留/清理项、设计模型和拟恢复 TODO,等待明确核对。核对前不得接受最终争议语义,也不得为“剩余 TODO”新建或恢复实现 worktree;核对后先更新 accepted owning truth,所有高置信并行 worktree 再只从该 exact canonical `main` 创建。 - -检查点只保存以后仍需要的结论、当前状态、下一步和阻塞项。不要保存原始对话、完整日志、工具输出、临时文件路径、重复的代码结构、可随时重新搜索的常识、过程性 TODO 流水账或已经在其他规范中存在的内容。 - -恢复上下文时按以下顺序读取:`AGENTS.md`、`docs/project-memory.md`、`docs/status.md`、相关需求/ADR/调研,然后检查当前分支、`git status`、最近 commit 和 PR 状态。任何摘要与仓库冲突时,以当前代码、测试和正式文档为准。 +恢复上下文时依次读取 `AGENTS.md`、project memory、status、TODO、相关需求/设计/ADR/研究,然后检查当前分支、`git status`、近期 commit 和 PR。摘要与仓库冲突时,以当前代码、测试和权威文档为准。 ## 清理规则 - 新事实与旧事实冲突时,在同一变更中替换或删除旧内容。 -- `project-memory.md` 达到约 80% 上限时,先合并同类条目并删除可从代码或 Git 重新发现的内容。 -- `status.md` 接近上限时,删除已完成或已失效且可由 Git 恢复的内容;仍需保留的细节迁到现有权威文档。只有跨模块的当前验证台账没有自然归属时才进入 `verification-status.md`,主状态留一句结论和链接。不要为了通过门禁反复微压缩句子。 -- `verification-status.md` 只保留仍影响当前验收的证据和边界;专业结论成熟后迁回对应 requirement、ADR、research 或 deployment 文档,过期结果直接删除。 -- 需求文档保持单一现行版本,不在末尾无限追加确认记录。 -- 调研结论失效时更新正文并保留“最后验证日期”;只有仍有历史解释价值的决策才进入 ADR。 -- 已完成的临时计划从 `status.md` 删除,由合并后的 PR 和 commit 保存历史。 -- 每个重要 PR 都检查文档是否互相矛盾、链接是否仍有效、memory/status 是否仍代表当前事实。 +- `project-memory.md` 和 `status.md` 接近预算时,先删除可从代码或 Git 恢复的完成/失效内容,再把仍需保留的细节迁到现有 owner;不要反复微压缩句子。 +- `todo.md` 只保留真实未完工作、明确 hold 和待用户决策,不保留完成清单或一次性审计过程。 +- `verification-status.md` 只保留仍影响验收的跨模块证据;成熟结论迁回 requirement、ADR、research 或 deployment,过期结果删除。 +- 需求和设计不在末尾追加确认流水;研究失效时更新正文及最后验证日期;只有仍有解释价值的历史取舍进入 ADR。 +- 每个重要 PR 检查 current 文档是否互相矛盾、链接是否有效、memory/status/TODO 是否仍代表现在。 -## 验证节奏 +## 验证与自动化 -- 迭代中运行能覆盖当前改动的最小定向检查;同一未变化路径已经通过的昂贵检查不因相邻小改动自动失效。 -- 完整测试套件、真实浏览器矩阵、长时稳定性、网络整形、跨设备和部署验证按一个完整候选或验收阶段批量运行。只有相关媒体路径、协议、配置、依赖、环境或验收门槛改变,或者上次结果失败/含糊时才重跑对应证据。 -- 文档改动默认只运行 Markdown/链接/whitespace/diff 检查;不为与运行时代码无关的改动重跑 npm 全套或媒体基准。 -- 在 `status.md`、调研或 ADR 中记录昂贵验证对应的 commit、环境、结果和适用边界,PR 可以引用该记录,使后续工作能判断证据是否仍有效,而不是机械重跑。 -- 发布、安全边界、数据库迁移和部署切换仍按其风险执行必要的最终门禁;“批量验证”不是跳过会被当前变更影响的检查。 -- `gate:*` 只承载当前验收确实需要的正确性、安全、协议或资源上限;`probe:*` 是可选研究入口,不进入 Web 验收。localhost synthetic loopback 可记录诊断耗时,但不能用该耗时判定产品性能、标定阈值或阻断可逆标准能力。 - -## 阶段验收后的有界架构收敛 - -> `AUDIT-HOLD / HISTORICAL`:本节是 2026-08-19 的延期审查提案,不是当前授权 TODO、当前里程碑或架构不变量。其旧文件规模、owner、百分比假设、`SFU roots`/exceptional-edge/Host-two 表述只能作为 provenance 输入;必须等 [TODO audit hold](./todo-audit-hold.md) 完成、canonical root 回到最新 `main`,并由用户重新接受范围后才可重建或删除本节。 - -这是一项明确延期的收官任务。只有当前主要功能基本完成、相关主线变更合并且阶段验收完成后才可启动;它不阻断或抢占当前的质量证据、自动路由 exact-room canary、native sender 或部署工作。启动时从届时最新 `main` 建立一个短期 `refactor/` 或 `chore/` 分支。当前只记录审查契约,不实施重构。 - -2026-08-19 的一次只读盘点估算约有 14,153 行产品源码和 31,094 行第一方文本,并标出以下高集中度 owner: - -- `src/server/hybrid-media-router.ts`:约 1,605 行; -- `src/client/pages/HostPage.tsx`:约 1,455 行; -- `src/server/signaling.ts`:约 1,057 行; -- `scripts/peer-assisted-benchmark.ts`:约 2,271 行,当前入口是仅验证解码、拓扑与两边上限的 `gate:peer-topology-loopback`。 - -这些总量、分类口径和“产品源码可能净减少 7%-14%”都只是当日待重测的审查假设,不是删除指标、承诺区间或 LOC KPI。文件长度也不单独证明职责错误。正式审查必须先重新读取 `AGENTS.md`、当前 memory/status、需求、相关研究及 [ADR-0004](adr/0004-peer-assisted-media-experiment.md)/[ADR-0005](adr/0005-automatic-hybrid-media-routing.md),重新统计届时 `main`,明确计数口径,并按消费者和测试把代码分为: - -1. 当前生产路径必需; -2. 自动 `direct/peer UDP -> SFU roots -> optional exceptional-edge TURN` 目标必需; -3. 有明确下一阶段消费者和退出条件的实验能力; -4. 已被替代、重复、无消费者或仅服务一次性定位的代码。 - -任何实现开始前,先输出基于证据的候选清单、行为与迁移风险,以及按 owner 说明依据的预计净减少范围;范围可以低于 7%、为零,或证明当前不值得改。评审该输出后再决定是否实施,不能用预设比例反推删除。 - -审查优先减少状态 owner、重复路径和认知负担:先删除已证明无消费者的代码,再合并重复逻辑,只在已有独立职责和消费者时提取窄模块。重点检查: - -- `SignalingServer` 是否能只保留认证、WebSocket 分发与薄协调职责; -- `HybridMediaRouter` 的拓扑计划、SFU grant/lifecycle、intent drain 和广播是否形成可独立验证的自然边界; -- `HostPage`/`ViewerPage` 的纯媒体编排与展示状态能否适度分离,而不建立新的前端架构层; -- peer topology loopback 的一次性探针和重复 CDP 逻辑能否收缩,同时保留可重复的正确性边界; -- 测试 setup/fixtures 能否合并,同时完整保留竞态、鉴权和 host edge 上限覆盖。 - -不要为了缩短文件机械拆分,也不要引入通用框架、工厂、事件总线或更多状态机。Peer/SFU 代码不能因默认关闭就视为多余:若 canary 和 ADR 门槛支持该路线,则保留并整理;若路线被实测否决或明确替代,则删除失败路径,不长期保留无消费者的负担。 - -任何候选改动都必须保持以下行为不变: - -- 用户无感的自动 `direct/peer UDP -> SFU roots -> optional exceptional-edge TURN` 梯级; -- 每条媒体边独立执行 ICE,普通 peer 默认不收到 TURN;启用兼容层时只有授权的异常 edge 可使用短期凭据; -- host 下游活跃媒体边不超过两个; -- 普通桌面或移动浏览器 Viewer 无需安装应用; -- break-before-make、安全授权、重连、回滚和陈旧消息隔离语义; -- 轻量实例可关闭 controller;旗舰部署以 `PEER_ASSISTED_MEDIA=true` 对所有正常房间启用,room `1` 仅是历史 smoke,已不再保留 exact-room allowlist 灰度边界。 - -实现期间只运行覆盖当前改动的窄测;完整候选形成后统一运行一次 full check。除非媒体行为或证据采集逻辑改变,不重跑昂贵浏览器/网络矩阵。实现 PR 必须原位同步 status、project memory 和相关 ADR,删除失效描述,不新增过程流水账文档。 - -## 在线调研规则 - -非平凡的设计、实现和 bug 修复先读仓库,再查当前的一手资料。优先级为规范/官方文档、官方源码与维护者说明、原始论文,然后才是高质量社区复现与运行经验。安全、网络协议、浏览器能力、依赖版本和云服务行为属于易变化信息,必须按当前日期重新验证。 - -调研输出应说明问题、来源日期、已证实事实、推断、候选方案、许可证、适用边界和最终采用理由。不要把搜索结果原样堆进常驻上下文。 - -## 自动化 - -- `.githooks/pre-commit` 和 CI 调用 `scripts/check-project-state.sh`;原生 Windows 可运行等价的 `scripts/check-project-state.ps1`。两份入口共享 `scripts/required-project-paths.txt`,检查 whitespace、清单内必需文件的 Git 跟踪状态和上下文文件上限。 -- `.github/workflows/repository-hygiene.yml` 在 push/PR 上运行 tracked POSIX 入口。 -- hook 只做快速、确定、可复现的检查;需要工程判断的内容留给评审和测试。 -- 新脚本必须有明确当前用途、适用平台、失败信息和调用方。可选研究 probe 不要求进入 CI;没有现实使用者的 hook 或框架不进入仓库。 +- 迭代中运行覆盖当前改动的最小定向检查;完整套件、真实浏览器矩阵、网络整形、跨设备、长时稳定性和部署检查在相关候选的验收边界批量运行。 +- 文档改动默认运行 Markdown、链接、whitespace、diff 和 repository-hygiene 检查;不因无关文档变化重跑媒体基准。 +- 昂贵证据记录 commit、环境、结果和适用边界。只有相关路径、协议、配置、依赖、环境或门槛改变,或旧证据失败/含糊时才重跑。 +- `.githooks/pre-commit`、`scripts/check-project-state.sh` 和 PowerShell 等价入口共享 `scripts/required-project-paths.txt`。Hook 只做快速、确定、可复现的检查;工程判断留给评审和测试。 diff --git a/docs/project-memory.md b/docs/project-memory.md index f02c3b7..ed4d1d9 100644 --- a/docs/project-memory.md +++ b/docs/project-memory.md @@ -2,79 +2,46 @@ Last updated: 2026-08-22 -## Confirmed Intent +## Current Product Truth -- Build low-latency game sharing for one broadcaster and trusted friends; public or large broadcasts belong on OBS/Twitch-class services. -- Browser first. Viewers use desktop/mobile Web; an optional sender requires a proven browser capability gap. -- Use a small central service for access, rooms, signaling, deterministic topology, STUN, observability, and bounded fallback resources without turning the product into public or always-central streaming. -- Minimize server bandwidth. Ordinary peer ICE is STUN-only, and TURN remains optional selected compatibility transport rather than a topology. The exact SFU publication/subscription model, TURN placement, fallback order, and transitional resource accounting are on [truth-audit hold](./todo-audit-hold.md); earlier root, lease, or edge-count wording is not implementation authority. -- Every non-server endpoint has one server-authoritative downstream capacity, default `2` and configurable as `1`, `2`, or `3`; upstream receive is excluded, Browser role/UA/visibility does not define a separate tier, and a client cannot advertise above the deployment value. Production `9461e20` still implements the disputed Browser1/config-1-or-2 behavior; keep that divergence as a dated implementation fact until the holistic routing model is accepted and migrated. -- 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. Production currently disables SFU Dynacast, publishes `q,h`, and starts each subscriber at a `HIGH` ceiling; the fixed root and Host-publication accounting wrapped around that path are disputed policy, while generation isolation, current authorization, and actual packet/resource evidence remain valid review inputs. -- Proposed SFU quality evacuation and recovery rules remain research inputs, not executable TODOs, until the routing truth audit decides their current consumer and ownership. -- Site access protects room creation, Host publication, and every code-only Viewer entry. A valid room-scoped fragment grant bypasses that site gate; otherwise site access is checked before public-watch or a private room password. Persist neither raw credential; keep accounts, ACLs, users, and session tables out. -- Prefer primary sources, maintained implementations, target-device evidence and sanitized production stats. Ordinary-PC synthetic runs prove only correctness/interoperability; never use them to calibrate performance or block a reversible standard API. Keep accepted truth in Git and replace stale facts in place. -- Migrate client, server, and deployment atomically. After a canary, delete superseded config/wire/parsers/tests; do not retain compatibility layers, dual writes, or a second architecture without a current consumer. Git history owns the old implementation. -- Deploy after narrow tests, independent review, one local full gate, and rollback preflight. Build/test off the 960 MiB production host; prepare audited dist with one bounded production install. Actions remain for a necessary main/release gate only. +- Build private, low-latency game screen sharing for one broadcaster and a small group of trusted friends. Public or large broadcasts belong on OBS/Twitch-class services. +- Web is the current delivery target. Viewers join from desktop or mobile browsers; a packaged or native sender is a later optimization. +- Keep routing automatic and media distributed. Direct/peer UDP is preferred; centralized media is fallback infrastructure, not the default topology. +- Every non-server endpoint uses one server-authoritative ordinary downstream capacity: default `2`, statically configurable as `1`, `2`, or `3`. Upstream receive is free, 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. +- SFU publication/subscription, selected TURN, fallback ordering, temporary overlap, and server-resource accounting must be decided as one route model before routing code changes. No older constant, experiment, or branch is authority for that decision. +- Ordinary peer ICE is STUN-only. Credentials, room secrets, candidate details, and diagnostic data remain private and narrowly scoped. +- Screen audio offers 64/128/256 kbps sender ceilings, defaults to 128, and locks the choice during a share. These are configuration ceilings, not fidelity claims. +- Current UI, presence, route labels, and diagnostics describe observed state only; they do not create route authority. -## Current Recommendation +## Current Source And Production -- Keep desktop Chrome/Edge and the responsive Web viewer as the baseline. Observe Android Chrome and iOS Safari compatibility without assigning a separate relay-capacity class. -- Keep Viewer playback on one persistent, user-started audible media element while hidden. Continuity evidence uses media/connection counters; hidden video composition is presentation-only, and iOS lock-screen, reclamation and background reconnection remain device gates. -- Use direct host P2P for one or two viewers, then the bounded controller for every room when `PEER_ASSISTED_MEDIA=true`. Room `1` is historical smoke, not a runtime gate; resource, quality, recovery, SFU/UDP, bounded-failure, and browser/mobile evidence remain separate. -- Browser relays resend remote `MediaStreamTrack` values and re-encode at each hop; WebRTC does not guarantee a shared encoder across peer connections, so measure the cost. -- Keep experiments bounded: the standard representation sequence is below; native RTP relay, encoded-object striping, and FEC remain separate. -- Web defaults to browser/LiveKit codec negotiation; advanced settings offer Automatic, H.264, or VP8 for the next share while retaining fallback codecs and `backupCodec=false`. A codec choice neither proves hardware encoding nor shared encode. ADR-0006's browser bridge remains no-go. -- Mobile endpoints are Web Viewer-only. Current Android/iOS browsers do not expose Web Host capture; AirPlay/system mirroring is Viewer-local output. -- Keep automatic P2P-first routing, one authoritative upstream per Viewer, exact session/share/revision/generation authorization, bounded failure, and media-proven make-before-break. Capacity, canonical SFU publication/subscription, selected TURN, fallback order, and temporary resource leases must now be reconciled as one holistic model before another routing implementation change. -- 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. -- Screen audio: production Web capture requests speech processing off and ideal stereo. Share advanced settings offer 64/128/256 kbps sender ceilings, default 128, and lock the choice for the active share across P2P, browser relay, and SFU. Peer answers permit stereo up to 256 kbps; SFU keeps `forceStereo`, DTX off, and RED retained. Local Viewer details expose non-uploaded A/V playout, jitter-buffer, and concealment evidence. Target-browser/route proof remains open; voice and Native stay separate. -- Presence reports active upstream for topology; assignment alone never proves media. A Web SFU Viewer becomes connected only after session-bound first media; track/route/session loss clears it. Status/details stay neutral until current P2P/SFU/TURN evidence. Names stay socket/localStorage-only; Native remains outside. -- Do not add scene detection, dynamic-FPS control, or forced AV1 without negotiation, encode, game, CPU/GPU, and sender evidence. -- ADR-0002 grant/public access, the v3 room-password migration, and Web Host display names are deployed. Names/presence remain session-only without account or roster tables. +- Canonical source is the clean `main` branch. New branches and worktrees start from its exact current commit after accepted truth is merged. +- Production runs exact `9461e207af62b4f38f6b7a8aa16f8beb49e0e4ee`, release `9461e20`, wire `screener-v5`; `21d5cd9f7139` is the rollback release. +- Production still enforces Host downstream `2`, ordinary Browser downstream `1`, deployment values `1/2`, a fixed SFU-root limit of `2`, and legacy publication/selected accounting. This is a dated implementation divergence, not current product policy. +- Production has automatic routing, bad-relay corroboration, peer-quality make-before-break with Viewer and Host provisional parents, selected-edge transport, stable route/connection details, 64/128/256 audio choices, access controls, self-check, diagnostic export, and signaling watchdog behavior. +- `screener`, LiveKit, coturn, and nginx are healthy; local and public health return 200 and `screener` has `NRestarts=0`. Deployment detail is owned by [deployment](./deployment.md). +- Chrome loopback, cap2/cap3, short resource, recovery, MBB, and A/V fixtures are bounded evidence only. They do not establish quality, resource limits, or product policy. -## Current Execution Principle +## Current Priority -- Durable truth precedes action: reconcile each discussion or correction against the whole product model, challenge conflicts, update all affected owning truth and current snapshots, and create a Git checkpoint before implementation or implementation sub-agents begin. Unresolved semantics enter a truth hold instead of code. -- While the 2026-08-22 truth audit is open, load [the TODO audit hold](./todo-audit-hold.md) before selecting work. Old checkboxes, branch names, agent suggestions, experiments, and post-boundary follow-ups are inputs, not authorization. -- TODO provenance/current truth, the workspace schedule, the audit-guard merge, canonical-root restoration, and approved D cleanup are complete. The current stop gate is the requested user report; do not start another TODO cleanup, route model, or implementation before that checkpoint is explicitly confirmed. A later holistic route model remains a held proposal until its own user reconciliation and owning-truth merge. +1. Keep this truth set and [the TODO ledger](./todo.md) concise and internally consistent. +2. Report the converged facts and TODO classification to the user and stop for confirmation. +3. After confirmation, produce one holistic routing model covering topology, transport, fallback, capacity, resource admission, and recovery; do not implement it yet. +4. After route-semantics confirmation, update the owning requirement/design/ADR and merge that truth into canonical `main`. +5. Rebuild only approved runtime changes from that exact `main`, validate cap `1/2/3` and route recovery, independently review, then deploy with rollback evidence. -## Current Implementation +## Working Rules -- The repository is one npm package using Node.js 24, React, TypeScript, Vite, native WebRTC, `ws`, Zod, Vitest, and separate coturn. -- Production: exact `9461e207af62b4f38f6b7a8aa16f8beb49e0e4ee`, release `9461e20`, Web wire `screener-v5`; `21d5cd9f7139` is the rollback release. -- Web keeps one SFU stream, clears unavailable video, exposes evidence-backed routes/transports and paused preview, and drops signals to a grace-retained offline current-edge target. Routing is automatic; entry/nickname UI is live; Native remains source-only. -- Production rooms default to eight Viewers and accept an explicit admission limit from one through sixteen. The deployed release is Host2/ordinary Browser Viewer1 and accepts deployment 1/2 only as further tightening, without a UA/visibility split. The unchanged `0/1/2/3` relay-capacity wire is a future envelope, not current authority. -- Relay evidence is opt-in; direct proof, selected-pair addresses, and bounded same-pair STUN-response counters stay local, never in relayed evidence. Production keeps identity-bound remote-evidence detail shells stable, expires each field five seconds after its own last observation, and excludes stale presentations from export. The deployed pre-share self-check uses disposable health/WSS/STUN probes and reports configured SFU as unverified until a real route. Click-only diagnostic JSON uses a fixed privacy allowlist; neither path uploads or persists a report. -- `screener`, LiveKit, coturn, and nginx are active; `screener` has `NRestarts=0`, local/public health are 200, and the endpoint-cap environment is unset so effective capacity is Host2/ordinary Browser Viewer1. SQLite v3 has five rooms, SHA-256 `aef724ae52c4107be8ec8791e72f8dcaa11b0ec849fb432d022e2cfb9cfe0974`, owner/mode `screener:screener`/0600. -- Production keeps ordinary ICE STUN-only, SFU roots <=2, selected-edge UDP TTL 120, and one `peer-selected` lease per room excluding Host ingress. Release `9461e20` deploys the root invariant, `dynacast: false`, Host-parent provisional probing, the authenticated Web signaling-partition watchdog, and stable relayed-detail presentation. Real shaped LiveKit media, BWE, resource, SFU, TURN, and broader performance evidence remain open. Release `21d5cd9f7139` is retained for rollback. Earlier exact-`16f6eab` evidence proves active Host ingress only. -- Production requires independent `SITE_ACCESS_PASSWORD`: its stateless cookie authorizes creation/Host and code-only Viewer attempts; private grants stay direct, while public/private code entry needs site access alone/plus room password. Env/endpoint/cookie/nginx naming migrated atomically; failures remain neutral; there are no accounts/JWT/session rows. Local `gate:access-privacy` proves fragment isolation, site/grant transport-log absence, and raw room-password SQLite absence; room-password WebSocket/log ingress and live/headful evidence remain open. -- Production uses nginx, Node.js 24.19.0, coturn 4.17.2, and LiveKit 1.13.5 on UDP 7882 (TCP fallback off). Ordinary peer ICE receives no TURN; the selected-edge tuple is configured only for the current controller edge, with no credential pre-advertised to ordinary peers. Coturn retains the old authenticated-relay ports. Services are healthy with zero restarts. -- ADR-0005 rejects `PEER_ASSISTED_ROOM_IDS`; `PEER_ASSISTED_MEDIA=true` enables every room and ordinary peers stay STUN-only. Initial or active Host ingress may consume one bound relay-only grant; ready/abort clears it and failure restores peer baseline. Stale room-ID config fails startup. Production and source-only Native use `screener-v5`; Native does not subscribe to Web presence. -- Defaults are balanced with automatic codec and 128 kbps screen audio; clarity/fluid, H.264/VP8, and 64/128/256 kbps audio remain explicit. Codec and audio quality are locked during a share. LiveKit 2.22.0 SFU explicitly disables Dynacast and uses ordered active `q,h`; the deployed gate rejects root3, charges the Host publication edge, and retains a zero-child root/subscription. Production exposes one Host-local current-`h` A+B snapshot and clears it on publisher identity changes. Target-browser field availability, actual media values, shaped BWE/resources, and route observation remain open. -- Chrome 151 local functional evidence includes five-Viewer capacity-two and sixteen-Viewer cap2/cap3 runs. These are retained historical experiments, not release policy. All Viewers decoded, and a ten-second follow-up recorded guarded sender and aggregate Chromium CPU evidence, but resolution/FPS stayed low and the same-machine ordinary-PC runs do not rank caps or close resource, visual-quality, endurance, heterogeneous-network, or 20-viewer gates. +- Discussion and evidence are inputs, not executable TODOs. Accepted semantics are written to their owners before implementation. +- Current documents state the current model directly. Rejected alternatives and removal narratives are deleted; Git history owns them. +- Old branches may contribute scoped code or evidence only after reconciliation. Their memory, requirements, ADRs, status, and deployment snapshots never overwrite newer mainline truth. +- Preserve dirty, unique, open-stack, and evidence worktrees until their disposition is explicitly decided. Workspace counts are sampled on demand rather than stored here. -## Provisional Quality Targets +## Source Map -- Controlled direct path at RTT no more than 40 ms and loss no more than 1%: glass-to-glass p50 no more than 150 ms and p95 no more than 250 ms. -- Regional SFU/UDP roots under the same endpoint conditions: glass-to-glass p95 no more than 350 ms; optional TURN is measured separately. -- First picture within 3 seconds after a watch request, excluding explicit permission time. -- A 60 fps profile must not remain below 50 decoded fps for more than 5 seconds without a visible downgrade or limiting reason. - -These are measurement gates, not performance claims. - -## Open Decisions - -- 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 re-encoding, depth-three/eight-viewer latency, reparenting, silent-partition, and representative resource gates. -- Selected-edge: Host ingress works; initial ingress, `peer-selected`, expiry/failure, mobile/resource/bandwidth gates remain. Coturn bearer lasts to expiry; the app lease ends on edge/session/share/budget loss. Media TCP is out. -- Exact mobile Viewer lifecycle behavior across autoplay, rotation, iOS lock-screen/page reclamation, background reconnection and network changes. -- 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. -- Whether voice chat ever enters scope or Screener stays complementary to an existing voice application. -- Production calibration of reparent thresholds and any adaptive codec policy. Hardware attribution gates claims, not H.264 preference; #28's minimum-of-two rule is not a candidate. - -## Source Of Truth - -- Current phase is in `docs/status.md`; requirements/design, ADRs, research, deployment and maintenance are indexed by `docs/README.md`. +- Current execution: [status](./status.md) +- Executable and held work: [TODO ledger](./todo.md) +- Requirements and design: [requirements](./%E9%9C%80%E6%B1%82%E7%90%86%E8%A7%A3.md), [design](./%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1.md), and [ADRs](./adr/) +- Evidence and limits: [research](./research/) and [verification status](./verification-status.md) +- Operations: [deployment](./deployment.md), [maintenance](./maintenance.md), and [contributing](../CONTRIBUTING.md) diff --git a/docs/research/advanced-peer-distribution.md b/docs/research/advanced-peer-distribution.md index 7c5feae..e603100 100644 --- a/docs/research/advanced-peer-distribution.md +++ b/docs/research/advanced-peer-distribution.md @@ -8,7 +8,9 @@ evidence is unverified; relay-capacity `0 -> 1`, multi-root reselection, and advanced encoded-media routes remain unimplemented candidates -> Truth-audit note: this document remains research evidence, not a backlog. Any current-capacity, SFU-root, TURN, fallback, or accounting assertion is held by the [TODO audit hold](../todo-audit-hold.md) until the holistic route model is accepted. +This document is research evidence, not current architecture or a backlog. See +[ADR-0005](../adr/0005-automatic-hybrid-media-routing.md) for accepted routing +invariants and [the TODO ledger](../todo.md) for pending work. ## Terms @@ -204,9 +206,9 @@ not evidence. `sending`, either report alone, and healthy or ambiguous pairs reset or do not advance the streak. It holds at most one pending/streak state per connected Viewer and one room cooldown, with no timer or weighted score. One confirmed child event stays -edge-local. The two-distinct-child corroboration logic is retained only for a -future accepted Viewer-capacity tier; current ordinary Browser Viewer capacity -one makes that state unreachable in release routing. +edge-local. Corroboration from two distinct current children can affect relay +parent eligibility only when the configured ordinary capacity makes that state +reachable; it does not define the capacity policy. The conservative Viewer C hard predicates are: freeze duration at least half of the one-to-five-second window; positive received-packet delta with zero @@ -218,9 +220,8 @@ required. A healthy or incomplete correlated window, any Viewer/parent session, connection, route revision or parent change, or a gap over five seconds clears the streak. Severe and relative-FPS quality evidence each tries one parent through make-before-break: either a Viewer whose active upstream is peer/SFU or the Host. -Session/share/revision plus one strict effective-capacity slot are required -before breadth-first selection continues; Host accounting includes its active -children, SFU publication, selected overlay, and provisional child. No candidate, probe failure, or timeout keeps +Session/share/revision plus one strict ordinary-capacity reservation are required +before breadth-first selection continues. No candidate, probe failure, or timeout keeps the old edge without SFU, TURN, or an error. A started attempt spends a 30-second room migration budget, so a new public Viewer identity cannot bypass it. Per-edge state clears @@ -262,27 +263,17 @@ standards-derived or claimed optimum values. One correlated pair still attributes a problem only to its current parent-to-child edge generation: remote loss comes from that child and -`qualityLimitationReason` belongs to one outbound stream. Screener therefore -keeps a single event edge-local. In a future accepted capacity-two tier, two independently confirmed current children -within five seconds are instead treated as bounded corroboration to pause that -Viewer parent's relay eligibility for 30 seconds, not as proof of device-wide -quality or a numeric score. The client-advertised capacity remains intact; new -capacity messages cannot override the server-owned pause. Existing children -try deterministic peer reassignment and keep their old edge when none exists; -the parent keeps its upstream and the pause itself never starts SFU or TURN. -Host parents are excluded, while expiry, a new parent session or a new share -restores eligibility. Alternate-parent selection remains breadth-first; -preferring recent healthy candidates remains later. The triggering child now -uses bounded make-before-break; converting the quarantined parent's remaining -children from their existing immediate reassignment is a separate slice. +`qualityLimitationReason` belongs to one outbound stream. Two independently +confirmed current children may provide bounded corroboration for that relay +parent, but do not prove device-wide quality or create a numeric score. Existing +children keep their old edge when no authorized alternate exists. The triggering +child uses media-proven make-before-break, and current Viewer/SFU-upstream and +Host provisional-parent paths preserve the candidate connection identity. -With current Host degree two and Browser Viewer degree one, deterministic -assignment forms at most two bounded chains. A tree for `N` viewers -still has `N` media edges, approximately `N*B` useful upload in aggregate, and -approximately `2B` host upload once both roots are used. Local reparenting does -not reduce either bandwidth quantity. Reordering an already balanced healthy -tree also cannot reduce depth; a move needs a discrete admission, path, TURN, -relay-resource, recovery, or future native-capacity benefit. +For `N` Viewers a peer tree still has `N` media edges and approximately `N*B` +useful upload in aggregate. Local reparenting redistributes that traffic; it does +not eliminate it. Reordering a healthy tree therefore needs a discrete current +benefit rather than a continuous optimizer. The source admission-rescue slice is limited to its discrete capacity case. If a relay-capable viewer is unassigned because two zero-capacity roots occupy @@ -294,8 +285,8 @@ host -> new relay -> existing leaf host -> other root ``` -This admits the waiting viewer while keeping Host fanout at two and Browser -Viewer fanout at one. The router permits it only on an active peer-only route with no SFU +At the exact release that introduced this rescue, the move kept Host fanout at +two and Browser Viewer fanout at one. The router permits it only on an active peer-only route with no SFU publication or pending prepare, when the connected candidate currently has no upstream or children, offers relay capacity, and has no failed-parent history. The host must have exactly two children and the chosen child must be a @@ -321,10 +312,9 @@ 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. -Use make-before-break only when the new parent has a free downstream media slot -and every affected endpoint remains within its current release limit: Host at -most two, ordinary Browser Viewer at most one. A future native role requires its -own accepted limit. Admission rescue +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 +that slot until commit or rollback. Admission rescue starts with both host slots occupied, so it is break-before-make: retire the chosen host-to-leaf media edge before activating host-to-new-relay media. A control-only `RTCPeerConnection` may prewarm ICE but cannot prove media uplink: @@ -332,8 +322,8 @@ WebRTC exposes `availableOutgoingBitrate` only after congestion-controlled RTP has used that candidate pair. Zero interruption, active standby media, and a hard endpoint edge limit cannot all be guaranteed at once. -Proceed only after the current evidence-alignment and peer canary gates pass. -Every sampled instant must retain host/relay fanout limits; unaffected branches +Any calibration study must retain configured fanout limits at every sampled +instant; unaffected branches must not freeze; make-before-break must decode at every affected receiver on the new path before retiring the old edge, while break-before-make must do so within one second p95 after the break and before declaring the move successful. Stable diff --git a/docs/research/agent-context-governance.md b/docs/research/agent-context-governance.md index 57d954b..d01ffa6 100644 --- a/docs/research/agent-context-governance.md +++ b/docs/research/agent-context-governance.md @@ -1,60 +1,66 @@ -# Agent 上下文与仓库治理调研 +# Agent Context And Repository Governance -- 调研日期:2026-08-18 -- 最后复核:2026-08-19 -- 范围:Codex、Claude Code、Hermes Agent 与 GitHub Flow 的官方资料 -- 目的:让长 session 和多设备开发可恢复,同时限制常驻上下文膨胀与陈旧事实污染 +- Reviewed: 2026-08-22 +- Scope: official Codex, Claude Code, Hermes Agent, and GitHub guidance +- Status: adopted repository-maintenance evidence -## 官方资料中的共同原则 +## Findings ### Codex -Codex 从 Git 根目录向当前工作目录发现 `AGENTS.md`,越接近当前目录的文件优先级越高;默认合并上限为 32 KiB。官方还要求规则保持简洁,并建议长任务在重大里程碑后压缩,而不是每轮压缩。因此仓库级不变量应放在根文件,具体流程按需放进维护文档或相邻目录,且同一规则只表达一次。 +OpenAI recommends giving Codex a map rather than a large instruction manual. +Its own agent-first repository keeps a short root `AGENTS.md` that points to a +structured documentation tree. OpenAI reports that giant instruction files +crowd out task context, become hard to verify, and accumulate stale rules. Codex +works best when the repository also provides reliable tests, clear documentation, +and project-specific navigation and commands. -来源: +Sources: -- [Codex AGENTS.md 官方说明](https://learn.chatgpt.com/docs/agent-configuration/agents-md) -- [Codex 项目配置官方说明](https://learn.chatgpt.com/docs/config-file/config-basic) -- [OpenAI 模型指南:精简提示与里程碑压缩](https://developers.openai.com/api/docs/guides/latest-model) +- [Harness engineering: leveraging Codex in an agent-first world](https://openai.com/index/harness-engineering/) +- [Introducing Codex](https://openai.com/index/introducing-codex/) ### Claude Code -Claude Code 官方建议项目指令具体、简洁、结构化,单个 `CLAUDE.md` 目标控制在 200 行以内,并定期清理冲突或过时指令。根 `CLAUDE.md` 和 auto memory 会在压缩后从磁盘重新注入,而仅存在于对话的指令可能丢失;`MEMORY.md` 只把前 200 行或 25 KB 作为常驻索引,并要求临近上限时合并或删除陈旧内容。官方还区分“上下文中的建议”和“确定执行的 hook”:必须在固定时机运行的机器检查应写成 hook,大型读取可以交给独立 subagent,主上下文只接收结论。 +Anthropic recommends specific, concise project instructions. Facts needed for +every session belong in the root memory file; multi-step procedures and +path-specific rules belong in skills or scoped rule files. The official guide +suggests keeping `CLAUDE.md` below 200 lines and periodically removing stale or +conflicting instructions. It also supports importing an existing `AGENTS.md` +instead of maintaining a second copy. -Claude Code 原生读取 `CLAUDE.md`,官方建议已有 `AGENTS.md` 的仓库通过 `@AGENTS.md` 导入,避免复制两份规则。本仓库因此保留一个极小的根 `CLAUDE.md` 作为兼容入口。 - -来源: - -- [Claude Code 项目记忆与 CLAUDE.md](https://code.claude.com/docs/en/memory) -- [Claude Code 上下文窗口与压缩](https://code.claude.com/docs/en/context-window) -- [Claude Code hooks](https://code.claude.com/docs/en/hooks-guide) +Source: [Claude Code memory](https://code.claude.com/docs/en/memory) ### Hermes Agent -Hermes Agent 的内置 memory 采用硬字符上限,并明确要求容量接近上限时先合并或删除旧条目。官方把“始终需要的关键事实”与“需要时再搜索的历史 session”分开;还明确不应保存原始数据、临时上下文、容易重新发现的信息或已经存在于项目指令中的重复内容。其 memory 是 session 启动时的冻结快照,进一步说明跨设备、跨 agent 的项目事实应进入 Git 跟踪的项目文件,而不是机器本地 memory。 +Hermes separates project conventions in `AGENTS.md` from user memory, +personality, and other persistent state. Its project instructions are discovered +progressively, so nested files can own narrower rules without loading every +detail at repository startup. -Hermes Agent 的开发指南强调保持核心窄小、优先扩展已有接口、避免没有当前使用者的 speculative hook,并要求真实路径验证而不只依赖 mock。这些原则适合约束本项目未来的 agent、hook 和脚本增长。 +Source: [Which File Does What?](https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/which-file-does-what.md) -来源: +### GitHub -- [Hermes Agent 持久记忆](https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/memory.md) -- [Hermes Agent 开发指南](https://github.com/NousResearch/hermes-agent/blob/main/AGENTS.md) +Git history and pull requests are the durable record for completed work. Current +documents should not duplicate a release diary or preserve rejected alternatives +solely for history. -### GitHub Flow +Source: [GitHub Flow](https://docs.github.com/en/get-started/using-github/github-flow) -GitHub 官方流程是短分支、隔离的完整 commit、push 远端、PR 评审与检查、merge 后删除分支。远端 push 同时提供跨设备备份;PR 和 commit 历史比在 memory 中复制时间线更适合保存完成记录。 +## Adopted Shape -来源:[GitHub Flow](https://docs.github.com/en/get-started/using-github/github-flow) +- Root `AGENTS.md` targets roughly 50 lines and contains only cross-task hard + constraints, product contract, truth navigation, and essential safety rules. +- `CONTRIBUTING.md` owns branch, research, validation, PR, merge, and cleanup + procedures. +- Project memory and status are short current snapshots; `docs/todo.md` is the + only current work ledger; requirements, design, ADRs, and research own detail. +- Rejected alternatives and explanations of their removal leave current truth. + Git owns that history. +- Hooks verify required tracked files, links, size budgets, and deterministic + repository hygiene. They do not replace semantic review. -## 本仓库采用的设计 - -- 根 `AGENTS.md` 最多 200 行,只放跨任务不变量;`.codex/config.toml` 将指令发现预算固定为 32 KiB。 -- `CLAUDE.md` 只导入 `AGENTS.md`,不维护第二份规则。 -- `project-memory.md` 是最多 16 KB 的当前事实快照;`status.md` 是最多 12 KB 的 current/index,只保留生产、活跃里程碑、阻塞和明细链接。跨模块且没有自然归属的当前验证证据按需进入 `verification-status.md`;三者都就地更新,不追加对话、过程 TODO 或完成时间线。 -- 上下文按层归属:`AGENTS.md` 保存顶级约束与读取触发器;memory/status 保存当前最小快照;现行需求/设计和 ADR 保存已接受约束;research 保存证据、候选与门槛;Git/PR 保存时间线。顶层只索引深层事实,不复制正文。 -- 已接受的需求、设计变化、调研结论、可执行 TODO、阻塞和状态必须在依赖工作继续前落到对应 Git 跟踪文件。阶段边界、分支/PR/agent 交接和压缩风险是自动检查点;恢复后先重读快照和 Git 状态。 -- 非平凡变更先做一手资料调研,耐久结论才进入 `docs/research/`;大量原始资料不进入常驻 memory。 -- 快速确定的约束由 tracked Git hook 和 CI 执行,判断型检查留给测试和评审。 -- 每个重大 PR 都包含一次上下文卫生检查:更新新事实、删除旧事实、确认没有未跟踪项目文件。 - -上述“自动检查点”和分层归属是本仓库根据官方机制做出的工程约束,不是这些工具自动提供的跨工具同步能力。来源均于 2026-08-19 访问;Hermes Agent 来源为其官方仓库,Claude Code 与 Codex/OpenAI 来源为各自官方文档。 +This structure is intentionally below the published size ceilings. A ceiling is +not a target: additional top-level text must be necessary on every task or move +to its closest owner. diff --git a/docs/research/built-in-peer-ice-turn.md b/docs/research/built-in-peer-ice-turn.md index 180fbca..0e49753 100644 --- a/docs/research/built-in-peer-ice-turn.md +++ b/docs/research/built-in-peer-ice-turn.md @@ -5,18 +5,14 @@ exact-room canary - Status: rejected after bounded production canary; historical evidence only -> Truth-audit boundary: the rejection of participant-wide TURN and the -> STUN-only ordinary-peer fact remain valid inputs. The exact SFU/TURN order, -> one-lease room policy, Host-ingress exception, and resource accounting below -> describe the deployed candidate or a historical recommendation and are held -> by the [TODO audit](../todo-audit-hold.md); they are not current design -> authority. +The remainder records the tested candidate and exact release behavior. Current +transport design is owned by [ADR-0005](../adr/0005-automatic-hybrid-media-routing.md). ## Decision Do not deploy or continue the built-in participant-wide candidate. Ordinary peer connections remain STUN-only. Exact release `9461e20` implements this -ladder, whose future placement and accounting are now held: +ladder: ```text direct/peer UDP -> SFU/UDP virtual parent -> selected-edge TURN -> failure diff --git a/docs/research/low-server-media-routes.md b/docs/research/low-server-media-routes.md index f6d8f6f..8d133e8 100644 --- a/docs/research/low-server-media-routes.md +++ b/docs/research/low-server-media-routes.md @@ -3,42 +3,22 @@ - Research date: 2026-08-22 - Scope: one broadcaster, explicit admission up to sixteen trusted viewers, low latency, and bounded host media fanout -- Status: ADR-0005 accepts STUN-only direct/peer UDP, bounded SFU/UDP roots, - then optional authenticated TURN/UDP for one controller-selected exceptional - edge. Production enables the controller for all rooms and has a configured - selected-edge TURN tuple; ordinary peer connections remain STUN-only. A - post-deploy exact-Web-source canary against the production LiveKit/coturn tuple - passed active Host SFU -> selected TURN/UDP ingress. Initial ingress, - peer-selected last mile, external cohorts, and performance remain unverified. +- Status: research and dated route evidence. Current invariants are in + [ADR-0005](../adr/0005-automatic-hybrid-media-routing.md); unresolved transport + and resource design is in [the TODO ledger](../todo.md). -> Truth-audit note: ordinary STUN-only ICE, automatic P2P-first intent, exact generation authorization, and dated canary facts remain inputs. Fixed root counts, room-wide selected-lease limits, SFU/TURN placement, fallback order, and resource accounting are disputed under the [TODO audit hold](../todo-audit-hold.md) and cannot authorize implementation yet. +## Historical Candidate Route Ladder -## Current Route Ladder +At this dated checkpoint, the candidate plan used direct Host P2P first, then a +bounded browser relay DAG. Browser relays decoded and re-encoded each stream, +while separate native shared-encode and encoded-RTP relay ideas remained +research candidates. A single-node SFU was tested as optional capacity after +peer recovery; its source checkpoint used one or two roots. These measurements +do not define the current topology, fallback order, or capacity policy. -The smallest current plan is: - -1. Use direct host P2P for one or two viewers. -2. For later viewers, use the bounded browser relay DAG in ADR-0004. It is - controlled by `PEER_ASSISTED_MEDIA`, shares the one-through-sixteen room - admission limit, and decodes and re-encodes at every relay. The retained - representative resource/quality gate remains eight viewers. -3. Plan a separate native shared-encode sender regardless of the browser relay - result. It reduces duplicate host encoding while retaining at most two - standard WebRTC edges, so it does not remove their upload cost. The bounded - risk spike is specified in - [Native Shared-Encode Sender](./native-shared-encode-sender.md). -4. Only if browser relay re-encoding is the isolated failure should another - experiment add opt-in native volunteer encoded-RTP relays. -5. Keep a user-operated mini-SFU and a centrally operated single-node SFU as - optional capacity. The ADR-0005 controller uses that capacity - automatically only after deterministic peer recovery is exhausted. The SFU - is a virtual parent for only one or two necessary roots; those roots continue - bounded peer descendants. A necessary viewer may be a zero-descendant root - under that same total only when no reliable relay root exists. Server - capacity transfers fanout bandwidth to its operator; it does not disappear. - -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 +At that checkpoint, any browser-spike failure other than isolated relay +re-encoding closed that browser-relay candidate. Native sender work remained a +separate research track. Closed PR #12's explicit whole-room SFU mode is superseded. ADR-0005 and merged PR #17 own the automatic cross-mode controller. Production first enabled it only for room `1` and later removed that rollout boundary. The deployed release observed @@ -82,26 +62,21 @@ effects. Its roughly 86% result must not be extrapolated to a public network; internet DNS/TLS reuse, RTT/loss, audio, transport fallback, and browser variance still require measurement. -The 2026-08-22 in-process `gate:sfu-root-invariants` closes the corresponding -source-accounting preflight: controller state and LiveKit token allowlists reject -a third normal root; an active Host SFU publication counts as one outbound media -edge in addition to direct children; and the benchmark acceptance evaluator -rejects either three roots or three Host media edges. A committed root with zero -peer descendants remains selected across reauthentication; a separate client -transition check keeps the active subscriber on a newer same-kind empty-child -assignment. This is executable topology and accounting evidence only; without a -LiveKit process, browser RTC stats, and packet counters it does not measure -`B_pub`, root egress, forwarding, or congestion behavior. +At that source checkpoint, the 2026-08-22 in-process +`gate:sfu-root-invariants` proved its fixed-root and Host-publication accounting, +token allowlist, zero-child reauthentication, and subscriber-retention behavior. +It used no LiveKit process, browser RTC stats, or packet counters, so it did not +measure `B_pub`, root egress, forwarding, or congestion behavior and does not +select the current resource model. ## Traffic Conservation And The Impossible Triangle This is an engineering accounting identity, not a named theorem. Let `B` be the measured useful media bitrate for one same-representation branch, `B_pub` -the actual SFU publication bitrate, `B_i` root `i`'s selected bitrate, `N` the -viewer count, `R` the one or two normal roots, `D` their peer descendants, `E` -the separately admitted exceptional server-fed viewers, `B_exc,j` exception -`j`'s selected bitrate, and `B_edge` a descendant edge's actual bitrate. Thus -`N = R + D + E`. For the equal-representation normal-root baseline `E=0`, +the actual SFU publication bitrate, `B_i` server-fed subscriber `i`'s selected +bitrate, `N` the Viewer count, `R` the number of server-fed subscribers, and `D` +their peer descendants. Thus `N = R + D` for this screening model. For the +equal-representation baseline, useful last-hop delivery is approximately `N*B`. Ignoring protocol overhead: `host last-hop copies + peer last-hop copies + server last-hop copies = N * B` @@ -149,15 +124,11 @@ it cannot remove the network copy delivered to each viewer. ## Transport And Topology Layers -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, current Host -fanout at most two, ordinary Browser Viewer/root fanout at most one, maximum -depth, and bounded failure radius still apply. Only the absence of any reliable relay root permits a necessary viewer -to consume one of the same one or two root slots with zero descendants. +TURN is an ICE transport and an SFU is a media service; neither fact alone +defines logical parentage or resource admission. Ordinary peer PCs gather only +STUN candidates. The holistic model must bind any SFU subscription or selected +TURN transport to current route authority while preserving sticky unaffected +subtrees, the configured ordinary endpoint cap, and bounded failure. This distinction follows TURN's allocation/relay role in RFC 8656 and the media topology boundary in RFC 7667. A relay candidate proves transport for one edge; @@ -177,17 +148,15 @@ network changes, but their selected ICE path must be measured. | --- | --- | --- | | Endpoint-independent mapping with UDP | Full ICE can check host, server-reflexive, and peer-reflexive paths | Keep direct/peer UDP first | | One endpoint-dependent mapper | Coordinated checks can sometimes create a peer-reflexive path; success is not guaranteed | Exhaust the existing bounded restart/rebuild/alternate-parent steps | -| Both peer endpoints use endpoint-dependent mapping | A direct peer path is not reliable; RFC 8835 requires TURN support for this case | A public SFU/UDP connection can still feed one or two roots when outbound UDP works; an eligible edge may then use selected TURN/UDP | +| 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 | | All outbound UDP is blocked | The UDP media ladder has no reachable candidate | End with a clear bounded failure | | Wi-Fi/cellular or address change | Old mappings and candidate pairs can become invalid | Use a new opaque generation and bounded ICE restart/rebuild, then re-run the same priority ladder | Peer-reflexive discovery records an address only after a connectivity check -succeeds; it does not cross two incompatible mappings by itself. An SFU remains -a virtual parent and TURN remains transport for one selected edge. A restricted -Host may therefore reach the SFU directly or through selected TURN, the SFU may -serve one or two roots, and those roots may still serve peer descendants. If -every endpoint needs a server path, the existing root, exceptional-viewer, and -egress caps must bound it. +succeeds; it does not cross two incompatible mappings by itself. A restricted +endpoint may still reach SFU or TURN infrastructure over outbound UDP. The +holistic route model must decide publication ownership, subscription placement, +selected transport, and resource admission without changing this NAT boundary. Do not infer endpoint-dependent mapping from a `cellular` or `CGNAT` label. A 2023 experiment reached three of four Dutch mobile carriers, but that cohort is @@ -256,8 +225,8 @@ change or explicit reconnect; it does not support periodic probing, a carrier retains one cooldown-time opportunity as a current tuple and requires two new evidence windows at expiry. Later production deployed that bounded continuation and subsequent peer-quality MBB work. Exact current capacity, SFU, TURN, and - accounting semantics are now frozen by the truth-audit hold. None of these - generations polls network state. + accounting semantics remain unaccepted pending the holistic route model. None + of these generations polls network state. - Retained candidate: a discrete network-change or explicit-reconnect event may open one opportunity through that same controller after cooldown. It must not create a second route controller or treat an ICE restart on the SFU @@ -398,14 +367,13 @@ public, `no-store` self-check config returns only the existing STUN-only `iceConfig` and an SFU-configured boolean. It returns no SFU or TURN URL, token, username, credential, address, candidate, or selected-edge grant. -## SFU/UDP And Selected-Edge TURN +## Historical SFU/UDP And Selected-Edge TURN Slice -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. +Every ordinary bounded peer `RTCPeerConnection` used STUN-only ICE. The tested +source attempted bounded peer recovery, then its fixed-root SFU/UDP slice, then +one controller-selected authenticated TURN/UDP rebuild before failure. That +ordering and accounting are historical implementation evidence, not current +route authority. 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 @@ -518,10 +486,10 @@ One bounded exact-room gate owns rollout evidence: 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 - capped exceptional server edges stay bounded, and unaffected peer subtrees - do not migrate. +4. Exercise assisted-route departure, reconnect, service unavailability, and + rollback. Non-server ordinary downstream edges stay within the configured + cap, server allocations stay within accepted resource admission, and + unaffected peer subtrees do not migrate. 5. Correlate only selected candidate type/protocol/relayProtocol and opaque generations; never upload raw SDP, candidate/address/IP, credentials, or device identifiers. @@ -531,12 +499,12 @@ 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. -A peer root simultaneously re-publishing its received stream to the SFU and -serving peer children is only a later bounded experiment. The current browser -relay would decode and re-encode, and publication ownership adds another -failure domain. Do not add it to the controller until measurements prove a -specific consumer. No global score, continuous optimizer, geography, IP, UA, -or self-reported capability chooses these routes. +A peer simultaneously re-publishing its received stream to the SFU and serving +peer children is an unresolved route-model alternative. A browser relay would +decode and re-encode, and publication ownership adds another failure domain; the +holistic model must compare that cost with a single authoritative source +publication. No global score, continuous optimizer, geography, IP, UA, or +self-reported capability chooses these routes. On today's unicast Internet, a design cannot maximize all three of these for more than one viewer: @@ -552,21 +520,21 @@ lost. ## Route Screening -| Route | Where copies are emitted | Endpoint cost | Current disposition | +| Route | Where copies are emitted | Endpoint cost | Evidence status | | --- | --- | --- | --- | -| Direct host P2P | Host emits one copy per viewer | Host upload and sender pipelines grow with viewers | Keep for one or two viewers | +| Direct host P2P | Host emits one copy per Viewer | Host upload and sender pipelines grow with Viewers | Baseline evidence; current capacity is owned by the product contract | | Bounded browser relay DAG | Host and each Web relay emit at most the configured endpoint cap | Ordinary browser, but every relay decodes and re-encodes and adds a hop | Admission defaults to eight and permits 1-16; representative resource/quality gate remains eight viewers | -| Native shared-encode host | Host targets one encode for at most two standard WebRTC edges | libwebrtc public-API proxy risk spike, with Pion as fallback | Planned separate sender phase; still pays per-edge upload | -| Native volunteer encoded-RTP relay | Each volunteer forwards one encoded copy | Native install, RTP/RTCP forwarding, packaging, and opt-in relay policy | Conditional experiment only if relay re-encoding is the sole browser-spike failure | -| SFU virtual parent | SFU normally emits one or two root copies; roots keep peer descendants | Service pays measured root egress; host sends one publication | Accepted primary central fallback after direct/peer UDP; local SFU/UDP and active Host-ingress relay function pass, while public-room/performance evidence remains open | -| Exceptional server-fed viewers | SFU/TURN emits necessary copies that no healthy root can distribute | Additional capped central egress | Explicit compatibility exception only; never unbounded whole-room fanout | +| Native shared-encode host | Host targets one encode for standard WebRTC edges | libwebrtc public-API proxy risk spike, with Pion as fallback | Research evidence; still pays per-edge upload | +| Native volunteer encoded-RTP relay | Each volunteer forwards one encoded copy | Native install, RTP/RTCP forwarding, packaging, and opt-in relay policy | Research only; no current product authorization | +| SFU service | SFU emits authorized subscription copies | Service pays measured egress; an authoritative publisher supplies media | Functional single-root evidence exists; topology and admission remain pending | +| Additional server-assisted paths | SFU/TURN emits authorized copies | Additional central ingress, egress, or allocation cost | Resource-admission input; exact topology remains pending | | SVC plus multiple trees | Peers emit striped layer copies across several trees | Layer scheduling, reassembly, redundancy, and more churn state | Separate conditional spike; target endpoint upload near `B` | | Network coding | Peers or servers emit coded blocks | Generations, buffering, decoding, integrity, and a custom media plane | Trace/FEC spike only; optimize loss recovery, not clean bandwidth | | MoQ | Publishers and MoQ relays emit object copies | New transport, packaging, player, relay, and auth stack | Optional central-fallback benchmark; still pays server egress | | Peer-assisted CDN | Peers cache or upload segments/objects | Discovery, locality, scheduling, incentives, abuse, and privacy systems | Reject for this trusted group | | IP multicast | Multicast routers replicate packets | Requires multicast-enabled hosts and routed networks unavailable to ordinary Internet browsers | Reject outside managed networks | -## Planned Native Sender And Conditional Encoded-RTP Relay +## Native Sender And Encoded-RTP Relay Research Pion exposes the necessary native building blocks: `TrackRemote.ReadRTP()` reads encoded RTP, `TrackLocalStaticRTP.WriteRTP()` writes pre-packetized RTP into @@ -588,15 +556,14 @@ the narrower transport oracle: one Pion v4.2.18 `TrackLocalStaticRTP.WriteRTP` call fans a pre-packetized semantic payload to two independent PeerConnections with binding-specific SSRCs. Because that oracle contains no encoder, browsers, RTCP arbitration, or bandwidth-control -loop, it proves neither one physical encode nor a usable native relay. Its next -gate is browser-to-native-to-two-browser interoperability with bounded -RTCP/PLI, congestion, queues, throughput, and latency. +loop, it proves neither one physical encode nor a usable native relay. The +unresolved evidence boundary is browser-to-native-to-two-browser interoperability +with bounded RTCP/PLI, congestion, queues, throughput, and latency. -The native shared-encode sender is a planned independent phase. Native volunteer -relays remain conditional on measurements isolating relay re-encoding as the -sole browser-spike failure. They must not rescue failures in deterministic -assignment, host fanout, relay loss recovery, ICE/TURN connectivity, mobile -leaves, or end-to-end latency. +The native shared-encode sender and volunteer relay remain research candidates, +not current product work. Any later decision must keep deterministic assignment, +fanout, recovery, connectivity, mobile, and end-to-end latency concerns in their +proper owners. ## Outside The Current Browser-Track Spike diff --git a/docs/research/peer-assisted-media.md b/docs/research/peer-assisted-media.md index 1500fb0..48ee94e 100644 --- a/docs/research/peer-assisted-media.md +++ b/docs/research/peer-assisted-media.md @@ -7,15 +7,15 @@ accepted ADR-0005 owns automatic peer/SFU routing. Production later removed the room-`1` rollout boundary; retained SFU media remains unverified. -> Truth-audit note: dated measurements and exact deployed behavior remain valid within their stated environments. Browser1, fixed SFU-root-two, publication/selected-edge accounting, and future capacity conclusions are disputed inputs under the [TODO audit hold](../todo-audit-hold.md), not current product policy or executable gates. +This document owns dated measurements and implementation evidence. Current +capacity and routing authority live in [ADR-0005](../adr/0005-automatic-hybrid-media-routing.md) +and [project memory](../project-memory.md). ## Conclusion -Peer-assisted forwarding can cap the broadcaster at one or two outgoing media -edges without making an SFU carry every viewer's traffic. The current release -policy allows at most two Host physical media edges and one ordinary Browser -Viewer downstream edge. The earlier capacity-two Browser relay work below is a -historical experiment, not release policy. The smallest credible path uses only +Peer-assisted forwarding can reduce broadcaster fanout without making an SFU +carry every viewer's traffic. The release measurements below are historical +evidence rather than current capacity policy. The smallest tested path uses only standard WebRTC media: 1. the browser host sends its screen stream to at most two first-level viewers; @@ -103,7 +103,7 @@ Sources: - [W3C WebRTC `addTrack` and sender model](https://www.w3.org/TR/webrtc/) (accessed 2026-08-21) - [WebRTC remote-stream forwarding primitives](https://webrtc.org/getting-started/remote-streams) (accessed 2026-08-21) -### Server Authority And Current Release Clamp +### Server Authority And Ordinary Capacity The WebRTC 1.0 sender model associates transmission with an individual `RTCRtpSender`; it does not turn a client-declared fanout value into server @@ -114,17 +114,14 @@ provisional or selected replacement, rather than treating a logical route as one upload. OWASP's server-side input-validation guidance treats client input as untrusted and requires semantic validation in the trusted service. -The resulting Screener policy is an engineering inference: authenticate role -first, then compute effective Host capacity as `min(deployment, 2)` and ordinary -Browser Viewer capacity as `min(deployment, 1)`. A Viewer advertisement of two -or three remains syntactically valid for the unchanged future protocol envelope, -but cannot authorize a second current-release child. Host SFU publication and -active/provisional/selected physical overlays consume Host upload budget; -ordinary upstream receive does not. No UA or visibility classification is used. -The Web client independently clamps received child assignments to Host two or -Viewer one before opening physical peer connections. That client check limits +The resulting Screener policy is an engineering inference: authenticate the +session first, then let the server apply one ordinary downstream capacity to +every non-server endpoint. The default is two and a deployment may choose one, +two, or three. Upstream receive is free; role, UA, visibility, and client +advertisement cannot raise or create another tier. A client-side clamp can limit damage from a server regression, but does not replace authenticated server -authority or promote the protocol envelope into release policy. +authority. Assisted transport and temporary-overlap accounting are separate +routing questions and are not defined by this resource observation. Sources, accessed 2026-08-22: @@ -282,7 +279,7 @@ fanout stayed at two and relay fanout at one. This is control and continuity evidence only, not visual-quality, full-resolution, load, TURN, public-network, or endurance evidence. -A 2026-08-22 current-release rerun at exact source `b77f4eb58944` used Chrome +A 2026-08-22 release-checkpoint rerun at exact source `b77f4eb58944` used Chrome 151.0.7922.138, three Viewers, the Host2/ordinary Browser Viewer1 policy, an eight-second sample window, and the existing hard-departure recovery gate. All three Viewers decoded; Host media fanout peaked at two, Browser relay fanout at @@ -317,14 +314,14 @@ games, mobile resources, public networking, or endurance. The historical loopback runner used cap2 by default; `BENCHMARK_EXPECTED_ENDPOINT_CAP=3` configured only its local server and gate. -The current runner no longer admits cap3 because it verifies release policy. +That source checkpoint later rejected cap3 in its release-policy mode. A 2026-08-22 Chrome 151 two-second smoke passed with three Viewers at Host2/relay1 and six at Host3/relay3, with all Viewers decoding. This was an experimental control-path result, not a release-policy, production-default, SFU-capacity, or resource claim. -The following cap2/cap3 results are retained historical experiments and must not -be used to lift the Host2/Browser Viewer1 release policy. A 2026-08-22 Windows +The following cap2/cap3 results are retained historical experiments and do not +select product policy. A 2026-08-22 Windows Chrome 151.0.7922.138 headless loopback ran sixteen Viewers for two seconds at 720p30 with cap2 and cap3. Both runs had all sixteen Viewers decoding and no fatal/check failure. Cap2 peaked at Host2/relay2 with @@ -333,8 +330,8 @@ Host3/relay3 with depth three and 977 ms. Final samples were only 320x180 at 9-10 fps. The 8-core/16-thread Ryzen 7 9700X runner had 47.1 GiB RAM and 13.7 GiB free after the runs, but no process CPU, GPU, NIC, or peak-memory totals were captured. This proves the explicit admission, bounded topology, and decode paths -only; resource, visual quality, endurance, heterogeneous networks, and the -separate 20-viewer gate remain open. +only; resource, visual quality, endurance, heterogeneous networks, and larger-room +acceptance remain open. A same-machine 2026-08-22 Chrome 151 follow-up ran sixteen Viewers for ten seconds at 720p30 and introduced report schema v2; sender means are unweighted connected sender-sample means, while encode cost is weighted by guarded interval frame deltas. Cap2 passed at Host2/relay2 with 2 Host and 14 relay senders: Host/relay bitrate was 1,716/1,748 kbps, FPS 11.17/11.20, available outgoing 5,393/5,704 kbps, and encode cost 2.086/2.048 ms per frame. @@ -342,7 +339,7 @@ Cap3 passed at Host3/relay3 with 3 Host and 13 relay senders: the same fields we Both runs observed only 320x180 and 480x270; every Host limitation sample was `bandwidth`, every relay sample was `none`, and maximum first-decode diagnostics were 1,071/1,021 ms for cap2/cap3. [CDP `SystemInfo.getProcessInfo`](https://chromedevtools.github.io/devtools-protocol/tot/SystemInfo/) exposes process type, PID, and cumulative CPU seconds, but no resident-set field; v2 therefore reports peak RSS as `null` and rejects intervals when the exact type/PID set changes or any counter retreats. Cap2 measured 221.4% aggregate Chromium CPU (250.7% peak) over two valid and three rejected intervals; cap3 measured 209.8% (246.9% peak) over four valid and one rejected interval. Multicore totals may exceed 100%. -The different valid coverage and one short run per arm forbid a cap2/cap3 CPU ranking. CDP cannot attribute these all-process totals to an individual Host or relay page, and this ordinary-PC synthetic run does not close the resource, game-quality, endurance, mobile, or 20-viewer gates. +The different valid coverage and one short run per arm forbid a cap2/cap3 CPU ranking. CDP cannot attribute these all-process totals to an individual Host or relay page, and this ordinary-PC synthetic run does not close resource, game-quality, endurance, mobile, or larger-room acceptance. Peer multicast research such as SplitStream demonstrates why load-balanced, failure-tolerant overlays normally introduce multiple trees and content @@ -375,24 +372,21 @@ Sources: ## Resource Model -For observed encoded bitrate `B`, `N` viewers, and the current Host2/Browser -Viewer1 release limits: +For observed encoded bitrate `B`, `N` Viewers, and configured ordinary +downstream capacity `C`: -- host upload is at most approximately `2 * B`, plus transport overhead; -- each Browser Viewer relay upload is at most approximately `B`; +- each non-server parent has at most approximately `C * B` ordinary downstream + upload, plus transport overhead; - aggregate viewer delivery still requires approximately `N * B` across hosts, relays, and any TURN paths; and - signaling/STUN carry negligible media traffic, while a TURN path still incurs relay ingress and egress for that edge. Peer assistance distributes traffic; it does not eliminate it. The product -keeps topology automatic and invisible while enforcing advertised capacity and -the deployment ceiling. -The standalone ADR-0004 spike has no runtime capability flag and relies on -controlled join order. The accepted ADR-0005 controller starts every viewer at -zero, then the current Web client advertises an explicit per-session capacity -of one. The first production smoke scoped it to room `1`; current production no -longer has that room boundary. There is no mobile/iPad, UA, or visibility branch. +keeps topology automatic and invisible while the server enforces the configured +capacity. The standalone ADR-0004 spike relied on controlled join order; the +ADR-0005 controller starts each Viewer at zero and waits for an authenticated +capacity message. There is no mobile/iPad, UA, or visibility capacity branch. ## Selected-Pair Response Observation diff --git a/docs/research/realtime-quality-adaptation.md b/docs/research/realtime-quality-adaptation.md index 498233a..7d82046 100644 --- a/docs/research/realtime-quality-adaptation.md +++ b/docs/research/realtime-quality-adaptation.md @@ -11,13 +11,10 @@ others. `maintain-framerate` may preserve motion by reducing resolution until game UI, maps, subtitles, and text become unreadable; `maintain-resolution` may instead lower frame rate. Neither preference overrides congestion control. -The user accepted `balanced` as the recommended-profile and advanced default. -A previous iteration temporarily returned those defaults to clarity-first after -field feedback, but that overcorrection is superseded: current source uses -`balanced`, while `maintain-resolution` and `maintain-framerate` remain explicit -choices. Independently, the P2P answer-time whole-profile workaround rested on -confounded evidence and is removed below. These preferences still leave actual -degradation to the browser; Screener observes readback and stats. +Current source uses `balanced` as the recommended profile and advanced default; +`maintain-resolution` and `maintain-framerate` remain explicit choices. These +preferences leave actual degradation to the browser, so Screener observes +readback and stats rather than claiming a fixed quality outcome. The inspected Chromium/libwebrtc source chain makes a screen-only shortcut especially unsafe to assume for Screener. The JavaScript `motion` hint reaches @@ -483,13 +480,10 @@ that feature. If built-in selection fails the product gates, test explicit standard subscriber quality selection before manual sender activation/deactivation. -### SFU Root Invariant Preflight +### Historical SFU Source Gate -> Truth-audit boundary: this section records what the 2026-08-22 source gate -> asserted. Its fixed root count and Host-publication edge accounting are under -> the [TODO audit hold](../todo-audit-hold.md), not accepted product invariants. -> Dynacast/layer configuration and the limits of in-process evidence remain -> valid observations. +This section records the exact 2026-08-22 source gate; it is evidence rather +than current routing policy. The 2026-08-22 `gate:sfu-root-invariants` source gate proves that the publisher passes explicit `dynacast: false`, exposes exactly the ordered active `q,h` diff --git a/docs/research/webrtc-p2p-screen-sharing.md b/docs/research/webrtc-p2p-screen-sharing.md index db7804f..db8ba27 100644 --- a/docs/research/webrtc-p2p-screen-sharing.md +++ b/docs/research/webrtc-p2p-screen-sharing.md @@ -6,7 +6,8 @@ - 目标场景:一名玩家向少量熟人私密分享,观看者可用手机/桌面浏览器加入,低延迟,尽量不消耗媒体服务器带宽 - 结论状态:本文记录已部署 PoC 的 P2P/coturn 基线。ADR-0005 与[低服务器成本媒体路由](./low-server-media-routes.md)已取代本文早期“每条 peer edge 必带 TURN”的旗舰建议;生产后续移除了 room `1` 边界,ordinary ICE 仍为 STUN-only,selected-edge TURN 已配置但尚未完成真实媒体验收 -> Truth-audit boundary: direct/peer first、ordinary STUN-only 和 dated production evidence 仍是输入;固定 SFU/TURN 顺序、root/lease 数值和资源计账受 [TODO audit hold](../todo-audit-hold.md) 约束,不是当前实现授权。 +本文是可行性与历史证据,不定义当前路由策略;当前约束见 +[ADR-0005](../adr/0005-automatic-hybrid-media-routing.md) 与 [TODO 台账](../todo.md)。 ## 结论 @@ -20,7 +21,7 @@ | +---- direct/peer UDP -> SFU root -> selected TURN edge -2026-08-18 的历史目标顺序是 direct/peer UDP -> SFU virtual parent -> optional selected-edge TURN -> 明确失败;room `1` 只是首个 bounded production smoke,生产后来移除了 exact-room 边界并对所有房间运行 automatic controller。retained-media 验收仍未完成,未来 SFU/TURN 顺序与计账受 truth-audit hold 约束。 +2026-08-18 的历史目标顺序是 direct/peer UDP -> SFU virtual parent -> optional selected-edge TURN -> 明确失败;room `1` 只是首个 bounded production smoke,生产后来移除了 exact-room 边界并对所有房间运行 automatic controller。retained-media 验收仍未完成,未来 SFU/TURN 顺序与计账尚未接受。 系统必须无感完成拓扑分配、恢复和必要迁移;大规模公开分享仍直接使用现有直播服务。 ``` @@ -119,10 +120,10 @@ IETF 对 mesh/SFU 的拓扑说明见 [RFC 7667](https://www.rfc-editor.org/rfc/r - 生产已移除 exact-room allowlist,所有房间由 bounded peer-assisted/SFU controller 自动路由;ordinary peer 仍使用 STUN-only ICE。 - 当前 PoC 默认允许八名观看者,部署者可配置 1 至 16,超额连接会被明确拒绝。该数值只控制接入,不代表 1:8 已通过性能验收;必须收集可用上行、实际发送码率、`qualityLimitationReason`、编码耗时和发送队列来确定真实可持续人数。 - 每条 ordinary peer 链路独立使用 STUN-only ICE;只有控制器选中的异常 edge 才会获得短期 TURN credential。 -- Exact release `9461e20` 的所有房间在 peer recovery 与 alternate parent 耗尽后可准备最多两个 SFU/UDP roots;这是受 truth-audit hold 约束的已部署事实,不是未来固定 root 数的授权。首次 room `1` smoke 已观察到 participant entry,但 retained media 尚未验收。 +- Exact release `9461e20` 的所有房间在 peer recovery 与 alternate parent 耗尽后可准备最多两个 SFU/UDP roots;这是已部署事实,不是未来固定 root 数的授权。首次 room `1` smoke 已观察到 participant entry,但 retained media 尚未验收。 - 桌面和手机观看者使用同一个 Web 播放端;分享者不要求朋友安装完整客户端。 -当前接受的非服务器 endpoint downstream 默认值为二,并允许部署静态配置为一、二或三;分享端及 Viewer 遵守同一规则。ADR-0004 的可删除实验验证第三名及后续 viewer 可由客户端转发;ADR-0005 的自动 controller 最初在 room `1` 受限部署,现已覆盖所有房间。未来 SFU/TURN 层级、计账和 fallback 顺序仍在 truth-audit hold 中。retained SFU media、移动端矩阵和 broad rollout 仍未通过门槛;已关闭 PR #12 的显式整房 SFU 模式不再是当前方案。超过小房间上限时仍建议使用外部直播服务。观察项包括: +当前接受的非服务器 endpoint downstream 默认值为二,并允许部署静态配置为一、二或三;分享端及 Viewer 遵守同一规则。ADR-0004 的可删除实验验证第三名及后续 viewer 可由客户端转发;ADR-0005 的自动 controller 最初在 room `1` 受限部署,现已覆盖所有房间。未来 SFU/TURN 层级、计账和 fallback 顺序尚未接受。retained SFU media、移动端矩阵和 broad rollout 仍未通过门槛;已关闭 PR #12 的显式整房 SFU 模式不再是当前方案。超过小房间上限时仍建议使用外部直播服务。观察项包括: - 正常工作负载持续超过实测可承载的 P2P 人数。 - 当前 edge 已耗尽 peer recovery、alternate parent 与 SFU/UDP;未来只有该 edge 可进入 selected-edge TURN 决策。 diff --git a/docs/status.md b/docs/status.md index ee39c84..058e083 100644 --- a/docs/status.md +++ b/docs/status.md @@ -2,51 +2,36 @@ Last updated: 2026-08-22 -This file is the current execution index. Load [verification status](./verification-status.md) only when evidence or acceptance boundaries are relevant; Git history owns completed timelines. - -Execution is temporarily constrained by the [TODO audit hold](./todo-audit-hold.md). Held items remain preserved but must not be continued, merged, deployed, deleted, or treated as current product truth until their provenance and semantics are reviewed. +This is the current execution index. Git history owns completed timelines; [verification status](./verification-status.md) owns evidence boundaries. ## Production - `https://share.bonfire.icu` runs exact `9461e207af62b4f38f6b7a8aa16f8beb49e0e4ee`, release `9461e20`, wire `screener-v5`; `21d5cd9f7139` is the rollback release. -- `screener`, LiveKit, coturn, and nginx are active, local/public health return 200, and `screener` has `NRestarts=0`. SQLite remains schema v3 with five rooms and mode 0600. -- Ordinary ICE is STUN-only. Production allows SFU roots <=2, one `peer-selected` lease across negotiating and answered states excluding Host ingress, selected UDP TTL 120, and room admission default eight with explicit limits from one through sixteen. `MAX_PEER_RELAY_DOWNSTREAM_EDGES` is unset, so effective downstream capacity is Host2/ordinary Browser Viewer1; active, provisional, selected, and Host SFU publication edges count. The wire remains `screener-v5`. -- Stable SFU, route/transport truth, pause notices, entry/nickname UI, selected-edge lease authority, corroborated bad-relay handling, Share audio presets, local media-path diagnostics, pre-share self-check, privacy-safe diagnostic export, signaling-partition recovery, and peer-quality MBB are live. An ordinary Viewer with a peer/SFU upstream or the Host may own one separately budgeted provisional child; sibling MBB remains open. Native remains source-only. +- `screener`, LiveKit, coturn, and nginx are active; local/public health return 200, `screener` has `NRestarts=0`, and SQLite schema v3 remains protected with mode 0600. +- Production has automatic routing, bad-relay handling, peer-quality make-before-break, Viewer/Host provisional parents, selected-edge transport, stable connection details, 64/128/256 audio choices, access/privacy controls, self-check, diagnostic export, and signaling watchdog behavior. +- Exact deployed routing remains Host downstream `2`, ordinary Browser downstream `1`, deployment values `1/2`, fixed SFU roots `2`, and legacy publication/selected accounting. This is production fact only; it differs from the accepted endpoint-cap direction. -## Current Source And Production Snapshot +## Current Source -The route-capacity, SFU-root, publication-accounting, and selected-lease statements below describe exact deployed/source behavior, not an accepted future product model. Their semantics are frozen by the [TODO audit hold](./todo-audit-hold.md). +- Canonical source is the clean `main` branch; auxiliary branches and worktrees do not own current truth. +- The accepted endpoint direction is one ordinary downstream cap for every non-server endpoint: default `2`, static `1/2/3`, upstream receive free, and no Browser/UA/visibility tier. +- Automatic P2P-first routing, one active upstream, an acyclic graph, exact identity/generation authority, media-proven make-before-break, and bounded failure remain accepted. +- SFU/TURN placement and accounting are not yet accepted as a complete model. No routing runtime candidate may be merged or deployed until that model is reconciled and written to the owning truth. -- Capture precedes room creation. Balanced, automatic codec, and 128 kbps screen audio are defaults; clarity/fluid, H.264/VP8, and 64/128/256 kbps audio are deployed next-share choices. Codec and audio quality lock during a share. -- Local-only connection details expose current Host SFU sender evidence, actual codec/fmtp and encoder fields, media-source/encode FPS evidence, A/V playout/jitter/concealment evidence, and selected candidate endpoints. These fields do not enter quality signaling, server state, logs, persistence, or route selection. -- Deployed local details additionally show the selected pair's cumulative STUN responses and a bounded same-pair sample delta. This remains observation-only and does not trigger recovery or upload the pair identity. -- The deployed pre-share self-check uses disposable, no-room resources for health, WSS, and STUN `srflx` checks and treats configured SFU as unverified until a real route. Expanded connection details provide click-only, versioned JSON export; its fixed allowlist excludes candidate addresses/ports, participant and room identity, raw signaling, URLs, and credentials, and no report is uploaded or persisted. -- Production now stabilizes identity-bound remote-evidence detail shells, applies exact per-field five-second expiry with earliest-deadline timers, clears terminal/session/share/route generations, and admits only fresh presentations to diagnostic export. It adds no wire, server, or media-generation state. Target-browser field availability and actual media values remain an open verification gate. -- Private fragment grants enter directly; code-only Viewers need site access and private rooms additionally need their password. Credentials remain unstored. Web names and presence are session-only; duplicate names alone show an ID suffix. -- Rooms default to eight Viewers and accept an explicit admission limit from one through sixteen. Current source is server-authoritative and role-aware: Host physical media edges are bounded to two, ordinary Browser Viewer downstream edges to one, and upstream receive is free. Active/provisional/selected overlays count, as does Host SFU publication. Deployment values 1/2 may only tighten these limits; old/malicious Viewer advertisements 2/3 cannot lift them. Web assignment execution also truncates physical children to Host2/Viewer1 as defense-in-depth, and deployment limit one promotes an occupied Host branch root before reserving the SFU publication slot. The wire retains `0/1/2/3` only as a future envelope, and SFU roots remain separately bounded to two. -- Production deploys the root <=2 controller/token invariant, Host-publication edge accounting, zero-child assignment/subscriber retention, and `dynacast: false` with ordered active `q,h`. Local `gate:sfu-root-invariants` proves that control-plane boundary; real shaped LiveKit packet flow, BWE downshift/recovery, and resource cost remain open. -- Relay quality handling compares fresh child FPS with parent input. Two-child corroboration is retained only for a future accepted capacity tier and is unreachable under the ordinary Browser Viewer1 release clamp. Deployed MBB keeps the triggering child's old edge while an ordinary Viewer with peer/SFU upstream or the Host, each with a strict spare slot, owns a separate provisional child PC. Matching current-generation RTP, decoded frames, live video, revision, and identity promote that same PC; Host2 accounting includes its SFU publication and selected/provisional overlays. Failure, timeout, session/share change, or no candidate keeps the old edge without SFU/TURN. Sibling MBB remains open. -- The opt-in local Chrome `BENCHMARK_CANARY=viewer-mbb` now injects synthetic correlated bad-relay windows and verifies real provisional-PC retention, promotion, identity continuity, edge cap, and no route-failed output using sanitized counters only. It is control-path evidence, not detector or network-performance evidence; Host-candidate and signaling-response-blackhole canaries remain deferred. -- A generation-bound 15-second initial-connect deadline enters ICE restart. One-root healthy-SFU make-before-break is deployed but media-unverified; capacity `0 -> 1`, multi-root, browser, and mobile gates remain open. +## Current Milestone -## Active Milestone +1. Converge active constraints, requirements, design, ADR, memory, status, and [the TODO ledger](./todo.md) without retaining obsolete-policy commentary. +2. Report the concrete done/obsolete/decision-needed/unfinished classification and organized workspace state to the user, then stop. +3. After confirmation, write one holistic routing proposal and stop again for route-semantics confirmation. +4. Only then update and merge accepted route truth, rebuild the cap/accounting implementation from exact canonical `main`, validate, review, and deploy. -1. TODO provenance/current-truth reconciliation, the archive/workspace disposition schedule, the audit-guard merge, canonical-root restoration, and the approved D cleanup are complete. See [workspace disposition](./workspace-disposition.md) for the executed checkpoint and retained material. -2. Stop at the requested user checkpoint and report the TODO classifications, durable truth changes, canonical `main`, and retained/removed workspace state. Do not start another TODO cleanup, routing implementation, or feature work. -3. After explicit user confirmation, produce the holistic routing model as a held proposal from exact canonical `main`; it remains analysis, not accepted architecture or implementation authorization. -4. Stop again for route-semantics confirmation before updating their owning truth. Only after that truth is merged may new implementation worktrees start from the exact newer canonical-main SHA and retained candidates be resumed, parked, superseded, or cleaned one at a time. Previous audio, route-canary, SFU-shaping, Native, mobile, and scale items remain preserved in their owners but are not the active execution order. +## Active Boundaries -## Decisions And Blockers +- `fix/configurable-relay-cap` is an old, incomplete draft and must not be merged as-is. +- Open PR #192 and the Native stack are evidence/research, not pending product releases. +- Deployed surfaces and retained candidates that still need product decisions are indexed only in [the TODO ledger](./todo.md); do not extend or roll them back automatically. +- Real SFU/TURN recovery, heterogeneous networks, mobile lifecycle, audio/A-V device behavior, and endurance/resource measurements remain external acceptance evidence, not blockers for unrelated reversible work. -- ADR-0005's automatic controller is deployed for all rooms. ADR-0004's experimental claims and any broad media, performance, resource, scale, or device acceptance remain No-Go until their explicit evidence gates pass; DNS/TLS, independent secrets, UDP 7882, and bounded services are deployed, while real external media/device evidence and the isolated VM/IP clean-port gate remain open. -- Initial deployment region and expected network cohort are undecided. -- Project license and distribution model are undecided; GPL/AGPL sources remain study-only. +## Next Stop -## Detail Index - -- [Verification status](./verification-status.md): current cross-cutting evidence, open proof boundaries, and expensive-test applicability. -- [Project memory](./project-memory.md): accepted product constraints, recommendations, implementation snapshot, targets, and open decisions. -- [Requirements](./%E9%9C%80%E6%B1%82%E7%90%86%E8%A7%A3.md) and [design](./%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1.md): current acceptance contract and implementation design. -- [ADR-0004](./adr/0004-peer-assisted-media-experiment.md) and [ADR-0005](./adr/0005-automatic-hybrid-media-routing.md): peer-assisted and automatic-routing decisions. -- [Deployment](./deployment.md): production configuration, rollback boundary, and transport operations. -- [Research index](./README.md): media, routing, audio, Native, security, and context-governance evidence owners. +Wait for the user's review of this convergence before starting route modeling, another TODO, candidate integration, cleanup, or deployment. diff --git a/docs/todo-audit-hold.md b/docs/todo-audit-hold.md deleted file mode 100644 index 6952d3d..0000000 --- a/docs/todo-audit-hold.md +++ /dev/null @@ -1,118 +0,0 @@ -# TODO Execution Audit Hold - -Last updated: 2026-08-22 - -This is a temporary execution quarantine, not a backlog and not a deletion list. It prevents old assumptions, unchecked requirements, experimental conclusions, and agent-generated follow-ups from becoming authorized work after context compaction. - -## Boundary And Rules - -- Audit boundary: 2026-08-22 02:51:57 HKT, when broad TODO discovery and parallel execution began. Work already active before that point keeps its prior status; it does not automatically authorize new follow-ups. -- `CONFIRMED`: the user explicitly requested the product scope. The implementation still needs normal design and risk review. -- `REVIEW-REQUIRED`: the direction is relevant, but routing/state semantics must be reconciled before more changes merge or deploy. -- `AUDIT-HOLD`: preserve and inspect only. Do not continue, merge, deploy, delete, or revert it until provenance, necessity, and current semantics are reviewed. -- `EVIDENCE-ONLY`: retain reproducible measurements, but do not infer product policy, performance, capacity, or a release gate from them. -- `PRODUCTION-FACT`: a dated deployment record. It describes what ran, not what the product should continue to do. -- `SUPERSEDED`: a retained recovery/reference input that must not be executed. Remove it only after the truth audit and the normal Git/link safety checks. - -Latest direct user decisions override contradictory older text during this audit: every non-server endpoint uses one server-authoritative downstream cap, default `2` and configurable as `1`, `2`, or `3`; upstream receive is free; Browser/UA/visibility does not define a separate tier. The routing audit must still settle SFU publication/subscription semantics, TURN accounting, and transitional resource leases as one system. - -## Audit Order - -1. Reconcile TODO provenance and the current truth set. Classify contradictions and holds without implementing them. -2. Produce the documentation/archive and worktree/branch/folder disposition schedule. Preserve all disputed or unknown material; do not delete, move, rebase, or merge it yet. -3. Merge this audit-guard truth, restore the canonical root to exact current `main`, and execute only the approved workspace cleanup schedule. -4. Stop and reconcile the organized canonical state and TODO classifications with the user before route modeling or another cleanup pass. -5. After explicit confirmation, produce the holistic routing model from those reconciled inputs as a held proposal; do not implement or silently accept disputed semantics. -6. Stop again for route-semantics confirmation. Only after accepted owning truth is merged may high-confidence worktrees start from that exact newer canonical `main`, or other TODOs be resumed, parked, superseded, or cleaned from the reviewed ledger. - -Parallel work may accelerate one active phase, but it must not start a later phase early. - -## Requirement Checkbox Audit - -`docs/需求理解.md` currently contains 41 checkbox lines: 37 unchecked and 4 checked. They are acceptance-contract formatting, not a backlog: - -- At least 18 unchecked lines describe behavior that current status/memory already call live or substantially deployed, including entry/room flows, capture/pause, presence, P2P-first routing, quality settings, recovery, observability, access, grants, persistence, private entry, automatic routing, and peer-assisted forwarding. -- Nine unchecked lines overlap disputed capacity, SFU, TURN, fallback, quality-parent, or accounting semantics (`27`, `29`, `31`, `32`, `44`, `48`, `53`, `57`, `63`). They cannot authorize implementation while held. -- Eight unchecked lines are conditional experiments, no-go/reopen conditions, later native/desktop work, platform-output boundaries, or regional questions (`47`, `49`-`52`, `54`, `65`, `66`), not current implementation tasks. -- The four checked lines are not proof of current authorization: connection self-check (`56`) and diagnostic export (`59`) remain provenance holds, while codec choice (`58`) and the Native capture helper (`64`) retain their separately scoped facts and evidence boundaries. - -These groups overlap. Therefore neither 37 nor any subtraction from it is the executable TODO count. A normal TODO may be created only after its owning requirement is rewritten as current accepted behavior or a specific remaining gap, its implementation and production facts are reconciled, and any dependent hold is removed. - -## Current Truth Owner Risk - -- `AGENTS.md`, `docs/project-memory.md`, and `docs/status.md` are auto-loaded and must never mix disputed policy into confirmed intent or active milestones. During this audit they point here and separate dated production behavior from future product truth. -- `docs/需求理解.md`, `docs/方案设计.md`, and accepted ADR-0005 contain mixed deployed, historical, experimental, and disputed routing text. Their temporary banners freeze only the disputed route-dependent meaning; the accepted P2P-first, automatic, exact-generation, bounded-failure direction remains. -- `docs/verification-status.md` and `docs/deployment.md` may retain Browser1/root-two statements only as dated `9461e20` production evidence. Evidence does not authorize the next design. -- Research and historical ADR text remains source material. A proposal, threshold, benchmark, future gate, or past accepted status cannot become current work until its owner is reconciled against this hold. - -## Pre-Boundary Active Scope - -These scopes were already active or directly requested before broad TODO execution. Keep them, but do not extrapolate adjacent work from them: - -Git history also shows mixed-provenance PRs: #164's core demotion commit was authored before the boundary while later selected-lease fixes landed in the same PR after it; #166's Host SFU and selected-endpoint work started before the boundary while encoder/A-V evidence was integrated afterward. Audit behavior and source, not just PR number. - -| Scope | Status | Boundary | -| --- | --- | --- | -| Bad-relay corroboration, downgrade, reparenting, and make-before-break correctness | `REVIEW-REQUIRED` | Core user request; must now be reconciled with cap, SFU/TURN fallback, odd/full topology, and route generations as one design. | -| Share advanced audio settings and 64/128/256 kbps sender ceilings | `CONFIRMED` | Sender ceilings and observability are not fidelity proof. | -| Local connection details, selected candidate endpoints, and A/V observations | `CONFIRMED` | Local observation only; do not infer public IP, correction, or route authority. | -| Status archive/index policy and bounded NAT research | `CONFIRMED` | Documentation governance/research only; neither creates product runtime work. | - -## Post-Boundary Mainline Changes - -The range begins at PR #164. A merged or deployed item can still be under audit; do not rewrite production history. - -| Item | Status | Audit reason | -| --- | --- | --- | -| #164 relay-parent demotion | `REVIEW-REQUIRED` | Requested behavior, but the bundled selected-lease/cap state must be checked against the unified route model. | -| #165 share audio presets | `CONFIRMED` | Directly requested product behavior. | -| #166 local media-path evidence | `CONFIRMED` | Covers requested endpoint/A-V observability; remains observation-only. | -| #167 status/verification split | `CONFIRMED` | Direct response to the status-budget/archive decision. | -| #168 cap3 topology benchmark | `EVIDENCE-ONLY` | Functional evidence for configurable cap; not a performance ranking or release gate. | -| #169 connection self-check | `AUDIT-HOLD` | Automatically expanded from an old broad checkbox; its reduced health/WSS/STUN contract was not independently reconfirmed. | -| #170 explicit 16-Viewer admission | `AUDIT-HOLD` | Restored an older configurable contract without a fresh product-size decision; default remained eight. | -| #171 diagnostic JSON export | `AUDIT-HOLD` | Automatically expanded from an old diagnostic-page checkbox; no confirmed support workflow. | -| #172 relay resource benchmark | `EVIDENCE-ONLY` | Short same-machine evidence cannot select cap or block reversible cap2. | -| #173 peer-quality MBB | `REVIEW-REQUIRED` | Requested routing direction; state/mutex/fallback behavior needs holistic audit. | -| #174 and #176 rollout records | `PRODUCTION-FACT` | Dated deployment records only. | -| #175 Viewer-parent provisional edge | `REVIEW-REQUIRED` | Required by MBB safety, but must fit the final lease/accounting model. | -| #177 Browser1 clamp | `AUDIT-HOLD` | Reversed prior cap2 behavior without failure evidence and conflicts with the latest explicit cap decision. | -| #178 routing-lifecycle truth | `AUDIT-HOLD` | Propagated Browser1 and room-global lease assumptions that are now disputed. | -| #179 SFU root-two invariant | `AUDIT-HOLD` | Hardened a conservative design constant without LiveKit/resource evidence; publication/subscription topology is under review. | -| #180 audio/production truth | `PRODUCTION-FACT` | Audio facts remain useful; adjacent routing claims are not reauthorized. | -| #181 selected-pair response observation | `AUDIT-HOLD` | Agent-generated diagnostic slice with no prior product requirement or recovery authority. | -| #182 signaling silent-partition watchdog | `AUDIT-HOLD` | Useful failure detection is plausible, but Host continuity and selected-edge session replacement remain unproven. | -| #183 Host-parent provisional edge | `REVIEW-REQUIRED` | Requested routing direction; retain while auditing the unified topology/lease model. | -| #184 and #189 production records | `PRODUCTION-FACT` | Dated deployment records only. | -| #185 SFU shaping prerequisites | `EVIDENCE-ONLY` | Environment prerequisite record; not a product feature or blocker for unrelated work. | -| #186 and #190 Browser1 truth changes | `AUDIT-HOLD` | Repeated the disputed Browser1 assumption into auto-loaded truth. | -| #187 relayed-detail stability | `CONFIRMED` | Completes the requested stable connection-detail presentation without new wire or route authority. | -| #188 route-recovery evidence | `EVIDENCE-ONLY` | Retain exact test limits; do not treat it as real network/SFU/TURN proof. | -| #191 Viewer MBB canary | `EVIDENCE-ONLY` | Synthetic evidence plus real local PCs; no detector, network, or performance claim. | -| #192 A/V sync fixture (open) | `EVIDENCE-ONLY` | Freeze merge while the audit is open; measurement plumbing is not a correction loop or quality proof. | - -## Frozen Local Candidates - -| Candidate | Status | Next audit action | -| --- | --- | --- | -| `fix/configurable-relay-cap` | `REVIEW-REQUIRED` | Preserve the draft; do not continue until the unified route/accounting model is accepted. | -| `test/signaling-blackhole-canary@e972b75` | `EVIDENCE-ONLY` | Retain for the #182 decision; it proves Viewer continuity only. | -| `docs/resource-matrix-gate@140dd00` | `EVIDENCE-ONLY` | Preserve the short-run data; do not merge old Browser1 policy text. | -| `test/real-relay-canary@f135302` | `EVIDENCE-ONLY` | One-line #191 report correction may still be useful even though the later local canary helper also contains it. | -| `docs/routing-algorithm-convergence@43b1f46` | `AUDIT-HOLD` | Historical design input only; it contains stale capacity assumptions. | - -## Unresolved Routing Inputs - -These are user-supplied scenarios to test against the holistic route model. They are neither accepted implementation designs nor executable TODOs yet. - -1. A Host may reach media only through selected TURN while two first-level roots also cannot relay onward directly and need SFU-backed delivery. The final model must avoid two independent peers republishing the same screen into two competing SFU source truths. Audit whether one canonical share-generation publication, with TURN used only for its ingress when necessary, lets both roots and their descendants subscribe without duplicated encoding or lineage. -2. A mostly healthy room may already have two ordinary root branches when a newly joined leaf cannot receive its assigned P2P edge and needs SFU. Audit whether the leaf should leave that peer edge and subscribe to the room's canonical publication while its previous parent reclaims the ordinary child slot. Do not assume the intermediate parent must republish the same source. -3. Retain optional selected TURN during this audit. Decide keep, narrow, or remove only after comparing its exact-edge and Host-to-SFU-ingress value against credential, lease, recovery, and state-machine complexity. The examples above are not by themselves evidence that TURN should be removed. - -The preferred simplicity test is one Host screen lineage, at most one canonical SFU publisher per share generation, selective SFU subscribers wherever P2P is unavailable, ordinary peer descendants where healthy, and TURN as a transport lease rather than another topology. This is a review hypothesis, not yet accepted architecture. - -## Release Of A Hold - -For each held item, record: original user requirement or lack of one; current consumer; behavior and rollback cost; security/privacy/resource boundary; conflicting facts; and a keep, revise, park, or remove decision. Only then update the owning requirement/ADR/research/status text in place. Do not use this temporary file as permanent architecture documentation. - -Merge this audit-guard truth first; that merge does not accept a final routing model. Then preserve any root user changes, release `main` from auxiliary worktrees, restore the canonical repository root to the exact latest `main`, and complete only the approved workspace cleanup. Report that organized state and the TODO classifications, then wait for explicit user confirmation before producing the holistic routing proposal as a held document. Stop again for route-semantics confirmation. Only afterward may accepted route semantics be written into their owning truth and merged into `main`; retained candidates or remaining TODOs may be rebuilt or resumed only from that newer exact canonical-main commit. Candidate-era truth files must never overwrite it. diff --git a/docs/todo.md b/docs/todo.md new file mode 100644 index 0000000..edb486c --- /dev/null +++ b/docs/todo.md @@ -0,0 +1,40 @@ +# Current TODO Ledger + +Last reviewed: 2026-08-22 + +Only items in **Now** are executable after their stated decision gate. A branch name, unchecked requirement, experiment, review suggestion, or deployed behavior is not a TODO by itself. + +## Now + +1. **Approve one holistic routing model.** After the user confirms this truth/TODO convergence, write a held proposal covering ordinary topology, SFU/TURN transport roles, fallback ordering, temporary overlap, resource admission, recovery, and the user-supplied constrained-network scenarios. Preserve the accepted endpoint cap and route safety invariants, then stop again for route-semantics confirmation. +2. **Migrate routing from accepted truth.** After route semantics are confirmed and their owning truth is merged, rebuild the scoped runtime change from exact canonical `main`: every non-server endpoint defaults to ordinary downstream cap `2`, deployment accepts `1/2/3`, upstream receive is free, and no Browser tier exists. Replace policy magic numbers with named configuration and shared invariants. +3. **Validate and release.** Run focused state-machine tests, cap `1/2/3` topology and recovery matrices, independent routing review, build/full gates, bounded production preflight, deployment, postflight, and rollback verification. + +The deployed #164/#173/#175/#183 routing mechanisms are inputs to these tasks, not four separate TODOs. Keep their safe generation, authorization, make-before-break, and media-proof behavior while reconciling capacity and fallback accounting. + +## Product Decisions Needed + +| Deployed or retained item | Decision | +| --- | --- | +| #169 connection self-check | Keep, revise, or remove the user-facing pre-share probe. Do not extend it automatically. | +| #170 explicit 1-16 Viewer admission | Confirm the small-room admission range independently of endpoint fanout. | +| #171 diagnostic JSON export | Confirm a support workflow or remove the extra surface. | +| #181 selected-pair response counter | Keep as local observation or remove; it has no route authority. | +| #182 signaling watchdog | Confirm Host, mobile/background, and selected-edge session semantics before calling it complete. | +| `feat/local-oneclick-bundle` | Decide whether local one-click startup belongs in the current Web roadmap. | +| Windows/native sender work | Keep as later optimization unless a browser capability gap changes priority. | + +These decisions are not permission to continue old branches, and current deployment remains unchanged until a scoped decision is made. + +## Evidence And Later Work + +- 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. +- Real heterogeneous-network SFU/TURN, mobile lifecycle, audio/A-V device, and endurance/resource acceptance are scheduled at the relevant release boundary. + +## Candidate Handling + +- Preserve `fix/configurable-relay-cap`, but do not commit or merge it. Its relevant intent must be rebuilt after route truth is accepted. +- Preserve dirty or unique worktrees and open stacked branches until reviewed. Never resolve their conflicts by importing old truth into `main`. +- Sample worktree, branch, PR, artifact, and reparse-point state immediately before cleanup; do not maintain a permanent workspace inventory here. diff --git a/docs/verification-status.md b/docs/verification-status.md index e812422..68d43be 100644 --- a/docs/verification-status.md +++ b/docs/verification-status.md @@ -6,7 +6,7 @@ Last updated: 2026-08-22 This is the demand-loaded ledger for cross-cutting validation evidence and open proof boundaries that do not fit one requirement, ADR, research note, or deployment guide. It is current state, not a release diary: replace superseded facts in place, move durable specialist conclusions to their owning documents, and let Git history retain completed timelines. -During the 2026-08-22 truth audit, Browser1/root-two/accounting entries below describe exact release evidence only. They do not define the next product model or create executable gates; see the [TODO audit hold](./todo-audit-hold.md). +Capacity and routing entries below describe the exact tested source or release only. Product direction and executable work are owned by [project memory](./project-memory.md) and [the TODO ledger](./todo.md). ## Deployment Evidence @@ -32,7 +32,7 @@ During the 2026-08-22 truth audit, Browser1/root-two/accounting entries below de - Routing: verify current-release initial ingress, `peer-selected` expiry/failure, corroborated bad-relay reassignment, one-root healthy SFU reselection, silent partitions, and bounded failure on real browser/media paths. Silent control partitions may wait 30 to 60 seconds for heartbeat detection before the default five-second grace. - Quality: production startup blur remains unclassified. One same-`balanced` local A/B retained route/PC/SSRC/track while 720p rose to 1080p, but natural ramp prevents causal, production, or SFU conclusions. Correlate Host capture/encode/send with Viewer receive/decode before changing policy. -- Relay resources: production `9461e20` is Host2/ordinary Browser Viewer1 and rejects Viewer child2 regardless of advertised capacity. The next endpoint-cap and corroboration contract is held, while CPU/GPU/upload and heterogeneous-network measurements remain optional evidence for later tuning rather than authority to reinstate Browser1. +- Relay resources: production `9461e20` is Host2/ordinary Browser Viewer1 and rejects Viewer child2 regardless of advertised capacity. The next endpoint-cap and corroboration contract is held, while CPU/GPU/upload and heterogeneous-network measurements remain optional evidence for later tuning rather than authority to change endpoint-cap policy. - SFU: production `9461e20` deploys ordered `q,h`, roots <=2, and Dynacast off. Root count and accounting are held; real shaped packet flow, BWE downshift/recovery, resource cost, and root-with-children behavior remain unverified evidence boundaries. Use Linux `tc` or an authorized public canary for per-leaf shaping, never CDP throttling. - Audio: verify the 64/128/256 kbps Share settings, negotiated/observed bitrate, audible music quality, route changes, and game A/V synchronization. Bitrate settings are ceilings, not quality proof. - Observability: the UI and local stats plumbing are deployed, including identity-bound relayed-detail retention and export freshness guards, but target-browser availability and actual values remain unverified for Host SFU sender evidence, codec/fmtp and encoder implementation/power efficiency, media-source and encoded FPS, A/V playout/jitter/concealment, selected candidate endpoints, and bounded same-pair STUN-response observation. These fields diagnose and have no recovery authority; they do not by themselves prove hardware encode, synchronization quality, or route quality. diff --git a/docs/workspace-disposition.md b/docs/workspace-disposition.md deleted file mode 100644 index c8eaf6f..0000000 --- a/docs/workspace-disposition.md +++ /dev/null @@ -1,147 +0,0 @@ -# Workspace Disposition - -Last sampled: 2026-08-22 after the approved cleanup pass - -This is the demand-loaded schedule for restoring one canonical repository root and disposing of auxiliary work safely. It is not permission to delete, merge, rebase, or resume a TODO. Shared state is dynamic, so every action must re-sample Git status, refs, PRs, and links immediately before execution. - -## Executed Cleanup Checkpoint - -The detailed tables below retain the pre-execution 30-worktree snapshot so the -disposition evidence is reviewable. This checkpoint supersedes those counts: - -- The canonical root `Screener` was restored to clean `main@5bb54b0`, equal to - `origin/main` and GitHub `main` before this docs-only record. The two staged - user `AGENTS.md` rules are present in that mainline truth; their recovery copy - remains in `stash@{0}` through the user checkpoint. -- After this record branch is merged and removed, 26 registered worktrees and - 29 local branches remain. Five worktrees are dirty; only - `Screener-local-oneclick` has nonignored untracked files, exactly five. Every - dirty, held, unknown, evidence-only, archive, and open-stack item was - preserved. -- The clean auxiliary-main, current-routing-truth, - connection-details-stability, and detached current-route-canary worktrees - were removed after exact status and non-following link checks. Their eligible - local branches were removed together with the merged-equivalent - `feat/viewer-presence-polish`, `feat/room-viewer-access-merge-history`, and - main-ancestor `fix/ice-status-wording` refs. -- Fifteen merged remote heads were deleted only after exact PR-head equality, - merge ancestry, and open head/base checks. Ten remote branches remain: - `main`, open PR #192, the six open Native stack refs, held - `test/native-two-viewer-fanout`, and closed-unmerged - `feat/screen-audio-stereo`. -- The signaling-canary junction and both junction-only unregistered shells were - unlinked without following their targets. The only known registered-worktree - junction left is the preserved configurable-cap `node_modules` link to the - canonical dependency directory. -- Four approved empty/junction-only sibling directories were removed. The empty - `Screener-android-pilot-apk` directory remains because Windows reports an - external open handle; no process was killed and no forced recursive deletion - was attempted. Deployment archives, extracted artifacts, the independent - audio acceptance clone, and all uncertain directories remain retained. -- `feat/sfu-media-mode` remains local because it is closed-unmerged and not - commit-equivalent to main. Apparent semantic supersession was not used as - permission to destroy its recovery input. - -No routing implementation or remaining TODO resumed during this cleanup pass. - -## Canonical Target And Stop Gate - -- The canonical repository root is the registered worktree whose sibling-directory name is `Screener`; all other `Screener-*` worktrees are auxiliary. -- Its required terminal state is a clean, exact latest `main`. An auxiliary worktree must not retain `main` afterward. -- The audit-guard truth checkpoint merges first; it does not accept a final routing model. The root's staged user rules must be proved present in that updated `main` or explicitly preserved before its index changes. -- After truth integration, canonical-root restoration, and the approved workspace cleanup below, report the main SHA, dirty/untracked count, retained worktrees/branches, held candidates, and TODO classifications to the user before producing the holistic route proposal or resuming cleanup. -- After that checkpoint is explicitly confirmed, produce the holistic routing model as a held proposal from exact canonical `main`, then stop again for route-semantics confirmation. Do not create or resume an implementation worktree until the accepted owning truth is merged; every later worktree starts from that exact newer `main` SHA. - -## Disposition Classes - -- `A PRESERVE`: required before truth integration, dirty, disputed, open, or carrying unique/unknown work. No cleanup. -- `B RECONCILE`: evaluate only after accepted truth is in `main`; transplant approved scoped code/tests/evidence, never an old truth snapshot. -- `C CANONICAL`: handle only while restoring the canonical root to latest `main`. -- `D CLEANUP`: eligible only after semantic disposition, integration/equivalence, clean-tree, open-reference, and non-following link checks all pass. -- `E RETAIN`: active stack, external evidence, archive, or unknown provenance; keep until a separate reviewed decision. - -## Registered Worktrees (Pre-Execution Snapshot) - -The snapshot contains 30 registered worktrees. Seven are dirty. Only -`Screener-local-oneclick` has nonignored untracked files: five unique project -files. - -| Worktree | Branch / HEAD | Snapshot | Class and next action | -| --- | --- | --- | --- | -| `Screener` | `feat/viewer-presence-polish@d10e879` | staged `AGENTS.md`, no untracked | `A -> C`; preserve two user rules, then restore this root to exact `main`. PR #153 is merged; after restoration, its local and remote branch become `D` only after a fresh merged-head and open-head/base recheck. | -| `Screener-current-routing-truth` | `docs/current-routing-truth` audit branch | truth owner; sampled from `origin/main@7df8217` | `A -> D after merge`; finish, validate, review, and merge before any other integration, then remove its clean merged auxiliary worktree/branch during canonical cleanup. | -| `Screener-connection-details-presentation` | `main@7df8217` | clean | `A -> C`; currently occupies `main`. Release only after dependent junction handling and latest-main verification. | -| `Screener-configurable-relay-cap` | `fix/configurable-relay-cap@7df8217` | 10 tracked edits; junction to canonical `node_modules` | `A -> B`; disputed routing draft, preserve and later rebuild only approved intent. | -| `Screener-av-sync-next-slice` | `spike/av-sync-next-slice@d9bd4e2` | clean, open PR #192 | `A/E`; evidence-only, preserve open refs. | -| `Screener-real-relay-canary` | `test/real-relay-canary@f135302` | clean; local differs from merged PR head | `A -> B`; retain possible one-line report correction, never delete solely because #191 merged. | -| `Screener-resource-matrix-gate` | `docs/resource-matrix-gate@140dd00` | clean | `A -> B`; retain measurements only, exclude stale Browser1 policy. | -| `Screener-signaling-blackhole-canary` | `test/signaling-blackhole-canary@e972b75` | clean; junction to auxiliary-main `node_modules` | `A -> B`; evidence for held watchdog decision; handle junction before releasing auxiliary main. | -| `Screener-routing-architecture` | `docs/routing-algorithm-convergence@43b1f46` | clean | `A -> B`; historical/disputed model input only. | -| `Screener-relay-parent-quarantine` | `fix/relay-parent-quarantine@5bab93b` | 2 tracked server edits | `A -> B`; unknown routing draft, preserve pending model review. | -| `Screener-connection-details-stability` | `fix/connection-details-stability@55f4cbc` | clean | `A -> D`; superseded design input, cleanup only after #187 equivalence recheck. | -| `Screener-current-route-canary` | detached `b77f4eb` | clean | `D`; old exact-main fixture, recheck no consumer before removal. | -| `Screener-host-generation-debug` | `spike/host-generation-quality-evidence@fc4b0b0` | clean, local-only | `E`; unknown provenance. | -| `Screener-local-oneclick` | `feat/local-oneclick-bundle@e3ab39c` | 7 tracked edits plus 5 untracked project files | `A/E`; unique dirty work, never clean or remove automatically. | -| `Screener-native-live-bridge` | `spike/native-live-encoded-bridge@8121b69` | clean, draft PR #22 | `E`; preserve Native stack. | -| `Screener-native-live-feedback-loop` | `spike/native-live-feedback-loop@aad560e` | clean, draft PR #28 | `E`; preserve Native stack. | -| `Screener-native-primary-nack-retransmit` | `spike/native-primary-nack-retransmit@a6c3456` | clean, draft PR #25 | `E`; preserve Native stack. | -| `Screener-native-shared-encode` | `spike/native-shared-encode-browser@80f65d0` | clean, draft PR #18 | `E`; preserve Native stack. | -| `Screener-native-shared-feedback-control` | `spike/native-shared-feedback-control@eb9aabe` | clean, draft PR #23 | `E`; preserve Native stack. | -| `Screener-pion-rtp-fanout` | `spike/native-rtp-fanout-oracle@5b09f0a` | clean, draft PR #16 | `E`; preserve Native stack. | -| `Screener-native-two-viewer-fanout` | `test/native-two-viewer-fanout@bf898b6` | clean, remote branch, no PR | `E`; held Native candidate. | -| `Screener-windows-shared-encode-sender` | `feat/windows-shared-encode-sender@1ce732e` | clean, 14 local commits | `E`; unique held Native candidate. | -| `Screener-participant-connection-truth-plan` | `docs/participant-connection-truth-plan@4b536d0` | clean, local-only | `E`; unknown docs input. | -| `Screener-quality-evidence-followups` | `docs/quality-evidence-followups@d3f160f` | clean, local-only | `E`; unknown docs input. | -| `Screener-recovery-details-release` | `fix/recovery-details-release@b0e013f` | 2 tracked docs edits | `A/E`; stale code ancestry plus unique unknown docs. | -| `Screener-route-status-visual` | `fix/route-status-visual-recall@79ee46e` | clean, local-only | `E`; unknown and likely overlapped. | -| `Screener-sfu-healthy-reselection` | `docs/sfu-healthy-reselection@47c2ce7` | clean, local-only | `E`; old design, semantic review required. | -| `Screener-sfu-stable-stream` | `fix/sfu-stable-subscriber-stream@4d46a4a` | clean, local-only | `E`; unknown. | -| `Screener-target-peer-error` | `fix/target-peer-not-connected@7141cd9` | clean, local-only | `E`; unknown. | -| `Screener-viewer-c-evidence` | `docs/viewer-c-evidence-contract@911b03b` | 4 staged/add plus `docs/status.md` conflict | `A/E`; unresolved conflict and unique work, never remove/rebase automatically. | - -## Local Branches Without Worktrees - -- `docs/production-smoke-record@231a7c3`: `E`, unknown. -- `feat/room-viewer-access-merge-history@921e8a2`: `D`, merged-equivalent after final recheck. -- `feat/sfu-media-mode@fb7d4f1`: `D` candidate, closed-unmerged PR #12; semantic supersession must be proved first. -- `fix/ice-status-wording@4f7fee4`: `D`, current-main ancestor after final recheck. -- `fix/screen-audio-bitrate-target@5a71462`: `E`, local unique commits despite a related merged remote head. - -## Open And Remote References - -- Preserve `main`, open PR #192, and the Native stack #16/#18/#22/#23/#25/#28, including every stacked base branch. Preserve `test/native-two-viewer-fanout`, which has a remote branch without a PR. These are nine retained remote refs in this snapshot. -- The following 14 merged-PR heads are `D` candidates only after a fresh exact remote-head equals merged-PR-head check and confirmation that no open PR uses the branch as head or base: `chore/hygiene-ignore-untracked`, `docs/nat-web-boundary`, `feat/host-display-name`, `feat/host-roster-details-disclosure`, `feat/viewer-presence-polish`, `feat/viewer-topology-map`, `fix/codec-lock-hint`, `fix/peer-relay-quality-release`, `fix/relay-relative-quality`, `fix/revert-low-fps-reselection`, `fix/screen-audio-bitrate-target`, `fix/sfu-viewer-recovery`, `fix/sticky-sfu-cooldown-mainline`, and `test/real-relay-canary`. -- Preserve `feat/screen-audio-stereo` as `E`: PR #147 is closed-unmerged, so the merged-branch deletion rule does not apply merely because later work appears equivalent. -- The remote listing also contains the symbolic `origin/HEAD` alias; it is not a branch cleanup target. Every count and disposition must be re-sampled immediately before action because this 2026-08-22 snapshot is not permanent authority. - -## Unregistered Directories And Artifacts - -- Empty directory candidates: `Screener-android-pilot-apk`, `Screener-deploy-261e980c`, `Screener-production-truth-66eb` (`D` after exact empty recheck). -- Junction-only shells: `Screener-quality-evidence-alignment`, `Screener-room-peer-canary`. Each points to canonical `node_modules`; unlink the junction itself without following it before considering the empty root (`D`). -- Independent acceptance clone: `Screener-audio-main-acceptance` plus bare `Screener-audio-main-acceptance.git` at `960ae42` (`E`). It is not a canonical worktree; if later removed, use its own bare repository to remove its registered worktree first. -- Deployment archives: `Screener-deploy-artifacts`, `Screener-deploy-builds`, two extracted artifact directories, and five sibling `.tar.gz` files (`E/D`). Pair each with commit, checksum, and deployment retention records before archive/removal; never glob-delete `Screener-*`. - -## Known Reparse Points - -The non-following scan found exactly four junctions and no scan errors: - -1. `Screener-configurable-relay-cap\node_modules` -> canonical root `node_modules`. -2. `Screener-signaling-blackhole-canary\node_modules` -> auxiliary-main `node_modules`. -3. `Screener-quality-evidence-alignment\node_modules` -> canonical root `node_modules`. -4. `Screener-room-peer-canary\node_modules` -> canonical root `node_modules`. - -Never recursively remove a containing directory while one of these links remains unresolved. Delete only the link itself using a non-following operation, then verify the external target still exists. - -## Execution Order - -1. Stop writers and re-sample worktrees, dirty/untracked state, local/remote refs, open PRs, and reparse points. -2. Finish, validate, independently review, and merge the audit-guard truth checkpoint first; do not treat it as final route acceptance. -3. Prove the root's staged user rules exist in accepted `main`; explicitly preserve any unmatched content. -4. Handle the signaling-canary junction without following it. Recheck the auxiliary-main worktree is clean, untracked-free, and at exact latest `main`, then release it. -5. Make the canonical root clean without losing unmatched user work, switch it to exact latest `main`, and verify path, branch, HEAD, dirty/untracked state, and GitHub main equality. -6. Execute only already-approved `D` cleanup items, one exact target at a time, after semantic/equivalence/open-reference/link checks. Preserve all `A`, `B`, and `E` items. -7. Stop and obtain user confirmation of the organized canonical state, retained material, and TODO classifications before route modeling or another cleanup pass. -8. After that confirmation, produce only the holistic routing held proposal from exact canonical `main`; do not implement it or resume another TODO. -9. Stop again and obtain explicit route-semantics confirmation. -10. Only after confirmation, update accepted owning truth and merge that truth into canonical `main`. Then create new implementation worktrees from that exact newer canonical-main commit and reconcile `B` candidates one at a time. Old branch truth files never win conflicts. -11. Process independent archives and deployment artifacts last, with checksum and retention evidence. diff --git a/docs/方案设计.md b/docs/方案设计.md index c88826f..73e9d9f 100644 --- a/docs/方案设计.md +++ b/docs/方案设计.md @@ -1,14 +1,12 @@ # 屏幕共享 WebRTC PoC 方案设计 -> 2026-08-22 truth audit hold: 自动 P2P-first 方向仍保留,但容量、SFU publication/subscription、TURN、fallback 与临时资源计数尚未完成整体裁决。本文件相关旧段落是审计输入,不是当前实现授权;详见 [TODO audit hold](./todo-audit-hold.md)。 - ## 2026-08-18 14:45 -状态:首个 PoC 基线及其已接受扩展的当前设计。Production 仍使用 STUN-only ordinary ICE;旗舰进程以 `PEER_ASSISTED_MEDIA=true` 对所有正常房间启用 peer-assisted 与 LiveKit SFU/UDP,room `1` 仅是历史 smoke。built-in Peer ICE TURN canary 已失败并回滚,不再是产品方向;production 已配置仅由 controller 向单条异常 edge 发放的 selected-edge TURN,但真实中继媒体仍未验收。旧 release 与 coturn relay 仅保留为隔离回滚路径,不是同进程兼容分支。 +状态:首个 PoC 基线及其已接受扩展的当前设计。Production 普通 peer ICE 保持 STUN-only,并已部署自动 route controller;当前路由不变量由 ADR-0005 持有,SFU/TURN 整体资源模型定案后再迁移实现。精确 deployed behavior 只由 deployment 文档记录。 ## 目标与边界 -产品面向一名分享者和小范围可信朋友的低延迟游戏画面分享。观看端使用普通桌面或手机浏览器;每条媒体边独立 ICE。当前 production 的普通边只接收自托管 STUN;所有正常房间在 peer 恢复耗尽后均可由 LiveKit ICE/UDP virtual parent 服务一至两个必要 roots,再失败时仅可为 controller 选中的异常 edge 尝试一次短期 TURN,随后有界明确结束。当前房间默认最多八名观看者、可配置范围为 1 至 16;自动分布式拓扑通过 20 人门禁后,目标 release 才把默认值和上限原子改为 20。容量配置只是接入边界,不是性能声明。 +产品面向一名分享者和小范围可信朋友的低延迟游戏画面分享。观看端使用普通桌面或手机浏览器;每条普通媒体边独立 ICE 并优先 direct/peer UDP。所有非服务器端点共享一个服务端权威 downstream capacity,默认 `2` 且只允许静态配置 `1`、`2` 或 `3`;upstream receive 免费,角色、浏览器、UA、设备和页面可见性不改变该值。活动媒体图无环,每名 Viewer 同时至多一个 active upstream。SFU/TURN publication、subscription、transport、server-resource、temporary overlap 和 exact fallback sequence 仍待整体定案。 本阶段必须形成完整的最小纵向链路: @@ -32,7 +30,7 @@ | 可选房间持久化 | Node.js 内置 `node:sqlite` | 单文件、无额外依赖;只保存房间 ID、Host token 摘要、nullable Viewer grant 摘要与 nullable Viewer-password KDF material | | 运行时校验 | Zod | WebSocket 和 HTTP 输入不可信,需要与 TypeScript 类型一致的运行时边界 | | 静态文件 | `sirv` | 生产环境由同一 Node 进程提供前端构建结果,避免引入完整 Web 框架 | -| 地址发现/鉴权回退 | coturn 4.17.2 或更新补丁版本 | 当前 tracked template 与 production 应用只下发 STUN,但共享机保留旧 authenticated-relay daemon 配置/端口规则且不下发 credential;未来只为 controller 选中的异常 edge 签发短期 TURN,先验收 TURN REST、UDP relay range 与 quotas,不启用媒体 TCP | +| 地址发现/可选兼容传输 | coturn 4.17.2 或更新补丁版本 | 普通 peer 只下发 STUN;TURN 仅可服务整体路由模型明确授权的短期 edge,并受 UDP relay range、credential、quota 和真实媒体门禁约束 | | 测试 | Vitest | 覆盖纯逻辑和 Node 集成测试;真实媒体能力另做浏览器/网络矩阵 | | TLS 入口 | Caddy 或 nginx,由部署者选择 | 统一 HTTPS/WSS;不把证书管理写进应用进程 | @@ -46,13 +44,13 @@ flowchart LR H <-->|WSS: 鉴权与信令| A V[观看者 Web 1..N] <-->|WSS: 鉴权与信令| A H <-->|ordinary direct/peer DTLS-SRTP UDP| V - H -.peer 路径耗尽.-> S[LiveKit SFU/UDP virtual parent] - S -.一至两个必要 roots.-> V + H -.经 controller 授权.-> S[有界 SFU/TURN 资源] + V -.经 controller 授权.-> S H -.地址发现.-> T[coturn STUN-only] V -.地址发现.-> T ``` -每个观看者的候选检查和最终路径互不影响。同一 candidate 房间可同时存在健康 direct/peer 子树与必要的 SFU roots;Node 房间服务不复制、解码或转发媒体,LiveKit 只在有界 root 路径上转发。 +每个观看者的候选检查和最终路径互不影响;健康 direct/peer 子树不会因另一条弱路径被全房迁移。Node 房间服务不复制、解码或转发媒体。SFU/TURN 只按待定模型中的明确 owner、edge 与 deployment budget 参与,不能由图中的连线推导 fallback 顺序或 server fanout。 ## 代码布局 @@ -90,7 +88,7 @@ rotate、revoke 或从 public-watch 切回 private-link 都是当前 Viewer 的 房间规则: -- 一名在线分享者;当前默认最多八名在线观看者、配置只接受 1 至 16,目标路由 release 通过门禁后统一改为 20; +- 一名在线分享者;房间 admission 与 endpoint capacity 分离,当前 production 默认八名、显式接受 1 至 16,未来范围另行决策和验收; - 分享者明确停止或捕获轨结束时只停止当前发布并通知观看者进入等待,不删除房间或使链接失效;host 信令短断仅更新在线状态并触发恢复,不能主动关闭仍健康的 P2P 媒体; - 临时房间到达 TTL 后才拒绝新连接、通知现有参与者并从内存清理;持久房间没有 TTL,进程启动时从 SQLite 恢复房间身份; - 设置全局房间上限防止内存耗尽;持久房间没有自动删除策略,运维方必须保护和备份数据库文件; @@ -144,8 +142,6 @@ Zod 在服务边界拒绝未知类型、超长字符串、非法 SDP/candidate 信令 WebSocket 暂时中断时,不立即关闭仍健康的 P2P 媒体;客户端指数退避恢复控制通道。明确不可重连的会话替换会释放旧页面的 peer、统计定时器和媒体引用,host 也停止旧捕获。首版只保证同一页面生命周期内恢复,不承诺浏览器进程被系统挂起后的连续播放。 -Web 客户端只在当前 socket 已鉴权、分享在线且本端拥有 active 权威路由时运行应用层静默断链探测:五秒后发送带安全整数序号的 challenge,首个精确两秒 miss 只进入内部 suspect 并立即发送新序号 confirm,第二个精确两秒 miss 才废弃旧 socket 代次并直接建立新连接,不等待旧 closing handshake。response 必须匹配当前 socket 代次和序号;旧序号、旧 socket、已替换鉴权会话一律忽略。页面隐藏、恢复可见或定时器明显迟到时清除未决证据并重新等待完整五秒。服务端只向主动 opt-in 的请求 socket 回送相同序号,每 socket 每秒最多一次,不附带房间、密钥、候选或统计信息;因此 strict Native 不会收到未知消息。watchdog 本身不发送 `route-failed`、不修改路由权威,也不要求普通 P2P 或同 revision SFU 关闭健康媒体;现有 `peer-selected` TURN 授权仍严格绑定两端鉴权 session,任一 session 替换后按既有有界恢复路径处理。服务端 30 秒 ping/pong 默认值保持不变。 - ## 捕获、编码与质量 捕获只能由按钮产生的用户手势调用 `getDisplayMedia()`。三档目标为: @@ -180,7 +176,7 @@ Host 本地预览直接绑定现有 capture stream,不创建 viewer、`RTCPeer Web peer 的最小 stereo 契约只改 Viewer 生成的 answer。实现把 MIT `sdp-transform` 2.15.0 与 `@types/sdp-transform` 声明为 root direct dependencies,不依赖 LiveKit 的 transitive install,也不用字符串或正则拼接 SDP。Helper 仅在解析出唯一非 rejected audio media、通过结构化 `rtp` entry 找到唯一 Opus payload 时,才在该 payload 的 fmtp 幂等 upsert `stereo=1` 与 `maxaveragebitrate=256000`;随后 `ViewerPeer` 把同一个结果交给 `setLocalDescription()` 和 signaling。首次连接、ICE restart、同 parent rebuild、浏览器 relay child 与 selected-edge TURN 都走该边界;TURN 只改变 ICE transport。空 SDP、无或歧义 Opus、多个有效 audio、重复目标参数、畸形 fmtp、parse/write 异常都按对象 identity 返回浏览器原 answer,保住仍可继续的 mono/video edge,不发送半修改 SDP。RFC 7587 说明这两个值都是可选的单向 receiver parameters;`maxaveragebitrate` 是 maximum 而非最低保证,实际发送仍可随拥塞下降。当前 libwebrtc 把缺省 full-band stereo codec bitrate 配成 64 kbps,而 `RTCRtpSender.maxBitrate` 只限制上界;本接收上限允许三档,实际发送预算仍由 64/128/256 kbps sender ceiling 与拥塞共同决定。`sprop-stereo` 只是 sender likelihood hint,`usedtx` 缺省已是 0,本 slice 不增加二者,也不改浏览器协商的 `useinbandfec`。 -SFU publisher 将 64 kbps 映射到 `AudioPresets.musicStereo`、128 kbps 映射到 `musicHighQualityStereo`,256 kbps 使用显式 `maxBitrate`;三者都保留 `forceStereo: true` 与 `dtx: false`。LiveKit 2.22.0 先合并 room publish defaults,其中 `red: true`;本实现不传 `red` 覆盖值,保留既有 RED request。RED 与 Opus in-band FEC 是不同修复机制,保留前者不声称实际启用、替代或等价于后者。只改 SFU 会导致 route switch 行为分叉,因此 P2P/browser relay 与 SFU 必须同一 slice 落地。Viewer 继续在同一个持久 `MediaStream`/媒体元素中增删 screen video/audio track;不新建 audio element、Web Audio mix/resample graph、第二 audio representation 或独立音频时钟。默认每条 outbound edge 的 audio RTP payload ceiling 为 128 kbps,显式最高档为 256 kbps;Host2 对应最多 256/512 kbps,ordinary Browser Viewer1 对应最多 128/256 kbps(不含 RTP/SRTP/UDP/IP overhead)。stereo 仍可能增加编码与网络成本,且浏览器 relay 不保证跨 PeerConnection shared encode。未来 microphone voice 若进入范围,另建独立 source/track 与 AEC、降噪、gain、DTX 设计,不能作用于电影/游戏屏幕音频。详细证据与 Oopz 未知边界见 `docs/research/browser-screen-audio-quality.md`。 +SFU publisher 将 64 kbps 映射到 `AudioPresets.musicStereo`、128 kbps 映射到 `musicHighQualityStereo`,256 kbps 使用显式 `maxBitrate`;三者都保留 `forceStereo: true` 与 `dtx: false`。LiveKit 2.22.0 先合并 room publish defaults,其中 `red: true`;本实现不传 `red` 覆盖值,保留既有 RED request。RED 与 Opus in-band FEC 是不同修复机制,保留前者不声称实际启用、替代或等价于后者。只改 SFU 会导致 route switch 行为分叉,因此 P2P/browser relay 与 SFU 必须同一 slice 落地。Viewer 继续在同一个持久 `MediaStream`/媒体元素中增删 screen video/audio track;不新建 audio element、Web Audio mix/resample graph、第二 audio representation 或独立音频时钟。默认每条 outbound edge 的 audio RTP payload ceiling 为 128 kbps,显式最高档为 256 kbps;非服务器端点容量为 C 时最多 C × 128/C × 256 kbps(不含 RTP/SRTP/UDP/IP overhead)。stereo 仍可能增加编码与网络成本,且浏览器 relay 不保证跨 PeerConnection shared encode。未来 microphone voice 若进入范围,另建独立 source/track 与 AEC、降噪、gain、DTX 设计,不能作用于电影/游戏屏幕音频。详细证据与 Oopz 未知边界见 `docs/research/browser-screen-audio-quality.md`。 Viewer 本地连接详情只从当前 route 的唯一 inbound screen video/audio stats 读取同步证据:`audio estimatedPlayoutTimestamp - video estimatedPlayoutTimestamp` 表示当前发送端时间线差;音视频 jitter-buffer delay 取相邻窗口累计 delay/count 的比值;音频 concealment 取相邻窗口 concealed/total sample 与 event 增量。每个媒体种类用 stats id、SSRC 和 track identifier 共同守住区间身份,换流或累计计数回退时先重新基线,字段缺失或分母为零时保持未知。该证据不进入 signaling、质量上报或持久化,也不设置 `jitterBufferTarget`、Web Audio 或任何媒体控制参数;真实同步仍由同一持久 `MediaStream`/媒体元素和 WebRTC playout 负责。 @@ -188,9 +184,7 @@ Viewer 本地连接详情只从当前 route 的唯一 inbound screen video/audio 质量自适应采用 ADR-0007 的 built-in-first 路径隔离双表示,而不是全房间按最差链路降档。Direct/peer 保留每条 PeerConnection 的 stock WebRTC GCC。每条 SFU path 从 `HIGH` ceiling 开始;优先发布共享 `HIGH+LOW` 并让 LiveKit BWE 独立降层和恢复。若该 gate 通过,Screener 不实现媒体层 selector。应用证据只决定下述 parent/leaf 拓扑资格与诊断。`LOW` 可以常驻;关闭空闲层只是资源优化。active representation/layer 数量硬限制为二,不能按 viewer 增长。标准浏览器不保证跨 PeerConnection 共用一个物理编码器,inactive 层也不保证释放 GPU;实际层数、实例数、CPU/GPU、游戏 FPS/p1 low、区间 encode cost、上传与释放行为必须实测。若没有合格 hardware/power-efficient 路径或额外表示破坏游戏/上传预算,则 fail closed,保留健康路径并明确显示弱路径降级或失败。 -单个三窗确认的 C+B 事件仍是 edge-local,`severe` 与 `relative-fps` 都先且只尝试一个 active upstream 为 peer 或 SFU 的 Viewer;Host 排除,候选的 session/share/revision 必须当前,且 active children 加一不得超过服务端有效 relay capacity。服务端在现有 route prepare 上保留旧 active edge 与 connection generation;新 parent 在 active child IDs/map 外只建立一个 provisional `HostPeer`,answer/candidate 按 exact child/connection/revision 优先路由到该 PC。Viewer 以当前 prepare revision 的正 RTP、正 decoded frames 和 live video track 证明后,matching active revision 才把同一个 PC 原子移入 active map,再执行普通 child reconcile 并一次性提交拓扑、route 与候选 connection ID;provisional 禁止用新 connection ID 自动重试。prepare-ready 后的候选失败必须保留精确 revision/parent/connection identity 到 matching active 或 rollback;候选 parent 对同 revision 的重复 matching active 持续 fail-closed,不 promotion、不建新 PC,也不越权发送 `route-failed`,恢复只由 target Viewer probe 发起。其他 edge 的已鉴权 hard `route-failed` 先 abort soft probe 并让 target/candidate 回到 current authority,再立即进入原故障梯级。候选失败、超时、session/share 变化或无候选均回滚/保留旧边,不创建 SFU/TURN intent。同房同时最多一个 peer migration。若同一 Viewer parent peer/session/share 在五秒内由两个不同 child 分别确认任一类型,服务端把该 parent 的独立 `relayEligible` 暂停 30 秒,使拓扑有效容量为零但保留客户端广告值;后续 capacity 广播不能绕过。Host 不参与,parent 自身 upstream/观看不变;本 slice 只保证触发 child 的 MBB,其他 current children 暂沿既有有界重选,完整 sibling MBB 后续实现。到期惰性恢复,parent 新 session 或新分享立即恢复,stop/delete/disconnect 清理状态。该离散资格不命令 LiveKit/WebRTC 媒体层,也不把 RTT、CPU、码率、设备信息压成综合分数;窗口仍需真实质量矩阵校准。 - -上述 two-distinct-child corroboration 仅在未来独立验收并接受更高 relay-capacity tier 后有效;当前 Browser1 release 使其不可达,且不授权第二 child。 +质量证据保持 edge-local,只能影响证据覆盖的 parent/child authority,不能形成全房综合分数。Current source 已有 Viewer 与 Host provisional-parent、媒体证明后 promotion、失败保留旧边、坏 relay corroboration 和 bounded quarantine 等机制;整体模型必须统一 candidate eligibility、两 child 关联证据、sibling migration、migration mutex、capacity reservation 与 fallback 交互。无论最终策略如何,prepare/commit/rollback 都保留 exact session/share/revision/connection identity,ICE connected 不能替代 live track、正 RTP 和 decoded-frame progress。 状态决策必须关联同一时间区间和媒体代次的三段证据: @@ -208,11 +202,11 @@ Viewer 可通过已鉴权、绑定当前房间/路径代次的控制面报告需 SVC 的标准能力门禁已经静态关闭为 `no-go-web-svc-cross-path-hardware-contract`。WebRTC-SVC 只配置单个 outgoing sender,direct/peer receiver 没有对应的按层选择 API,独立 PeerConnection 之间也没有 shared-encode 契约;requested/applied codec/`scalabilityMode`、Media Capabilities `powerEfficient` 与 RTCStats encoder 字段不能证明编码器一定是硬件。Pinned LiveKit 虽能在 SFU 内为每个 subscriber 设置最大空间/时间层请求,但实际选择依 codec/selector 而异:server 1.13.5 只为 VP8 安装 temporal selector,当前 H.264 `q,h` simulcast 只有空间层选择。Client 2.22.0 还会把 screen-share SVC 强制成三层 temporal-only `L1T3`,既没有低分辨率 base,也超过最多两层的门槛;这种 SFU 选层也不延伸到 direct/peer descendants。当前静态结论不运行 browser harness。未来只能由能明确锁定硬编、最多两层、跨 direct/peer/SFU 复用同一输出且通过游戏性能/延迟门槛的 native sender 以新决策重开。普通非分层编码包不能只靠转发生成另一档画质,实现路径差异只能用第二表示、合格的 scalable layers 或 relay/SFU 转码。 -质量控制不放宽媒体拓扑预算:host 仍最多两条下游媒体边;native relay 只转发选定表示的编码包,不解码重编码。后续双树或条带化可把 host 上传从约两份完整码率推向一份加必要冗余,但不阻塞路径隔离双表示的实现。 +质量控制不放宽 ordinary media topology 预算:所有非服务器端点继续受统一静态 downstream capacity 约束;native relay 只转发选定表示的编码包,不解码重编码。后续双树或条带化必须另行验收,不能绕过当前 graph、identity 和 capacity invariant。 应用不得稳定保留经交叉证据确认的 `FALLBACK` parent。SFU BWE 可能先自主 downshift;该观测进入 `suspect`,跨有界窗口确认后再迁走 children 并撤销 capacity,不能承诺 packet-level 先迁后降。Root-with-children gate 必须限制 descendants 的短暂影响。计划或显式质量 fallback 只在内建路径失败后考虑,并先在 assignment/connection generation guard 下迁走全部 children;失败或 stale 操作保持原 assignment 与 quality。更长恢复窗口和 cooldown 后才能重新发布 capacity。Viewer 自报不能单独改变拓扑。详细 wire 由后续 routing owner 持有。 -标准表示能力排在任何自定义双表示媒体实现之前。当前 Web P2P simulcast 因 receiver 不能选 RID 且跨 PeerConnection 无 shared-encode 契约而静态 no-go;当前 Web/LiveKit SVC 因 direct/peer 无独立选层、硬件不可证明且 pinned screen share 为 `L1T3` 而静态 no-go。Pinned LiveKit 的标准 exactly-two screen-share simulcast 已显式配置 `dynacast: false` 并通过 roots <=2、Host publication 上传边记账和 zero-descendant assignment/subscription 保留的 source preflight;Dynacast off 阻止 pinned SDK 按 subscribed-quality 更新切换 publisher encoding,不关闭 SFU per-subscriber BWE,也不保证实际收到 `HIGH`。下一 gate 在隔离整形下验证 zero-descendant SFU leaf 从 `HIGH` maximum 由 SFU BWE 独立降层和恢复;通过后不实现自定义媒体 selector。Root-with-children 的 suspect/确认/有界迁移是 default-on 前硬门。只有内建选择不满足门槛时才显式设置标准 subscriber quality;常驻成本违反硬件、游戏或上传预算时才测标准 sender 手动启停,全部标准路径失败后才进入 custom/native dual encode。当前 subscriber 未 attach `RemoteTrack`,因此不直接启用 `adaptiveStream`,但这不阻塞 SFU BWE leaf 候选。Cumulative Dynacast-on 只作为另一种 always-on 形态。本 SVC 静态审查不跑 browser。具体门槛由 ADR-0007 和[实时质量调研](./research/realtime-quality-adaptation.md)持有,任何方案都不得复用 PR #28 的 minimum-of-two 或绕过 stock WebRTC congestion control。 +标准表示能力排在任何自定义双表示媒体实现之前。当前 Web P2P simulcast 缺少 receiver 选 RID 与跨 PeerConnection shared-encode 契约;当前 Web/LiveKit SVC 也缺少 direct/peer 独立选层和可移植硬件保证。Production 的 exactly-two screen-share simulcast 与 `dynacast: false` 只是实现和质量实验事实,不证明 root topology、publication accounting 或 subscriber model。具体表示门槛由 ADR-0007 和[实时质量调研](./research/realtime-quality-adaptation.md)持有,不得绕过 stock WebRTC congestion control。 ## 移动分享端能力边界与排期 @@ -234,78 +228,49 @@ Cast sender/receiver SDK、不建转码或自有投屏协议。无论输出到 ## 媒体传输配置 -当前仓库已经删除旧全房 TURN contract 和被拒绝的 participant-wide built-in -config、issuer、capability、`refresh-ice`/`ice-config` wire 与客户端传播。 -Production `ServerConfig` 必须配置 `STUN_URLS`;旧 `PEER_ICE_TURN_*` 即使为空也 -启动失败,且绝不是 selected-edge tuple 的 alias。当前 source 已加入完整可选的 -`SELECTED_EDGE_TURN_*` tuple、controller consumer 与 -Host/ViewerRelay/Viewer rebuild;部分配置、非法 UDP TURN URI、弱或复用 secret、越界 -TTL 都启动失败。Production 已配置该 tuple,但尚未通过真实 relay-media 验收。 -旧 `TURN_URLS`、`TURN_SHARED_SECRET`、`TURN_CREDENTIAL_TTL_SECONDS` 继续 -fail-fast,绝不作为 alias。 +Production `ServerConfig` 必须配置 `STUN_URLS`。所有 ordinary direct/peer +`RTCPeerConnection` 的 authenticated snapshot 只包含 STUN;不得把 TURN +candidate 预发给 participant、房间或普通 peer edge。HTTPS/WSS 始终使用 TLS/TCP, +不属于媒体 transport 策略。 -普通 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 -配置。pending 超时、失败或绑定变化清理 attempt;answer 后签发 revision 保持不可改写, -controller 另持 current route revision。只有 exact edge/session/share 与 parent downstream -budget 仍成立时才先发 carry、再发新的 active authority;否则撤销该 edge owner。普通 PC、其他 edge、 -Source-only Native 与 authenticated snapshot 继续 STUN-only。凭据不进入 URL、日志、browser -storage、room row 或 SQLite。 +`PEER_ASSISTED_MEDIA` 启用同一个 process-wide route controller,但每个房间拥有 +独立的 assignment、session、revision、generation、connection 和 capacity 状态。 +所有非服务器端点读取同一个 deployment downstream capacity,默认 `2`,只接受 +`1`、`2` 或 `3`;upstream receive 免费。服务端是权威,客户端广告只能收紧, +不能按 Host/Viewer、浏览器、UA、设备或页面状态放宽或分叉。 -该 selected edge 不改写 active SFU assignment 或另建 route owner。未 answer 的 -negotiating attempt 与同房其他 soft MBB 串行;answer 后 exact lease 成为绑定有效期间的 -长期 active transport authority,不是房间级迁移锁,可与无关 soft MBB 共存。两种状态下 -presence 的 active upstream 仍为 `sfu`,本地 ICE path 才报告 TURN transport。签发前按当前 assignment -重新计算 parent 下游预算,Host 的 SFU publication 也占一边;Host 或 ViewerRelay -两边已满且 target 不是现有 child 时 fail closed,不替换无关 child。 +每个 candidate 和异步结果都必须绑定 exact room、share generation、endpoint +sessions、route revision、assignment generation、connection identity,以及存在时的 +publication generation。迁移保留旧 active media;只有新 candidate 的 live track、 +正 RTP 与 decoded-frame progress 都属于当前代时才原子 commit。ICE +`connected`、token 签发或 assignment 本身都不是媒体证明。失败、超时、stale +identity 或授权撤销销毁 candidate,保留或恢复最后一个有效 route。 -普通 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。 +SFU 与 TURN 是可选、有界、独立验收的 server-assisted resources。以下内容必须在 +ADR-0005 的整体模型中同时定案,本文不预设答案: -当前可执行梯级是 `direct/peer UDP -> SFU ICE/UDP -> selected-edge TURN -> failure`。 -Production 已配置整条梯级,但仍缺真实 SFU/TURN 媒体验收。Viewer 的 -现有两次有界 ICE restart/rebuild 后报告 route failure;router 先尝试局部 -reparent,再在 root budget 内准备 SFU,最终没有可行 route 时返回明确的 -`PEER_NOT_FOUND`,不长时间维持伪连接。HTTPS/WSS 始终使用 TLS/TCP,不 -属于媒体 TCP;clarity/balanced/fluid 只改变编码策略。 +- SFU publisher 位于 Host、relay parent 或其他明确层级时的 owner 与 generation; +- subscriber 是分布式 root、受限 leaf 或其他 endpoint 时的准入; +- selected TURN 承载 ordinary edge、SFU ingress 或其他 edge 的授权边界; +- endpoint ordinary capacity、server ingress/egress、SFU、TURN 与 provisional + overlap 的分别记账;以及 +- Host、relay parent、leaf 受限时的 exact fallback、downgrade、sibling migration + 和 bounded failure sequence。 -旧 release 需要 coturn TURN/UDP+TCP,目前仅作为独立回滚实例保留;current -production 不根据 room allowlist 保留旧 release TURN 兼容分支。 -`PEER_ASSISTED_MEDIA` 是 process-wide topology/SFU route 开关;启用后每个房间 -拥有独立 controller 状态,普通 peer edge 仍不能默认收到 TURN candidates。 -`PEER_ASSISTED_ROOM_IDS` 已退役,旧变量必须在迁移时删除。因此验证与回滚必须使用 -隔离 candidate/旧实例,不能假设旧 release 与新 transport 共存。 +短期 server-assisted credential 必须严格绑定当前 route identity、只存内存、受 TTL +与 quota 限界,并且不进入 URL、日志、browser storage、room row 或 SQLite。Coturn +只能验证 bearer 的 HMAC 与 expiry,应用仍需逐次重验 room/edge/generation。普通 +SFU 终止 DTLS-SRTP;没有应用层 E2EE 时 operator 可接触媒体,产品必须如实说明。 +所有物理 hop 按[低服务器成本媒体路由](./research/low-server-media-routes.md)逐项计量。 -Selected-edge TURN 复用 `HybridMediaRouter` 的 assignment/revision owner,不新增 -第二个路由器或周期优化器。Coturn 只能验证 HMAC 与 expiry,不能自行验证 room、 -edge 或 generation;应用层必须在签发、parent/child rebuild、signaling/failure 与清理 -边界逐次重验,并用短 TTL、one-use attempt、endpoint fanout 与 allocation/bandwidth -quota 限界。LiveKit participant-wide TURN 是独立 ICE 域,不能冒充 selected edge。 -Coturn bearer 在 TTL 内不可撤销,HMAC 只证明 expiry 与 secret;应用 guard 限制谁能 -收到和使用 rebuild,但不能宣称 coturn 对 room/edge 做了密码学绑定。 - -SFU 正常只作为 virtual parent 向一至两个必要 roots 发布;可靠 root 收到 -媒体后继续服务 sticky descendants。当前 Host 最多两条下游 physical media edges,普通 Browser Viewer/root/relay 最多一条;上游接收不计预算。无法挂到健康 root 的极端 viewers 只能 -通过独立 `E`/central-egress cap 准入。普通 SFU 终止两侧 DTLS-SRTP;没有 -应用层 E2EE 时 operator 可接触媒体,这一产品取舍必须保持可见。所有物理 -hop 按[低服务器成本媒体路由](./research/low-server-media-routes.md)逐项计量。 +Production `9461e20` 的 exact behavior 只由 deployment 记录;新实现仅复用满足当前 +identity、media-proof 和 bounded-recovery 不变量的 scoped code 与 tests。 ## 可观测性 每两秒在本地采集一次 `getStats()`。首版不上传原始统计。折叠详情可在产生该连接的本机显示 selected local/remote candidate 的 `address`/`port`,但浏览器未暴露时保持未知;地址不得进入信令、Viewer C、日志、诊断导出、持久化或路由/质量决策。在 peer 边中它只是当前连接端点,在 SFU 边中通常是 SFU 端点,不能标成参与者的权威公网 IP。 -Host 与 Viewer 复用同一个连接详情开关;展开后且至少存在一组本地 metrics 时显示下载图标。点击才把当前内存快照投影为 `schemaVersion: 1` JSON 并创建本地 Blob 下载,随后回收 object URL;不上传、不写 browser storage。投影器只按固定字段白名单读取 route/direction、连接状态、protocol/candidate type、codec、分辨率/FPS/码率/loss/jitter/RTT、限制、encoder 和音视频播放/修复指标,所有缺失值写为 `null`。它不接受或输出 candidate address/port、stats/connection/media ID、peer/client/room/name、raw candidate/SDP、STUN/TURN URL、username、credential 或 token。Host 的 peer-relay 接收项必须先经过已有 `ViewerQualityEvidence` 脱敏转换,不能序列化 evidence guard 或原消息。 - -Host 与 relay Viewer 复用同一个纯 presentation reducer 保存远端 `ViewerQualityEvidence`。代际 identity 是 viewer、parent、connection 与 route revision;只有同 identity 且 sequence 严格递增才可承接旧字段,其他输入从新 evidence 重建。每个非空 metric 保存自己的本地观测时间并在五秒边界清空,页面 timer 总是指向最早边界;整块详情不因某个采样缺字段而卸载。`new`/`connecting`/`disconnected` 保留同 identity shell 但立即标为非 fresh,`failed`/`closed` 或 identity/session/share/route 变化清空。诊断导出只读取 fresh presentation;该层不增加 wire 字段、服务端状态或 `mediaGeneration`。 +Host 与 relay Viewer 复用同一个纯 presentation reducer 保存远端 `ViewerQualityEvidence`。代际 identity 是 viewer、parent、connection 与 route revision;只有同 identity 且 sequence 严格递增才可承接旧字段,其他输入从新 evidence 重建。每个非空 metric 保存自己的本地观测时间并在五秒边界清空,页面 timer 总是指向最早边界;整块详情不因某个采样缺字段而卸载。`new`/`connecting`/`disconnected` 保留同 identity shell 但立即标为非 fresh,`failed`/`closed` 或 identity/session/share/route 变化清空。任何依赖当前状态的消费者只读取 fresh presentation;该层不增加 wire 字段、服务端状态或 `mediaGeneration`。 质量诊断必须能按顺序区分:捕获源设置、sender 实际参数、CPU 或带宽 `qualityLimitationReason`、selected candidate/TURN 路径、outbound 与 inbound 差异,以及接收端 dropped/freeze。当前有界实现显示 sender 参数的 requested/applied 读回;同一个非 `none` 原生 `qualityLimitationReason` 连续出现三个采样周期后显示单条原因提示,原因变化、缺失或恢复为 `none` 即重新计数。这只是去抖后的观察,不组合评分、不触发调档或迁移。完整跨路径分类仍由后续真实矩阵验收;首轮使用浏览器 WebRTC internals 导出和一份脱敏 run manifest,不新增服务端遥测。 @@ -357,7 +322,7 @@ Host 与 relay Viewer 复用同一个纯 presentation reducer 保存远端 `View - SiteAccess 的 production 必配、8 至 128 visible-ASCII bytes/独立 secret、local-dev 显式关闭、12 小时签名 cookie、过期、篡改、cookie 属性、已移除 access key fail-fast 和无服务端 session 状态; - RoomStore 的随机临时房间、TTL、Host token/Viewer grant 摘要验证、private/public policy、grant 到期/跨房拒绝、rotate/revoke 强撤销、停止分享等待、Viewer 上限和超额拒绝; - SQLite 房间库从 `1` 自增、重启恢复、v1/v2 -> v3 单事务迁移、nullable 32-byte grant 与 48-byte password material CHECK、畸形值 fail-closed、rotate/revoke/password 写失败原子性和数据最小化;数据库路径无 site access 时启动失败; -- 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` 保持独立边界; +- STUN/TURN UDP URL 严格校验、生产 STUN 必需、server-assisted tuple 完整性/TTL/secret 独立性、issuer HMAC/expiry/opaque tag、当前 route/session/revision/connection generation guard,以及 ordinary peer、未授权房间和 Native 的 STUN-only isolation;LiveKit `sfu-config` 保持独立边界; - 单一 `screener-v5` 信令 literal、v3 旧页面 mismatch 的一次性刷新终态,以及协议非法 JSON、未知消息、错误方向、跨房间目标和 payload 限制; - 质量协议的 host 授权、严格字段与数值范围、last-wins、当前/未来 relay、已配置 SFU 和普通 P2P wire 兼容;sender 的四字段 requested/applied 读回、拒绝与持续受限提示;screen audio SDP helper 覆盖 CRLF、existing/missing fmtp、unknown key preservation、幂等、payload number、rejected/non-audio,以及 empty/no-Opus/ambiguous/parser 异常原对象 fallback,`ViewerPeer` 覆盖 initial/restart/rebuild 时同一结果的 local/signaling 一致性,SFU publisher 精确断言 high-quality stereo/forceStereo/DTX 且不覆盖 RED; - HTTP/WS 集成:nginx per-source site-access `limit_req` 与统一错误不泄露 secret,应用不建立 IP limiter 状态;Viewer 可在 Origin/容量门下 upgrade;Host 首条鉴权同时要求 upgrade-captured site access 与 Host token,Viewer 要么持 exact-room grant,要么先有 site access 再按 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 拒绝; @@ -368,7 +333,7 @@ Host 与 relay Viewer 复用同一个纯 presentation reducer 保存远端 `View - 同机双标签页与同 LAN direct; - 真实跨网络、同房间 direct/peer UDP + SFU/UDP root; -- 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; +- 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; - 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 未完成前不得把默认上限写成性能承诺。 @@ -383,48 +348,26 @@ Host 与 relay Viewer 复用同一个纯 presentation reducer 保存远端 `View 3. 完成屏幕捕获、逐观看者 PeerConnection、观看播放和手动质量档。 4. 完成连接恢复和 stats 面板;随后以 STUN-only snapshot 替换旧全房 TURN credential/refresh wire。 5. 完成 automatic peer/SFU controller,并让 room `1` 仅作为历史 smoke fixture。 -6. 可选 selected-edge TURN tuple、revision-bound one-use grant 与 parent/child rebuild 已在 source 和 production 配置闭环;下一功能验收只需一条 forced relay 媒体 smoke,完整网络/资源 benchmark 后置。ordinary STUN-only、SFU/UDP、coturn rollout 仍保持独立边界。 +6. 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、第二表示或自定义降级算法。 任何一步只实现当前消费者需要的接口。不得为未来账号、第三种媒体拓扑或原生 sender 预建抽象层。 -## 2026-08-19 Peer-Assisted 实验边界 +## 自动路由设计 -历史状态(2026-08-19):Proposed / Experiment Only。后续 ADR-0005 已接受 -bounded controller,且 room `1` 曾作为 exact smoke;下列内容保留为 -最初实验门槛,不代表当前 rollout 状态。 +服务端维护每个房间唯一的版本化 route owner。候选 parent 可以形成图,但 committed media graph 必须无环;Host 没有 upstream,每名 Viewer 同时只有零或一个 active upstream。加入、容量释放、节点离开、当前边失败和已接受的 path-health evidence 才能触发一次有界重选,未受影响分支保持 sticky。 -当前部署设置 `PEER_ASSISTED_MEDIA=true` 后,所有正常房间进入 -`HybridMediaRouter`、房间质量同步和 LiveKit 兜底;每个房间的 route 字段、定向 -信令、停止、重连和删除状态仍独立。`PEER_ASSISTED_ROOM_IDS` 已退役并在启动时拒绝, -不能以旧变量隐式限制到 room `1`。所有 ordinary peer PC 与 source-only Native 仍保持 -STUN-only;只有 controller-selected edge 可接收一次短期 grant。该边界不增加 UI、 -用户选择或百分比框架。 +所有非服务器端点共享一个 server-authoritative ordinary downstream capacity:默认 `2`,只允许静态配置 `1`、`2` 或 `3`,upstream receive 免费。客户端 capability 只能收紧该值,不能按角色、浏览器、UA、设备或页面可见性建立另一套 policy。代码通过一个 named config 和 shared guard 执行,不在 Host、Viewer、active、recovery 或 migration 分支重复 magic number。 -当前 release policy 的硬目标是 Host 任何时刻最多两条下游 physical media edges、普通 Browser Viewer 最多一条;普通 upstream receive 不计上传预算。系统按一个或两个观看者直接 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,并为所有正常房间配置同一 controller 和 replacement selected-edge tuple;旧 `PEER_ICE_TURN_*` 配置启动失败。公网 SFU media、selected relay 与资源仍未验收,旧 release/coturn 仅作回滚。ADR-0004/0005 的媒体验收尚未完成。 +普通 Web relay 把收到的 remote track 加入独立 downstream `RTCPeerConnection`,因此可能逐跳解码和重编码;浏览器不保证跨连接 shared encode。每条 ordinary child 是独立网络副本,实际 encode time、CPU/GPU、bitrate、loss、frame rate、depth 和 recovery 必须测量。 -恢复目标按证据和成本逐级升级:短暂 `disconnected` 先观察同代 bytes/stats,`failed` 才执行一次 ICE restart;仍失败时重建一次相同 parent,再尝试不同的合格 peer parent,peer 候选耗尽后才申请 SFU。每个动作最多一次且成功即停,不实现“三次相同重试”。初始 `connecting`/`checking` 的软提示和硬窗口尚未实现;硬窗口必须先用移动网络实测确定,不能直接复用现有 3 秒恢复常量。Viewer SFU root 只刷新一次短期 grant,随后至多重建一次原 failed parent selected edge;Host publisher/global SFU failure 维持 bounded peer failback。Host 只保留当前 route revision 的 `connect | source | video-publish | sender-config | audio-publish | transport` 枚举阶段作为本地事件提示,不上传原始错误、URL、token、candidate 或地址。 +迁移使用 exact room/share/session/revision/assignment/connection identity。旧 route 保持 active,candidate 在 active map 外准备;只有当前代 live track、正 RTP 与 decoded-frame progress 同时成立时才原子 promotion。失败、超时、代次变化或授权撤销销毁 candidate 并保留或恢复旧 route。Make-before-break 是否有 overlap 空间由统一端点容量和待定 server-resource model 同时决定。 -当前部署实现一个由服务端确定的广度优先活动拓扑:Host 最多两个 child,普通 Browser Viewer 最多一个 child。服务器可在多个合格 parent 中选择,但每名 viewer 同时只有一个 active upstream,活动媒体图保持无环。新 viewer 加入或容量释放时只接纳等待节点;节点断开或当前 edge 失败时只重新分配失去 parent 的直接 subtree root,其 descendants 保持连接。客户端先关闭不再分配的旧 edge,再建立替代 edge;每次变更推进 route revision,stale signal/recovery 不得复活旧边。该规则不采集或组合 RTT、带宽、CPU、地理位置与质量评分,也不主动重平衡、多源接收或持续优化。没有 parent 时明确等待或拒绝,Host 不能创建第三条边,Browser Viewer 不能创建第二条边。 - -当前 Host2/Browser Viewer1 形成两条有界转发链;它满足发布资源边界,但不证明八人质量、更不证明 20 人能力。目标 20 人仍必须实测逐跳重编码、上行、延迟和故障半径,或使用受独立中央 egress cap 约束的 SFU 例外。未通过时保持当前默认八人,不增加 Host fanout 或绕过 Viewer 上限来凑容量。 - -所有边都继续使用标准 WebRTC。每名普通 Browser Viewer 播放收到的 remote `MediaStream`,并可把同一组 remote tracks 加入最多一个 child 的独立 `RTCPeerConnection`。该路径会逐跳解码并为 outbound sender 重编码;实验必须记录每层 encode time、CPU/GPU、`qualityLimitationReason`、帧率、分辨率和延迟,不能称为 zero-copy 或 shared encode。 - -部署参数 `MAX_PEER_RELAY_DOWNSTREAM_EDGES` 默认 `2`、只允许 `1/2`,是 release policy 的进一步收紧项:服务端有效 Host 上限为 `min(config, 2)`,普通 Browser Viewer 上限为 `min(config, 1)`。Host 的 active children、SFU publication、provisional/selected physical overlays 共用 Host 预算;Viewer 的 active child 与 provisional/selected physical overlays 共用 Viewer 预算,上游接收免费。旧/恶意 Viewer 广告 `2/3` 仍只能得到一条。Web assignment 执行层也用同一组当前 Host2/Viewer1 named constants 截断 child IDs,再创建 active、provisional 或 selected PC;这只提供客户端 defense-in-depth,服务端仍是路由权威。部署收紧到 `1` 时,SFU fallback 必须先把占用 Host 唯一 peer slot 的必要 branch 提升为 root,再建立占一条预算的 Host publication;root 预算独立且仍最多二。wire 不升级,`relay-capacity` 的 `0/1/2/3` 仅保留为未来 capability envelope;当前 Web client 广告 `1`。动态 evidence-driven capacity 仍须独立验收,不按 device/UA 分支,并由持续 sender bitrate、`availableOutgoingBitrate`、区间 encode time、`qualityLimitationReason`、loss/RTT 健康窗与 cooldown 共同限界。H.264、hardware encode 或 shared encode 可降低编码成本或同观感码率,但每个 child 的网络副本仍独立存在。 - -Peer-assisted 信令只接受一个严格对象:`resolution` 为 `720p | 1080p | 1440p`,`maxFramerate` 为 15 至 60 的整数,`maxBitrate` 为 2,000,000 至 12,000,000 的整数,`degradationPreference` 为 `maintain-resolution | balanced | maintain-framerate`,可选 `videoCodec` 为 `automatic | h264 | vp8`,可选 `screenAudioQuality` 为 `saver | music | very-high`,其余字段和越界值拒绝。服务端用房间内存 Map 保存完整设置,默认 1080p60、8 Mbps、`balanced`、`automatic`、`music`(128 kbps),旧的缺失音频字段也解析为该默认,不写 SQLite;停止分享时保留,房间被放弃或过期时删除。只有 peer-assisted host 可更新,鉴权快照和在线广播携带同一设置;本 wire 使用 `screener-v5`,普通 P2P 严格鉴权结构不变。SFU publisher 复用同一设置。 - -Viewer 在处理 peer-assisted 鉴权快照时先更新 `ViewerRelay` 的 desired settings,再应用拓扑分配;同一个串行队列协调 remote stream、当前 child 更新和以后重建的 child,因此它们读取同一最新目标。每个 `HostPeer` 也把初始 sender 配置、`replaceStream()` 和 `updateProfile()` 放进同一 sender-mutation queue;desired settings 在入队前同步更新,已排队操作执行时读取最新值,使快速连续修改保持 last-wins,而不增加版本号或确认协议。这里同步的是浏览器 sender 的目标上限,不是实际分辨率、帧率、码率或 shared encoding 的性能保证。 - -spike 复用现有 screen stream 和浏览器 codec 协商,当前普通 Web relay 不按桌面、Android 或 iOS UA 拆分 capacity;房间接入默认八名、显式允许 1 至 16,代表性资源/质量门禁仍按八名执行。捕获存在时沿同一标准 WebRTC 路径带上音频。移动真机表现作为非阻断兼容观察;Encoded Transform/DataChannel/WebCodecs 媒体、SVC/simulcast、多树、转码、codec ladder、综合评分、移动端后台 relay 和跨模式 SFU 迁移不在本次 spike 内实现。该范围限制不能解释为拒绝产品所需的自动兜底;验收与失败即放弃条件以 ADR-0004 为准,自动路由由后续独立 ADR 定义。 - -浏览器不保证多个 `RTCPeerConnection` 共享一次编码。实验允许 host 对一至两个 seed 各自编码。后续 native sender 必须让每个 active representation 只编码一次并跨 direct、peer 与 SFU transport 复用;每条连接仍拥有独立 packetizer、SSRC、pacing、加密、反馈和上传。健康路径选择共享 `HIGH`,最多另有一份弱路径共用的 `LOW`,是否常驻由硬件、游戏和上传门槛决定。该计划不依赖本次浏览器 relay 实验成败,也不在本 spike 中提前抽象或实现。 - -详细证据见 `docs/research/peer-assisted-media.md`,提案见 `docs/adr/0004-peer-assisted-media-experiment.md`。 +SFU/TURN 的 publisher、subscriber、selected transport、central admission、resource accounting、temporary overlap 和 exact fallback sequence 仍是一个不可拆分的设计项。普通端点 child count 不能隐式成为 server cap,兜底也不能形成无界 whole-room fanout。Host、relay parent 或 leaf 受限时,只执行 ADR-0005 最终接受的分支;定案前不从旧 root 数、publication 公式或 lease 规则推导实现。 +ADR-0004 只保留 browser relay、re-encode 和测量门槛的历史证据。Production `9461e20` 是 rollback reference;新实现只按当前 invariant 迁移可复用的 exact-generation、media-proof 和 bounded-recovery 代码。 ## 官方与参考资料 访问日期为 2026-08-18 至 2026-08-21: diff --git a/docs/需求理解.md b/docs/需求理解.md index c303b00..277659c 100644 --- a/docs/需求理解.md +++ b/docs/需求理解.md @@ -1,94 +1,90 @@ # 屏幕共享项目需求理解 -> 2026-08-22 truth audit hold: 本文件的 checkbox 是验收条款组织方式,不是可直接执行的 backlog。容量、SFU publication/subscription、TURN、fallback、临时资源计数相关段落含已部署事实、历史假设与未决语义;在 [TODO audit hold](./todo-audit-hold.md) 解除并原位更新前,不得据此实现、回滚、合并或清理。 - ## 2026-08-18 13:31 ## 需求概述 -### 2026-08-21 路由范围修订 +### 当前路由不变量 -`PEER_ASSISTED_MEDIA=true` 是旗舰部署的单一总开关,启用后所有正常房间都使用同一个 direct/peer UDP -> SFU/UDP -> selected-edge TURN 决策控制器;每个房间仍独立保存 route revision、会话代次、失败状态和容量预算。`PEER_ASSISTED_ROOM_IDS` 已退役,旧变量必须在迁移时删除,不能再把 room `1` 当作功能开关。room `1` 只保留为历史 smoke fixture。普通 peer 连接继续 STUN-only,TURN 仍只向 controller 选中的受限 edge 短期发放。 +`PEER_ASSISTED_MEDIA=true` 是旗舰部署的单一总开关;每个房间独立保存 route revision、会话代次、失败状态和容量预算。系统优先使用 direct/peer UDP,普通 peer 连接保持 STUN-only。所有非服务器端点使用同一个服务端权威下游容量:默认 `2`,只允许静态配置 `1`、`2` 或 `3`;上游接收不消耗该预算,角色、浏览器、UA、设备或页面可见性不产生特例。 -构建一个面向熟人游戏场景的低延迟屏幕共享工具。分享者可以在私密房间内分享游戏、应用窗口或显示器画面及声音,多名朋友可以即时观看。系统采用类似 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/原生增强;观看端必须可通过桌面或手机浏览器打开邀请链接,不绑定专用客户端。 +构建一个面向熟人游戏场景的低延迟屏幕共享工具。分享者可以在私密房间内分享游戏、应用窗口或显示器画面及声音,多名朋友可以即时观看。中心服务负责房间、身份、确定性路由和建立连接;每名 Viewer 同时至多一个 active upstream,活动媒体图保持无环。切换必须绑定精确身份与代次,保留旧边直到新边出现当前代 RTP、解码帧和 live track,所有路线最终进入有效媒体、明确等待或有界失败。系统自动完成分配和恢复,用户不手动选择拓扑。分享端 Web 优先并允许后续 Electron/原生增强;观看端使用普通桌面或手机浏览器。 -当前 PoC 默认允许一名分享者和最多八名观看者,部署者可在 1 至 16 之间配置上限,超额观看者会被明确拒绝。已接受的自动分布式拓扑目标把默认值和可配置上限提高到 20,但只能与通过 20 人资源、质量、深度和恢复门禁的路由 release 原子启用;在此之前不得只改常量,让普通逐观看者 P2P 建立 20 条 Host 边。产品仍不承担公开直播或大规模观看,相关需求交给 OBS/Twitch 等成熟直播服务。 +SFU/TURN 的 publication、subscription、transport、server-resource accounting、临时 overlap 和具体 fallback 序列必须作为一个整体另行定案,不能从普通端点 child 数量推导。SFU 与 TURN 只能是有界兜底资源,不能把产品改成无界或 always-SFU 的整房转发。当前 PoC 的观看人数限制是实现事实,不是性能声明;任何扩容目标必须以真实资源、质量、深度和恢复矩阵另行验收。产品不承担公开直播或大规模观看。 ## 功能需求 ### P0 - 必须实现 -- [ ] 首屏双入口:分享入口通过站点访问后,主工作区必须同等突出“开始分享”和“加入房间”两个主要动作,并用清楚的视觉分隔表达发送与接收;持合法房间 grant 的观看者不需要知道站点访问密码。“加入房间”在当前舞台内就地展开房间码输入并提交,不先跳转到独立页面;直接访问 `/join` 仍提供同一输入能力。不增加营销首页、模式向导或拓扑选择。浏览器标签页使用与现有 `MonitorUp` 品牌语言一致的 favicon。 -- [ ] 简单房间:分享者获得纯数字房间码,但房间码只负责定位。现有 `/r/{code}#v={grant}` 合法未过期邀请继续直接进入;仅有 `/r/{code}` 或 `/join` 房间码的 Viewer 必须先通过站点访问验证,再按房间 policy 进入:public-watch 接受房间码,private room 还需正确观看密码。所有入口只授予该房间 Viewer,不能建房、发布、冒充 Host 或跨房。Host 与 Viewer 标题附近都紧凑、显眼地显示房间号并提供只复制房间号的动作,Host 保留复制完整邀请链接;持久房间码从 `1` 开始自增,其他模式继续使用随机临时房间码。 -- [ ] 免安装观看:桌面和手机观看者可直接通过浏览器链接进入、授权并播放,不依赖与分享端相同的桌面客户端。 -- [ ] 屏幕采集:分享者可主动选择显示器或应用窗口,开始、停止、临时同步暂停音视频和切换分享来源;暂停保留现有连接并禁用当前 capture stream 的全部媒体轨道。服务端按当前分享代次同步这一主动暂停状态,观看端在视频舞台显示“分享者已暂停”,不得从黑帧、track mute 或网络状态推断暂停;晚加入和信令重连使用权威快照,恢复、停止、新分享或会话替换清除旧提示。Host 本地预览必须直接使用 capture stream,支持独立全屏;页面失焦或隐藏时只暂停本地 video element,并明确提示“本地预览已暂停,分享仍在继续”,恢复焦点后继续预览,不得停止 capture、编码或上行。预览不得建立 viewer/媒体边或消耗 TURN、SFU、应用服务器 media egress;预览仍可能产生本地合成、缩放和 GPU 显示成本,不能把隐藏预览称为停止采集或编码。 -- [ ] 游戏声音:Windows Chrome/Edge 目标环境可请求系统音频;没有返回音轨时必须明确提示,而不是静默无声。 -- [ ] 观看端本地音量:播放控制栏提供 `0%` 至 `100%` 音量条和静音按钮,只改变当前浏览器的媒体元素,不修改 Host 音轨、编码、房间状态或其他观看者。切流、换父和 ICE 恢复保留当前页面内的值;刷新后不持久化。静音保留上次非零音量,拖动到非零值自动取消静音。 -- [ ] 本地昵称与房间成员:用户可设置一个只保存在当前浏览器同源 `localStorage` 的显示名,界面统一称为“昵称”;默认以只读文本显示,铅笔编辑按钮紧贴昵称,点击后才进入编辑。Viewer 顶部标题直接显示“{Host 名称} 的屏幕”,不得在下方重复一行 Host 名称。信令只在当前在线 socket 会话内保存和广播 NFC 规范化、最多 24 个 Unicode code points、拒绝控制/换行/双向覆盖符与孤立 surrogate 的纯文本标签,不写 SQLite 或日志。空值使用通用角色 fallback。Web Host 与 Viewer 都必须看到全部当前在线 Viewer 的权威总数与 roster,不能拿最多两条 Host 媒体直连数冒充在线人数;presence 更新也不得创建媒体连接。展示身份默认只显示名称;仅当房内出现同名时追加当前房间 `peerId` 的至少六字符短后缀,并只在同名项后缀碰撞时有界延长。显示名、后缀和 `clientId` 都不是账号、授权身份或全局 UUID,重复名称允许且不得参与授权、路由或质量决策。Web Host 与 Viewer 通过同一 opt-in participant roster 看到 Host 名称;Native 使用同一版本化鉴权 wire,但不发送 presence capability,也不接收 Web roster。 -- [ ] 一对多观看:当前安全默认上限为 8、可配置范围为 1 至 16;自动分布式拓扑验收后,同一 release 将默认值和上限改为 20。当前 PoC 的逐观看者连接是测量基线,不再是超过两名观看者时的目标拓扑;未通过 20 人门禁时继续明确拒绝超额加入。 -- [ ] 端点 fanout 硬上限:当前 release 的 Host 任何时刻最多维护两条 active downstream media edges,普通 Browser Viewer 最多一条;普通 upstream receive 不消耗上传预算。服务端按已鉴权角色执行有效上限,旧版或恶意 Web Viewer 即使广告 `relay-capacity=2/3` 也只能得到一条,部署配置只能把 Host/Viewer 分别从 `2/1` 进一步收紧,不能提高上限。Web 客户端还必须在执行服务端 assignment、建立物理 PC 前按当前角色截断为 Host2/Viewer1;这是服务端回归或版本错配时的 defense-in-depth,不把 wire envelope 变成执行许可。预算计入 active、provisional 和 selected physical overlays,Host 的 active SFU publication 也占一条;不能用 logical replacement 名义临时超额。不按 UA、设备类型或页面可见性另建容量策略;实际 edge 失败进入同一有界换父路径。第三名及后续观看者由系统自动分配给通过验收的 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 必须可用,所有 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;answer 后只在 exact edge/session/share 与 parent downstream budget 仍成立时跨无关 route revision carry,且 carry 必须先于新 active authority。失败、超时或任一绑定/预算失效即撤销;同一房间最多一个 `peer-selected` lease(negotiating 或 answered active)。未 answer 的协商与其他 soft MBB 串行;answered lease 是该 exact edge 的长期 active transport,不是全房迁移锁,可与无关 soft MBB 共存,carry 仍按上述绑定和预算重验。`host-sfu-ingress` 使用独立状态且不占该 last-mile 名额。不得按 participant、房间或普通 peer PC 下发,source-only Native 保持 STUN-only。凭据不进入 URL、日志、浏览器持久化或 SQLite。Coturn 只验证 HMAC 与 expiry,应用必须重验 edge 代次并用短 TTL、one-use state 与 quota 限界。媒体路径保持 UDP-only;全部 UDP 路径耗尽后明确失败。HTTPS/WSS 仍独立使用 TLS/TCP。 -- [ ] 质量控制:保留 1080p60(8 Mbps)、1080p30(5 Mbps)、720p30(3 Mbps)三档推荐上限,三档与高级设置初始值统一采用 `balanced`(平衡)与 `automatic`(自动编码),`maintain-resolution`(清晰优先)、`maintain-framerate`(流畅优先)和 H.264/VP8 保留为显式选择;分享期间可直接换档或应用有界高级设置,不重新选择来源或重建健康连接,codec 在直播中锁定并于下一次分享生效。Peer-assisted relay 与已配置的 SFU 必须接收同一个严格校验的房间级权威设置,覆盖当前和以后建立的 sender;普通 P2P 信令结构保持不变。任何设置都只是浏览器上限和偏好,具体降级由浏览器决定;必须读回实际 sender 参数并通过 stats 观测结果、显示参数拒绝、改写和持续受限原因。不做自定义动态 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 地址或原始设备/网络标识。 -- [ ] 断线恢复:临时网络变化时按不同层级做一次性恢复,而不是把同一动作机械重复三次。`failed` 边先做一次 ICE restart,再重建一次同 parent 的 `RTCPeerConnection`,随后尝试一个不同的合格 peer parent;只有 peer 路径耗尽才进入 SFU。`connecting`/`checking` 先显示不改变拓扑的慢连接状态,硬超时必须由真实移动网络证据确定,不能为了更快使用服务器而过早放弃最终可成功的 P2P。任一步恢复即停止后续动作;所有异步结果受 session、route revision、assignment/connection generation 约束。active SFU 只允许一次 fresh-token 重试,再失败即有界回到 peer baseline 或明确失败,并向 Host 显示不含 URL、地址、token 或原始错误的失败阶段。 -- [ ] 可观测性:按目标 RTP transport 与不透明连接/ICE 代次关联 selected path、`iceGatheringState`/ICE/connection state、事件推导的建立时长、generation-baselined pair-change delta,以及 RTT、RTP 丢包率、抖动、码率、帧率、编码耗时和受限原因。候选与错误的 wire、日志和导出只使用[低服务器成本媒体路由](./research/low-server-media-routes.md)的脱敏字段和 code bucket,禁止 raw SDP、candidate、地址、URL、`errorText` 或网络标识。只有创建该 `RTCPeerConnection` 的本机可在折叠详情中显示浏览器对 selected local/remote candidate 实际暴露的地址与端口;该值不上传、不持久化、不用于地理/NAT 推断,缺失或被隐私保护时显示未知。 -- [ ] 渐进式诊断:分享者和观看者默认只看到房间、画面、核心状态和常用控制;Host 的 Viewer 卡片只把本机连接或当前接收证据称为“已连接”,其余状态保持中性的“线路分配中”或具体连接状态,不把 active assignment 当作连接成功。展开连接详情也只在当前媒体证据成立后显示媒体方式:`P2P` 使用绿色与直观图标,`SFU` 使用黄色的 `SFU fallback`;实际 relay 时另标 `TURN`,只有本地 relay protocol 可见时才附加 `/UDP` 等实际值。尚未连接时不预告 P2P、SFU 或未知 ICE,只保留中性状态。Host/Peer 上下游仍只在独立的权威拓扑视图中展示。传输协议、候选路径、RTT、视频/音频 RTP 区间丢包率、实际编解码器、音频区间码率/jitter 和视频编码耗时等技术详情由一个明确的“显示连接详情”开关展开。远端 Viewer evidence 的详情 shell 在同一 viewer/parent/connection/route identity 内保持稳定;非空字段从各自最后一次实际观测起精确保留五秒,`connecting`/`disconnected` 只降为非 fresh,`failed`/`closed`、identity/session/share/route 变化或非单调 sequence 则清空/换代。非 fresh evidence 不得继续证明“已连接”或进入诊断导出。音频 codec MIME、RTP clock、channels 与 fmtp 只能来自当前 RTP 对象引用的 `RTCCodecStats`,缺失即未知;`opus/48000/2` 不得写成 48 kHz 音源或实际立体声。隐藏诊断不能隐藏失败原因、音频缺失、持续画质受限或需要用户操作的告警。 -- [ ] 站点访问:生产部署必须配置独立的 `SITE_ACCESS_PASSWORD` 控制建房、Host 信令角色和所有 code-only Viewer 入口;只有本地开发/测试可显式关闭。非空值接受 8 至 128 个可见 ASCII bytes,不能复用 TURN、LiveKit、TLS 或其他部署 secret。`POST /api/site-access` 验证成功签发 12 小时、无服务端会话状态的 HttpOnly、`SameSite=Strict` site-access cookie。有效 room-scoped fragment grant 可绕过站点 gate 直接进入对应房间;其余 `/r/{code}`、`/join` 和 public-watch 必须先持有效站点 cookie,之后才检查房间 public/private policy 和房间密码。失败统一且不得泄露房间是否存在或 policy;建房 HTTP 只认该 cookie。已移除的旧环境变量、endpoint 和 cookie 不保留兼容。不引入账号、JWT、服务端 session Map 或通用 logout。 -- [ ] 房间级观看授权:新房间默认生成含 256-bit 随机量、限时且可撤销的 room-scoped Viewer grant;服务端只保存完整 grant 的 SHA-256 摘要。到期阻止以后鉴权/重连,但不为已鉴权 Viewer 设置踢出定时器;即时清退使用 rotate/revoke。浏览器从 URL fragment 严格读取 grant 后,只写该房间的 `sessionStorage` 并立即用 `history.replaceState` 清除 fragment;刷新和页面内重连可继续。私密房间缺失 grant 时必须先通过站点访问,再提供正确房间观看密码;任一缺失都 fail closed。grant 不得进入 `localStorage`、cookie、query、WebSocket URL、Referrer、日志或错误。Host 可 rotate 或 revoke;revoke 只锁定 grant 路径,已配置的房间密码仍是独立的后续进入方式。新 grant 摘要成功提交后必须清除 Viewer grace/路由、终止旧授权代次的信令和媒体并断开全部旧 Viewer,数据库写失败则不得改变连接。public-watch 是显式、可逆的 room policy,只放宽房间级观看,不绕过站点访问。 -- [ ] 最小持久化:`ROOM_DATABASE_PATH` 可选且必须与 `SITE_ACCESS_PASSWORD` 同时配置。两者都存在时,SQLite 只持久保存从 `1` 开始自增的房间 ID、Host token 摘要、nullable Viewer grant 摘要,以及 nullable 48-byte Viewer-password material(16-byte salt + 32-byte scrypt verifier);数据库 `CHECK` 约束非空 BLOB 长度,不新增用户、成员或 session 表。明文房间密码只短暂存在于当前页面和鉴权/派生调用内存,不进入 URL、cookie、浏览器持久存储、日志或 SQLite。持久房间停止后仍显示等待状态;私密邀请按期限或 Host rotate 更新。旧 schema 行在原子迁移时必须 fail-closed 为没有可用明文 grant 的锁定私密状态,Host rotate 后才能生成新邀请。 +- 首屏双入口:分享入口通过站点访问后,主工作区必须同等突出“开始分享”和“加入房间”两个主要动作,并用清楚的视觉分隔表达发送与接收;持合法房间 grant 的观看者不需要知道站点访问密码。“加入房间”在当前舞台内就地展开房间码输入并提交,不先跳转到独立页面;直接访问 `/join` 仍提供同一输入能力。不增加营销首页、模式向导或拓扑选择。浏览器标签页使用与现有 `MonitorUp` 品牌语言一致的 favicon。 +- 简单房间:分享者获得纯数字房间码,但房间码只负责定位。现有 `/r/{code}#v={grant}` 合法未过期邀请继续直接进入;仅有 `/r/{code}` 或 `/join` 房间码的 Viewer 必须先通过站点访问验证,再按房间 policy 进入:public-watch 接受房间码,private room 还需正确观看密码。所有入口只授予该房间 Viewer,不能建房、发布、冒充 Host 或跨房。Host 与 Viewer 标题附近都紧凑、显眼地显示房间号并提供只复制房间号的动作,Host 保留复制完整邀请链接;持久房间码从 `1` 开始自增,其他模式继续使用随机临时房间码。 +- 免安装观看:桌面和手机观看者可直接通过浏览器链接进入、授权并播放,不依赖与分享端相同的桌面客户端。 +- 屏幕采集:分享者可主动选择显示器或应用窗口,开始、停止、临时同步暂停音视频和切换分享来源;暂停保留现有连接并禁用当前 capture stream 的全部媒体轨道。服务端按当前分享代次同步这一主动暂停状态,观看端在视频舞台显示“分享者已暂停”,不得从黑帧、track mute 或网络状态推断暂停;晚加入和信令重连使用权威快照,恢复、停止、新分享或会话替换清除旧提示。Host 本地预览必须直接使用 capture stream,支持独立全屏;页面失焦或隐藏时只暂停本地 video element,并明确提示“本地预览已暂停,分享仍在继续”,恢复焦点后继续预览,不得停止 capture、编码或上行。预览不得建立 viewer/媒体边或消耗 TURN、SFU、应用服务器 media egress;预览仍可能产生本地合成、缩放和 GPU 显示成本,不能把隐藏预览称为停止采集或编码。 +- 游戏声音:Windows Chrome/Edge 目标环境可请求系统音频;没有返回音轨时必须明确提示,而不是静默无声。 +- 观看端本地音量:播放控制栏提供 `0%` 至 `100%` 音量条和静音按钮,只改变当前浏览器的媒体元素,不修改 Host 音轨、编码、房间状态或其他观看者。切流、换父和 ICE 恢复保留当前页面内的值;刷新后不持久化。静音保留上次非零音量,拖动到非零值自动取消静音。 +- 本地昵称与房间成员:用户可设置一个只保存在当前浏览器同源 `localStorage` 的显示名,界面统一称为“昵称”;默认以只读文本显示,铅笔编辑按钮紧贴昵称,点击后才进入编辑。Viewer 顶部标题直接显示“{Host 名称} 的屏幕”,不得在下方重复一行 Host 名称。信令只在当前在线 socket 会话内保存和广播 NFC 规范化、最多 24 个 Unicode code points、拒绝控制/换行/双向覆盖符与孤立 surrogate 的纯文本标签,不写 SQLite 或日志。空值使用通用角色 fallback。Web Host 与 Viewer 都必须看到全部当前在线 Viewer 的权威总数与 roster,不能拿最多两条 Host 媒体直连数冒充在线人数;presence 更新也不得创建媒体连接。展示身份默认只显示名称;仅当房内出现同名时追加当前房间 `peerId` 的至少六字符短后缀,并只在同名项后缀碰撞时有界延长。显示名、后缀和 `clientId` 都不是账号、授权身份或全局 UUID,重复名称允许且不得参与授权、路由或质量决策。Web Host 与 Viewer 通过同一 opt-in participant roster 看到 Host 名称;Native 使用同一版本化鉴权 wire,但不发送 presence capability,也不接收 Web roster。 +- 一对多观看:房间 admission 与端点下游容量分别配置,超额加入必须明确拒绝。Production 当前默认 8、显式接受 1 至 16;未来默认值和上限是独立产品决策,必须经过相应资源、质量、深度和恢复验收,不能从端点 cap 或一次短跑推导。 +- 端点下游容量:所有非服务器端点使用同一个服务端权威值,默认 `2`,只允许静态配置 `1`、`2` 或 `3`;上游接收不消耗该预算。客户端只能声明不高于部署值的可用容量,角色、浏览器、UA、设备类型和页面可见性不产生不同上限。active ordinary children 与获准的 provisional ordinary children 必须在切换全过程受同一规则约束。SFU/TURN publication、subscription、transport、server-resource 和临时 overlap 另由整体路由模型定义,不能从普通 child 预算推断,也不能以替换名义无界超额。 +- P2P 优先:每条普通逻辑媒体边先尝试 direct/peer UDP,服务器不得默认中转可直连媒体。更多观看者优先沿满足统一端点容量的无环 peer 图分布;每名 Viewer 同时至多一个 active upstream,失败时只影响需要重选的分支。 +- NAT 与传输边界:STUN 必须可用,所有 ordinary peer `RTCPeerConnection` 只接收 STUN ICE servers,普通边不得预发 TURN candidate。所有恢复和候选授权必须绑定 exact room/share generation、endpoint session、route/assignment generation 与 connection identity;成功即停止,失败、超时或绑定失效即撤销,全部可用路径耗尽后明确等待或失败。SFU/TURN 的具体 fallback 顺序、edge 类型、lease multiplicity 和资源记账待整体路由模型定案。凭据不得进入 URL、日志、浏览器持久化或 SQLite;HTTPS/WSS 继续独立使用 TLS/TCP。 +- 质量控制:保留 1080p60(8 Mbps)、1080p30(5 Mbps)、720p30(3 Mbps)三档推荐上限,三档与高级设置初始值统一采用 `balanced`(平衡)与 `automatic`(自动编码),`maintain-resolution`(清晰优先)、`maintain-framerate`(流畅优先)和 H.264/VP8 保留为显式选择;分享期间可直接换档或应用有界高级设置,不重新选择来源或重建健康连接,codec 在直播中锁定并于下一次分享生效。Peer-assisted relay 与已配置的 SFU 必须接收同一个严格校验的房间级权威设置,覆盖当前和以后建立的 sender;普通 P2P 信令结构保持不变。任何设置都只是浏览器上限和偏好,具体降级由浏览器决定;必须读回实际 sender 参数并通过 stats 观测结果、显示参数拒绝、改写和持续受限原因。不做自定义动态 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 地址或原始设备/网络标识。 +- 断线恢复:临时网络变化只触发整体路由模型授权的有限恢复动作;任一步成功即停止,失败最终进入明确等待或有界失败。`connecting`/`checking` 先显示不改变拓扑的慢连接状态,硬超时必须由真实移动网络证据确定。所有异步结果受 session、route revision、assignment/connection generation 约束,未受影响分支保持健康,Host 只看到不含 URL、地址、token 或原始错误的阶段信息。具体 peer/SFU/TURN 次序在整体模型定案前不写入当前需求。 +- 可观测性:按目标 RTP transport 与不透明连接/ICE 代次关联 selected path、`iceGatheringState`/ICE/connection state、事件推导的建立时长、generation-baselined pair-change delta,以及 RTT、RTP 丢包率、抖动、码率、帧率、编码耗时和受限原因。候选与错误的 wire、日志和导出只使用[低服务器成本媒体路由](./research/low-server-media-routes.md)的脱敏字段和 code bucket,禁止 raw SDP、candidate、地址、URL、`errorText` 或网络标识。只有创建该 `RTCPeerConnection` 的本机可在折叠详情中显示浏览器对 selected local/remote candidate 实际暴露的地址与端口;该值不上传、不持久化、不用于地理/NAT 推断,缺失或被隐私保护时显示未知。 +- 渐进式诊断:分享者和观看者默认只看到房间、画面、核心状态和常用控制;Host 的 Viewer 卡片只把本机连接或当前接收证据称为“已连接”,其余状态保持中性的“线路分配中”或具体连接状态,不把 active assignment 当作连接成功。展开连接详情也只在当前媒体证据成立后显示媒体方式:`P2P` 使用绿色与直观图标,`SFU` 使用黄色的 `SFU fallback`;实际 relay 时另标 `TURN`,只有本地 relay protocol 可见时才附加 `/UDP` 等实际值。尚未连接时不预告 P2P、SFU 或未知 ICE,只保留中性状态。Host/Peer 上下游仍只在独立的权威拓扑视图中展示。传输协议、候选路径、RTT、视频/音频 RTP 区间丢包率、实际编解码器、音频区间码率/jitter 和视频编码耗时等技术详情由一个明确的“显示连接详情”开关展开。远端 Viewer evidence 的详情 shell 在同一 viewer/parent/connection/route identity 内保持稳定;非空字段从各自最后一次实际观测起精确保留五秒,`connecting`/`disconnected` 只降为非 fresh,`failed`/`closed`、identity/session/share/route 变化或非单调 sequence 则清空/换代。非 fresh evidence 不得继续证明“已连接”或被其他 current-state 消费者使用。音频 codec MIME、RTP clock、channels 与 fmtp 只能来自当前 RTP 对象引用的 `RTCCodecStats`,缺失即未知;`opus/48000/2` 不得写成 48 kHz 音源或实际立体声。隐藏诊断不能隐藏失败原因、音频缺失、持续画质受限或需要用户操作的告警。 +- 站点访问:生产部署必须配置独立的 `SITE_ACCESS_PASSWORD` 控制建房、Host 信令角色和所有 code-only Viewer 入口;只有本地开发/测试可显式关闭。非空值接受 8 至 128 个可见 ASCII bytes,不能复用 TURN、LiveKit、TLS 或其他部署 secret。`POST /api/site-access` 验证成功签发 12 小时、无服务端会话状态的 HttpOnly、`SameSite=Strict` site-access cookie。有效 room-scoped fragment grant 可绕过站点 gate 直接进入对应房间;其余 `/r/{code}`、`/join` 和 public-watch 必须先持有效站点 cookie,之后才检查房间 public/private policy 和房间密码。失败统一且不得泄露房间是否存在或 policy;建房 HTTP 只认该 cookie。已移除的旧环境变量、endpoint 和 cookie 不保留兼容。不引入账号、JWT、服务端 session Map 或通用 logout。 +- 房间级观看授权:新房间默认生成含 256-bit 随机量、限时且可撤销的 room-scoped Viewer grant;服务端只保存完整 grant 的 SHA-256 摘要。到期阻止以后鉴权/重连,但不为已鉴权 Viewer 设置踢出定时器;即时清退使用 rotate/revoke。浏览器从 URL fragment 严格读取 grant 后,只写该房间的 `sessionStorage` 并立即用 `history.replaceState` 清除 fragment;刷新和页面内重连可继续。私密房间缺失 grant 时必须先通过站点访问,再提供正确房间观看密码;任一缺失都 fail closed。grant 不得进入 `localStorage`、cookie、query、WebSocket URL、Referrer、日志或错误。Host 可 rotate 或 revoke;revoke 只锁定 grant 路径,已配置的房间密码仍是独立的后续进入方式。新 grant 摘要成功提交后必须清除 Viewer grace/路由、终止旧授权代次的信令和媒体并断开全部旧 Viewer,数据库写失败则不得改变连接。public-watch 是显式、可逆的 room policy,只放宽房间级观看,不绕过站点访问。 +- 最小持久化:`ROOM_DATABASE_PATH` 可选且必须与 `SITE_ACCESS_PASSWORD` 同时配置。两者都存在时,SQLite 只持久保存从 `1` 开始自增的房间 ID、Host token 摘要、nullable Viewer grant 摘要,以及 nullable 48-byte Viewer-password material(16-byte salt + 32-byte scrypt verifier);数据库 `CHECK` 约束非空 BLOB 长度,不新增用户、成员或 session 表。明文房间密码只短暂存在于当前页面和鉴权/派生调用内存,不进入 URL、cookie、浏览器持久存储、日志或 SQLite。持久房间停止后仍显示等待状态;私密邀请按期限或 Host rotate 更新。旧 schema 行在原子迁移时必须 fail-closed 为没有可用明文 grant 的锁定私密状态,Host rotate 后才能生成新邀请。 ### P1 - 重要功能 -- [ ] 私密房间双入口: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;不存在、错误密码、队满和正确密码后的满房都返回通用结果。 +- 私密房间双入口: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` 后,所有正常房间都进入同一个 direct/peer -> SFU -> selected-edge controller;每个房间独立保存 assignment/session/revision/connection generation 和容量状态。合格父节点构成网状候选关系,但实际媒体保持无环且每个 Viewer 只有一个 active upstream;加入、容量变化、掉线和当前边失败触发有界重选,切换重叠必须受现有边预算与当前代媒体证据约束,不做常驻多源分片。一个 child 的已确认 C+B 质量事件仍只处理该 edge:候选只允许 active upstream 为 peer 或 SFU、session/share/revision 当前且有效 relay capacity 严格多一个空槽的 Viewer,Host 排除;保留旧边并在候选 parent 的 active child map 外建立至多一个 provisional `HostPeer`,只有候选在当前 prepare revision 收到 RTP、解码出帧且持有 live video track 后才把同一个 PC 原子 promotion 并换父。prepare-ready 后候选失败时,精确 revision/parent/connection identity 必须保留到 matching active 或 rollback;候选 parent 对同 revision 的重复 matching active 持续 fail-closed,不 promotion、不建新 PC,也不越权发送 `route-failed`,恢复只由 target Viewer probe 发起。其他 edge 的真实 `route-failed` 必须先抢占 soft probe、恢复 target/candidate current authority,再进入原故障梯级。候选失败、超时、session/share 变化或无候选都保留旧边,质量事件本身不得进入 SFU/TURN。只有在未来独立验收并接受更高 relay-capacity tier(当前 Browser1 release 不可达,且不授权第二 child)后,同一 Viewer parent peer/session/share 才可在五秒内收到两个不同 child 各自三窗确认的 `severe` 或 `relative-fps`,服务端才把该 parent 的独立 relay 资格暂停 30 秒,使有效容量为零但不覆盖客户端广告。Host 不退位,parent 的上游观看不变;其他当前 children 暂沿既有有界重选,完整 sibling MBB 是后续 slice。新 parent session、新分享或 30 秒到期立即/惰性恢复资格。`PEER_ASSISTED_ROOM_IDS` 已退役,room `1` 只作为历史 smoke fixture,不能作为功能开关。未启用总开关的开发/轻量部署仍可保留 ordinary P2P;普通 peer 始终 STUN-only。selected-edge TURN 只能复用当前 controller 选中的异常 edge,不能向普通 peer edge 预发 candidates。该能力不增加 UI、用户选择、百分比发布或第二套路由器。 -- [ ] 质量控制扩展:推荐档位仍是默认入口;“分享高级设置”只允许 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。视频档位在直播中显式应用;codec 与音频档位在直播中锁定并于下一次分享生效,避免控件产生连续控制流量。所有当前及以后建立的 Web Host、Browser relay 与 SFU sender 必须请求并读回同一房间锁定的 64/128/256 kbps 上限,实际码率仍由浏览器拥塞控制和 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。只有稳定收益且捕获、编码和远端画面不受影响时,才允许直播开始后默认显示静止缩略图或隐藏动态预览并提供按需展开;未测前不宣称收益。 -- [ ] Peer-assisted 媒体路径:当前 release 的分享端最多两个 child,每名普通 Browser Viewer 最多一个 child。信令服务维护可从多个合格节点选择 parent 的候选图,但每名 viewer 任一时刻只有一个 active upstream,活动媒体图保持无环;只在新加入、容量释放、节点断开或当前 edge 失败时按深度和服务端加入序号做有界分配或换父,不加入综合评分、主动重平衡、多源媒体或持续优化。每个 viewer 将收到的标准 remote track 通过最多一个新的下游 `RTCPeerConnection` 转发,因此会逐跳解码和分别重编码;必须测量而不能称为 shared encode。服务端只在受房间数限制的内存状态中保存当前严格校验的质量设置,不写入 SQLite;host 鉴权后先重报设置,在线 viewers 接收更新,后加入者从鉴权快照取得当前值。 -- [ ] 原生 shared encode 分享端:后续独立的 Electron/原生分享端必须让每个 active representation 只产生一份兼容编码结果,并在 direct、peer 与 SFU transport 间复用;健康路径选择共享 `HIGH`,最多另有一份弱路径共用的 `LOW`,是否常驻由硬件、游戏和上传门槛决定。每条连接仍保留独立 WebRTC packetization、SSRC、pacing、加密和反馈,且各自消耗上传带宽。浏览器不保证跨 `RTCPeerConnection` 复用编码器,因此当前 Web spike 不能宣称满足该保证。 -- [ ] 原生 shared encode relay:具备原生 relay 能力的 audience 同样不得超过其后续单独验收的服务端角色上限,并应复用兼容编码输出而不再次编码。当前普通浏览器 audience 只允许一条下游、使用逐边重编码的标准 WebRTC 路径,不伪称 shared encode。 -- [ ] SVC 重开条件:当前标准 Web/LiveKit SVC 已静态拒绝为 `no-go-web-svc-cross-path-hardware-contract`。sender `scalabilityMode` 没有 direct/peer receiver 对应的按层选择或跨 PeerConnection shared-encode 契约;requested/applied mode、Media Capabilities `powerEfficient` 和 RTCStats encoder 字段也不能证明没有静默软件 fallback。Pinned client 2.22.0 还会把 screen-share SVC 覆盖为三层 temporal-only `L1T3`,不满足低分辨率 base 与最多两层。只有未来 native sender 能明确选择硬件编码器、最多发布两层、将同一编码输出复用于 direct/peer/SFU、保持每条路径独立选择并通过真实游戏和亚秒延迟门槛时,才可用新的 capability decision 重开 SVC;不能把浏览器 API 支持或 UA 推断当成采用理由。 -- [ ] 分包/分层多树实验:把编码包、对象或可独立解码层拆到多个 peer 路径以分散上传,只能在独立 spike 中验证。必须保持亚秒级,处理多父重组、排序、丢包恢复、关键帧、同步和节点退出;未通过门槛前不进入产品路由控制器。 -- [ ] SFU-root 中央兜底:产品部署目标必须提供经 canary 验收的单节点 SFU 资源;系统在 direct/peer UDP 不可用或明确拓扑预算无法接纳时自动把它作为主要中央兜底,不要求用户选模式。它只向总计一至两个必要 roots 出口;root 优先承担受限 peer descendants,没有可靠 relay root 时必要 viewer 只作为同一总上限内的零 descendants root。已关闭 PR #12 的显式整房 SFU 模式已被取代。room `1` 的历史 smoke 没有 retained SFU media;公网媒体、资源和恢复门禁仍适用于全站路由。 -- [ ] 分享端桌面封装:用 Electron 固定 Chromium 版本、改善选源/热键/系统音频体验,同时保留 Web 观看端。 -- [ ] Windows 应用音频:Windows 11 Native sender 使用本地 opaque window target 绑定 HWND、PID 与进程创建时间,只捕获所选游戏及其子进程的音频,避免把语音聊天或通知声一并分享;“全部系统声音”只能由分享者显式选择。普通 Windows 10 不具备当前 process-loopback API 门槛,必须明确标为未解决/不支持而不是伪兼容。浏览器无法证明或隔离任意进程音频时必须如实提示,原生捕获失败、无声或进程退出不得静默扩大为整机捕获。 -- [x] 连接自检:开始分享前的本地自检先用无房间、无媒体、无凭据的临时资源验证站点健康、WSS 握手和当前部署 STUN 是否实际 gathered `srflx`;结果不得显示地址、URL、原始 candidate 或把 candidate gathering 称为带宽、性能或端到端可达。仅配置 SFU 时必须显示“已配置,需实际路由验证”,因为 token-free HTTP 预热不能证明 SFU ICE/UDP,真实 SFU/UDP canary 仍是未完成验收。启用 selected-edge TURN canary 时只报告脱敏的授权阶段与 selected candidate type,不输出 URL、username、credential、地址或原始 candidate;自检只覆盖当前 UDP 媒体梯级。 -- [ ] 受限网络增强:短期鉴权 TURN 只能由完整、默认关闭的部署 tuple 与当前 room controller 启用,并且只能在 direct/peer UDP 与 SFU/UDP 均已按有界梯级失败后,向 controller 选中的当前异常 edge 一次性授权。普通 peer PC 从创建起保持 STUN-only;不得向整个 participant、房间或所有 peer connections 预发 TURN candidates。凭据必须绑定当前授权 session 与 route/connection generation、短期有效、内存态、一次性消费且不进入 URL、日志、浏览器持久化或 SQLite。媒体保持 UDP-only,全部路径耗尽后明确失败;TURN 不得被描述为更高清的传输方式。 -- [x] Web 编码选择:分享高级设置提供 `自动 | H.264 | VP8`,默认自动使用浏览器/LiveKit 协商;显式选择只在下一次分享前用标准 codec preference 生效。直播中控件锁定并明确提示停止分享后可修改。P2P/浏览器 relay 保留完整 repair 与其他 codec 回退列表,SFU 保持 `backupCodec = false`,不维护第二套应用编码链。实际 codec/profile、encoder implementation 与 stats 是结果真相;H.264 可选不等于浏览器必走硬编,AV1 仍不得仅凭压缩率推断默认启用。 -- [x] 诊断导出:Host 与 Viewer 在展开现有连接详情且存在本地 metrics 时可点击下载图标导出版本化 JSON。导出只逐字段白名单包含 route、direction、连接状态、protocol/candidate type、codec、分辨率、帧率、码率、丢包、jitter、RTT、限制、encoder 与音视频播放/修复指标;缺失值为 `null`。禁止导出 local/remote candidate address/port、连接或媒体内部 ID、peer/client/room/name、raw candidate/SDP、STUN/TURN URL、username、credential 或 token。文件只在点击时以本地 Blob 生成并立即回收 URL,不上传或持久化;Host 收到的 peer-relay 数据只可使用已有脱敏 evidence 字段。 +- 统一自动路由:旗舰部署开启 `PEER_ASSISTED_MEDIA` 后,所有正常房间使用同一个服务端 route controller,并各自保存 assignment、session、route revision、assignment generation、connection identity 和容量状态。合格父节点可形成候选图,但 committed media graph 必须无环且每名 Viewer 同时至多一个 active upstream。加入、容量释放、节点离开、当前边失败或已接受的 path-health evidence 只开启一次有界重选;其余健康分支保持 sticky,不做常驻多源、周期评分或全房重平衡。迁移保留旧边,只有 exact current candidate 取得 live track、正 RTP 和 decoded-frame progress 后才原子 commit;失败、超时、代次变化或授权撤销销毁候选并保留或恢复旧权威。普通 peer 始终 STUN-only。SFU/TURN 参与迁移时的 overlap 和资源准入由待定整体模型约束。该能力不增加 UI、用户选择、百分比发布或第二套路由器。 +- 质量控制扩展:推荐档位仍是默认入口;“分享高级设置”只允许 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。视频档位在直播中显式应用;codec 与音频档位在直播中锁定并于下一次分享生效,避免控件产生连续控制流量。所有当前及以后建立的 Web Host、Browser relay 与 SFU sender 必须请求并读回同一房间锁定的 64/128/256 kbps 上限,实际码率仍由浏览器拥塞控制和 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。只有稳定收益且捕获、编码和远端画面不受影响时,才允许直播开始后默认显示静止缩略图或隐藏动态预览并提供按需展开;未测前不宣称收益。 +- Peer-assisted 媒体路径:所有非服务器 parent 都受同一个服务端权威下游容量约束,默认 `2` 且可静态配置为 `1`、`2` 或 `3`。每名 Viewer 任一时刻只有一个 active upstream,活动媒体图保持无环;只在离散事件中做有界分配或换父。浏览器把收到的标准 remote track 加入独立下游 `RTCPeerConnection` 时会逐跳解码并分别重编码,必须测量而不能称为 shared encode。服务端只在有界房间内存中保存严格校验的质量设置,不写入 SQLite。 +- 原生 shared encode 分享端:后续独立的 Electron/原生分享端必须让每个 active representation 只产生一份兼容编码结果,并在 direct、peer 与 SFU transport 间复用;健康路径选择共享 `HIGH`,最多另有一份弱路径共用的 `LOW`,是否常驻由硬件、游戏和上传门槛决定。每条连接仍保留独立 WebRTC packetization、SSRC、pacing、加密和反馈,且各自消耗上传带宽。浏览器不保证跨 `RTCPeerConnection` 复用编码器,因此当前 Web spike 不能宣称满足该保证。 +- 原生 shared encode relay:具备原生 relay 能力的非服务器端点同样受统一服务端权威下游容量约束,并应复用兼容编码输出而不再次编码。当前普通浏览器 relay 使用逐边重编码的标准 WebRTC 路径,不伪称 shared encode。 +- SVC 重开条件:当前标准 Web/LiveKit SVC 已静态拒绝为 `no-go-web-svc-cross-path-hardware-contract`。sender `scalabilityMode` 没有 direct/peer receiver 对应的按层选择或跨 PeerConnection shared-encode 契约;requested/applied mode、Media Capabilities `powerEfficient` 和 RTCStats encoder 字段也不能证明没有静默软件 fallback。Pinned client 2.22.0 还会把 screen-share SVC 覆盖为三层 temporal-only `L1T3`,不满足低分辨率 base 与最多两层。只有未来 native sender 能明确选择硬件编码器、最多发布两层、将同一编码输出复用于 direct/peer/SFU、保持每条路径独立选择并通过真实游戏和亚秒延迟门槛时,才可用新的 capability decision 重开 SVC;不能把浏览器 API 支持或 UA 推断当成采用理由。 +- 分包/分层多树实验:把编码包、对象或可独立解码层拆到多个 peer 路径以分散上传,只能在独立 spike 中验证。必须保持亚秒级,处理多父重组、排序、丢包恢复、关键帧、同步和节点退出;未通过门槛前不进入产品路由控制器。 +- SFU/TURN 中央兜底:产品部署可以提供经 canary 验收的有界 SFU 与可选 TURN 资源,但不要求用户选模式,也不得默认变成整房 always-SFU。publication owner、subscriber 选择、受限 Host/relay/leaf 的路径、TURN edge 类型、central egress/admission、temporary overlap 和 exact fallback sequence 必须在同一个路由模型中定案并测量,不能由固定 root 数或普通端点 child 数代替。 +- 分享端桌面封装:用 Electron 固定 Chromium 版本、改善选源/热键/系统音频体验,同时保留 Web 观看端。 +- Windows 应用音频:Windows 11 Native sender 使用本地 opaque window target 绑定 HWND、PID 与进程创建时间,只捕获所选游戏及其子进程的音频,避免把语音聊天或通知声一并分享;“全部系统声音”只能由分享者显式选择。普通 Windows 10 不具备当前 process-loopback API 门槛,必须明确标为未解决/不支持而不是伪兼容。浏览器无法证明或隔离任意进程音频时必须如实提示,原生捕获失败、无声或进程退出不得静默扩大为整机捕获。 +- 受限网络增强:短期鉴权 TURN 只有在完整、默认关闭的部署配置和当前 route controller 明确授权时可用;普通 peer PC 从创建起保持 STUN-only,不得向整个 participant、房间或所有 peer connections 预发 TURN candidates。凭据必须绑定 exact room/share/session/route/connection generation,短期有效、内存态、受配额约束且不进入 URL、日志、浏览器持久化或 SQLite。它服务的 edge 类型、相对 SFU 的顺序和资源记账由整体路由模型定案;TURN 不得被描述为更高清的传输方式。 +- Web 编码选择:分享高级设置提供 `自动 | H.264 | VP8`,默认自动使用浏览器/LiveKit 协商;显式选择只在下一次分享前用标准 codec preference 生效。直播中控件锁定并明确提示停止分享后可修改。P2P/浏览器 relay 保留完整 repair 与其他 codec 回退列表,SFU 保持 `backupCodec = false`,不维护第二套应用编码链。实际 codec/profile、encoder implementation 与 stats 是结果真相;H.264 可选不等于浏览器必走硬编,AV1 仍不得仅凭压缩率推断默认启用。 ### P2 - 可选功能 -- [ ] 证据驱动的统一 relay capacity:当前 ordinary Browser Viewer 上线档固定为 `1`。协议保留离散 `0/1/2/3` 作为未来能力 envelope,不代表当前发布授权;后续只有独立验收并修改服务端 release policy 后才能使用更高档,不按设备或 UA 分类。持续 sender bitrate、`availableOutgoingBitrate`、区间 encode time、`qualityLimitationReason`、loss 与 RTT 健康窗可为未来升降提供证据,服务端 deployment hard ceiling 与 cooldown 继续限界。H.264、hardware encode 或 shared encode 可以降低编码成本或同观感码率,但每个 child 仍消耗一份网络副本。本次 slice 不实现动态控制。 -- [x] 原生 Windows 捕获辅助程序:显式 `native-window-h264` 模式使用同一 bound window target 的 WGC/D3D11 画面与 WASAPI process-tree PCM,并用 adapter-bound、hardware-only Media Foundation H.264 `42c01f` 直接进入现有 Go fanout;普通 Web Host 独立采用默认 `automatic`、可显式选择 H.264/VP8 的标准协商路径,不允许运行时静默改用软件编码、显示器画面或系统混音。一名普通 Chrome Viewer 已 decoded/rendered 203 帧并收到 500 Opus packets,同一 helper PID/adapter 的 `VideoEncode` 与 LUID 匹配。 -- [ ] Viewer 向电视输出:系统级屏幕镜像现在可直接使用;产品只复用平台 AirPlay/Cast,不自造投屏协议。截至 2026-08-21,Chromium 的 Remote Playback URL 路径和 WebKit 明确拒绝 wireless target 的 live `MediaStream` player 均未给当前 WebRTC `srcObject` 提供可用契约,因此不显示伪可用入口。只在浏览器实现明确支持并由实体 Apple TV 或 Chromecast/Google TV 证明当前 live 画面与音频后,才按通过的平台加入 Viewer 本地图标按钮。投屏不能改变 room、TURN/SFU/peer route、Host fanout 或服务端媒体分发;Google Cast receiver 需要可获取的媒体 URL,当前 P2P `MediaStream` 不满足该输入,因此不加入 Cast SDK/receiver。 -- [ ] 多地区 SFU-root 调度,以及可选 TURN 兼容层的按地区、运营商可达率。 +- 证据驱动的动态 relay capacity:当前只接受统一静态容量 `1`、`2` 或 `3`,默认 `2`,不按角色、设备或 UA 分类。若未来引入运行时升降,必须由独立需求、稳定的 sender/transport evidence、cooldown 和服务端 hard ceiling 共同定义;当前协议能力字段不自行授权动态策略。H.264、hardware encode 或 shared encode 可以降低编码成本或同观感码率,但每个 ordinary child 仍消耗一份网络副本。 +- 原生 Windows 捕获辅助程序:显式 `native-window-h264` 模式使用同一 bound window target 的 WGC/D3D11 画面与 WASAPI process-tree PCM,并用 adapter-bound、hardware-only Media Foundation H.264 `42c01f` 直接进入现有 Go fanout;普通 Web Host 独立采用默认 `automatic`、可显式选择 H.264/VP8 的标准协商路径,不允许运行时静默改用软件编码、显示器画面或系统混音。一名普通 Chrome Viewer 已 decoded/rendered 203 帧并收到 500 Opus packets,同一 helper PID/adapter 的 `VideoEncode` 与 LUID 匹配。 +- Viewer 向电视输出:系统级屏幕镜像现在可直接使用;产品只复用平台 AirPlay/Cast,不自造投屏协议。截至 2026-08-21,Chromium 的 Remote Playback URL 路径和 WebKit 明确拒绝 wireless target 的 live `MediaStream` player 均未给当前 WebRTC `srcObject` 提供可用契约,因此不显示伪可用入口。只在浏览器实现明确支持并由实体 Apple TV 或 Chromecast/Google TV 证明当前 live 画面与音频后,才按通过的平台加入 Viewer 本地图标按钮。投屏不能改变 room、TURN/SFU/peer route、Host fanout 或服务端媒体分发;Google Cast receiver 需要可获取的媒体 URL,当前 P2P `MediaStream` 不满足该输入,因此不加入 Cast SDK/receiver。 +- 多地区 SFU-root 调度,以及可选 TURN 兼容层的按地区、运营商可达率。 ## 非功能需求 - 性能:在 RTT 不高于 40 ms、丢包不高于 1% 的受控 P2P 路径中,玻璃到玻璃延迟目标为 p50 不高于 150 ms、p95 不高于 250 ms;区域 SFU/UDP root 路径的 p95 目标不高于 350 ms。可选 TURN 单独测量,不能借用该目标宣称通过。 - 启播:观看者请求观看后,除用户授权耗时外,3 秒内出现首帧。 - 流畅性:选择 60 fps 档位时,不能在没有降档或提示的情况下连续 5 秒低于 50 个解码帧每秒。 -- 成本:direct/peer UDP 不经过应用服务器;SFU 正常只承担 publisher ingress 与一至两个 root egress,另行准入的极端 viewer 必须受独立中央 egress cap 约束。候选筛选使用[低服务器成本媒体路由](./research/low-server-media-routes.md)的 `B`/`B_pub`/`B_i`、`B_exc,j`、`N=R+D+E` 模型;验收必须保留 host/root/exception NIC、每条可选 TURN ingress/egress、SFU ingress/egress 和供应商计费的真实 hop bytes,不得用有效载荷近似折叠物理流量。 +- 成本: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。 - 隐私:P2P 会向房间内好友暴露网络地址,首版威胁模型仅覆盖可信熟人;若要求隐藏 IP,必须切换为服务器转发。 - 可维护性:代码、需求、调研、项目记忆、`AGENTS.md` 和 `.codex/` 均在 Git 中跟踪;开发配置不得依赖单一操作系统的绝对路径。 - 迁移完整性:同一 release 的客户端、服务端和部署配置原子迁移;canary 结束后删除被替代的配置、wire、parser、双写与测试,不为无当前消费者的旧版本保留兼容层或第二套架构。Git 历史承担旧实现追溯。 - Web 优先:每项用户能力先使用普通浏览器的标准 API 和正常权限流程;只有当前目标浏览器存在已证实的能力缺口且该功能仍属当前范围时,才允许独立、可选的 Sender。不得为假设中的未来能力预建客户端、兼容层或第二套会话架构。 -- 兼容性:分享端首个目标矩阵为 Windows 10/11 的当前 Chrome/Edge;普通移动浏览器当前只属于 Viewer 矩阵。截止 2026-08-21,MDN Browser Compatibility Data 仍将 Chrome Android、Firefox Android 和 Safari iOS 的 `getDisplayMedia()` 标为不支持;Web Host 必须用运行时能力检测,API 缺失时明确显示不支持,API 存在但调用拒绝时区分用户拒绝/取消与 capture failure,不能用 UA/设备型号伪造兼容。观看端仍必须覆盖当前 Android Chrome 和 iOS Safari,并实测自动播放限制、横竖屏、锁屏恢复与移动网络切换。普通 Web Viewer 统一使用当前 relay capacity `1`,直到逐跳重编码与资源门禁通过;移动真机表现是兼容观察,不建立单独 route class。 +- 兼容性:分享端首个目标矩阵为 Windows 10/11 的当前 Chrome/Edge;普通移动浏览器当前只属于 Viewer 矩阵。截止 2026-08-21,MDN Browser Compatibility Data 仍将 Chrome Android、Firefox Android 和 Safari iOS 的 `getDisplayMedia()` 标为不支持;Web Host 必须用运行时能力检测,API 缺失时明确显示不支持,API 存在但调用拒绝时区分用户拒绝/取消与 capture failure,不能用 UA/设备型号伪造兼容。观看端仍必须覆盖当前 Android Chrome 和 iOS Safari,并实测自动播放限制、横竖屏、锁屏恢复与移动网络切换。所有 Web Viewer 使用统一的 server-authoritative endpoint capacity;移动真机表现是兼容观察,不建立单独 route class。 ## 边界与约束 - 首版不包含远程键鼠控制、公开/大规模直播、录制、社区系统或完整语音聊天;大规模分享直接使用现有直播服务。 - public-watch 只省略房间级 grant/password,不省略站点访问;房间码只定位,不提供隐私保证、目录或防转发承诺。现有每房 Viewer 上限、全局/未鉴权连接上限、房间上限和中央 egress/admission cap 仍适用,public-watch 不能创建无界服务器 fanout。 - SQLite 只用于持久房间身份与最小授权材料:房间 ID、Host token 摘要、nullable Viewer grant 摘要和 nullable 48-byte Viewer-password salt+verifier。不得保存 site access 密码、房间观看密码、token/grant 明文、cookie、显示名、成员、`clientId`、`peerId`、SDP、ICE candidate、IP 地址、媒体或 TURN 凭据;首版仍是单进程、单实例控制面。 -- 当前八名观看者和 1 至 16 配置范围是接入边界;目标默认 20 也只是通过指定矩阵后的接入边界,不承诺任意机器、档位、网络或跨洲路径都稳定承载。 +- Production 当前八名观看者和 1 至 16 配置范围只是接入边界,不承诺任意机器、档位、网络或跨洲路径都稳定承载;任何未来 admission 变化另行决策和验收。 - 浏览器规范要求每次由用户主动选择分享源,权限不能永久保存;纯网页不能保证指定游戏窗口或始终获得系统音频。 - 1080p60 是能力目标,实际分辨率、帧率和硬件编码由浏览器、GPU、驱动、画面复杂度及网络共同决定。 -- 旗舰 release 的正常房间都可使用 peer-assisted/SFU 跨模式 controller;部署未启用总开关的轻量实例才保持标准 WebRTC P2P。Peer relay 只复用标准 remote-track WebRTC,不加入 Encoded Transform/DataChannel/WebCodecs 媒体协议,也不自研 UDP、编解码器、拥塞控制、FEC/RTX、多树、转码、codec ladder 或综合评分器。独立 native shared encode sender 不在本实验内,也不能挽救拓扑门槛失败;跨模式迁移继续由 ADR-0005 的 generation 与 edge budget 约束,不使用短时双发。 +- 旗舰 release 的正常房间都可使用同一自动 controller;部署未启用总开关的轻量实例才保持标准 WebRTC P2P。Peer relay 只复用标准 remote-track WebRTC,不加入 Encoded Transform/DataChannel/WebCodecs 媒体协议,也不自研 UDP、编解码器、拥塞控制、FEC/RTX、多树、转码、codec ladder 或综合评分器。独立 native shared encode sender 不在本实验内;跨模式迁移受 ADR-0005 的 identity、generation、media proof 和待定资源准入共同约束。 - GPL/AGPL 项目在许可证策略确认前仅用于研究,不能直接复制到计划闭源的产品中。 ## 验收标准 @@ -112,12 +108,10 @@ ### P2P、SFU 与可选 TURN 兜底 - 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 界面快速显示通用连接失败,不静默黑屏、不长时间假连接,也不尝试未配置的媒体路径。 -- Given 未配置完整 selected-edge TURN tuple、当前 route/connection generation 不匹配或该 edge 未经过 controller 选择,When 会话鉴权、peer PC 创建或旧消息重放,Then authenticated snapshot 与 ordinary peer PC 必须保持 STUN-only,服务端不得签发或接受该 TURN grant;不得把 credential 发给 source-only Native、普通房间或未鉴权会话。 -- 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 同一房间中一名观看者可以 direct/peer UDP、另一名只能通过 SFU root 接收,When 两者同时观看,Then 前者保持分布式 P2P,后者使用 SFU,双方互不迁移且 host/SFU root 上限不变。 +- Given ordinary peer edge 被创建或重建,When 检查其 ICE 配置,Then 只包含部署 STUN,不包含预发给 participant、房间或普通 peer 的 TURN candidate。 +- Given route candidate、signal、recovery 或 commit 的 room/share/session/revision/assignment/connection identity 任一不匹配,When 旧消息或异步结果到达,Then 该操作 fail closed 且不能替换当前媒体。 +- Given 新路线正在准备,When 尚未同时证明当前代 live track、正 RTP 与 decoded-frame progress,Then 旧路线保持权威;证明后只原子提交 exact candidate,失败或超时销毁候选并保留或恢复旧路线。 +- Given 所有已接受且已配置的媒体路径耗尽,When 有界连接窗口结束,Then 界面显示通用等待或连接失败,不静默黑屏、不长时间假连接,也不尝试未授权路径。SFU/TURN 的精确 path order 和 resource accounting 由整体路由模型的独立验收条款补充。 ### 浏览器与移动端观看 @@ -131,16 +125,16 @@ ### 一对多与带宽保护 -- Given 一名分享者和一至两名观看者,When 运行直接 P2P,Then 分享端为每人建立独立媒体边且总 fanout 不超过二。 -- Given peer-assisted 路径分别运行 1、3、5、8 名观看者,When 用户加入、容量释放、节点离开、重鉴权或当前 edge 失效,Then 服务端使用同一个确定性广度优先规则,只为新 viewer、等待 viewer 或失去 parent 的 subtree root 选择一个 active parent;其余健康边保持 sticky。Host 在每个瞬时采样都不超过两个 physical media edges,普通 Browser Viewer 不超过一个 child;已有 Viewer child 在重鉴权/reconcile 后继续占用其唯一预算,第二个 child 不建立。没有可用 parent 时明确等待或拒绝,不主动重平衡、引入多源媒体或增加分享端 fanout。 +- Given 部署的非服务器端点下游容量为 `C`,其中 `C` 只能是 `1`、`2` 或 `3` 且默认 `2`,When Host 或任一 Viewer 建立、恢复、重鉴权或迁移 ordinary downstream edge,Then 服务端和客户端执行边界都不超过 `C`,upstream receive 不计数,角色、浏览器和 UA 不改变 `C`。 +- Given peer-assisted 路径分别运行 1、3、5、8 名观看者,When 用户加入、容量释放、节点离开、重鉴权或当前 edge 失效,Then 服务端只为新 Viewer、等待 Viewer 或失去 parent 的 subtree root 选择一个 active parent;每名 Viewer 同时至多一个 active upstream,活动图无环,其余健康边保持 sticky。没有可用 parent 时明确等待或拒绝,不主动重平衡或引入常驻多源媒体。 - Given 一名分享者和八名观看者,When 在受控网络中运行已记录的质量档位,Then 记录每条边的拓扑深度、候选类型、码率、编码/解码负载、分享端与 relay 上行、首帧和玻璃到玻璃延迟;该条在真实矩阵完成前保持未验收。 -- Given candidate 将房间默认上限改为 20,When 在历史 room-1 smoke fixture 或其他正常房间中连续运行 20 名观看者 30 分钟并注入 relay departure、重连和一个弱路径,Then 每名观看者持续解码、Host 下游边不超过二、每个 relay 不超过已验收能力、深度与故障半径有界、未受影响 subtree 不迁移,并记录端点/SFU 的 CPU、GPU、内存、上行、出口、帧率、丢包率、冻结和恢复;任一门槛失败时不得切换默认值。 +- Given candidate 改变房间 admission 默认值或上限,When 在目标数量与代表性路径上运行规定时长并注入 relay departure、重连和一个弱路径,Then 每名观看者持续解码、每个非服务器端点不超过配置容量、深度与故障半径有界、未受影响 subtree 不迁移,并记录端点与中央服务的 CPU、GPU、内存、上行、出口、帧率、丢包率、冻结和恢复;任一门槛失败时不得切换默认值。 - Given peer-assisted host 鉴权或直播中连续修改质量设置,When 服务端校验其分辨率、FPS、码率和偏好,Then 只有 host 的完整有界设置被接受,last-wins 地应用到当前和以后建立的 relay child sender 以及启用的 SFU publisher;越界、缺字段和多余字段被拒绝,普通 P2P 的鉴权 wire 不新增该字段且不接受同步消息。 - Given 标准浏览器把收到的 remote track 直接加入下游 `RTCPeerConnection`,When 检查 relay 统计和 libwebrtc 路径,Then 明确记录新的 outbound encoder、编码耗时和 CPU/GPU 限制,不把它当作 encoded forwarding 或 shared encode 成果。 -- Given ADR-0004 的任一硬门槛只有在引入 Encoded Transform/DataChannel/WebCodecs 媒体、自定义拥塞控制、FEC/RTX、多树、综合评分、转码、codec ladder、浏览器 RTP 注入 hack 或放宽 host fanout 后才能满足,When 结束 bounded spike,Then 放弃并删除浏览器 relay 实验运行时代码,保留标准 P2P 和 SFU-root fallback;后续 native shared encode sender 仍按独立 ADR 推进,但不能据此把失败的 relay 拓扑判为通过。 -- Given 新观看者会使预计总上行超过安全预算,When 其请求加入,Then 系统先遵守两边 fanout 预算选择 peer-assisted 或已配置的 SFU;没有合格路线时明确等待或拒绝,而不是创建第三条 host 边或持续堆积发送队列。Direct/peer GCC 与 SFU BWE 各自适应该路径,应用证据只改变拓扑资格,不能合成全房降档。 -- Given TURN 或 SFU 被某条路线使用,When 媒体已抵达一至两个可靠 root,Then 其余可行 viewers 仍由 sticky、受深度与故障半径限制的 peer descendants 服务;Host 的 children、SFU publication、provisional/selected overlays 合计不超过二,普通 Browser Viewer 的 children 与 provisional/selected overlays 合计不超过一,中央 SFU roots 仍不超过二。 -- Given 没有任何满足能力、深度、路径和恢复门槛的可靠 relay root,When 控制器穷尽 peer recovery,Then SFU 可把必要 viewers 作为零-descendant roots;正常仍限一至二 roots,多个异常 viewers 只能使用另行配置的中央 egress cap,否则明确等待或失败,不得变成无界整房 SFU。 +- Given ADR-0004 的任一硬门槛只有在引入 Encoded Transform/DataChannel/WebCodecs 媒体、自定义拥塞控制、FEC/RTX、多树、综合评分、转码、codec ladder、浏览器 RTP 注入 hack 或放宽 endpoint capacity 后才能满足,When 结束 bounded spike,Then 放弃并删除该浏览器 relay 实验路径,保留标准 P2P 与整体路由模型已接受的 fallback;后续 native shared encode sender 仍按独立 ADR 推进,但不能据此把失败的 relay 拓扑判为通过。 +- Given 新观看者会使预计总上行超过安全预算,When 其请求加入,Then 系统只选择整体路由模型已授权且满足配置容量与资源准入的路径;没有合格路线时明确等待或拒绝,而不是创建超过配置容量的 ordinary edge 或持续堆积发送队列。各 transport 的拥塞控制适应该路径,应用证据只改变拓扑资格,不能合成全房降档。 +- Given SFU 或 TURN 参与路线,When 计算准入、迁移 overlap 或失败恢复,Then 同时检查统一端点容量和独立的 server ingress/egress、SFU、TURN 资源预算;不得把普通端点 child 数当作 server cap,也不得借兜底形成无界整房 fanout。 +- Given Host、relay parent 或 leaf 处于受限网络,When direct/peer 路径不足,Then 只执行整体路由模型已明确授权的 publisher、subscriber、selected transport 和 fallback sequence;模型尚未定案的分支不得靠历史 root 数或 lease 规则猜测实现。 ### 质量和恢复 @@ -155,8 +149,8 @@ - Given 受控 RTT/丢包测试条件,When 连续分享高动态游戏画面 30 分钟,Then 延迟、帧率与丢帧达到上述目标,且没有不可恢复的冻结。 - Given 部署版出现游戏低帧、持续 `bandwidth` 限制或只有 Host 刷新才能恢复,When 归因该报告,Then 用同一机器、浏览器、驱动、场景和单个有线 direct Viewer 对照精确 release `769de201f7cc` 与最新 `main`,依次比较受影响状态、只重建该 `HostPeer`、只替换 capture、完整 Host 重建和本地预览开/关;记录实际 codec/encoder、A+B/C、游戏 FPS、CPU 与 GPU encode 使用,不能把部署版报告直接归为最新代码缺陷。 - Given 分享过程中切换 Wi-Fi/有线网络,When ICE 连接失败,Then 客户端触发 ICE restart,并在成功或超时后给出明确状态。 -- Given 一个 active SFU root 在新 signaling session 上复用权威 SFU assignment、信令重认证或 LiveKit 恢复后持续产出 current-generation RTP/解码帧,或一个新 Viewer 的 relay capacity 从 0 变为 1,When cooldown 已结束且候选 parent 有满足瞬时 fanout 上限的重叠空槽,Then 控制器只为一个确定性 root/subtree 建立一条 provisional P2P edge,保留原 SFU 媒体直到新路径出现首个可解码视频帧和正向 RTP 进展后再原子提交,不因 ICE `connected` 单独切换。 -- Given SFU 健康回迁没有重叠空槽,或 provisional edge 失败、超时、代次过期、参与者变化、房间停止、权限撤销或 session 被替换,When 本次机会结束,Then 原 SFU root、descendants 和未受影响 branches 继续原路且不黑屏,临时 edge 被销毁,不写入 `failedParentPeerIdsByViewer`、不消耗普通 route-failure intent,并在 cooldown 前不因同一事件重试。 +- Given 当前路径仍可播放且控制器准备迁移 logical parent 或替换 transport,When 候选路径拥有模型授权的 overlap 资源,Then 旧路径保持权威,直到候选的 exact current generation 出现正向 RTP、解码帧进展和 live video 后才原子提交;ICE `connected` 单独不能触发切换。 +- Given 候选没有准入资源,或 provisional 路径失败、超时、代次过期、参与者变化、房间停止、权限撤销或 session 被替换,When 本次机会结束,Then 原路径和未受影响 branches 继续工作,临时资源被销毁,重试遵守模型定义的有界策略。 - Given 浏览器未返回系统音轨,When 分享开始,Then 分享者在启播前看到“当前来源没有可共享音频”的提示。 - Given 分享者选择标签页或窗口,When 浏览器支持对应音频捕获,Then picker 提供该来源音频而不扩大为系统混音。Given 分享者选择整个屏幕,When 浏览器支持系统音频,Then picker 提供由用户决定的系统音频开关。应用同时请求 `windowAudio: "window"` 和 `systemAudio: "include"`,不根据 track 存在性伪造来源标签。 - Given 浏览器返回分享音轨,When 开始发布,Then 应用设置标准 `contentHint = "music"` 作为内容意图元数据;该值可读回,但不作为 Opus、码率、声道、采样率、FEC、DTX 或立体声的质量证据。 diff --git a/scripts/required-project-paths.txt b/scripts/required-project-paths.txt index 492f20c..2cf41d6 100644 --- a/scripts/required-project-paths.txt +++ b/scripts/required-project-paths.txt @@ -18,6 +18,7 @@ tsconfig.json docs/README.md docs/project-memory.md docs/status.md +docs/todo.md docs/verification-status.md docs/maintenance.md docs/需求理解.md