From d2ccc5fe241d8495494dca9216e65978ab8fc223 Mon Sep 17 00:00:00 2001 From: AAA <35992542+TNTcraftHIM@users.noreply.github.com> Date: Mon, 24 Aug 2026 21:56:15 +0800 Subject: [PATCH] docs: accept audio and room v12 contract --- AGENTS.md | 2 +- docs/README.md | 4 +- docs/adr/0001-p2p-first-media-topology.md | 4 +- .../0002-memory-resident-protected-rooms.md | 66 +++++++++++-------- .../0005-automatic-hybrid-media-routing.md | 63 +++++++++--------- docs/deployment.md | 31 ++++----- docs/project-memory.md | 18 ++--- docs/research/advanced-peer-distribution.md | 4 +- docs/research/browser-background-capture.md | 4 +- docs/research/browser-screen-audio-quality.md | 24 ++++++- docs/research/low-server-media-routes.md | 8 +-- docs/research/peer-assisted-media.md | 4 +- docs/research/realtime-quality-adaptation.md | 4 +- docs/research/webrtc-p2p-screen-sharing.md | 4 +- docs/session-handoff.md | 31 +++++---- docs/status.md | 35 +++++----- docs/todo.md | 11 ++-- docs/verification-status.md | 13 ++-- docs/方案设计.md | 42 ++++++------ docs/需求理解.md | 24 +++---- 20 files changed, 210 insertions(+), 186 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 334af76..4e9ddce 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ ## 1. Decision And Durable Truth -- Before substantial work, read and follow the complete repo-tracked [`stop-that-shit` skill](./.agents/skills/stop-that-shit/SKILL.md). That file is the Screener authority for generic task boundaries; keep any installed copy synchronized with it rather than maintaining divergent variants. +- Every task and action in this repository must follow the complete repo-tracked [`stop-that-shit` skill](./.agents/skills/stop-that-shit/SKILL.md). Read it before work; that file is the Screener authority for generic task boundaries, and any installed copy must stay synchronized with it rather than becoming a divergent variant. - Core summary: within higher-priority instructions, the latest explicit user decision defines the task; take only requested actions and proven necessary consequences, and do not treat review/research/implementation as authority for implementation/deployment/cleanup beyond its task type. Broad words such as improve or optimize add no unnamed scope. Prefer standards and one owning invariant over example-specific mechanisms, while preserving unrelated work and necessary callers/tests/security. Reconstruct each deliverable from the accepted current result so it stands alone without rejected alternatives, process narration, or modification traces; inspect only the pending changes and report the result with completion evidence. The full skill governs when this summary is incomplete. - Highest priority for repository changes: before implementation, cleanup, or deployment, settle the meaning against the whole product model and evidence, challenge contradictions or needless complexity, and write the accepted decision to its owning truth documents. Never turn a partial interpretation into code or a branch. - Until the owner explicitly declares a public release, maintain only the current internal product contract. Do not preserve backward or forward compatibility, legacy wire/config aliases, dual parsers or writers, translators, deprecated APIs, migration shims, or tests whose only consumer is an older release. Upgrade the private deployment atomically, reject stale clients before authority, delete replaced surfaces, and use Git history for rollback. Reassess compatibility only when public release requirements are explicitly opened. diff --git a/docs/README.md b/docs/README.md index bd6a14b..933d8a9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -23,9 +23,9 @@ - [Display name and presence boundaries](./research/display-name-presence.md): browser-local storage failure behavior, Unicode normalization, control-character policy, and the Web-only presence capability boundary. - [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-memory-resident-protected-rooms.md): four-digit leased in-memory rooms, local Host defaults, scoped Viewer grants, and orthogonal code entry. +- [ADR-0002](./adr/0002-memory-resident-protected-rooms.md): four-digit leased in-memory rooms, local Host defaults, accepted room-lived short Viewer grants, explicit missing-room entry, and orthogonal code entry. - [ADR-0004](./adr/0004-peer-assisted-media-experiment.md): historical standard-WebRTC peer-assisted experiment and reusable evidence. -- [ADR-0005](./adr/0005-automatic-hybrid-media-routing.md): accepted P2P-first single-reconcile route model; its core is deployed, Browser SFU ICE-server isolation is source-complete and production-pending, and real-network validation is tracked in verification status. +- [ADR-0005](./adr/0005-automatic-hybrid-media-routing.md): accepted P2P-first single-reconcile route model, including deployed Browser SFU ICE-server isolation; remaining real-network validation is tracked in verification status. - [ADR-0006](./adr/0006-fixed-high-native-sender-canary.md): proposed fixed-`HIGH` native canary, current no-go result, and staged revalidation boundary. - [ADR-0007](./adr/0007-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 4726c89..7d2121e 100644 --- a/docs/adr/0001-p2p-first-media-topology.md +++ b/docs/adr/0001-p2p-first-media-topology.md @@ -32,8 +32,8 @@ contradict the private-room cost model or duplicate mature WebRTC capabilities. [ADR-0005](./0005-automatic-hybrid-media-routing.md) owns current routing invariants and assisted-route roles. Production runs exact deployed -application/runtime revision `679fe3e7af634309322bea83b316641f51ad3d09`, release -`679fe3e`; canonical `main` contains the same runtime code. Current source and +application/runtime revision `c4962f54443ad5f98bc65861195a3d9c74a48996`, release +`c4962f5`; canonical `main` contains the same runtime code. Current source and production use the strict `screener-v11` Browser wire and the same accepted route model. Real-network validation remains open. [ADR-0004](./0004-peer-assisted-media-experiment.md) diff --git a/docs/adr/0002-memory-resident-protected-rooms.md b/docs/adr/0002-memory-resident-protected-rooms.md index e3eac73..ef94819 100644 --- a/docs/adr/0002-memory-resident-protected-rooms.md +++ b/docs/adr/0002-memory-resident-protected-rooms.md @@ -1,6 +1,8 @@ # ADR-0002: Memory-Resident Rooms And Scoped Viewer Access -- Status: Accepted; implemented and deployed in exact release `679fe3e` +- Status: Accepted; the existing room model is deployed in exact release + `c4962f5`, while the room-lived short grant and explicit missing-room result + are pending implementation - Date: 2026-08-23 ## Context @@ -75,7 +77,14 @@ update path, derives the verifier, and never stores or logs the plaintext. ### Orthogonal Viewer Access -Every room has an expiring, room-scoped Viewer grant independent of code entry. +Every room has one room-scoped Viewer grant independent of code entry. The grant +is 16 cryptographically random bytes encoded as 22 base64url characters; its +SHA-256 digest is stored directly on that exact in-memory room incarnation. It +has no separate expiry clock or embedded room metadata. It remains usable only +while that room exists, and therefore ends on room reclamation or process +restart as well as explicit rotation or revocation. Reusing the same four-digit +code creates a new digest and never revives an old invitation. + The primary share action copies `/r/{code}#v={grant}`. A valid grant directly authorizes only the Viewer role in that room. The browser consumes it into room-scoped `sessionStorage`, immediately clears the fragment with @@ -101,15 +110,13 @@ all code-only Viewer attempts. A valid room grant may bypass that site gate only for the exact Viewer role and room. Neither a code, room password, nor Viewer grant can create a room or become Host authority. -The current Browser room-entry boundary uses exactly one -`ROOM_ACCESS_DENIED` result. It applies only after site access to a well-formed -code-only Viewer attempt whose expected admission rejects an unknown or expired -room, disabled code entry, an absent or incorrect room password, a full room, or -another bounded admission refusal. Every such case uses the same code, public -message, and connection-close behavior, so neither the wire nor the UI reveals -room existence or code-entry policy. The UI may offer an optional room-password -retry and tell the Viewer to use an invitation link supplied by the Host; it -does not request, discover, or mint an invitation through the server. +After site access, a well-formed code-only attempt for an unallocated, reclaimed, +or expired room returns `ROOM_NOT_FOUND`; the Viewer shows "房间不存在或已过期" +and does not offer a room-password input. Other expected code-only admission +failures retain `ROOM_ACCESS_DENIED` and the existing optional password retry. +This intentionally reveals only that no current room owns the submitted code; +it does not expose the policy or password state of an existing room. The UI does +not request, discover, or mint an invitation through the server. Exact-room grant failures remain `INVALID_TOKEN`, Host authentication retains its independent role-specific outcomes, and unexpected internal faults remain @@ -145,30 +152,37 @@ the generic `SERVER_ERROR`. None of those paths is folded into password, and code-bound room state. Reusing a released code never accepts the old grant. - Default creation atomically produces `open` code entry with no room password - and an independent token-bearing invitation. `open`, `password`, and - `disabled` code entry are covered independently from grant rotate/revoke. + and an independent 22-character token-bearing invitation. The grant remains + valid for exactly the current room incarnation and has no independent expiry. + `open`, `password`, and `disabled` code entry are covered independently from + grant rotate/revoke. - Same-browser recreation reapplies the Host profile; another browser or cleared storage does not. No fingerprint or server user record participates. - Raw site passwords, Host tokens, Viewer grants, and room passwords remain out of application/proxy logs and server durable storage. -- On the current Browser wire, every expected denial of a well-formed, site-authorized - code-only attempt yields only `ROOM_ACCESS_DENIED` with the same public - message and connection-close behavior. Tests cover unknown/expired rooms, - disabled entry, absent/wrong passwords, full and bounded admission, while - exact-room grant, Host-authentication, and unexpected-server-fault paths keep - their independent typed outcomes. +- On the accepted `screener-v12` Browser wire, a well-formed, site-authorized + code-only attempt for an unallocated, reclaimed, or expired room yields + `ROOM_NOT_FOUND` and no + password prompt. Existing-room policy, password, full, and bounded admission + failures retain `ROOM_ACCESS_DENIED`; exact-room grant, Host-authentication, + and unexpected-server-fault paths keep their independent typed outcomes. ## Implementation Status Production runs exact deployed application/runtime revision -`679fe3e7af634309322bea83b316641f51ad3d09`, release `679fe3e`; canonical +`c4962f54443ad5f98bc65861195a3d9c74a48996`, release `c4962f5`; canonical `main` contains the same runtime code. Current source and production run the -strict `screener-v11` wire and implement this complete room -boundary, including allocation, leases, orthogonal grant/code admission, -rotate/revoke, password policy, local profile replay, restart loss, browser -storage privacy, and the neutral `ROOM_ACCESS_DENIED` result. All expected -code-only denials use that same public result followed by the shared -authentication-failed close code. +strict `screener-v11` wire and implement allocation, leases, orthogonal +grant/code admission, rotate/revoke, password policy, local profile replay, +restart loss, and browser-storage privacy. They still use the previous +time-bearing 256-bit grant and neutral `ROOM_ACCESS_DENIED` result. The accepted +22-character room-lived grant and `ROOM_NOT_FOUND` split belong to one strict +`screener-v12` replacement and are not yet implemented or deployed. Exact release and operational evidence remain owned by deployment and verification status. + +## Decision Sources + +- [Node.js `crypto.randomBytes()`](https://nodejs.org/docs/latest-v24.x/api/crypto.html#cryptorandombytessize-callback) +- [NIST SP 800-57 Part 1 Rev. 5](https://csrc.nist.gov/pubs/sp/800/57/pt1/r5/final) diff --git a/docs/adr/0005-automatic-hybrid-media-routing.md b/docs/adr/0005-automatic-hybrid-media-routing.md index d7a90e4..f0eaea2 100644 --- a/docs/adr/0005-automatic-hybrid-media-routing.md +++ b/docs/adr/0005-automatic-hybrid-media-routing.md @@ -1,8 +1,8 @@ # ADR-0005: Automatic Hybrid Media Routing -- Status: Accepted. The core route model is deployed in exact release `679fe3e`; - the Browser SFU ICE-server-isolation amendment is accepted but not - yet implemented or deployed. +- Status: The route model is accepted and deployed in exact release `c4962f5`, + including Browser SFU ICE-server isolation and owner physical fallback proof; + removal of the product diagnostic-download UI is accepted for v12 and pending. - Date: 2026-08-20 - Last updated: 2026-08-24 @@ -291,32 +291,32 @@ candidate, retry hint, or other topology data. Access, Host presence, signaling, media, and autoplay remain independent presentation inputs. The room-serial operation does not divide its per-child deadline by Viewer -count, but simultaneous joins may form a linear queue. The current Browser diagnostic -surface observes existing route events without changing the controller: for -each current child it keeps only the latest route-demand, operation-start, +count, but simultaneous joins may form a linear queue. The current Browser +diagnostic owner observes existing route events without changing the controller: +for each current child it keeps only the latest route-demand, operation-start, current-candidate-start, first-decoded-frame, and final-outcome timing. A new demand overwrites that child's record; authoritative share stop/replacement, confirmed departure, and room deletion remove it. -An authenticated Host may request one snapshot only while creating the existing -user-initiated local diagnostic export. The response reads the current graph, -current operation, and latest records; the server does not push a periodic -snapshot stream, start a timer, retain an event ring, log or persist the result, -or create a second graph. Snapshot-local ordinals express parent/SFU relations -but are not participant identities and must not be treated as stable across -snapshots. Queue wait is the interval from the same route demand to operation -start and remains `null` when either event is absent. +An authenticated Host acceptance harness may request one snapshot. The accepted +v12 product UI exposes no diagnostic download button. The response +reads the current graph, current operation, and latest records; the server does +not push a periodic snapshot stream, start a timer, retain an event ring, log or +persist the result, or create a second graph. Snapshot-local ordinals express +parent/SFU relations but are not participant identities and must not be treated +as stable across snapshots. Queue wait is the interval from the same route +demand to operation start and remains `null` when either event is absent. The response uses only `direct | sfu | waiting | failed` final-route values and the closed rejection buckets `none`, `stale`, `endpoint-capacity`, `sfu-admission`, `candidate-failed`, `first-frame-timeout`, -`operation-deadline`, and `aborted`. It never -carries real peer, parent, session, connection or generation identity, SDP, -candidates, addresses, credentials, or raw error text, and it has no route -authority. A 20-Viewer burst reports the sample count and nearest-rank -p50/p95/max without a pass threshold before any different concurrency model or -deadline is accepted. Protocol tests must reject extra/private fields, deny the -request to Viewers, and prove departure and room-deletion cleanup. +`operation-deadline`, and `aborted`. It never carries real peer, parent, session, +connection or generation identity, SDP, candidates, addresses, credentials, or +raw error text, and it has no route authority. A 20-Viewer burst reports the +sample count and nearest-rank p50/p95/max without a pass threshold before any +different concurrency model or deadline is accepted. Protocol tests reject +extra/private fields, deny the request to Viewers, and prove departure and +room-deletion cleanup. For the single-process deployment, SFU admission is one injected authority with deployment-wide ingress and egress counters. Enabling LiveKit requires explicit @@ -361,18 +361,15 @@ deployment-wide. ## Current Source And Deployment Boundary -Production runs exact deployed application/runtime revision -`679fe3e7af634309322bea83b316641f51ad3d09`, release `679fe3e`. Current source -adds exact Browser SFU ICE-server-isolation implementation -`ae09c760adec76fd26da611d4928486d105c6d3b`; both source and production run the -strict `screener-v11` Browser wire and use the direct/STUN peer and -LiveKit SFU/UDP route model, including the route, first-frame, typed-status, and -Host-only diagnostic contract described above. The operation owner is only -`route`. Current source uses empty external ICE-server lists on Browser SFU PCs; -production still inherits LiveKit-provided STUN and remains held until the -application cutover and physical Host/Viewer proof. Exact operational evidence -is owned by the deployment document; real heterogeneous-network and SFU media -validation remains open. +Production and canonical `main` run exact deployed application/runtime revision +`c4962f54443ad5f98bc65861195a3d9c74a48996`, release `c4962f5`, including exact +Browser SFU ICE-server-isolation implementation +`ae09c760adec76fd26da611d4928486d105c6d3b`. Both run strict `screener-v11` and +the direct/STUN peer plus LiveKit SFU/UDP route model. Browser SFU PCs use empty +external ICE-server lists, and the owner physically verified fallback media +after deployment. The operation owner remains only `route`. Exact operational +evidence is owned by the deployment document; broader heterogeneous-network, +codec, and SFU lifecycle validation remains open. ## Acceptance Boundary diff --git a/docs/deployment.md b/docs/deployment.md index 90892ce..b26bc29 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -3,7 +3,7 @@ Last verified against upstream documentation: 2026-08-24. This page records production running exact deployed application/runtime revision -`679fe3e7af634309322bea83b316641f51ad3d09`, release `679fe3e`, and the single +`c4962f54443ad5f98bc65861195a3d9c74a48996`, release `c4962f5`, and the single Browser `screener-v11` contract. Product direction and pending work are owned by [project memory](./project-memory.md) and [the TODO ledger](./todo.md). @@ -12,10 +12,9 @@ process provides the built Web client, room API, and WebSocket signaling behind Caddy or nginx; ordinary peer ICE advertises only STUN, while Browser LiveKit PCs configure no external ICE server and retain LiveKit-signaled UDP candidates. LiveKit supplies bounded SFU fallback capacity. Normal media remains distributed -through direct or peer edges whenever those paths work. Current source -implementation `ae09c760adec76fd26da611d4928486d105c6d3b` supplies that Browser -override; exact production release `679fe3e` still inherits the LiveKit join -response's STUN endpoint. +through direct or peer edges whenever those paths work. Production includes +implementation `ae09c760adec76fd26da611d4928486d105c6d3b`; the owner physically +verified SFU fallback media after the cutover. A deployment may additionally provide one dedicated single-node LiveKit process as the current controller's automatic final media fallback. This capacity is @@ -26,18 +25,16 @@ ICE/UDP only. Ordinary peer ICE remains STUN-only and production coturn uses the tracked STUN-only configuration with TCP/TLS disabled. The source and production configure no TURN, ICE/TCP, media TCP, or TLS-relayed media. -Production runs exact `679fe3e7af634309322bea83b316641f51ad3d09`, release -`679fe3e`, from `/opt/screener/releases/679fe3e`. The immutable runtime ZIP +Production runs exact `c4962f54443ad5f98bc65861195a3d9c74a48996`, release +`c4962f5`, from `/opt/screener/releases/c4962f5`. The immutable runtime ZIP SHA-256 is -`fc96137f0fdc4dc4d8e083513d8fe99b9c55f7067a8b1de9202551edc3776c50`. +`734815479a03728bb81b44ce5dc04fe010323acdbb91cdbea0e806548278164c`. Its 38-file path/size/hash manifest SHA-256 is -`73f67c530acd5d4c77b546c19b5752c066f82682a584a72cd7ca596d6c5bec7e`. -Local and trusted-IP public `/` and `/healthz` return 200; Screener, LiveKit, -coturn, and nginx are active with `NRestarts=0`, and all six served client files -match the immutable release. The public main Browser asset is -`assets/index-CGzoJSvs.js` with SHA-256 -`31cf628d96be88f373cd0c3ac09e4966929fe320414aa1f0f9841264708d5a7e`; -the retired v10 asset `assets/index-BtFMxoNI.js` returns 404. +`aefc414238655e9cb33d33975813bcc44afd22c5ad4e056ab719fae6c20aed47`. +Local and public `/healthz` return 200; Screener, LiveKit, coturn, and nginx are +active with `NRestarts=0`. The public main Browser asset is +`assets/index-DQgIVwby.js` with SHA-256 +`f819df6c94652d0c5847daa602431384f7b6bf58aa38615fa327526755ede4ef`. The release deploys the single Browser `screener-v11` wire, fixed VP8 for Browser direct, browser-relay, and SFU video, no video `contentHint`, no codec UI/state/wire, @@ -265,7 +262,7 @@ lifetime. An actively connected Host prevents expiry; explicit stop or Host disconnect starts the lease, and only the exact Host token renews it before expiry. Viewer activity never renews ownership. `ROOM_DATABASE_PATH` and `ROOM_TTL_SECONDS` fail startup even when blank. -Production `679fe3e` accepts 1 through 20 and explicitly selects 20. This is an +Production `c4962f5` accepts 1 through 20 and explicitly selects 20. This is an admission limit, not evidence that every publisher, network, or quality profile can sustain that many streams. `ENDPOINT_MEDIA_COPY_CAPACITY` defaults to 2 and accepts only 1, 2, or 3. It is @@ -278,7 +275,7 @@ must fail or wait before a fourth endpoint copy is issued. Supplying the removed `MAX_PEER_RELAY_DOWNSTREAM_EDGES`, even blank, fails startup. -Production release `679fe3e` runs the deployed server and Browser assets +Production release `c4962f5` runs the deployed server and Browser assets atomically on `screener-v11`; every stale Browser or executable-sender wire fails before room authority. Native senders and helpers are outside this release. diff --git a/docs/project-memory.md b/docs/project-memory.md index aa0de51..489385d 100644 --- a/docs/project-memory.md +++ b/docs/project-memory.md @@ -8,7 +8,7 @@ Last updated: 2026-08-24 - A room supports one Host and up to `20` authenticated Viewers. Admission is independent of endpoint fanout and SFU capacity; the event-driven route controller must remain efficient at that bound without all-pairs probing. - Authenticated Hosts and shipped clients are trusted media participants in these private rooms; route authority, generation fencing, and server admission remain fail-closed. - Rooms use one bounded, process-memory model: a random free code from `1000..9999`, default 24-hour configurable dormant lease, and no database or cross-restart recovery. Active sharing never expires; stop/disconnect starts the lease, the exact Host token may resume before expiry, Viewer activity does not renew it, and expiry releases the code. Process restart invalidates every room, token, invitation, password verifier, and participant. -- Viewer grants and code entry are orthogonal. Every room has an expiring token-bearing invitation; code-only entry is `open | password | disabled` and defaults to `open` with no room password after site access. The Host browser locally retains its display name, code-entry policy, optional room password, and raw Host ownership token; the server keeps only current-process digests/verifiers and never uses IP, UA, device properties, or a fingerprint as identity. Recreating after restart reapplies local preferences but produces a new code, Host token, and grant. +- Viewer grants and code entry are orthogonal. Each room has one random 128-bit/22-character grant digest bound to that exact in-memory incarnation; it has no independent expiry and ends on reclamation, restart, rotate, or revoke. Code entry is `open | password | disabled`, defaulting to `open`; an unallocated/reclaimed/expired code returns `ROOM_NOT_FOUND` without a password prompt, while other existing-room denials remain `ROOM_ACCESS_DENIED`. The Host browser retains its creation preferences and raw ownership token; the server keeps only current-process digests/verifiers and uses no fingerprint. Recreation reapplies preferences but creates a new code and credentials. - Web is the current delivery target. The route release covers Browser Host, Viewer, and relay endpoints; packaged/native senders, capture helpers, shared-encode executables, and their acceptance runs are lowest-priority later work outside this milestone. - Keep routing automatic and media distributed. Direct/peer UDP is preferred; the dedicated LiveKit SFU/UDP path is the only application fallback and is not the default topology. Screener configures no TURN, ICE/TCP, media TCP, or TLS-relayed media path. - Every non-server endpoint uses one server-authoritative steady outbound media-copy capacity: default `2`, statically configurable as `1`, `2`, or `3`. A peer child or the Host publication consumes one slot; upstream receive is free, and SFU subscriber egress is accounted at the server. Browser role/UA/visibility does not create a separate tier, and clients cannot raise the deployment value. @@ -24,24 +24,24 @@ Last updated: 2026-08-24 - Current UI, presence, route labels, and diagnostics describe observed state only; they do not create route authority. - Before an explicit public-release decision, the private deployment and repository keep only the current product contract. Incompatible wire, configuration, parser, and API changes ship atomically; stale clients fail before authority, replaced compatibility surfaces are deleted, and Git history owns rollback. - A routine application-only deployment verifies one new immutable artifact and switches to it atomically while reusing unchanged environment, service, LiveKit, coturn, reverse-proxy, and firewall configuration. The pre-cutover application release is guaranteed only through bounded health and postflight checks; it has no retention contract afterward and is not a maintained backup. A future change to infrastructure, secrets, persistent data, or irreversible state must define recovery only for the surfaces it actually changes. -- Viewer presentation remains on the stage until current-generation frame presentation is proven, distinguishes access/Host/queue, peer, SFU, media, autoplay, recovery, and typed failure phases, and keeps `Play` specific to autoplay rejection. A failed Viewer can export its privacy-safe stage/revision/code even before connection metrics exist; an authenticated Host may request one current route/timing snapshot only when downloading diagnostics. Route decisions, queue timing, and current-path quality are diagnostic only. Browser ICE decides direct reachability; Screener does not predict ports, classify NATs, probe TCP, synthesize remote candidates, or reparent a healthy edge for speculative quality gain. +- Viewer presentation stays on stage until current-generation frame proof, distinguishes typed access/Host/route/media/autoplay/recovery phases, and reserves `Play` for autoplay rejection. Pages expose local connection details but no diagnostic download; the bounded Host-only route snapshot remains for acceptance tooling. Route/quality facts are diagnostic only. Browser ICE owns direct reachability; Screener does not predict ports, classify NATs, probe TCP, synthesize candidates, or reparent healthy edges speculatively. ## Current Source And Production -- Production runs exact deployed application/runtime revision `679fe3e7af634309322bea83b316641f51ad3d09`, release `679fe3e`, on the single strict `screener-v11` Browser wire. Current source retains that v11 contract and adds exact implementation `ae09c760adec76fd26da611d4928486d105c6d3b`; auxiliary branches or older worktrees do not supersede canonical `main`. -- Current source and production admit up to `20` Viewers and implement the uniform `1/2/3` endpoint cap, exact prepare candidate, first-decoded-frame commit, strict rollback revision, decoded-progress stall, a two-stage direct/SFU operation deadline, and one event-driven room controller. A short-lived exact-generation decoded-frame observer proves only the pending candidate inside the existing total deadline; the periodic stats sampler remains responsible for active-path diagnostics and stalls. Ordinary peer ICE is STUN-only and the sole application fallback is SFU/UDP; stale TURN environment keys fail startup. SFU admission uses exact publication and Viewer-subscription handles with explicit no-default ingress/egress capacities and a listener-fenced dedicated LiveKit owner. Current source applies fresh empty ICE-server options to Browser SFU publisher/subscriber connects; deployed release `679fe3e` still inherits LiveKit-provided STUN and remains under the route hold. -- Current source and production use the memory-resident room model: random free four-digit codes, the configurable dormant lease, exact-Host resume, restart loss, local creation-preference replay, orthogonal Viewer grants and `open | password | disabled` code entry, and direct invitation/policy controls. SQLite room persistence, `ROOM_DATABASE_PATH`, `ROOM_TTL_SECONDS`, and a writable room StateDirectory are absent from the current runtime. +- Production and canonical `main` run exact deployed application/runtime revision `c4962f54443ad5f98bc65861195a3d9c74a48996`, release `c4962f5`, on the single strict `screener-v11` Browser wire; auxiliary branches or older worktrees do not supersede canonical `main`. +- Current source and production admit up to `20` Viewers and implement the uniform `1/2/3` endpoint cap, exact prepare candidate, first-decoded-frame commit, strict rollback revision, decoded-progress stall, a two-stage direct/SFU operation deadline, and one event-driven room controller. A short-lived exact-generation decoded-frame observer proves only the pending candidate inside the existing total deadline; the periodic stats sampler remains responsible for active-path diagnostics and stalls. Ordinary peer ICE is STUN-only and the sole application fallback is SFU/UDP; stale TURN environment keys fail startup. SFU admission uses exact publication and Viewer-subscription handles with explicit no-default ingress/egress capacities and a listener-fenced dedicated LiveKit owner. Browser SFU publisher/subscriber connects use fresh empty ICE-server options; the owner physically verified deployed SFU fallback media after release `c4962f5`, closing the prior core-route hold. +- Current source and production use random four-digit memory rooms, dormant leases, exact-Host resume, restart loss, local preference replay, orthogonal grants/code entry, and no SQLite runtime. They still use the time-bearing 256-bit grant and neutral missing-room denial. Accepted `screener-v12` makes grants 22-character/room-lived, adds `ROOM_NOT_FOUND`, removes product diagnostic-download buttons, and compacts invitation UI; it is not implemented or deployed. - Exact production artifacts, services, ports, and any change-scoped infrastructure recovery boundary are owned by [deployment](./deployment.md) and indexed by [status](./status.md). - Current source and production require v11, fix Browser direct, browser-relay, and SFU media to VP8, leave video `contentHint` unset, and contain no codec UI, quality state, wire field, fallback media codec, or share-lifetime codec branch. Both use `shareGeneration`-fenced Pause/Resume and implement live 64/128/256 kbps audio-ceiling mutation, typed first-frame Viewer presentation, privacy-safe on-demand route diagnostics, neutral room-code denial, responsive entry controls, exactly three recommended profiles with `1080p30` as default and advanced-only `854x480`. - Browser SFU subscribers reconcile eligible Host screen publications across connect/activate event ordering, participant/track arrival, and reconnect. Production LiveKit logs only warn/error, and the application service has a bounded same-host LiveKit readiness preflight. -- Browser SFU ICE-server isolation implementation `ae09c760adec76fd26da611d4928486d105c6d3b` passed TypeScript, all 605 Web tests in 45 files, both production builds, repository hygiene, and independent review. Production deployment and exact Host/Viewer physical media proof remain pending. +- Browser SFU ICE-server isolation implementation `ae09c760adec76fd26da611d4928486d105c6d3b` passed TypeScript, all 605 Web tests in 45 files, both production builds, repository hygiene, independent review, deployment postflight, and owner physical SFU fallback proof. - Chrome loopback, cap2/cap3, short resource, recovery, MBB, and A/V fixtures are bounded evidence only. They do not establish quality, resource limits, or product policy. ## Current Priority -1. Deploy Browser SFU ICE-server isolation, then prove Host publication and Viewer first-frame media under the reproducing TUN path and a mobile network. -2. Diagnose the screen-audio `1 kbps` readout and physically validate live audio ceilings. -3. Then physically validate exact direct, browser-relay, and SFU codec behavior on deployed Browser v11, refine the invitation UI, and finish real-network route acceptance. Browser VP8 hardware acceleration and mobile Viewer lifecycle remain Accepted Later; Native/executable, distribution, and repository-wide simplification remain later phases. +1. Bind audio stats to the exact current audio track across ordinary Host and SFU publisher reports, then physically validate the `1 kbps` report and live audio ceilings without confusing a configured ceiling with observed bitrate. +2. Implement the single `screener-v12` room/UI contract: room-lived 22-character grant, explicit `ROOM_NOT_FOUND`, no product diagnostic-download buttons, and compact copy-first Host invitation controls. +3. Then physically validate exact direct, browser-relay, and SFU codec behavior on the deployed Browser release and finish real-network route acceptance. Browser VP8 hardware acceleration and mobile Viewer lifecycle remain Accepted Later; Native/executable, distribution, and repository-wide simplification remain later phases. The desktop Host background/minimized report is deferred until it reproduces on current production with a real game and synchronized media plus CPU/GPU evidence. The bounded current-Chrome screening found no immediate Host-page lifecycle drop, and removal of the old harmful `contentHint = "motion"` remains a plausible confound rather than a proven background fix. Mobile Viewer playback and assigned-relay survival remain a separate lifecycle gate. diff --git a/docs/research/advanced-peer-distribution.md b/docs/research/advanced-peer-distribution.md index 333a7a7..73028d8 100644 --- a/docs/research/advanced-peer-distribution.md +++ b/docs/research/advanced-peer-distribution.md @@ -9,8 +9,8 @@ `fdd5a4a529ff297f41c05ea3388bf484d76afe8f` implements ADR-0005's single child-reparent reconciliation and exact first-frame transaction and remains dated implementation evidence. Production runs exact deployed - application/runtime revision `679fe3e7af634309322bea83b316641f51ad3d09`, release - `679fe3e`; canonical `main` contains the same runtime code. Current source and + application/runtime revision `c4962f54443ad5f98bc65861195a3d9c74a48996`, release + `c4962f5`; canonical `main` contains the same runtime code. Current source and production run the strict `screener-v11` form of that route contract. Advanced encoded-media routes remain unimplemented candidates. diff --git a/docs/research/browser-background-capture.md b/docs/research/browser-background-capture.md index 01e7c88..96e5f21 100644 --- a/docs/research/browser-background-capture.md +++ b/docs/research/browser-background-capture.md @@ -74,8 +74,8 @@ gate and do not inherit this desktop Host result. The no-video-hint review baseline is `a26eb39dc677003110787b0ed1581c208f894fd7`. Production runs exact deployed -application/runtime revision `679fe3e7af634309322bea83b316641f51ad3d09`, release -`679fe3e`; canonical `main` contains the same runtime code. Current source and +application/runtime revision `c4962f54443ad5f98bc65861195a3d9c74a48996`, release +`c4962f5`; canonical `main` contains the same runtime code. Current source and production use strict `screener-v11`, fixed VP8, no video hint, and no codec UI, quality state, or wire field. The current path does not claim or implement a page-keepalive mechanism, and diff --git a/docs/research/browser-screen-audio-quality.md b/docs/research/browser-screen-audio-quality.md index b19a9fb..a2d6593 100644 --- a/docs/research/browser-screen-audio-quality.md +++ b/docs/research/browser-screen-audio-quality.md @@ -7,7 +7,7 @@ still report speech-gated movie/game audio, including on a phone connected directly through the SFU. Current Chromium web `getDisplayMedia()` defaults to local speech processing unless the request disables it. The source request is explicit. Production runs exact deployed application/runtime revision -`679fe3e7af634309322bea83b316641f51ad3d09`, release `679fe3e`; canonical +`c4962f54443ad5f98bc65861195a3d9c74a48996`, release `c4962f5`; canonical `main` contains the same runtime code. Current Browser source and production use strict `screener-v11`, fixed VP8, no video hint, and no codec UI, quality state, or wire field. They expose bounded 64/128/256 kbps choices, @@ -15,6 +15,26 @@ apply them to new P2P, browser-relay, and SFU senders, and implement serialized live mutation with applied readback. Target-device audible proof remains open. +## Observed-Bitrate Identity Boundary + +The reported Host audio diagnostic remaining at `1 kbps` after a source switch +is not explained by a unit conversion or a configured ceiling. The existing +delta calculation `(bytes * 8) / milliseconds` already returns kbps, and the UI +only rounds that observed value. A muted or idle Opus stream can produce a value +around 1 kbps, but the cause of the reported live-source value remains unproven +until physical reproduction. + +The current source nevertheless has two exact-observation defects. Ordinary +peer stats select video by the current track identity but do not explicitly bind +the selected audio RTP record to the current audio track. The SFU publisher +requests stats only from the video sender even though +`RTCRtpSender.getStats()` is scoped to that sender. The accepted fix reuses the +existing exact media selector and report merge: ordinary Host and SFU publisher +audio metrics bind to the current audio sender track, explicit no-audio returns +unknown, video and audio sender reports are merged for SFU, and an audio-track +identity change resets the interval baseline. It does not multiply the observed +value, substitute the 64/128/256 kbps ceiling, or add an inferred audio state. + ## Scope And Decision This review covers audio returned with `getDisplayMedia()` and sent through a @@ -450,7 +470,7 @@ deltas in earlier video loopbacks. This is functional evidence, not packaging, real-game sync, second-Viewer, SFU/UDP, or endurance evidence. Run one bounded matrix rather than a full route Cartesian product against -production release `679fe3e` and its matching current runtime source on Windows +production release `c4962f5` and its matching current runtime source on Windows Chrome/Edge for tab/window/monitor, audio selected/unselected, and a simultaneous voice call; then the native candidate on current Windows 11 with game parent and child audio, an independent diff --git a/docs/research/low-server-media-routes.md b/docs/research/low-server-media-routes.md index 7c59a37..0fd4a7c 100644 --- a/docs/research/low-server-media-routes.md +++ b/docs/research/low-server-media-routes.md @@ -6,10 +6,10 @@ - Status: research and dated route evidence. Current invariants and assisted transport roles are in [ADR-0005](../adr/0005-automatic-hybrid-media-routing.md). Production runs exact deployed application/runtime revision - `679fe3e7af634309322bea83b316641f51ad3d09`, release `679fe3e`; canonical - `main` adds Browser SFU ICE-server-isolation implementation - `ae09c760adec76fd26da611d4928486d105c6d3b`; production does not yet include - it. Source and production retain the route transaction and diagnostics on + `c4962f54443ad5f98bc65861195a3d9c74a48996`, release `c4962f5`; canonical + `main` and production include Browser SFU ICE-server-isolation implementation + `ae09c760adec76fd26da611d4928486d105c6d3b` and owner physical fallback proof. + Source and production retain the route transaction and diagnostics on strict `screener-v11`. Real-network validation remains in [verification status](../verification-status.md). diff --git a/docs/research/peer-assisted-media.md b/docs/research/peer-assisted-media.md index 4dac410..c33623d 100644 --- a/docs/research/peer-assisted-media.md +++ b/docs/research/peer-assisted-media.md @@ -7,8 +7,8 @@ - Status: historical evidence plus the bounded controller. Exact Browser v10 source `fdd5a4a529ff297f41c05ea3388bf484d76afe8f` remains the dated local direct-loopback implementation evidence. Production runs exact deployed - application/runtime revision `679fe3e7af634309322bea83b316641f51ad3d09`, release - `679fe3e`; canonical `main` contains the same runtime code. Current source and + application/runtime revision `c4962f54443ad5f98bc65861195a3d9c74a48996`, release + `c4962f5`; canonical `main` contains the same runtime code. Current source and production run the strict `screener-v11` ADR-0005 route transaction. A historical v8 canary decoded SFU media. Heterogeneous-network, mobile, resource, and endurance evidence remains open. diff --git a/docs/research/realtime-quality-adaptation.md b/docs/research/realtime-quality-adaptation.md index 32da7ab..514e3f0 100644 --- a/docs/research/realtime-quality-adaptation.md +++ b/docs/research/realtime-quality-adaptation.md @@ -12,7 +12,7 @@ game UI, maps, subtitles, and text become unreadable; `maintain-resolution` may instead lower frame rate. Neither preference overrides congestion control. Production runs exact deployed application/runtime revision -`679fe3e7af634309322bea83b316641f51ad3d09`, release `679fe3e`; canonical +`c4962f54443ad5f98bc65861195a3d9c74a48996`, release `c4962f5`; canonical `main` contains the same runtime code. Current Browser source and production use strict `screener-v11`, fixed VP8, no video `contentHint`, and no codec UI, quality state, or wire field. They use `balanced` as the recommended profile @@ -236,7 +236,7 @@ SFU publication explicitly uses VP8 with no backup codec. The UI and quality wire expose no codec choice. Codec/profile/encoder stats remain diagnostic and do not authorize automatic switching, route changes, or another controller. Production runs exact deployed application/runtime revision -`679fe3e7af634309322bea83b316641f51ad3d09`, release `679fe3e`; canonical +`c4962f54443ad5f98bc65861195a3d9c74a48996`, release `c4962f5`; canonical `main` contains the same runtime code. Current source and production implement this strict `screener-v11` contract. diff --git a/docs/research/webrtc-p2p-screen-sharing.md b/docs/research/webrtc-p2p-screen-sharing.md index 0f9d777..9ef75fc 100644 --- a/docs/research/webrtc-p2p-screen-sharing.md +++ b/docs/research/webrtc-p2p-screen-sharing.md @@ -284,9 +284,9 @@ Electron 可以固定 Chromium 版本,枚举屏幕/窗口,改善选源、热 接受的 access boundary 使用 production 必配的 `SITE_ACCESS_PASSWORD` 无状态 cookie 保护建房、Host role 和所有 code-only Viewer 入口;合法未过期的 exact-room fragment grant 可直接观看。没有 grant 时,服务端先要求 site access,再检查 `open | password | disabled` code-only policy;房间码只定位,cookie 也不替代房间密码。WebSocket upgrade 仍只做 Origin/容量检查并记录 cookie 状态,首条 `authenticate` 完成上述 role/room 授权。该边界不引入账号、JWT、服务端 session Map 或逐人 ACL。 -RFC 3986 的规范事实是 fragment 在 URI dereference 前由 user agent 分离;WHATWG WebSockets 进一步规定含 fragment 的 constructor URL 必须抛 `SyntaxError`。因此把 256-bit room grant 放在 `/r/{code}#v=...`,再由页面在首个 WSS application message 发送,可以使它不进入 HTTP 或 WebSocket request-target。RFC 6750 对 OAuth bearer query 的警告并不直接规定本产品,但它提供了适用的安全类比:URI query 高概率被日志记录,不应承载此 grant。W3C Referrer Policy 的算法会从 referrer URL 移除 fragment,production 的 `no-referrer` header 再禁止整个 header;这是传输边界,不是“不会泄漏”的保证。 +RFC 3986 的规范事实是 fragment 在 URI dereference 前由 user agent 分离;WHATWG WebSockets 进一步规定含 fragment 的 constructor URL 必须抛 `SyntaxError`。因此把 128-bit room grant 放在 `/r/{code}#v=...`,再由页面在首个 WSS application message 发送,可以使它不进入 HTTP 或 WebSocket request-target。RFC 6750 对 OAuth bearer query 的警告并不直接规定本产品,但它提供了适用的安全类比:URI query 高概率被日志记录,不应承载此 grant。W3C Referrer Policy 的算法会从 referrer URL 移除 fragment,production 的 `no-referrer` header 再禁止整个 header;这是传输边界,不是“不会泄漏”的保证。 -W3C TAG 的 capability URL 指南指出 URL 仍会出现在地址栏、历史、扩展、同步服务、截图和转发路径中,建议高熵、到期与可撤销。目标因此使用 Node.js `randomBytes(32)` 的加密强随机量、完整 grant 的 SHA-256 摘要、有限期限,以及 Host rotate 或 locked revoke。浏览器首次严格解析后只写 room-scoped `sessionStorage`,立即 `history.replaceState` 到 canonical URL;刷新和页面内 WSS reconnect 可复用。HTML 标准明确新 auxiliary browsing context 可以复制同源 opener 的 session storage;这是已持有 bearer 的本地浏览器上下文转交边界,不是服务器扩大 room/role 权限,因此不增加导航状态机。raw grant 不进入 `localStorage`、cookie、query、Referrer、服务端持久存储、应用/代理日志或错误。OWASP 日志指南也明确把 access token、session identifier 和密码列为通常不应直接记录的数据。Fragment 降低服务端泄漏面,但 possession 仍等于该房间 Viewer 权限。 +W3C TAG 的 capability URL 指南指出 URL 仍会出现在地址栏、历史、扩展、同步服务、截图和转发路径中,建议高熵、有限寿命与可撤销。目标使用 Node.js `randomBytes(16)` 的 128-bit 加密强随机量和完整 grant 的 SHA-256 摘要;grant 的有限寿命由 exact in-memory room incarnation 统一拥有,并在房间回收、进程重启、Host rotate 或 locked revoke 时结束,不增加第二个 expiry 时钟。浏览器首次严格解析后只写 room-scoped `sessionStorage`,立即 `history.replaceState` 到 canonical URL;刷新和页面内 WSS reconnect 可复用。HTML 标准明确新 auxiliary browsing context 可以复制同源 opener 的 session storage;这是已持有 bearer 的本地浏览器上下文转交边界,不是服务器扩大 room/role 权限,因此不增加导航状态机。raw grant 不进入 `localStorage`、cookie、query、Referrer、服务端持久存储、应用/代理日志或错误。OWASP 日志指南也明确把 access token、session identifier 和密码列为通常不应直接记录的数据。Fragment 降低服务端泄漏面,但 possession 仍等于该房间 Viewer 权限。 来源(访问于 2026-08-19):[RFC 3986 section 3.5](https://www.rfc-editor.org/rfc/rfc3986.html#section-3.5)、[WHATWG WebSockets](https://websockets.spec.whatwg.org/#the-websocket-interface)、[RFC 6455](https://www.rfc-editor.org/rfc/rfc6455.html)、[RFC 6750 section 2.3](https://www.rfc-editor.org/rfc/rfc6750.html#section-2.3)、[W3C Referrer Policy](https://www.w3.org/TR/referrer-policy/)、[W3C TAG Capability URLs](https://www.w3.org/TR/capability-urls/)、[HTML Web Storage](https://html.spec.whatwg.org/multipage/webstorage.html)、[Web Cryptography Level 2](https://www.w3.org/TR/WebCryptoAPI/)、[Node.js Crypto](https://nodejs.org/api/crypto.html) 与 [OWASP Logging Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html)。 diff --git a/docs/session-handoff.md b/docs/session-handoff.md index 82b07d4..e0a2a8b 100644 --- a/docs/session-handoff.md +++ b/docs/session-handoff.md @@ -36,23 +36,26 @@ Current high-signal boundary: subscriber PCs explicitly use no external ICE server and check the dedicated public SFU candidate directly; ordinary peers and LiveKit server-side public IP discovery retain deployment STUN. Production exact deployed - application/runtime revision `679fe3e7af634309322bea83b316641f51ad3d09`, - release `679fe3e`, uses strict `screener-v11`; current source adds exact - Browser SFU ICE-server-isolation implementation - `ae09c760adec76fd26da611d4928486d105c6d3b`, while production has not deployed - it. The route uses one short-lived + application/runtime revision `c4962f54443ad5f98bc65861195a3d9c74a48996`, + release `c4962f5`, uses strict `screener-v11` and includes exact Browser SFU + ICE-server-isolation implementation + `ae09c760adec76fd26da611d4928486d105c6d3b`. The owner physically verified + deployed SFU fallback media. The route uses one short-lived exact-generation decoded-frame observer only for the pending candidate inside the existing total deadline; the periodic stats sampler still owns active-path - diagnostics and stalls. Real external-network and SFU media evidence remains - open. Do not run executable/native suites unless their later acceptance + diagnostics and stalls. Broader external-network and SFU lifecycle evidence + remains open. Do not run executable/native suites unless their later acceptance boundary specifically requires them. -- ADR-0002 owns the implemented room model: one process-memory RoomStore, - random free four-digit codes, configurable 24-hour dormant leases, restart - loss, local Host preference replay, an independent expiring Viewer grant, and - open/password/disabled code entry. Neither source nor production uses SQLite; - exact release operations and change-scoped infrastructure recovery are owned - by deployment. Routine application-only releases do not maintain a full - rollback/configuration backup. +- ADR-0002 owns the room model: one process-memory RoomStore, random free + four-digit codes, configurable 24-hour dormant leases, restart loss, local + Host preference replay, and open/password/disabled code entry. Accepted next + wire `screener-v12` replaces the current time-bearing grant with one 22-character + room-lived opaque grant, adds `ROOM_NOT_FOUND`, removes product diagnostic + download buttons, and compacts Host invitation controls; the Host-only + bounded snapshot remains for acceptance tooling. Current source and production remain + v11 until that batch is implemented and deployed atomically. Neither source + nor production uses SQLite; exact release operations and change-scoped + infrastructure recovery are owned by deployment. - Browser display video must leave `contentHint` unset; audio keeps `music`. Browser video is fixed VP8 across direct, browser-relay, and SFU paths; the current strict v11 source and production have no codec UI/state/wire or diff --git a/docs/status.md b/docs/status.md index fb4c5fb..f4ccab0 100644 --- a/docs/status.md +++ b/docs/status.md @@ -6,18 +6,18 @@ This is the current execution index. Git history owns completed timelines; [veri ## Production -- `https://share.bonfire.icu` runs exact deployed application/runtime revision `679fe3e7af634309322bea83b316641f51ad3d09`, release `679fe3e`, wire `screener-v11`, from `/opt/screener/releases/679fe3e`; that revision is integrated in canonical `main`. Its immutable runtime ZIP SHA-256 is `fc96137f0fdc4dc4d8e083513d8fe99b9c55f7067a8b1de9202551edc3776c50`, and its 38-file manifest SHA-256 is `73f67c530acd5d4c77b546c19b5752c066f82682a584a72cd7ca596d6c5bec7e`. -- `screener`, LiveKit, coturn, and nginx are active/running; local/public `/` and `/healthz` return 200, all six served assets match the immutable release, and all four services report `NRestarts=0`. `assets/index-CGzoJSvs.js` has SHA-256 `31cf628d96be88f373cd0c3ac09e4966929fe320414aa1f0f9841264708d5a7e`; the retired v10 asset `assets/index-BtFMxoNI.js` returns 404, and stale v10 authentication receives `INVALID_MESSAGE` with close 1008 before room authority. -- The service process resolves its working directory to exact release `679fe3e`; it has no writable room `StateDirectory` or SQLite runtime. +- `https://share.bonfire.icu` runs exact deployed application/runtime revision `c4962f54443ad5f98bc65861195a3d9c74a48996`, release `c4962f5`, wire `screener-v11`, from `/opt/screener/releases/c4962f5`; that revision is integrated in canonical `main`. Its immutable runtime ZIP SHA-256 is `734815479a03728bb81b44ce5dc04fe010323acdbb91cdbea0e806548278164c`, and its 38-file manifest SHA-256 is `aefc414238655e9cb33d33975813bcc44afd22c5ad4e056ab719fae6c20aed47`. +- `screener`, LiveKit, coturn, and nginx are active/running; local/public `/healthz` return 200 and all four services report `NRestarts=0`. The served main Browser asset is `assets/index-DQgIVwby.js` with SHA-256 `f819df6c94652d0c5847daa602431384f7b6bf58aa38615fa327526755ede4ef`. +- The service process resolves its working directory to exact release `c4962f5`; it has no writable room `StateDirectory` or SQLite runtime. - Production admits one Host plus 20 Viewers, applies endpoint capacity `2`, rejects stale Browser and executable-sender wires before room authority, and runs the one-controller exact-candidate route model with one staged total deadline. Rooms use random free four-digit codes, a 24-hour dormant lease, restart loss, local Host creation preferences, independent Viewer grants, and `open | password | disabled` code entry. LiveKit remains dedicated with `room.auto_create: false`, `max_participants: 21`, and global admission `1` publication ingress / `20` subscription egress. - Production media ports remain STUN-only UDP 3478 and LiveKit UDP 7882; coturn TCP/TLS, port 5349, media TCP, and relay ranges are disabled. The public Web ingress remains TCP 80/443; Node 8787 and LiveKit control/signaling 7880 are internal-only. The nft ruleset SHA-256 is `a895f0bb7b66ffe21071ee18acd84d4ea43ca16d4bca9517e6272e14d0ecced4`. -- SFU subscribers reconcile Host screen publications across connect, activation, participant arrival, track publication, and reconnect. LiveKit runs at `warn`/Pion `error`, and the Screener unit waits boundedly for the same-host LiveKit control listener before startup. Postflight found zero warning-level Screener journal entries and an empty LiveKit namespace, so physical direct, browser-relay, and SFU codec behavior remains an open current-release gate. -- Production Browser SFU PCs currently inherit the LiveKit join response's external STUN endpoint. Under the reproducing Mihomo TUN path, isolated Host publication succeeded only `2/8`; server input/output hooks counted return-path work while Browser ICE received no response. The accepted Browser SFU ICE-server isolation is not deployed. +- SFU subscribers reconcile Host screen publications across connect, activation, participant arrival, track publication, and reconnect. LiveKit runs at `warn`/Pion `error`, and the Screener unit waits boundedly for the same-host LiveKit control listener before startup. Postflight found healthy unchanged services and the owner physically verified current SFU fallback media. Full direct/browser-relay/SFU codec and real-network acceptance remain open. +- Production Browser SFU publisher and subscriber PCs explicitly use empty external ICE-server lists while retaining LiveKit-signaled UDP candidates. The owner verified that the deployed isolation fixes the reported SFU connection failure; the prior P1 core-route hold is closed. - Production leaves video `contentHint` unset, keeps audio `contentHint = "music"`, and fixes Browser direct, browser-relay, and SFU video to VP8 without codec UI, quality state, wire fields, or fallback media codecs. ## Current Source -- Canonical root `main` is the integration truth; auxiliary branches and older worktrees do not supersede it. Current source keeps strict Browser `screener-v11`, rejects every other wire before room authority, and adds Browser SFU ICE-server isolation implementation `ae09c760adec76fd26da611d4928486d105c6d3b` beyond deployed application revision `679fe3e7af634309322bea83b316641f51ad3d09`. +- Canonical root `main` is the integration truth; auxiliary branches and older worktrees do not supersede it. Current source equals deployed application revision `c4962f54443ad5f98bc65861195a3d9c74a48996`, keeps strict Browser `screener-v11`, rejects every other wire before room authority, and includes Browser SFU ICE-server isolation implementation `ae09c760adec76fd26da611d4928486d105c6d3b`. - Current source implements one Host plus up to `20` Viewers, one steady outbound media-copy cap for every non-server endpoint (default `2`, static `1/2/3`), and the process-memory room model with random four-digit codes, a default 24-hour dormant lease, exact-Host resume, restart loss, local Host preference replay, independent token invitations, `open | password | disabled` code entry, and no SQLite runtime. - One event-driven controller owns the committed graph and at most one room-serial child operation. It uses one deterministic candidate list/cursor and one total direct-then-SFU deadline; exact admission and physical resources remain charged through drain, and the pending candidate commits only after an exact-generation decoded-frame proof. A short-lived 100 ms stats observer proves only that candidate inside the existing deadline, while the 2-second sampler remains responsible for active-path diagnostics and decoded-frame stalls. Healthy edges remain sticky and current-path quality does not authorize reparenting. - Ordinary peer ICE is STUN-only and the only application fallback is the dedicated LiveKit SFU over UDP. SFU generations use explicit no-default ingress/egress capacities, exact `reserved | committed | draining` accounting, delete-plus-absence release, one Host publication, and exact Viewer subscription handles. No Screener TURN, ICE/TCP, media TCP, or TLS-relayed media route exists. @@ -25,16 +25,15 @@ This is the current execution index. Git history owns completed timelines; [veri - Current source leaves video `contentHint` unset and keeps audio `contentHint = "music"`. Browser direct and browser-relay offers contain VP8 as their only media codec, and SFU publication explicitly uses VP8 with no backup codec. Codec UI, quality state, wire fields, fallback media codecs, and share-lifetime codec branches are absent. Live 64/128/256 kbps audio-ceiling mutation and ordinary `shareGeneration`-fenced Pause/Resume remain unchanged. - Current source implements typed first-frame Viewer presentation and recovery, Chinese user-facing route/access failures, privacy-safe failed-page export, authenticated Host-only on-demand route snapshots, neutral `ROOM_ACCESS_DENIED` room-code admission, and responsive entry controls. - Recommended quality remains exactly `1080p60`, `1080p30`, and `720p30`; current source defaults to `1080p30`. Advanced resolution adds `480p` as `854x480` without adding a fourth preset, and advanced FPS and bitrate remain independent. -- Exact v11 implementation `f5a295c52e0ac7d18e5a7949217861c7aa74e9c9`, integrated into deployed application/runtime revision `679fe3e7af634309322bea83b316641f51ad3d09` and canonical `main`, passed the current automated source gates. Deployment postflight passed, but no physical direct, browser-relay, or SFU codec path was exercised; retained v10 loopback evidence applies only to that older source. Exact results are owned by [verification status](./verification-status.md). -- Exact SFU isolation implementation `ae09c760adec76fd26da611d4928486d105c6d3b` passed repository hygiene, TypeScript, all 605 Web tests in 45 files, client/server production builds, and three independent code reviews with no finding. It is not deployed and has no exact product Viewer first-frame evidence. +- Exact v11 implementation `f5a295c52e0ac7d18e5a7949217861c7aa74e9c9` plus SFU isolation implementation `ae09c760adec76fd26da611d4928486d105c6d3b` are integrated into deployed revision `c4962f54443ad5f98bc65861195a3d9c74a48996`. Repository hygiene, TypeScript, all 605 Web tests, client/server builds, the 254-test SFU gate, deployment postflight, and owner physical SFU fallback proof passed. Exact results are owned by [verification status](./verification-status.md). +- Accepted next source is one strict `screener-v12` contract with exact-current audio-track statistics, a 22-character room-lived Viewer grant, explicit `ROOM_NOT_FOUND`, no product diagnostic-download buttons, and a compact Host invitation surface. The Host-only bounded snapshot remains for automatic acceptance. None of that v12 batch is implemented or deployed at this checkpoint. ## Current Milestone -1. Deploy the source-complete Browser SFU ICE-server isolation, then prove exact Host publication and Viewer first-frame media under the reproducing TUN path and a mobile network. -2. Diagnose the reported screen-audio `1 kbps` readout and physically validate live audio ceilings. -3. Physically verify the deployed `screener-v11` fixed-VP8/no-video-hint direct, browser-relay, and SFU paths from actual codec, capture, outbound, and decoded stats. -4. Refine the Host invitation controls without changing grant semantics implicitly. -5. Finish representative ICE/STUN/SFU acceptance including mobile networks. Browser port prediction, NAT classification, TCP probing, fake page keepalive, and quality-driven reparenting remain outside the accepted model. +1. Fix the reported screen-audio `1 kbps` readout by binding ordinary Host and SFU publisher stats to the exact current audio track, then physically validate live audio ceilings. +2. Implement the accepted `screener-v12` grant/access/UI contract: 22-character room-lived grant, `ROOM_NOT_FOUND`, no product diagnostic-download buttons, and compact copy-first Host invitation controls. +3. Physically verify fixed-VP8/no-video-hint direct, browser-relay, and SFU paths from actual codec, capture, outbound, and decoded stats. +4. Finish representative ICE/STUN/SFU acceptance including mobile networks. Browser port prediction, NAT classification, TCP probing, fake page keepalive, and quality-driven reparenting remain outside the accepted model. ## Active Boundaries @@ -42,14 +41,10 @@ This is the current execution index. Git history owns completed timelines; [veri - The desktop Host background/minimized report is deferred until a current-production real-game reproduction supplies synchronized media and CPU/GPU evidence. Mobile Viewer background playback and relay survival remain a separate Accepted Later lifecycle gate. - Open PR #192 and the Native stack are evidence/research, not pending product releases. Native senders, capture helpers, shared-encode executables, and their test binaries are outside the current Browser milestone and are not built or run. - Deployed surfaces and retained candidates that still need product decisions are indexed only in [the TODO ledger](./todo.md); do not extend or roll them back automatically. -- Room lifetime, authorization/storage semantics, restart loss, entry/presentation, and the Browser v11 wire remain aligned between source and production; Browser SFU ICE-server isolation is source-only until the next application release. The prior application-only cutover reused unchanged infrastructure and configuration and does not maintain a full rollback/configuration backup; any future infrastructure or irreversible-state change requires recovery scoped to the surfaces it actually changes. +- Room lifetime, authorization/storage semantics, restart loss, entry/presentation, and Browser v11 remain aligned between source and production. The accepted v12 room/access/UI change stays source-pending until implemented and later deployed atomically. The prior application-only cutover reused unchanged infrastructure and configuration and does not maintain a full rollback/configuration backup; any future infrastructure or irreversible-state change requires recovery scoped to the surfaces it actually changes. - Real SFU recovery, heterogeneous networks, mobile lifecycle, audio/A-V device behavior, and endurance/resource measurements remain external acceptance evidence, not blockers for unrelated reversible work. ## Current Hold -Production has an active P1 core-route hold: Browser SFU initial ICE is -intermittent under the reproduced Mihomo TUN path; the best-supported cause is -the PC's external-STUN/SFU association conflict. Browser SFU ICE-server -isolation is source-validated but must be deployed and physically proven before -the route hold closes. Native/executable work and broad repository cleanup -remain outside the current evidence boundary. +There is no active core-route hold. Native/executable work and broad repository +cleanup remain outside the current evidence boundary. diff --git a/docs/todo.md b/docs/todo.md index 9f635f5..b8c088b 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -6,11 +6,10 @@ Only items in **Now** are executable after their stated decision gate. A branch ## Now -1. **Deploy and physically prove Browser SFU ICE-server isolation.** Source implementation `ae09c760adec76fd26da611d4928486d105c6d3b` and its focused/full automated gates are complete. Deploy one immutable application artifact without changing LiveKit, coturn, nginx, systemd, firewall, ports, route deadline, or retry behavior. Close the P1 hold only after exact Host publication plus Viewer first-frame media passes under the reproducing TUN path and a separate mobile network; do not gate on candidate type/count or restore TURN. -2. **Diagnose and physically validate live screen audio.** First reproduce the Host diagnostic remaining at `1 kbps` after switching the shared source, and distinguish stale/reset statistics from an actual RTP bitrate problem. The app already requests audio and supplies system/window audio hints; record the selected surface, returned audio track, sender/receiver counters, and audible result because the browser owns the picker and may still return video only. Then switch the 64/128/256 kbps ceilings across direct, browser-relay, and SFU paths, create a new edge, and switch source while checking route continuity and A/V synchronization. Do not infer fidelity or room-wide atomic convergence from configuration alone. -3. **Physically validate fixed Browser VP8 with no video hint.** Production exact deployed application/runtime revision `679fe3e7af634309322bea83b316641f51ad3d09`, release `679fe3e`, already runs the strict v11 wire, VP8-only direct/browser-relay offers, VP8 SFU publication with no backup codec, unset video `contentHint`, and no codec UI/state/wire. Confirm actual codec, capture, outbound, and decoded stats on direct, browser-relay, and SFU paths; deployment postflight alone does not close this gate. -4. **Redesign the Host invitation controls.** Replace the current raw-link-heavy block with a compact copy-first layout while preserving the accepted copy-link, copy-room-code, code-entry policy, rotate, and revoke actions. Keep the credential out of logs and request URLs; changing the copied credential length is blocked on the explicit product decision below. -5. **Finish real-network route acceptance.** Measure standard ICE/STUN direct paths, peer relay, SFU ingress/subscription, exact first-frame commit and rollback, relay-ingress repair with subtree retention, disconnect/capacity drain, Pause/Resume, and bounded explicit failure across representative IPv4/IPv6 NATs and mobile networks. Include an all-UDP-blocked case to establish the current failure boundary. Do not add port prediction, NAT classification, TCP probes, guessed candidates, route scores, or quality-driven reparenting. +1. **Fix and physically validate live screen-audio statistics.** Bind ordinary Host and SFU publisher audio stats to the exact current sender track, merge the SFU video/audio reports, and reset the baseline when the audio identity changes or disappears. Then reproduce the reported `1 kbps` state with known audible content and switch the 64/128/256 kbps ceilings across direct, browser-relay, and SFU paths while checking sender/receiver counters, route continuity, audible result, and A/V synchronization. A real muted/idle track may legitimately use about 1 kbps; do not multiply the value or display the configured ceiling as observed bitrate. +2. **Implement the accepted Browser v12 room/access/UI contract.** Replace the time-bearing 256-bit Viewer grant with a 22-character 128-bit opaque grant whose digest belongs to the exact room incarnation, remove independent grant expiry fields, add `ROOM_NOT_FOUND` for unallocated/reclaimed/expired code-only entry without a password prompt, remove Host/Viewer diagnostic-download buttons and browser file export, and replace the Host raw-link block with a compact copy-first invitation layout. Preserve the Host-only bounded route snapshot used by automatic acceptance, plus copy-room-code, `open | password | disabled`, password, rotate, revoke, fragment/sessionStorage privacy, and strict atomic wire replacement; do not retain a v11 parser or add a short-link store. +3. **Physically validate fixed Browser VP8 with no video hint.** Production exact deployed application/runtime revision `c4962f54443ad5f98bc65861195a3d9c74a48996`, release `c4962f5`, runs strict v11, VP8-only direct/browser-relay offers, VP8 SFU publication with no backup codec, unset video `contentHint`, and no codec UI/state/wire. Confirm actual codec, capture, outbound, and decoded stats on direct, browser-relay, and SFU paths; automated gates and the owner SFU connectivity proof do not close the full codec gate. +4. **Finish real-network route acceptance.** Measure standard ICE/STUN direct paths, peer relay, SFU ingress/subscription, exact first-frame commit and rollback, relay-ingress repair with subtree retention, disconnect/capacity drain, Pause/Resume, and bounded explicit failure across representative IPv4/IPv6 NATs and mobile networks. Include an all-UDP-blocked case to establish the current failure boundary. Do not add port prediction, NAT classification, TCP probes, guessed candidates, route scores, or quality-driven reparenting. ## Accepted Later Roadmap @@ -32,8 +31,6 @@ These items are real product work, ordered after the current physical-media, rea | #169 connection self-check | Keep, revise, or remove the user-facing pre-share probe. Do not extend it automatically. | | #181 selected-pair response counter | Keep as local observation or remove; it has no route authority. | | #182 signaling watchdog | Confirm Host and mobile/background session semantics before calling it complete. | -| Viewer invitation credential length | The current grant is about 62 characters because its 43-character random suffix is the shortest base64url encoding of the accepted 256-bit secret. Decide whether a shorter copied URL justifies changing that security requirement or adding server-side indirection; do not truncate it ad hoc. | -| Viewer invitation lifetime | Align a grant with the exact in-memory room incarnation instead of an independent seven-day clock: it remains usable while that room exists and ends on rotate, revoke, process restart, or room reclamation. This deferred room change needs its own truth checkpoint before implementation. | | Project license and closed-commercial boundary | Decide the repository and distribution license before any public release or package distribution. GPL/AGPL implementations remain research-only until then. | These decisions are not permission to continue old branches, and current deployment remains unchanged until a scoped decision is made. diff --git a/docs/verification-status.md b/docs/verification-status.md index 11aa68f..d204d52 100644 --- a/docs/verification-status.md +++ b/docs/verification-status.md @@ -10,16 +10,17 @@ Capacity and routing entries below describe the exact tested source or release o ## Deployment Evidence -- Production runs exact deployed application/runtime revision `679fe3e7af634309322bea83b316641f51ad3d09`, release `679fe3e`, wire `screener-v11`, from `/opt/screener/releases/679fe3e`; that revision is integrated in canonical `main`. The immutable runtime ZIP SHA-256 is `fc96137f0fdc4dc4d8e083513d8fe99b9c55f7067a8b1de9202551edc3776c50`; its 38-file manifest SHA-256 is `73f67c530acd5d4c77b546c19b5752c066f82682a584a72cd7ca596d6c5bec7e`. -- Postflight found `screener`, LiveKit, coturn, and nginx active with zero restarts and no warning-level Screener journal entries. Local/public root, health, and all six served assets matched the release; `assets/index-CGzoJSvs.js` has SHA-256 `31cf628d96be88f373cd0c3ac09e4966929fe320414aa1f0f9841264708d5a7e`, and retired `assets/index-BtFMxoNI.js` returns 404. A stale-v10 authentication received `INVALID_MESSAGE` and close 1008 before room authority. +- Production runs exact deployed application/runtime revision `c4962f54443ad5f98bc65861195a3d9c74a48996`, release `c4962f5`, wire `screener-v11`, from `/opt/screener/releases/c4962f5`; that revision is integrated in canonical `main`. The immutable runtime ZIP SHA-256 is `734815479a03728bb81b44ce5dc04fe010323acdbb91cdbea0e806548278164c`; its 38-file manifest SHA-256 is `aefc414238655e9cb33d33975813bcc44afd22c5ad4e056ab719fae6c20aed47`. +- Postflight found `screener`, LiveKit, coturn, and nginx active with zero restarts. Local/public health matched the release; `assets/index-DQgIVwby.js` has SHA-256 `f819df6c94652d0c5847daa602431384f7b6bf58aa38615fa327526755ede4ef`. - Production uses endpoint capacity `2`, room admission `20`, random free four-digit memory rooms, `ROOM_LEASE_SECONDS=86400`, a dedicated LiveKit instance with `room.auto_create: false` and `max_participants: 21`, and global SFU admission `1` ingress / `20` egress. Room authority is process-memory-only. Coturn is `stun-only`, `no-tcp`, and `no-tls`; only UDP 3478 listens, and nft exposes only UDP 3478/7882 for Screener media. - The production service orders Screener after LiveKit and boundedly waits for its control listener. The LiveKit namespace is empty; media listeners remain STUN-only UDP 3478 and LiveKit UDP 7882 with no 5349, coturn TCP/TLS, relay range, ICE/TCP, or media TCP listener. The unchanged nft ruleset has SHA-256 `a895f0bb7b66ffe21071ee18acd84d4ea43ca16d4bca9517e6272e14d0ecced4`. - This application-only postflight proves archive identity, atomic application/assets, stale-wire rejection, process health, listener/firewall continuity, and log sentinels. It did not exercise direct, browser-relay, SFU, audio, codec, mobile, quality, or endurance behavior. Routine application-only releases do not maintain a full rollback/configuration backup; any future infrastructure or irreversible-state change requires recovery scoped to what it changes. ## Retained Functional Evidence -- Exact v11 implementation `f5a295c52e0ac7d18e5a7949217861c7aa74e9c9`, integrated into deployed application/runtime revision `679fe3e7af634309322bea83b316641f51ad3d09` and canonical `main`, passed TypeScript typecheck, all 605 Web tests in 45 files, client and server production builds, the 254-test SFU gate in 10 files, and the access/privacy gate with explicit Chrome. This validates source structure, strict-wire behavior, fixed-VP8 path configuration, build output, and the covered Browser gate only; it provides no physical direct, browser-relay, or SFU codec evidence. -- Exact Browser SFU ICE-server-isolation implementation `ae09c760adec76fd26da611d4928486d105c6d3b` passed repository hygiene, TypeScript typecheck, all 605 Web tests in 45 files, client and server production builds, and three independent code reviews with no finding. Focused publisher/subscriber and ordinary-peer isolation coverage passed 95 tests in three files. This proves source option propagation and unchanged peer STUN behavior; production application deployment and exact Host/Viewer first-frame media remain open. +- Exact v11 implementation `f5a295c52e0ac7d18e5a7949217861c7aa74e9c9`, integrated with the SFU isolation fix into deployed application/runtime revision `c4962f54443ad5f98bc65861195a3d9c74a48996` and canonical `main`, passed TypeScript typecheck, all 605 Web tests in 45 files, client and server production builds, the 254-test SFU gate in 10 files, and the access/privacy gate with explicit Chrome. This validates source structure, strict-wire behavior, fixed-VP8 path configuration, build output, and the covered Browser gate only; it does not provide full physical direct/browser-relay/SFU codec evidence. +- Exact Browser SFU ICE-server-isolation implementation `ae09c760adec76fd26da611d4928486d105c6d3b` passed repository hygiene, TypeScript typecheck, all 605 Web tests in 45 files, client and server production builds, three independent code reviews, focused 95-test publisher/subscriber and ordinary-peer isolation coverage, deployment postflight, and owner physical SFU fallback media proof. This establishes the reported production connection fix without turning candidate type/count into an invariant; full route/codec/network acceptance remains open. +- A fresh canonical `main@c4962f5` background gate passed repository hygiene, all 605 Web tests, both production builds, the 254-test SFU admission suite, and the Chrome 151 20-Viewer 720p30/30-second local route gate. All 20 Viewers advanced decoded frames, Host and relays stayed within endpoint cap `2`, no orphan SFU publication appeared, and all four current-child timing distributions contained 20 values. The browser gate uses deterministic canvas and does not start LiveKit, so it proves local direct/browser-relay control and media behavior only; it is not physical capture or SFU evidence. - Exact v10 runtime source `fdd5a4a529ff297f41c05ea3388bf484d76afe8f` passed repository hygiene, TypeScript typecheck, client and server production builds, all 610 Web tests in 45 files, the 257-test SFU admission gate, the access/privacy Browser gate, and the production-dependency audit with zero vulnerabilities. Its one-Host/20-Viewer Chrome 151 direct-loopback report passed all seven assertions: all Viewers decoded, the selected VP8 settings reached every participant, Host and relay media edges stayed within cap `2`, no orphan SFU publication appeared, and all four current-child timing distributions contained 20 values. The report SHA-256 is `802CBB7632E9AEA3C39FE831A7F7880D24140B2E83942CE0DFDA05DE4B408EED`. This validates the v10 source boundary, not H.264 quality, hardware attribution, physical capture, public-network behavior, or SFU packet flow. - Chrome 151 synthetic `1/3/5/8` and 720p30 runs preserved bounded fanout and decoding. A `9461e20`-era three-Viewer rerun kept Host2/Browser Viewer1, decoded every Viewer, and recovered a hard first-level relay departure in 5,380 ms; it did not exercise quality-triggered MBB. Sixteen-Viewer cap2/cap3 loopbacks decoded all Viewers and a ten-second follow-up recorded guarded sender and aggregate Chromium CPU evidence, but resolution/FPS remained low and the ordinary-PC data cannot rank caps, select product policy, or prove sustainable heterogeneous-network quality. See [peer-assisted media](./research/peer-assisted-media.md). - Chrome 151 with local LiveKit 1.13.5/client 2.22.0 proved one SFU/UDP root after `q,f` -> `q,h`: decoded frames advanced 3 -> 23, rendered frames reached 26, Host used one edge, and leaves were clean. This is functional evidence only. See [low-server media routes](./research/low-server-media-routes.md). @@ -36,9 +37,9 @@ Capacity and routing entries below describe the exact tested source or release o - Host background: deferred until the report reproduces on exact production with a continuously changing real game or replay, a separate direct wired Viewer, synchronized capture/outbound/inbound plus CPU/GPU evidence, and repeated focused/unfocused/hidden/minimized windows. The current-Chrome Browser `getDisplayMedia()` WPF-window screening did not observe an immediate Host-page media drop, and the old harmful `contentHint = "motion"` release remains a plausible confound rather than a proven background cause. Current evidence authorizes no keepalive, Wake Lock, timer, codec switch, route change, or Native fallback. - Relay resources: current v11 source and production expose the authenticated `1/2/3` endpoint-cap boundary, and retained v10 local loopback evidence covers 20 Viewers at cap `2` only for that source. CPU/GPU/upload and heterogeneous-network measurements remain diagnostics, not authority to change the contract. - Room scale: current source and production admit 20 Viewers plus one Host, and the retained v10 local Browser convergence gate covers that bound on direct loopback. A real public-network 1:20 session, quality, and sustained resource matrix remain open. -- SFU: production uses one Host publication with exact Viewer subscriptions and no fixed root-count or room-wide lease rule, but deployed Browser SFU PCs still mix the LiveKit-provided STUN endpoint with the public SFU candidate and are held on the reproduced TUN failure. Source now implements the accepted empty-ICE-server client rule; after deployment, exact Host publication plus Viewer first-frame media must pass on that TUN path and a separate mobile network. Real LiveKit lifecycle/JWT replay, shaped packet-flow, BWE downshift/recovery, resource use, and SFU-fed first-child evidence remain open. Use Linux `tc` or an authorized public canary for per-leaf shaping, never CDP throttling. +- SFU: production uses one Host publication with exact Viewer subscriptions and no fixed root-count or room-wide lease rule. Browser SFU PCs use the accepted empty-external-ICE-server rule and owner physical media proof closed the reported connection hold. Real LiveKit lifecycle/JWT replay, shaped packet-flow, BWE downshift/recovery, resource use, SFU-fed first-child behavior, and broader heterogeneous-network acceptance remain open. 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 current v11 Viewer stage, local stats/export plumbing, identity-bound relayed-detail retention, export freshness guards, and on-demand Host route snapshot are deployed. 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. All fields diagnose and have no recovery authority; they do not by themselves prove hardware encode, synchronization quality, or route quality. +- Observability: the deployed v11 Viewer stage, local stats/export plumbing, identity-bound relayed-detail retention, export freshness guards, and on-demand Host route snapshot remain current until the accepted v12 batch removes product download buttons and browser file export. The Host-only bounded snapshot stays for automatic acceptance. 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. All retained fields diagnose and have no recovery authority; they do not by themselves prove hardware encode, synchronization quality, or route quality. - Mobile: Android/iOS remain Viewer-only. Autoplay, rotation, iOS lock-screen/page reclamation, background reconnection, and network changes need device evidence. AirPlay/system mirroring is Viewer-local output, not a portable in-app WebRTC output contract. - Native: ADR-0006 has one-Viewer evidence only. Viewer2/FIFO, hardware diversity, endurance, downloaded-package/public-host, game A/V, and browser diversity remain open. - Access and operations: exact v11 implementation passed the access/privacy gate with explicit Chrome. Production passed atomic v11 application cutover/postflight plus stale-v10 rejection before authority. Restart-wide room loss, password-profile creation, grant/code orthogonality, rotate/revoke, memory-only runtime, and exact current journal/nginx sentinel checks retain their focused evidence. Headful same-browser preference replay and the isolated VM/IP clean-port matrix remain open; shared-IP smoke cannot approve them. diff --git a/docs/方案设计.md b/docs/方案设计.md index 3981d6e..f17a598 100644 --- a/docs/方案设计.md +++ b/docs/方案设计.md @@ -2,7 +2,7 @@ ## 2026-08-18 14:45 -状态:production 运行精确 deployed application/runtime revision `679fe3e7af634309322bea83b316641f51ad3d09`、release `679fe3e`;current source 在同一严格 `screener-v11` runtime 上增加 Browser SFU ICE-server-isolation implementation `ae09c760adec76fd26da611d4928486d105c6d3b`,尚未部署。Browser display video 不设置 `contentHint`、audio 保留 `music`,唯一视频媒体 codec 为 VP8,UI、quality state/wire 和分享生命周期均没有 codec 分支。Pause/Resume 继续由当前 Host session 和 `shareGeneration` 约束。 +状态:production 与 canonical `main` 运行精确 deployed application/runtime revision `c4962f54443ad5f98bc65861195a3d9c74a48996`、release `c4962f5` 和严格 `screener-v11`,包含 Browser SFU ICE-server isolation implementation `ae09c760adec76fd26da611d4928486d105c6d3b`;owner 已在生产实测 SFU fallback 成功取得画面。Browser display video 不设置 `contentHint`、audio 保留 `music`,唯一视频媒体 codec 为 VP8,UI、quality state/wire 和分享生命周期均没有 codec 分支。Pause/Resume 继续由当前 Host session 和 `shareGeneration` 约束。22 字符 room-lived Viewer grant、`ROOM_NOT_FOUND`、产品诊断下载移除与紧凑邀请 UI 已接受为单一 `screener-v12`,但尚未实现或部署。 ## 目标与边界 @@ -13,7 +13,7 @@ - 分享者主动选择屏幕、窗口或标签页,并请求可用音频; - 创建四位随机数字房间;production site access 密码严格控制建房/发布和 code-only 观看,默认 room-scoped capability link 让受邀 Viewer 直接观看,房间控制状态只驻留当前进程内存; - 每名观看者拥有独立的 `RTCPeerConnection`; -- 当前 production 要求 STUN,普通 peer ICE 只发 STUN servers;current source Browser SFU PC 使用空 ICE-server 列表、保留 LiveKit signaling 的 UDP candidates 并由标准 ICE 选择,该改动尚未部署; +- 当前 production 要求 STUN,普通 peer ICE 只发 STUN servers;source 与 production 的 Browser SFU PC 使用空 ICE-server 列表、保留 LiveKit signaling 的 UDP candidates 并由标准 ICE 选择; - 展示连接状态、direct/relay、RTT、实际码率、帧率和丢包等数据; - 超过部署配置上限的观看者被明确拒绝,不静默超载; - 在代码、测试和真实浏览器验证完成前,不宣称达到延迟或画质目标。 @@ -72,13 +72,13 @@ tests/ RoomStore、ICE 配置、协议和 WebSocket 集成测试 `SITE_ACCESS_PASSWORD` 是简单的部署访问口令,不是 Viewer 密码或账号凭据。production 启动时必须配置;本地开发/测试可显式关闭。非空值必须包含 8 至 128 个可见 ASCII bytes(`0x21` 至 `0x7e`),并作为不复用 LiveKit、TLS 或其他凭据的独立 secret。`POST /api/site-access` 只在严格允许的 Origin 下接收 `Authorization: Bearer`,成功后签发 12 小时的无状态 site-access cookie:值只包含版本、到期时间和 HMAC-SHA256 签名,不创建账号、JWT 或服务端 session Map。cookie 使用 `HttpOnly`、`SameSite=Strict`、`Path=/`、有限 `Max-Age`;HTTPS 生产环境使用 `Secure` 和 `__Host-` 前缀。`POST /api/rooms` 只认该 cookie,不接受同一 Bearer 旁路。原子 deployment change 在 nginx 为该 admission endpoint 配置 per-source `limit_req`,初始边界为 `5r/m`、`burst=5 nodelay`;超限返回通用 429,允许到达 Node 的错误 secret 使用常量时间比较和统一 401。limiter 只使用 nginx shared memory,不新增 IP 持久化/日志字段;Node 不建立 IP key/LRU/session table,并保留现有全局/未鉴权连接上限。删除旧 `ACCESS_PASSWORD`、`/api/session` 与旧 cookie;旧环境变量出现时 fail-fast。 -WebSocket upgrade 无法预知后续 role,因此仍允许没有 site-access cookie 的合法 grant Viewer 完成握手;握手前严格校验 `/signal` 无 query、Origin allowlist、总连接数和未鉴权连接数,并把当次 cookie 校验结果保存到 socket state。五秒内的第一条 `authenticate` 完成授权:Host 必须同时拥有 upgrade 时有效的 site-access 状态和当前房间 `hostToken`;Viewer 必须拥有目标房间合法未过期 grant,或者先通过 site access 再按 `open | password | disabled` code-only policy 校验。site-access cookie 只允许进行 code-only 尝试,不能替代 grant/房间密码;Viewer grant 也不能用于 HTTP 建房、Host role、其他房间或其他操作。 +WebSocket upgrade 无法预知后续 role,因此仍允许没有 site-access cookie 的合法 grant Viewer 完成握手;握手前严格校验 `/signal` 无 query、Origin allowlist、总连接数和未鉴权连接数,并把当次 cookie 校验结果保存到 socket state。五秒内的第一条 `authenticate` 完成授权:Host 必须同时拥有 upgrade 时有效的 site-access 状态和当前房间 `hostToken`;Viewer 必须拥有目标房间当前 digest 对应的 grant,或者先通过 site access 再按 `open | password | disabled` code-only policy 校验。site-access cookie 只允许进行 code-only 尝试,不能替代 grant/房间密码;Viewer grant 也不能用于 HTTP 建房、Host role、其他房间或其他操作。 -Viewer grant 与 code-only 入口是两个正交维度。每个房间都用 `randomBytes(32)` 生成含 256-bit 随机量和到期时间的 room-scoped opaque grant,服务端只在内存保存完整 grant 的 SHA-256 摘要并用定长比较验证。当前 grant 从建房或 rotate 起固定有效七天;房间删除、进程重启或 revoke 会使它更早失效。Host 的主分享动作始终复制 token-bearing invitation;到期在以后鉴权/重连时拒绝,即时清退使用 rotate/revoke。 +Viewer grant 与 code-only 入口是两个正交维度。每个房间用 `randomBytes(16).toString("base64url")` 生成 128-bit、22 字符 room-scoped opaque grant,服务端只在该 exact room incarnation 内保存完整 grant 的 SHA-256 摘要并用定长比较验证。grant 不编码版本、房间号或到期时间,也没有独立 expiry;它随房间存在,并在房间回收、进程重启、rotate 或 revoke 时失效。同号新房间生成新 digest,旧链接不能进入。Host 的主分享动作始终复制 token-bearing invitation;即时清退使用 rotate/revoke。 code-only policy 使用单一 `open | password | disabled` 状态:`open` 是无房间密码的默认值;`password` 要求已通过 site access 的 Viewer 再输入该房间密码;`disabled` 中性拒绝 code-only 尝试。它们都不改变有效 grant 的 exact-room Viewer authority,也不建立目录、账号、ACL 或额外媒体容量。Host 可独立改变 code-only policy、设置/替换/移除密码,以及 rotate/revoke grant。 -Host 的同源 `localStorage` creation profile 保存显示名、code-only policy 和可选房间密码,使进程重启或租约过期后的下一次显式分享可原子应用同一偏好。该密码只保存在 Host 浏览器;不保存在 Viewer 浏览器、URL/query/fragment、cookie、日志、错误或服务器持久存储。建房/更新请求通过 HTTPS/WSS 把明文短暂交给服务端,服务端使用 Node 标准异步 `scrypt` 固定 `N=16384,r=8,p=1`、16-byte 随机 salt 和 32-byte verifier,只在当前房间内存保存 48-byte material。进程级 gate 最多执行 2 个、等待 16 个派生,队满即有界失败;任务开始和提交前都复核当前 Host/Viewer session 与 room material。不存在房间、未配置/错误密码和正确密码后的满房都使用通用拒绝。 +Host 的同源 `localStorage` creation profile 保存显示名、code-only policy 和可选房间密码,使进程重启或租约过期后的下一次显式分享可原子应用同一偏好。该密码只保存在 Host 浏览器;不保存在 Viewer 浏览器、URL/query/fragment、cookie、日志、错误或服务器持久存储。建房/更新请求通过 HTTPS/WSS 把明文短暂交给服务端,服务端使用 Node 标准异步 `scrypt` 固定 `N=16384,r=8,p=1`、16-byte 随机 salt 和 32-byte verifier,只在当前房间内存保存 48-byte material。进程级 gate 最多执行 2 个、等待 16 个派生,队满即有界失败;任务开始和提交前都复核当前 Host/Viewer session 与 room material。不存在、已回收或已过期房间使用 `ROOM_NOT_FOUND`;现存房间的未配置/错误密码和正确密码后的满房使用 `ROOM_ACCESS_DENIED`。 邀请使用 `/r/{roomId}#v={grant}`。RFC 3986 要求 fragment 在 dereference 前由 user agent 分离,因此 HTTP request-target 与反向代理 access log 看不到它;WHATWG WebSocket 还明确拒绝含 fragment 的 URL。客户端只从页面 URL 严格解析一次,把 grant 写入 `screener:viewer-grant:{roomId}` 的 `sessionStorage`,随即以 `history.replaceState` 把地址栏替换为 canonical `/r/{roomId}`;刷新和页面内 WSS 重连继续使用该页面会话的值。Host 建房/rotate 收到的 raw grant 同样只在内存和 room-scoped `sessionStorage` 中保留,用它临时构造复制链接;`localStorage` 的 room ownership 只包含 canonical URL、room ID、Host token 和期限。grant 不进入 cookie、query、WebSocket URL、Referrer、日志或错误。生产继续发送 `Referrer-Policy: no-referrer`;fragment 与该 header 降低传输泄漏,但不能防止原邀请消息、浏览器扩展、截图或用户转发,UI 必须把链接称为访问凭据。 @@ -108,7 +108,7 @@ Web Host 在 `authenticate` 中显式发送 `viewerPresence: true` capability, WebSocket 使用无 query/fragment 的同源 `/signal`。upgrade 只记录 site-access cookie 是否有效;连接后五秒内第一条消息必须是当前单一 wire 的房间级 `authenticate`。Host 消息携带 room、role、内部 Host token 与 clientId,并由 socket state 复核 site access。Viewer 消息携带 room、role、clientId,以及 exact-room Viewer grant 或当前 code-only password attempt。成功的 `authenticated` 快照始终携带部署最终值 `endpointMediaCopyCapacity: 1|2|3`;普通模式的服务端 admission 只向 Host 激活稳定加入顺序中的至多该数量 Viewer,并在离开后提升等待者,双方信令只允许 active child。Browser Host 与 Viewer 严格校验该值,房间 `maxViewers` 只约束观看者总 admission。Native/executable sender 停留在旧 wire 并在 authority 前拒绝。服务端设置 64 KiB `maxPayload`、关闭 `perMessageDeflate`,并以 ping/pong 清理失效连接。单进程固定限制 2048 条总连接和 256 条未鉴权连接,容量在 upgrade 前预留,超限直接返回 HTTP 503 并销毁 socket。 -下表描述 current Browser 的单一 `screener-v11` wire;server 与 Browser assets 必须作为一个完整合同原子发布,其他 wire 在取得房间 authority 前拒绝,不存在旧 parser、writer、translator 或中间协议子集: +下表描述 accepted Browser 的单一 `screener-v12` wire;server 与 Browser assets 必须作为一个完整合同原子发布,其他 wire 在取得房间 authority 前拒绝,不存在旧 parser、writer、translator 或中间协议子集: | 方向 | 消息 | 作用 | | --- | --- | --- | @@ -123,12 +123,12 @@ WebSocket 使用无 query/fragment 的同源 `/signal`。upgrade 只记录 site- | viewer -> host | `restart-request` | 以 `rebuild` 标志请求 ICE restart 或完整重建该 peer | | host -> server | `stop-sharing` | 结束当前发布、清理本轮连接并让观看者等待;不删除房间或使邀请失效 | | server -> viewer | `route-status` | strict union 只允许 `{ revision, state: "waiting", reason: "sfu-admission" }` 或 `{ revision, state: "failed", reason: "route-exhausted" }`;只补充现有 `route-update` 无法表达的 SFU admission waiting 与 bounded route exhaustion,不携带 raw text、identity、candidate 或 retry hint | -| host -> server -> host | `request-route-diagnostic` / `route-diagnostic-snapshot` | 仅在 authenticated Host 点击下载诊断时请求并返回一份当前快照;不连续推送、不参与 route authority | +| host -> server -> host | `request-route-diagnostic` / `route-diagnostic-snapshot` | 仅供 authenticated Host 自动验收请求一份当前快照;产品 UI 不提供下载按钮,不连续推送、不参与 route authority | | host -> server -> viewers | `set-sharing-paused` / `host-status` | 当前 Host session 与 `shareGeneration` 约束一个 paused boolean;Resume 使用同一状态更新,不建立第二次授权、source acknowledgement 或媒体 proof | Zod 在服务边界拒绝未知类型、超长字符串、非法 SDP/candidate 结构和越权目标。服务端根据已认证会话决定路由:观看者不能给另一名观看者发信令,分享者也不能向本房间以外的 peerId 发送。 -current 的唯一 wire 是 `screener-v11`,不做版本协商、旧 parser、双写或 translator。任何其他 Browser wire 及 executable sender 的首条消息在角色进入 room lookup 前得到同一个 fatal refresh 终态并停止自动重连。Client 与 server 共用 `SIGNAL_CLOSE_CODES` 定义会话替换、鉴权失败和 Viewer access revoke 的终态语义,避免双方对重连边界产生分叉;非法协议消息使用 WebSocket policy-violation close code。缺失、过期、跨房、已 rotate 或 revoked 的当前 Viewer grant 使用同一个通用“邀请无效或已失效”结果,不区分房间不存在与凭据错误。已通过 site access 的 well-formed code-only 预期拒绝只发 `ROOM_ACCESS_DENIED`:unknown/expired room、disabled policy、absent/wrong password、room full 与其他已知有界 admission refusal 使用同一 code、公开文案和共享 authentication-failed close code。Grant 继续使用 `INVALID_TOKEN`,Host 鉴权保持独立,unexpected internal failure 使用不含 raw message 的 `SERVER_ERROR`。 +accepted 的唯一 wire 是 `screener-v12`,不做版本协商、旧 parser、双写或 translator。任何其他 Browser wire 及 executable sender 的首条消息在角色进入 room lookup 前得到同一个 fatal refresh 终态并停止自动重连。Client 与 server 共用 `SIGNAL_CLOSE_CODES` 定义会话替换、鉴权失败和 Viewer access revoke 的终态语义,避免双方对重连边界产生分叉;非法协议消息使用 WebSocket policy-violation close code。缺失、跨房、已 rotate 或 revoked 的当前 Viewer grant 使用同一个通用“邀请无效或已失效”结果。已通过 site access 的 well-formed code-only attempt 在没有 current room 时发 `ROOM_NOT_FOUND`;现存房间的 disabled policy、absent/wrong password、room full 与其他已知有界 admission refusal 继续发 `ROOM_ACCESS_DENIED`。两者都使用共享 authentication-failed close code。Grant 继续使用 `INVALID_TOKEN`,Host 鉴权保持独立,unexpected internal failure 使用不含 raw message 的 `SERVER_ERROR`。 ## WebRTC 协商 @@ -160,7 +160,7 @@ current 的唯一 wire 是 `screener-v11`,不做版本协商、旧 parser、 每次 `setParameters()` 都必须从当次 `getParameters()` 派生更新,然后读回 requested/applied `maxBitrate`、`maxFramerate`、`scaleResolutionDownBy` 和 degradation preference。浏览器负责具体降级,实际结果由 stats 观测;拒绝或浏览器改写必须进入可见状态,不能只写 `console.warn`。 -Current `screener-v11` Web 视频固定使用 `VP8`。P2P 与 browser relay 在首次 offer 前只保留 VP8 media capabilities 及 RTX/RED/FEC repair capabilities;无法应用该约束时明确失败,不静默协商另一媒体 codec。SFU publication 显式使用 VP8 且关闭 backup codec。UI、quality state/wire 和分享生命周期均没有 codec 分支;实际 outbound codec/profile、encoder implementation 与 stats 是只读结果真相。Viewer answer 的 Opus stereo receive preference 是当前唯一应用 SDP 变换边界。 +Accepted `screener-v12` Web 视频固定使用 `VP8`。P2P 与 browser relay 在首次 offer 前只保留 VP8 media capabilities 及 RTX/RED/FEC repair capabilities;无法应用该约束时明确失败,不静默协商另一媒体 codec。SFU publication 显式使用 VP8 且关闭 backup codec。UI、quality state/wire 和分享生命周期均没有 codec 分支;实际 outbound codec/profile、encoder implementation 与 stats 是只读结果真相。Viewer answer 的 Opus stereo receive preference 是当前唯一应用 SDP 变换边界。 Web display-audio 请求显式关闭 `echoCancellation`、`noiseSuppression`、`autoGainControl` 和 `voiceIsolation`,并以 `channelCount: { ideal: 2 }` 偏好双声道;这是当前 Chrome/Edge Host 基线的实现契约,用于避开网页 `getDisplayMedia()` 默认的语音处理和单声道 APM 候选,不宣称跨浏览器保证,也不固定采样率。所有值都是非强制 post-selection preference;不使用 `exact`、`min` 或 `advanced`。若 `getAudioTracks()` 为空,分享者界面必须显示“当前来源没有可共享音频”。Web 端同时传入 `windowAudio: "window"` 与 `systemAudio: "include"`:窗口选项请求该窗口/应用音频,整个屏幕选项允许用户勾选系统音频;标签页使用浏览器自身的标签页音频选择。浏览器可以忽略这些约束或 picker hints,且没有对应的标准来源类型读回,因此 UI 不额外推断或标注来源。系统音频由用户显式选择,可能包含语音或通知;video-only 始终可用。 @@ -270,7 +270,7 @@ media-proof 和 bounded-recovery 不变量的 scoped code 与 tests。 每两秒在本地采集一次 `getStats()`。首版不上传原始统计。折叠详情可在产生该连接的本机显示 selected local/remote candidate 的 `address`/`port`,但浏览器未暴露时保持未知;地址不得进入信令、Viewer C、日志、诊断导出、持久化或路由/质量决策。在 peer 边中它只是当前连接端点,在 SFU 边中通常是 SFU 端点,不能标成参与者的权威公网 IP。 -current `screener-v11` 在 authenticated Host 点击下载诊断时发送一次 `request-route-diagnostic`;服务端当场返回严格 Host-only 的 `route-diagnostic-snapshot`,不连续推送。每个 current child 只保留 latest demand timestamp、operation/candidate start、first-frame/final outcome;新的 route demand 覆盖该 child 的旧 record,confirmed departure 与 room delete 立即删除,且不增加 timer、事件环或第二张 graph。`queueWaitMs` 只从同一个 current demand 到其 room-serial operation start计算;没有权威 demand timestamp 就是 `null`。Serializer 为本次导出分配 snapshot-local opaque ordinal,用它表达 current parent/SFU 关系、effective capacity、child count、current operation reason/stage/cursor/candidate count与相对 timing;`finalRoute` 只允许 `direct | sfu | waiting | failed`,`rejectionBucket` 只允许 `none | stale | endpoint-capacity | sfu-admission | candidate-failed | first-frame-timeout | operation-deadline | aborted`。Ordinal 不是身份或稳定 ID。Snapshot 不包含 room code、display name、真实 peer/session/connection/generation、地址、URL、SDP、token、raw error或 planner 曾过滤的 parent,不形成路由输入或前台第二套状态。Viewer 的 schema v2 只携带自身 stage、粗粒度 revision、typed failure、signal/Host state、route kind 与 current-frame proof,即使 connection list为空也可下载;未鉴权 `ROOM_ACCESS_DENIED` 分支只含 stage/code,不含 Host、route、revision或timing。 +accepted `screener-v12` 的 Host/Viewer 产品 UI 不提供连接诊断文件下载。现有 periodic stats 只驱动当前页面的折叠连接详情、typed failure 与 accepted exact-edge stall;原始 stats、地址和身份不上传。authenticated Host 自动验收仍可按需请求一次 `route-diagnostic-snapshot`;服务端当场读取 current graph/operation 与 bounded latest timing records,不连续推送、不保留事件环、不建立第二张 graph。snapshot 使用临时 ordinal 和封闭 outcome bucket,不含 raw identity、address、candidate、credential 或 error,也不形成 route authority。 Host 与 relay Viewer 复用同一个纯 presentation reducer 保存远端 `ViewerQualityEvidence`。代际 identity 是 exact route identity(room/share、viewer/parent、assignment/connection、route revision 和当前可用的 media-binding generation,及适用时 publication generation);只有同 identity 且 sequence 严格递增才可承接旧字段,其他输入从新 evidence 重建。每个非空 metric 保存自己的本地观测时间并在五秒边界清空,页面 timer 总是指向最早边界;整块详情不因某个采样缺字段而卸载。`new`/`connecting`/`disconnected` 保留同 identity shell 但立即标为非 fresh,`failed`/`closed` 或 route identity 变化清空。任何依赖当前状态的消费者只读取 fresh presentation;该层不新增 wire 字段或服务端 route authority。 @@ -292,20 +292,20 @@ Host 与 relay Viewer 复用同一个纯 presentation reducer 保存远端 `View 首屏就是工具,不做营销页。视觉采用中性深灰工作台,绿色表示 direct、琥珀色表示 relay/警告、红色表示失败,避免单一蓝紫色主题;浏览器 favicon 复用页头 `MonitorUp` 的深绿底、绿色显示器和上行箭头语言。 -- 访问入口:未鉴权根路径、`/join` 和合法 code-only `/r/{code}` 显示“站点访问”与“站点口令”;malformed `/r/...` 直接显示房间号必须是 `1000..9999` 四位数字,其他未知路径只显示“无法访问”。有效 `/r/{code}#v={grant}` 直接进入;其余 Viewer 通过 site access 后以 code-only 方式进入或得到同一个 `ROOM_ACCESS_DENIED`。well-formed code 被拒绝时默认不展开密码、不暗示房间存在或需要密码,只提供“输入房间密码”动作并提示“使用分享者提供的邀请链接”;产品不向服务器索取链接。已有有效 site-access cookie 不重复显示整站验证;站点口令和房间密码不共享输入值或授权状态。 -- 分享者根页面:顶部显示“{分享者显示名} 的屏幕”和房间状态,并在标题旁显示可单独复制的紧凑四位房间号;Viewer 同一位置显示权威 Host 名称,不再在标题下重复。主区域保持稳定的 16:9 舞台。未发布、已停止或可恢复错误状态时,舞台内以同等尺寸和权重显示“开始分享”和“加入房间”,中间只用“或”与分隔线表达发送/接收;固定 action row 只能包含这两个按钮及分隔,disclosure 是紧随其后的 sibling,不得参与 action-row track sizing。前者直接开始现有分享流程,后者在当前舞台内展开共享的房间码表单,提交后才导航到 `/r/{code}`。按钮同步 `aria-expanded`/`aria-controls`,展开后输入获得焦点,DOM 与焦点顺序保持触发器后接表单;窄屏允许 disclosure 自己换行,不改变两个 action 的矩形或产生横向滚动。不增加首页、向导或拓扑选择。直播时舞台显示本地预览及独立全屏图标,底部保留停止、同步暂停音视频、质量档位,以及职责分离的“复制邀请链接”“复制房间号”“房间号加入:开放/密码/关闭”“更新邀请”“撤销邀请”控制;熟悉动作使用 lucide 图标、直接标签和 tooltip,不用一个含糊的“分享”菜单混装。主分享动作始终复制含 grant 的邀请链接。页面失焦仅暂停本地 video element,并在舞台提示“本地预览已暂停,分享仍在继续”;媒体暂停则禁用 capture stream 轨道,两者都保留现有连接。 +- 访问入口:未鉴权根路径、`/join` 和合法 code-only `/r/{code}` 显示“站点访问”与“站点口令”;malformed `/r/...` 直接显示房间号必须是 `1000..9999` 四位数字,其他未知路径只显示“无法访问”。有效 `/r/{code}#v={grant}` 直接进入;其余 Viewer 通过 site access 后以 code-only 方式进入。未分配、已回收或已过期的 well-formed code 显示“房间不存在或已过期”,不展开或提供密码输入;现存房间的 `ROOM_ACCESS_DENIED` 保留“输入房间密码”和使用 Host 邀请的动作。已有有效 site-access cookie 不重复显示整站验证;站点口令和房间密码不共享输入值或授权状态。 +- 分享者根页面:顶部显示“{分享者显示名} 的屏幕”和房间状态,并在标题旁显示可单独复制的紧凑四位房间号;Viewer 同一位置显示权威 Host 名称,不再在标题下重复。主区域保持稳定的 16:9 舞台。未发布、已停止或可恢复错误状态时,舞台内以同等尺寸和权重显示“开始分享”和“加入房间”,中间只用“或”与分隔线表达发送/接收;固定 action row 只能包含这两个按钮及分隔,disclosure 是紧随其后的 sibling,不得参与 action-row track sizing。前者直接开始现有分享流程,后者在当前舞台内展开共享的房间码表单,提交后才导航到 `/r/{code}`。按钮同步 `aria-expanded`/`aria-controls`,展开后输入获得焦点,DOM 与焦点顺序保持触发器后接表单;窄屏允许 disclosure 自己换行,不改变两个 action 的矩形或产生横向滚动。不增加首页、向导或拓扑选择。直播时舞台显示本地预览及独立全屏图标,底部保留停止、同步暂停音视频和质量档位。邀请控制使用紧凑 copy-first 布局:完整 raw URL 不占据主视觉,主动作直接复制含 22 字符 grant 的邀请链接;更新和撤销使用有 tooltip/accessible name 的 familiar icons,房间号复制、`开放 | 密码 | 关闭` code-entry segmented control 与密码编辑保持职责分离。页面失焦仅暂停本地 video element,并在舞台提示“本地预览已暂停,分享仍在继续”;媒体暂停则禁用 capture stream 轨道,两者都保留现有连接。 - `/join`:保留为可直接访问的回退入口,并复用同一个四位房间码表单;一个纯 route parser 返回 `host | join | viewer | malformed-room | unknown`,只把精确 `/r/{1000..9999}` 当成 Viewer route。输入保留用户原文,`inputMode="numeric"` 只提示软键盘,提交时由共享 schema 拒绝而不是 `trim`、删除非数字或接受 Unicode 数字。通过 site access 后输入合法 code 并进入 `/r/{code}`,后续行为由 code-only admission 决定。 - `/r/{code}` 观看者:首次解析合法 fragment grant 后立即清理地址栏并直达;仅有 code 时先通过 site access,再按 `open/password/disabled` policy 进入。鉴权后主区域最大化远端视频,标题旁显示可复制的紧凑房间号,顶部保留连接状态,并提供本地音量、静音、全屏和重试控制;Host 主动暂停时复用视频舞台遮罩显示“分享者已暂停”,恢复后立即移除。视频舞台用单一派生 presentation reducer 合成 access、Host presence、route prepare/active、peer/SFU connection、remote track、首个 composited frame、autoplay rejection 和 bounded failure。支持时用 `requestVideoFrameCallback()` 确认第一帧已提交,旧浏览器回退到当前代 decoded-frame delta 与 video readiness;`playing`、track object 或 ICE `connected` 单独都不足以结束占位层。连接阶段占用固定布局且不显示虚假百分比,播放按钮只处理实际 autoplay rejection,失败重试只调用现有有界 controller 入口。 - 首帧观察绑定当前 assignment/connection/media-binding generation 以及适用的 publication generation;`srcObject` 或 route identity 变化立即使旧回调和旧 decoded baseline 失效,迟到回调不能把新路线标成已播放。 - 移动端:视频保持稳定宽高比,控制与状态移到视频下方,任何文字和按钮不得溢出或遮挡。 -两端默认显示紧凑在线 roster;Viewer 端复用同一权威 participant snapshot,只列出当前 Viewer(含本机)并在新 snapshot 到达时整体替换,停止分享仅移除已离线 Host,鉴权失效、房间关闭或页面卸载则清空。Host 与 Viewer 的本机显示名在界面中统一标为“昵称”,默认只读,铅笔按钮紧贴昵称并只切换当前控件的输入、保存和取消,唯一名称仅显示名称,重名时才追加 room-scoped `peerId` 短后缀。一个不持久化的“显示连接详情”复选框按页展开媒体方式、传输协议、候选路径、浏览器实际暴露的本地/远端候选地址、codec、RTT、RTP 丢包率、编解码耗时及逐边统计;地址与端口只从当前页面 `RTCPeerConnection.getStats()` 精确关联的 selected pair 读取,浏览器隐藏或缺失任一字段就不显示,且不进入质量证据、信令、服务端、日志或持久化。真实连接失败、音频缺失、持续画质受限和需要用户动作的告警始终可见。显示名/roster 不进入 access credential 或服务器持久状态。 +两端默认显示紧凑在线 roster;Viewer 端复用同一权威 participant snapshot,只列出当前 Viewer(含本机)并在新 snapshot 到达时整体替换,停止分享仅移除已离线 Host,鉴权失效、房间关闭或页面卸载则清空。Host 与 Viewer 的本机显示名在界面中统一标为“昵称”,默认只读,铅笔按钮紧贴昵称并只切换当前控件的输入、保存和取消,唯一名称仅显示名称,重名时才追加 room-scoped `peerId` 短后缀。一个不持久化的“显示连接详情”复选框按页展开媒体方式、传输协议、候选路径、浏览器实际暴露的本地/远端候选地址、codec、RTT、RTP 丢包率、编解码耗时及逐边统计;地址与端口只从当前页面 `RTCPeerConnection.getStats()` 精确关联的 selected pair 读取,浏览器隐藏或缺失任一字段就不显示,且不进入质量证据、信令、服务端、日志或持久化。界面不提供诊断文件下载。真实连接失败、音频缺失、持续画质受限和需要用户动作的告警始终可见。显示名/roster 不进入 access credential 或服务器持久状态。 用户可见的非直播状态使用稳定、真实的阶段词:分享端使用“已停止分享”;观看端使用“等待开始分享”“正在分配线路”“正在建立 P2P”“正在连接备用线路”“正在接收画面”“需要点击播放”“正在恢复连接”或明确失败。阶段只描述当前用户可感知动作,不暴露房间寿命、媒体代次、parent identity 或内部迁移步骤。`room-closed` 显示“房间已关闭”或“房间已过期”,不得误写成“分享已结束”。服务端 raw `message` 永不直接渲染;单一纯 reducer 按下表优先级把 typed current-generation fact 派生为文案、遮罩、保留画面和动作,较旧 revision/generation 输入直接丢弃: | 优先级 | 权威输入 | 展示与动作 | | ---: | --- | --- | -| 1 | stale client、room closed/expired/lost、`ROOM_ACCESS_DENIED` | 阻塞终态;只显示对应刷新、可选密码或使用 Host 邀请的动作,不显示 route retry | +| 1 | stale client、room closed/expired/lost、`ROOM_NOT_FOUND`、`ROOM_ACCESS_DENIED` | 阻塞终态;`ROOM_NOT_FOUND` 只显示房间不存在/已过期且不显示密码,其他状态显示对应刷新、可选密码或使用 Host 邀请的动作,不显示 route retry | | 2 | Host authoritative pause | 保留仍属于 current binding 的最后画面并显示“分享者已暂停”;等待 Host,不显示 retry | | 3 | 同一 current media identity 的 `play()` 仅因 `NotAllowedError` 被拒绝 | 不要求先有 frame proof,立即显示唯一主动作 `Play`;点击后播放成功只清除 autoplay block,仍须等 current-generation frame proof 才撤遮罩,其他播放异常使用 typed failure | | 4 | current binding 已有 composited frame | 去掉阻塞层;若 signaling 恢复中但媒体仍健康,只显示非阻塞提示且不清空画面 | @@ -333,17 +333,17 @@ Token-free SFU standby prewarm 保持可选且不创建 participant、publicatio 自动测试: - SiteAccess 的 production 必配、8 至 128 visible-ASCII bytes/独立 secret、local-dev 显式关闭、12 小时签名 cookie、过期、篡改、cookie 属性、已移除 access key fail-fast 和无服务端 session 状态; -- RoomStore 的四位空闲码随机分配/释放、活动分享免过期、dormant lease/续租、Host token/Viewer grant 摘要验证、`open/password/disabled` 正交 policy、grant 到期/跨房拒绝、rotate/revoke 强撤销、Viewer 上限和超额拒绝; +- RoomStore 的四位空闲码随机分配/释放、活动分享免过期、dormant lease/续租、Host token/Viewer grant 摘要验证、22 字符 128-bit grant 与 exact room incarnation 同寿、`open/password/disabled` 正交 policy、跨房/同号重建拒绝、rotate/revoke 强撤销、Viewer 上限和超额拒绝; - 进程重启后 room state 全空、旧 Host/grant/password fail closed、释放号码复用时旧 grant 仍失败;`ROOM_LEASE_SECONDS` 的默认/边界与 `MAX_ROOMS <= 9000` 校验,已删除的 `ROOM_DATABASE_PATH`/`ROOM_TTL_SECONDS` fail-fast; - STUN UDP URL 严格校验、生产 STUN 必需、LiveKit tuple 完整性/TTL/secret 独立性、当前 route/session/revision/connection generation guard,以及 ordinary peer、未授权房间和 Native 的 STUN-only isolation;LiveKit `sfu-config` 保持独立边界; -- Current source contract 使用单一 `screener-v11` 信令 literal、严格 JSON schema、方向与房间授权校验、payload 上限和 client-mismatch 刷新终态;分享状态由 `shareGeneration`-fenced paused boolean 表达,访问失败使用 `ROOM_ACCESS_DENIED`,Viewer route failure 为 typed terminal,Host 诊断按需返回脱敏 snapshot。`qualityProfileIdSchema` 保持三档,`qualityResolutionSchema` 包含 `480p`,`DEFAULT_QUALITY_SETTINGS` 为 1080p30,quality schema 严格拒绝 codec 字段; +- Accepted v12 contract 使用单一 `screener-v12` 信令 literal、严格 JSON schema、方向与房间授权校验、payload 上限和 client-mismatch 刷新终态;分享状态由 `shareGeneration`-fenced paused boolean 表达,missing room 使用 `ROOM_NOT_FOUND`,现存房间准入失败使用 `ROOM_ACCESS_DENIED`,Viewer route failure 为 typed terminal,Host-only bounded route snapshot 保留给自动验收但不进入产品 UI。`qualityProfileIdSchema` 保持三档,`qualityResolutionSchema` 包含 `480p`,`DEFAULT_QUALITY_SETTINGS` 为 1080p30,quality schema 严格拒绝 codec 字段; - 质量协议的 host 授权、严格字段与数值范围、last-wins、当前/未来 relay、已配置 SFU 和普通 P2P wire 兼容;sender 的四字段 requested/applied 读回、拒绝与持续受限提示;screen audio SDP helper 覆盖 CRLF、existing/missing fmtp、unknown key preservation、幂等、payload number、rejected/non-audio,以及 empty/no-Opus/ambiguous/parser 异常原对象 fallback,`ViewerPeer` 覆盖 initial/restart/rebuild 时同一结果的 local/signaling 一致性,SFU publisher 精确断言 high-quality stereo/forceStereo/DTX 且不覆盖 RED; -- HTTP/WS 集成:nginx per-source site-access `limit_req` 与统一错误不泄露 secret,应用不建立 IP limiter 状态;Viewer 可在 Origin/容量门下 upgrade;Host 首条鉴权同时要求 upgrade-captured site access 与 Host token,Viewer 要么持 exact-room grant,要么先有 site access 再按 `open/password/disabled`。覆盖原子创建 profile、同浏览器重建偏好、fragment/sessionStorage/replaceState、无 site access 的 code-only 统一拒绝、unknown/expired/disabled/wrong-or-absent-password/full code-only 使用完全相同的 `ROOM_ACCESS_DENIED` code/文案/close、grant 与 Host 的独立错误、raw grant/密码不进入 URL/日志/服务端持久状态,以及 rotate/revoke 后 grace/route 清理、旧 Viewer/上游媒体边关闭和 reconnect 拒绝; +- HTTP/WS 集成:nginx per-source site-access `limit_req` 与错误不泄露 secret,应用不建立 IP limiter 状态;Viewer 可在 Origin/容量门下 upgrade;Host 首条鉴权同时要求 upgrade-captured site access 与 Host token,Viewer 要么持 exact-room grant,要么先有 site access 再按 `open/password/disabled`。覆盖原子创建 profile、同浏览器重建偏好、fragment/sessionStorage/replaceState、无 site access 的 code-only 拒绝、unknown/reclaimed/expired room 使用 `ROOM_NOT_FOUND` 且不显示密码、现存 disabled/wrong-or-absent-password/full code-only 使用 `ROOM_ACCESS_DENIED`、grant 与 Host 的独立错误、raw grant/密码不进入 URL/日志/服务端持久状态,以及 rotate/revoke 后 grace/route 清理、旧 Viewer/上游媒体边关闭和 reconnect 拒绝; - 客户端策略与 stats 解析:会话替换不重连,ordinary ICE 与 LiveKit protocol 分离; - SFU client isolation:`SfuPublisher` 与 `SfuSubscriber` 的 `Room.connect` mock 分别精确断言 `autoSubscribe: false` 与 `rtcConfig: { iceServers: [] }`,ordinary `HostPeer`/`ViewerPeer` 回归继续断言部署 STUN,防止空列表遗漏 Viewer 或扩散到 peer edge; - route planner property tests:候选输入乱序仍产生相同 list,健康边 sticky,硬过滤拒绝 stale/cycle/effective-capacity/unadmitted parent,优先结果深度和剩余容量并稳定打破平局;失败候选清理后 cursor 前进且总 deadline 不重置; -- Viewer stage reducer:按上表覆盖 access/Host/queue、peer/SFU prepare、remote track 无首帧、首个 composited frame、autoplay rejection、健康媒体上的 signaling recovery、Host stop/offline 和 bounded failure;stale revision不能覆盖 current frame,raw server message不能进入 UI,零连接失败仍可导出 allowlisted stage/revision/code bucket; -- route parser 覆盖 `/r/`、三位/五位、`0000`、ASCII 字母、Unicode 数字、额外 path segment、合法 query/hash 与 exact `1000..9999`,且 malformed 输入不发起鉴权;Host-only diagnostic request 拒绝 Viewer,schema 拒绝额外/raw/private 字段,departure/room delete清除 latest timing,zero-connection Viewer export仍成立; +- Viewer stage reducer:按上表覆盖 access/Host/queue、peer/SFU prepare、remote track 无首帧、首个 composited frame、autoplay rejection、健康媒体上的 signaling recovery、Host stop/offline 和 bounded failure;stale revision不能覆盖 current frame,raw server message不能进入 UI; +- route parser 覆盖 `/r/`、三位/五位、`0000`、ASCII 字母、Unicode 数字、额外 path segment、合法 query/hash 与 exact `1000..9999`,且 malformed 输入不发起鉴权;Host/Viewer UI 都不渲染诊断下载按钮,Host-only snapshot 继续拒绝 Viewer 与额外/raw/private 字段; - TypeScript 类型检查、Vite production build 和仓库 hygiene。 人工矩阵: @@ -360,7 +360,7 @@ Token-free SFU standby prewarm 保持可选且不创建 participant、publicatio ## 当前交付边界 -- Production 运行精确 deployed application/runtime revision `679fe3e7af634309322bea83b316641f51ad3d09`、release `679fe3e`;current source 增加 Browser SFU ICE-server-isolation implementation `ae09c760adec76fd26da611d4928486d105c6d3b`,尚未部署。两者使用严格单一 `screener-v11` wire;Browser 固定 VP8 且 video 不设置 `contentHint`,codec selector、quality-state/wire 字段与分享生命周期 codec 分支不存在。 +- Production 与 canonical `main` 运行精确 deployed application/runtime revision `c4962f54443ad5f98bc65861195a3d9c74a48996`、release `c4962f5`,包含 Browser SFU ICE-server-isolation implementation `ae09c760adec76fd26da611d4928486d105c6d3b`。两者使用严格单一 `screener-v11` wire;Browser 固定 VP8 且 video 不设置 `contentHint`,codec selector、quality-state/wire 字段与分享生命周期 codec 分支不存在。短 room-lived grant、明确 missing-room 结果、产品诊断下载移除与紧凑邀请 UI 仍是 accepted `screener-v12` design,尚未实现或部署。 - Pause/Resume 由当前 Host session 和 `shareGeneration` 约束。Host 后台性能和 VP8 hardware acceleration 分别按受控根因证据诊断;标准 ICE/SFU 按真实异构网络验收。 - 当前发布面只包含 Web Host、Viewer 和 browser relay。Native sender、capture helper、shared-encode executable 与两个分发包仍由各自后续决策和物理证据驱动,不属于当前 Browser release。 @@ -380,7 +380,7 @@ Pending candidate 的 receive-transport owner 只在当前 exact generation 下 首次需要 SFU 但 Host 已满时不建立另一条 rescue 流程。若 affected child 已是 Host direct child,就用它;否则用稳定加入顺序中最新的 connected Host direct child。`reconcileRoom` 为该 child 建立普通 SFU candidate,创建唯一 publication,保持其 subtree,并在该 child 首帧后提交。Host publication 取代释放的 peer slot,随后同一 fixed-point scan 再处理原 waiting/failed child。若旧 Host peer sender 在首帧前仍保留,candidate 必须持有同一 `TransitionOverlapSlot`。没有 overlap 时先 preflight server resource,再退休 exact producer;operation 保持原总 deadline、按新 graph 重算未尝试 suffix,并为被切断的 healthy direct edge追加一个最后 restore tuple。Forward candidate 与 restore 都失败才进入 waiting,不得产生第四份 endpoint copy或另一套恢复状态。 -所有非服务器端点共享一个 server-authoritative steady outbound media-copy cap `C`:唯一配置 `ENDPOINT_MEDIA_COPY_CAPACITY` 默认 `2`,只接受 `1`、`2` 或 `3`,不兼容旧键。服务端将 endpoint 当前广告的 `0..C` 可用值 clamp 为唯一 `effectiveDownstreamCapacity`;该值只表示当前可用 sender slots,不建立角色、浏览器、UA、设备或页面可见性 tier。peer child 或 Host publication 各占一份,upstream receive 免费;SFU subscriber egress 使用独立 server admission。Browser `screener-v11` 的 `authenticated.endpointMediaCopyCapacity` 把部署 `C` 交给 Host 与 Viewer;两端用该值校验稳态 assignment,并让 active+pending 的物理 copy 只在 controller 已授权 overlap 时达到 `min(C + 1, 3)`。代码通过一个 shared accounting guard 执行,不重复 magic number;其他 wire 与 Native executable 在 authority 前拒绝,不保留兼容路径。`C=3` 满载时先收到 active bounded-gap retirement再收到 prepare;任何会产生第四份 endpoint media copy 的 token都不得签发。 +所有非服务器端点共享一个 server-authoritative steady outbound media-copy cap `C`:唯一配置 `ENDPOINT_MEDIA_COPY_CAPACITY` 默认 `2`,只接受 `1`、`2` 或 `3`,不兼容旧键。服务端将 endpoint 当前广告的 `0..C` 可用值 clamp 为唯一 `effectiveDownstreamCapacity`;该值只表示当前可用 sender slots,不建立角色、浏览器、UA、设备或页面可见性 tier。peer child 或 Host publication 各占一份,upstream receive 免费;SFU subscriber egress 使用独立 server admission。Browser `screener-v12` 的 `authenticated.endpointMediaCopyCapacity` 把部署 `C` 交给 Host 与 Viewer;两端用该值校验稳态 assignment,并让 active+pending 的物理 copy 只在 controller 已授权 overlap 时达到 `min(C + 1, 3)`。代码通过一个 shared accounting guard 执行,不重复 magic number;其他 wire 与 Native executable 在 authority 前拒绝,不保留兼容路径。`C=3` 满载时先收到 active bounded-gap retirement再收到 prepare;任何会产生第四份 endpoint media copy 的 token都不得签发。 SFU admission 在应用单进程中只有一个注入实例。LiveKit credential tuple 完整时,必须同时显式配置私有 `LIVEKIT_API_URL`、正 safe integer `SFU_INGRESS_CAPACITY` 与 `SFU_EGRESS_CAPACITY`;容量没有产品默认值,也不从 Viewer 数、endpoint cap 或历史 root 数推导。专用 LiveKit 实例设置 `room.auto_create: false`。账本只保存全局已用计数,以及 exact room/share/publication generation 下的一份 publication handle 和按 Viewer identity 索引的 subscription handle。Publication 的 `reserved | committed | draining` 占 ingress `1`,每份 subscription 的同类状态占 egress `1`;离开 route 的 subscription 在该 generation 删除并确认 absent 前保持 draining,同一 Viewer handle 再次启用不重复计数。首个 SFU child 先 reserve publication 与自己的 subscription,再通过 `RoomService.CreateRoom` 创建 exact-generation room,成功后才 prepare 和签发 token;后续 child 只在同 generation reserve subscription 并复用 Host publication。exact candidate child 的 first decoded frame 后 commit 自己的资源;publication replacement 将旧 generation 及其全部 subscription 一起转入 draining,失败、超时、失联、分享换代、停止或删房按资源所有权进入同一生命周期。只有 `RoomService.DeleteRoom` 成功且 follow-up lookup 证明 room 不存在,账本才扣除该 generation 的 ingress/egress;auto-create 关闭后,旧 token 不能重建已删除 room。进程启动在接受流量前拒绝任何非 Screener room,删除全部遗留 Screener room并确认 namespace 为空;关闭时先 drain 再遗忘计数。Host 信令离线期间若 exact room 中的 Host participant 仍存在就继续计费并保留健康媒体,participant 消失后在一个有界轮询周期内回收。稳态 reserve/commit/drain bookkeeping 仍是 Map lookup 与整数加减;只有 generation 创建、删除、启动清场和 Host 离线检查访问 LiveKit control plane。多应用进程部署必须先增加共享原子 admission/lifecycle owner。 diff --git a/docs/需求理解.md b/docs/需求理解.md index 833a312..883b184 100644 --- a/docs/需求理解.md +++ b/docs/需求理解.md @@ -32,20 +32,20 @@ SFU 的 publication、subscription、server-resource accounting、临时 overlap - 路由失效输入:应用不推断 parent-wide 质量,也不计算综合分数。当前 authenticated child 只在 PeerConnection `failed/closed`,或非权威暂停时现有 periodic stats 在统一 deadline 内没有新 decoded frame 时,使自己的 exact edge 失效并唤醒同一个 reconcile。decoded progress 只保存 route-keyed `lastDecodedAt`,route/connection变化或新帧重置、暂停抑制,同一 edge只报告一次,不增加轮询器。该 operation 把失败的 exact direct parent 在当前 fact version 直接视为已尝试,先选择其他 direct parent,再进入有界 SFU suffix;新的外部事实才允许原 direct tuple 重新进入。Track availability、低码率、低 FPS、模糊、sender/viewer stats 与 SFU layer只用于stock WebRTC/LiveKit恢复和诊断。relay自身ingress失效时,它作为child换父并保留subtree;disconnect或effective downstream capacity降低只让direct/overflow children逐个走同一事务。 - 断线恢复:临时网络变化先交给 WebRTC/LiveKit 的 ICE、consent、重连和流状态。Direct edge 可在 exact current parent/child sessions 内采用 framework rebuild 的新 connection ID而不改变topology。应用只在上述exact edge失效后运行一次有界reconcile;任一步成功即停止。候选耗尽时hard-invalid、confirmed-departed或overflow edge被退休并进入明确waiting,healthy bootstrap edge保留。所有异步结果受authenticated session、share generation、base/pending route revision与candidate connection identity约束,未受影响分支保持sticky。权威暂停abort未提交candidate、保留active graph、抑制stall,并在resume后重新reconcile。 - 同一 operation 的一个总 deadline 只按实际存在的 `direct | sfu` 语义阶段等分,不配置每个 candidate 的 magic timeout。阶段内的硬失败继续推进 cursor;silent ICE 到达 direct 阶段边界时跳过剩余 direct tuple,必须把派生份额留给 SFU。20 名 Viewer 同时加入时,每名 child 的 operation deadline 不会被人数除分,但单一 room-serial operation 可能形成线性排队尾延迟;在接受另一种并发模型前,只测量 queue wait、candidate start、first decoded frame 与 final route 的 p50/p95/max,不用 NAT 猜测、TCP probe 或 magic timeout 绕过排队。 -- 可观测性:按目标 RTP transport 与不透明连接/ICE 代次关联 selected path、`iceGatheringState`/ICE/connection state、事件推导的建立时长、generation-baselined pair-change delta,以及 RTT、RTP 丢包率、抖动、码率、帧率、编码耗时和受限原因。候选、route decision、队列时序与错误的 wire、日志和导出使用封闭 schema 和 code bucket,禁止 raw SDP、candidate、地址、URL、`errorText`、真实 peer/parent/session/connection identity 或网络标识。失败前没有 connection metrics 的 Viewer 仍可导出自身 stage、粗粒度 revision bucket 和 typed failure code;未鉴权 access denial 只导出 stage 与 `ROOM_ACCESS_DENIED`。只有 authenticated Host 点击下载诊断时才向服务端请求一份当前 route snapshot:每次导出使用 snapshot-local opaque ordinal 表达当前 parent 关系,只携带当前 operation 与每个 current child 的 latest demand、queue/candidate/first-frame timing 和 closed outcome enum。Ordinal 不是身份或稳定 ID;服务端不连续推送、不保留事件环或第二张权威图,confirmed departure 与 room delete 立即清除对应 timing。只有创建该 `RTCPeerConnection` 的本机可在折叠详情中显示浏览器对 selected local/remote candidate 实际暴露的地址与端口;该值不上传、不持久化、不用于地理/NAT 推断,缺失或被隐私保护时显示未知。 +- 可观测性:按目标 RTP transport 与不透明连接/ICE 代次关联 selected path、`iceGatheringState`/ICE/connection state、事件推导的建立时长、generation-baselined pair-change delta,以及 RTT、RTP 丢包率、抖动、码率、帧率、编码耗时和受限原因。候选、route decision、队列时序与错误的 wire 和日志使用封闭 schema 和 code bucket,禁止 raw SDP、candidate、地址、URL、`errorText`、真实 peer/parent/session/connection identity 或网络标识。Host 与 Viewer 产品 UI 不提供连接诊断文件下载;本地折叠详情只显示当前页面直接观测到的连接事实。现有 Host-only bounded route snapshot 保留给自动验收,不连续推送、不进入产品 UI、也不形成 route authority。只有创建该 `RTCPeerConnection` 的本机可显示浏览器对 selected local/remote candidate 实际暴露的地址与端口;该值不上传、不持久化、不用于地理/NAT 推断,缺失或被隐私保护时显示未知。 - 渐进式诊断:分享者和观看者默认只看到房间、画面、核心状态和常用控制;Host 的 Viewer 卡片只把本机连接或当前接收证据称为“已连接”,其余状态保持中性的“线路分配中”或具体连接状态,不把 active assignment 当作连接成功。展开连接详情也只在当前媒体证据成立后显示媒体方式:direct peer使用绿色`P2P`,SFU使用黄色`SFU fallback`。尚未连接时不预告 P2P、SFU 或未知 ICE,只保留中性状态。Host/Peer 上下游仍只在独立的权威拓扑视图中展示。传输协议、候选路径、RTT、视频/音频 RTP 区间丢包率、实际编解码器、音频区间码率/jitter 和视频编码耗时等技术详情由一个明确的“显示连接详情”开关展开。远端 Viewer evidence 的详情 shell 在同一 viewer/parent/connection/route identity 内保持稳定;非空字段从各自最后一次实际观测起精确保留五秒,`connecting`/`disconnected` 只降为非 fresh,`failed`/`closed`、identity/session/share/route 变化或非单调 sequence 则清空/换代。非 fresh evidence 不得继续证明“已连接”或被其他 current-state 消费者使用。音频 codec MIME、RTP clock、channels 与 fmtp 只能来自当前 RTP 对象引用的 `RTCCodecStats`,缺失即未知;`opus/48000/2` 不得写成 48 kHz 音源或实际立体声。隐藏诊断不能隐藏失败原因、音频缺失、持续画质受限或需要用户操作的告警。 -- 站点访问:生产部署必须配置独立的 `SITE_ACCESS_PASSWORD` 控制建房、Host 信令角色和所有 code-only Viewer 入口;只有本地开发/测试可显式关闭。非空值接受 8 至 128 个可见 ASCII bytes,不能复用 LiveKit、TLS 或其他部署 secret。`POST /api/site-access` 验证成功签发 12 小时、无服务端会话状态的 HttpOnly、`SameSite=Strict` site-access cookie。有效 room-scoped fragment grant 可绕过站点 gate 直接进入对应房间;其余 `/r/{code}` 和 `/join` 必须先持有效站点 cookie,之后才检查 `open | password | disabled` code-only policy。失败统一且不得泄露房间是否存在或 policy;建房 HTTP 只认该 cookie。已移除的旧环境变量、endpoint 和 cookie 不保留兼容。不引入账号、JWT、服务端 session Map 或通用 logout。 -- 房间级观看授权:新房间总是生成含 256-bit 随机量、限时且可撤销的 room-scoped Viewer grant;服务端内存只保存完整 grant 的 SHA-256 摘要。到期阻止以后鉴权/重连,但不为已鉴权 Viewer 设置踢出定时器;即时清退使用 rotate/revoke。浏览器从 URL fragment 严格读取 grant 后,只写该房间的 `sessionStorage` 并立即用 `history.replaceState` 清除 fragment;刷新和页面内重连可继续。code-only 入口独立采用 `open | password | disabled`,默认 `open` 且无房间密码;所有 code-only 尝试先通过站点访问。grant 不得进入 `localStorage`、cookie、query、WebSocket URL、Referrer、日志或错误。Host 可独立 rotate/revoke grant 或改变 code-only policy;授权变化必须先提交新的内存代次,再清除 Viewer grace/路由、终止旧代次信令和媒体并断开受影响 Viewer。 +- 站点访问:生产部署必须配置独立的 `SITE_ACCESS_PASSWORD` 控制建房、Host 信令角色和所有 code-only Viewer 入口;只有本地开发/测试可显式关闭。非空值接受 8 至 128 个可见 ASCII bytes,不能复用 LiveKit、TLS 或其他部署 secret。`POST /api/site-access` 验证成功签发 12 小时、无服务端会话状态的 HttpOnly、`SameSite=Strict` site-access cookie。有效 room-scoped fragment grant 可绕过站点 gate 直接进入对应房间;其余 `/r/{code}` 和 `/join` 必须先持有效 site-access cookie,之后才检查房间是否存在与 `open | password | disabled` code-only policy。未分配、已回收或已过期的 code 返回 `ROOM_NOT_FOUND`;现存房间的 policy、密码与容量拒绝继续使用 `ROOM_ACCESS_DENIED`。建房 HTTP 只认该 cookie。已移除的旧环境变量、endpoint 和 cookie 不保留兼容。不引入账号、JWT、服务端 session Map 或通用 logout。 +- 房间级观看授权:新房间总是生成一个 128-bit 随机、22 字符 base64url、可 rotate/revoke 的 room-scoped Viewer grant;服务端内存只在该 exact room incarnation 保存完整 grant 的 SHA-256 摘要。grant 没有独立到期时间或内嵌 room/expiry 字段,与房间同寿,并在房间回收、进程重启、rotate 或 revoke 时失效;同号新房间使用新摘要,旧链接不能进入。浏览器从 URL fragment 严格读取 grant 后,只写该房间的 `sessionStorage` 并立即用 `history.replaceState` 清除 fragment;刷新和页面内重连可继续。code-only 入口独立采用 `open | password | disabled`,默认 `open` 且无房间密码;所有 code-only 尝试先通过站点访问。grant 不得进入 `localStorage`、cookie、query、WebSocket URL、Referrer、日志或错误。Host 可独立 rotate/revoke grant 或改变 code-only policy;授权变化必须先提交新的内存代次,再清除 Viewer grace/路由、终止旧代次信令和媒体并断开受影响 Viewer。 - 易失房间状态:服务器只在一个有界内存 RoomStore 中保存房间码、租期、Host token/Viewer grant 摘要、code-only policy、可选 48-byte 房间密码校验材料、参与者和控制状态。`ROOM_LEASE_SECONDS` 默认 `86400`;Host 正在分享时不回收,停止分享或 Host 断开后开始 dormant lease,同一 Host token 在期限内重新分享即续租,Viewer 活动不续租,过期后释放号码。应用进程重启时全部房间、租约、邀请和密码状态直接失效;不使用 SQLite、`ROOM_DATABASE_PATH`、schema migration、账号、用户表或服务器 session 表。 - 本地 Host 偏好:房间所有权继续使用随机 Host token,不使用 IP、UA、硬件信息或浏览器指纹。Host 浏览器在同源 `localStorage` 保存 raw Host token,以及昵称、code-only policy 和可选房间密码组成的创建偏好;服务器只在当前进程内保存 token 摘要和密码 verifier。旧房间不存在时,下一次显式分享创建新房间并原子应用本地偏好,同时生成新的四位房间号、Host token 和 Viewer grant。换设备、换浏览器或清理站点数据即不恢复。 ### P1 - 重要功能 - 公网服务器一键部署包:在用户自有的公网服务器上一键部署当前 Web/信令应用、易失房间状态和所需 STUN/SFU/反向代理组件,复用唯一配置、secret、路由、健康检查和恢复合同。普通 app-only 更新先验证新的不可变 artifact 再原子切换,切换前 application release 只保证保留到有限 health/postflight 完成;之后没有保留合同,也不作为维护中的备份。只有实际改动基础设施、配置、secret、持久或不可逆状态时,才为被触及表面定义恢复并预检。它不是托管服务,不能内置真实凭据或跳过域名、TLS、防火墙、端口、容量和适用的恢复预检。 - 本地自包含一键包:提供 Windows、macOS 和 Linux 可分发的桌面包,在分享者电脑上同时运行 Host、当前 Web/信令应用服务器和本地状态,朋友继续通过普通浏览器邀请观看。安装后启动不要求源码 checkout、Node 或任何外部 Screener、STUN、SFU、隧道或 relay 服务;它尽力使用本机可达入口和 direct P2P,但必须明确报告公网入口、可信 HTTPS/WSS、网关映射或 NAT 路径缺失,不能承诺任意 CGNAT/严格防火墙必通。两种分发包都排在当前功能、路由、NAT 和真实网络 TODO 完成之后。 -- 独立房间入口:合法 `/r/{code}#v={grant}` 邀请直接进入对应房间;没有 grant 的 `/join` 或 `/r/{code}` 先显示明确标为“站点访问”的整站验证,再提交 code-only 尝试。`open` 且可准入时直接进入;其余预期拒绝统一为 `ROOM_ACCESS_DENIED`,不得先披露 `password | disabled` policy。有效 site-access cookie 只跳过第一步,两类密码不得复用或互相替代。Host 可设置、轮换或移除房间观看密码;密码接受 1 至 64 个可见 ASCII 字符,异步 scrypt 全局最多执行 2 个、等待 16 个派生。Host 可把房间密码保存在同源本地创建偏好中;它不得进入 URL、cookie、日志或服务端持久存储。未知或过期房间、disabled policy、缺失/错误密码、队满和其他已知有界 admission refusal 都返回相同 code、公开文案和 close 行为。 +- 独立房间入口:合法 `/r/{code}#v={grant}` 邀请直接进入对应房间;没有 grant 的 `/join` 或 `/r/{code}` 先显示明确标为“站点访问”的整站验证,再提交 code-only 尝试。`open` 且可准入时直接进入;未分配、已回收或已过期的 code 明确显示“房间不存在或已过期”且不提供密码输入;现存房间的其他预期拒绝使用 `ROOM_ACCESS_DENIED`,不披露具体 policy。有效 site-access cookie 只跳过第一步,两类密码不得复用或互相替代。Host 可设置、轮换或移除房间观看密码;密码接受 1 至 64 个可见 ASCII 字符,异步 scrypt 全局最多执行 2 个、等待 16 个派生。Host 可把房间密码保存在同源本地创建偏好中;它不得进入 URL、cookie、日志或服务端持久存储。 - 统一自动路由:旗舰部署开启 `PEER_ASSISTED_MEDIA` 后,每个房间使用一个服务端 route controller,独占 committed graph、一个 pending child operation 和 event-driven reconcile。运行时只增加一个串行pump、一个operation deadline timer与typed resource lifecycle task,不维护另一份graph。operation持有candidate list/cursor、一个current candidate/资源预留、fact version和一个总deadline。committed graph必须无环、source-reachable,且每名Viewer同时至多一个active upstream。加入、容量变化、节点离开、exact edge `failed/closed`,以及非暂停decoded-frame stall都只唤醒同一个reconcile;其余健康分支保持sticky。候选按当前身份、source reachability、无环、effective capacity与sender transition硬过滤,再按结果深度、剩余sender slots、稳定加入顺序和peer identity排序;因此 Host root退出后新释放的Host槽位比已有root的更深槽位优先,但健康旧边不因周期性平衡而迁移。server admission在执行当前cursor时独立reserve。candidate child首帧ready触发`settleReady(commitAdmissionSync)`,成功prepare `P`提交active `P`;失败释放资源并用同一monotonic allocator广播`R>P` rollback,cursor推进但总deadline不重置。bounded-gap先preflight server resource,再retire producer、按新graph重算未尝试suffix,并在切断healthy edge时追加一个普通restore candidate。relay自身ingress失效时作为child换父并保留subtree;relay disconnect/cap降低让overflow children逐个走同一事务,childless后删除。房间admission和无环约束最大深度,不设独立hard cap。普通peer始终STUN-only,SFU使用独立admission。 -- 观看端连接反馈:Viewer 舞台从鉴权到首帧使用一个由现有权威状态推导的稳定阶段反馈,至少区分等待分享、分配线路、建立 P2P、连接备用线路、等待首帧、浏览器阻止自动播放、恢复连接和有界失败。阶段不是进度百分比,也不产生 route authority;只有当前 video element 实际提交首帧后才显示正在播放。播放按钮只在 `play()` 因自动播放策略被拒绝时稳定出现,连接中不得忽隐忽现;明确失败后才提供一次受现有 controller 约束的重试入口。make-before-break 或仍有可见旧帧时保留画面并叠加恢复状态,不为显示反馈主动清空健康 media element。Host 离线本身不使仍健康的媒体失效;exact upstream 硬失败或 route terminal failure 必须使 current frame proof 失效,已证明画面至多降为 retained background,不能遮蔽离线或有界失败。当前 `screener-v11` 服务端补充消息是 strict、current-revision-fenced `route-status` union,且只允许 `waiting/sfu-admission` 与 `failed/route-exhausted` 两种配对;它不携带 raw text、身份、candidate 或 retry hint,prepare/active、access、Host、signal、media 与 autoplay 继续由各自权威输入派生。 +- 观看端连接反馈:Viewer 舞台从鉴权到首帧使用一个由现有权威状态推导的稳定阶段反馈,至少区分等待分享、分配线路、建立 P2P、连接备用线路、等待首帧、浏览器阻止自动播放、恢复连接和有界失败。阶段不是进度百分比,也不产生 route authority;只有当前 video element 实际提交首帧后才显示正在播放。播放按钮只在 `play()` 因自动播放策略被拒绝时稳定出现,连接中不得忽隐忽现;明确失败后才提供一次受现有 controller 约束的重试入口。make-before-break 或仍有可见旧帧时保留画面并叠加恢复状态,不为显示反馈主动清空健康 media element。Host 离线本身不使仍健康的媒体失效;exact upstream 硬失败或 route terminal failure 必须使 current frame proof 失效,已证明画面至多降为 retained background,不能遮蔽离线或有界失败。已接受的 `screener-v12` 服务端补充消息是 strict、current-revision-fenced `route-status` union,且只允许 `waiting/sfu-admission` 与 `failed/route-exhausted` 两种配对;它不携带 raw text、身份、candidate 或 retry hint,prepare/active、access、Host、signal、media 与 autoplay 继续由各自权威输入派生。 - 质量控制扩展:推荐档位仍是默认入口且只保留 1080p60/1080p30/720p30 三档,当前页面初始 desired state 选择 1080p30 且不写 `localStorage`;“分享高级设置”允许 480p(854x480)/720p/1080p/1440p 分辨率 ceiling、15 至 60 的整数 FPS ceiling、2 至 12 Mbps 视频码率 ceiling、清晰/平衡/流畅三种偏好,以及离散的 `64 kbps 省流 | 128 kbps 音乐默认 | 256 kbps 极高音质` 屏幕音频 sender ceiling;480p 不形成推荐档或固定 FPS/码率组合,初始值采用 `balanced` 与 128 kbps。视频档位和音频档位可在直播中显式应用。所有当前及以后建立的 Web Host、Browser relay 与 SFU sender 都读取最新 desired 档位,并各自在 fresh sender parameters 上应用和读回 endpoint-local applied 值;实际码率仍由浏览器拥塞控制和 stats 决定。Peer 只在唯一明确的 active Opus audio 上做结构化、幂等的 Viewer answer `stereo=1;maxaveragebitrate=256000` 变换,使同一接收上限覆盖三档;非目标形态或 parser/writer 异常必须原样保留浏览器 answer,不得阻断可继续的 mono/video edge。不得用宽泛字符串/正则改 SDP,也不得改写浏览器协商的 FEC。SFU 将三档映射到 pinned SDK 的 music stereo preset 或显式 256 kbps ceiling,统一保留 `forceStereo = true`、`dtx = false` 与 pinned room default `red = true`。不得暴露任意码率、声道、采样率、audio codec、stereo、DTX、RED 或 FEC 控件;256 kbps 只是显式较高上限,不是无损、高保真或最低码率保证。实际质量持续低于所选上限时必须显示可解释的限制原因;在真实拥塞、编码器和接收端证据完成前,不把任一组合称为“完美平衡”。未来 microphone voice 若进入范围,必须是独立 track/处理链,不能把 AEC/降噪/DTX 套到电影或游戏音频。 - SFU 质量设置生命周期:直播中修改档位时,浏览器 sender 的 applied readback、LiveKit `LocalVideoTrack` 保存的 degradation preference、track restart options 与 `LocalTrackPublication` 的 republish options 必须作为同一次串行操作更新;任一步失败恢复旧 sender 与旧 SDK 保存态,旧 publication generation 不得回写当前状态。LiveKit full reconnect/republish 不得恢复初次发布的旧偏好。该修复不改变 codec、码率上限、层数或路由,也不能单独解释即时低帧。 - 本地预览性能候选:在相同动态场景中 A/B 测量 Host 动态预览开/关的 CPU、GPU、encode time 和实际 FPS。只有稳定收益且捕获、编码和远端画面不受影响时,才允许直播开始后默认显示静止缩略图或隐藏动态预览并提供按需展开;未测前不宣称收益。 @@ -76,7 +76,7 @@ SFU 的 publication、subscription、server-resource accounting、临时 overlap - 安全:应用控制面全程 HTTPS/WSS;site access、Host token 与 room-scoped Viewer grant 权限正交,任何一个 Viewer grant 只授予一个房间的观看角色。P2P 边在 WebRTC 端点间使用 DTLS-SRTP,普通 SFU 是逐跳 DTLS-SRTP;只有另行实现并验收应用层 E2EE 才能对 SFU operator 保持媒体端到端机密。仓库、服务端持久存储、日志和错误不得包含明文密码或 grant。已鉴权 Host 与 shipped clients 是当前私密房间中的可信媒体参与者;越权房间、角色和未授权 subscription 必须拒绝。旧 generation 不得重新取得 route authority,并在有界 drain 后由 exact-room 删除阻止再次加入。 - 隐私:P2P 会向房间内好友暴露网络地址,首版威胁模型仅覆盖可信熟人;若要求隐藏 IP,必须切换为服务器转发。 - 可维护性:代码、需求、调研、项目记忆、`AGENTS.md` 和 `.codex/` 均在 Git 中跟踪;开发配置不得依赖单一操作系统的绝对路径。 -- 迁移完整性:项目拥有者明确宣布公开发布前,只维护当前内部产品合同,不承担 backward/forward compatibility。Browser 客户端、服务端和部署配置使用单一 `screener-v11` wire 原子发布,旧客户端在 authority 前拒绝;codec selector、quality-state/wire 字段、旧 parser/writer、双写、translator、deprecated alias、migration shim 与仅服务旧版本的测试均不存在。Git 历史承担旧实现追溯;公开发布后才按明确的新要求重新评估兼容策略。 +- 迁移完整性:项目拥有者明确宣布公开发布前,只维护当前内部产品合同,不承担 backward/forward compatibility。Browser 客户端、服务端和部署配置使用单一 `screener-v12` wire 原子发布,旧客户端在 authority 前拒绝;codec selector、quality-state/wire 字段、旧 parser/writer、双写、translator、deprecated alias、migration shim 与仅服务旧版本的测试均不存在。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 使用统一的 server-authoritative endpoint capacity;移动真机表现是兼容观察,不建立单独 route class。 @@ -100,9 +100,9 @@ SFU 的 publication、subscription、server-resource accounting、临时 overlap - Given 分享期间只有 host 信令短暂断开,When 现有 P2P 媒体仍健康,Then 客户端继续播放媒体并恢复信令,而不是错误显示“已停止分享”。 - Given 应用以 production 模式启动但 `SITE_ACCESS_PASSWORD` 缺失,或配置仍提供已删除的 `ACCESS_PASSWORD`、`ROOM_DATABASE_PATH`、`ROOM_TTL_SECONDS`,When 应用启动,Then 配置校验明确失败,而不是兼容旧 gate、旧持久化模式或公开建房。 - Given production site access secret 短于 8 bytes、长于 128 bytes 或含非可见 ASCII,When 新 release 启动,Then 必须 fail-fast。Given 同一来源连续提交错误 secret,When 超过 deployment `limit_req` 的 `5r/m`、`burst=5 nodelay` 边界,Then nginx 返回不含凭据的通用限频结果;允许请求的错误值仍使用常量时间比较和统一鉴权失败,limiter 只使用 nginx 内存且不新增 IP 持久化/日志字段。 -- Given 新建房间,When 服务端返回邀请并由 Host/Viewer 客户端读取,Then grant 含 256-bit 随机量、只授权该房间 Viewer、只在该页面会话的 room-scoped `sessionStorage` 留存,并在首次读取后从地址栏移除;HTTP request-target、WebSocket URL、Referrer、cookie、`localStorage`、服务端持久存储、日志和错误均不包含 grant。 +- Given 新建房间,When 服务端返回邀请并由 Host/Viewer 客户端读取,Then grant 是 128-bit 随机量的 22 字符 base64url,只授权该 exact room incarnation 的 Viewer,并只在该页面会话的 room-scoped `sessionStorage` 留存且在首次读取后从地址栏移除;HTTP request-target、WebSocket URL、Referrer、cookie、`localStorage`、服务端持久存储、日志和错误均不包含 grant。Given 房间仍存在且未 rotate/revoke,Then grant 保持有效;Given 房间回收、进程重启、rotate/revoke 或同号新房间,Then 旧 grant 失效。 - Given 本地输入不是 `1000..9999` 的四位房间码,When 用户提交或直接访问 malformed `/r/...`,Then 客户端保留原输入并直接提示“四位数字”,不发起房间鉴权、不静默删除字符。Given 默认新房,When 已通过站点访问的 Viewer 只输入合法房间码,Then 无房间密码即可在现有 Viewer/连接/egress 上限内加入,同时 Host 的主分享动作仍复制独立的 token-bearing link。 -- Given 已通过 site access 的 well-formed code-only 尝试对应未知/过期房间、disabled policy、缺失/错误密码、满房或其他已知有界 admission refusal,When `screener-v11` 首条信令鉴权到达,Then 服务端返回唯一 `ROOM_ACCESS_DENIED`,并使用共享的 authentication-failed close code;同一公开文案和 close 行为不得泄露房间存在性或 policy。UI 不自动断言“需要密码”,只允许可选密码重试并提示使用 Host 提供的邀请链接,不向服务器索取链接。Given grant 无效,Then 继续使用通用 `INVALID_TOKEN`;Host 鉴权保持独立错误,unexpected internal failure 使用不含 raw message 的 `SERVER_ERROR`。Given Viewer 持另一个房间的 grant、缺失/过期 grant 或自行把 role 改为 Host,Then 不能建房、发布、跨房或绕过 Host token。 +- Given 已通过 site access 的 well-formed code-only 尝试对应未分配、已回收或已过期房间,When `screener-v12` 首条信令鉴权到达,Then 服务端返回 `ROOM_NOT_FOUND`,UI 显示“房间不存在或已过期”且不提供密码重试。Given code 属于现存房间但 disabled policy、缺失/错误密码、满房或其他已知有界 admission refusal,Then 服务端继续返回 `ROOM_ACCESS_DENIED` 并使用共享 authentication-failed close code,UI 可提供现有可选密码重试和 Host 邀请提示。Given grant 无效,Then 继续使用通用 `INVALID_TOKEN`;Host 鉴权保持独立错误,unexpected internal failure 使用不含 raw message 的 `SERVER_ERROR`。Given Viewer 持另一个房间的 grant、缺失 grant 或自行把 role 改为 Host,Then 不能建房、发布、跨房或绕过 Host token。 - Given Host rotate/revoke Viewer grant,When 新内存摘要和 authorization generation 提交,Then 服务端清除 Viewer grace/路由,命令 Viewer 关闭接收 PC/媒体并让每个 Host/relay/SFU upstream owner 关闭对应旧代次媒体边,最后关闭旧 Viewer socket;rotate 返回一次新 grant,revoke 锁定 grant 路径,code-only policy 保持独立。 - Given Host 同源本地偏好保存了 `open | password | disabled` 和可选房间密码,When 服务器重启后该 Host 下一次显式开始分享,Then 客户端清除失效 ownership、创建新四位房间并原子重放偏好;新的 Host token 和 grant 替换旧值。Given 换浏览器、换设备或清理站点数据,Then 不恢复这些偏好或所有权。 - Given 应用进程重启,When 旧 Host、Viewer 或 code-only 链接再次访问,Then 所有旧房间、租约、摘要、密码 verifier 和参与者均已丢弃并 fail closed;运行时不读取数据库、迁移旧 schema 或恢复旧号码。 @@ -134,9 +134,9 @@ SFU 的 publication、subscription、server-resource accounting、临时 overlap ### 一对多与带宽保护 -- Given `ENDPOINT_MEDIA_COPY_CAPACITY=C`,其中 `C` 只能是 `1`、`2` 或 `3` 且默认 `2`,When Browser Host 或任一 Browser Viewer 建立、恢复、重鉴权或迁移媒体路径,Then peer child 与 Host publication 的稳态实际发送副本总数不超过 `C`,upstream receive 不计数,角色、浏览器和 UA 不改变 `C`;每个 `authenticated` 快照都携带同一个 `C`,并行候选只有持有 `TransitionOverlapSlot` 时才可达到 `min(C + 1, 3)`,并在 commit/abort 后释放。Given 当前 `screener-v11` 房间合同,When Host 或 Viewer 取得房间 authority,Then server 与 Browser assets 属于同一 release,缺失或越界的 `C`、其他 wire 与 executable sender 得到明确 mismatch,且 `C=3` 满载 Host 在取得会产生第四份 endpoint media copy 的 publication/token 前明确等待或失败。 +- Given `ENDPOINT_MEDIA_COPY_CAPACITY=C`,其中 `C` 只能是 `1`、`2` 或 `3` 且默认 `2`,When Browser Host 或任一 Browser Viewer 建立、恢复、重鉴权或迁移媒体路径,Then peer child 与 Host publication 的稳态实际发送副本总数不超过 `C`,upstream receive 不计数,角色、浏览器和 UA 不改变 `C`;每个 `authenticated` 快照都携带同一个 `C`,并行候选只有持有 `TransitionOverlapSlot` 时才可达到 `min(C + 1, 3)`,并在 commit/abort 后释放。Given 已接受的 `screener-v12` 房间合同,When Host 或 Viewer 取得房间 authority,Then server 与 Browser assets 属于同一 release,缺失或越界的 `C`、其他 wire 与 executable sender 得到明确 mismatch,且 `C=3` 满载 Host 在取得会产生第四份 endpoint media copy 的 publication/token 前明确等待或失败。 - Given peer-assisted 路径分别运行 1、5、10、20 名观看者,When 用户加入、容量释放、节点离开、重鉴权或当前 edge 失效,Then 服务端只为新 Viewer、等待 Viewer 或失去 parent 的 subtree root 选择一个 active parent;每名 Viewer 同时至多一个 active upstream,活动图无环,其余健康边保持 sticky。没有可用 parent 时明确等待或拒绝,不主动重平衡或引入常驻多源媒体。 -- Given 一个 Host root 退出并释放 Host sender slot,When 新 Viewer 加入或一个失父 child 进入同一 reconcile,Then 候选按结果深度优先,选择 Host 而不是另一 root 的更深空槽;已健康挂在另一 root 下的旧 child 不被周期性搬动。Given authenticated Host 点击下载诊断,Then 服务端按需生成一份 current route snapshot,用 snapshot-local opaque ordinal 表达当前 parent 关系,并只包含 current operation reason/stage/cursor、每个 current child 的 latest demand/queue/candidate/first-frame timing 与封闭 outcome enum;该 ordinal 不是稳定身份,snapshot 不反推丢失事件、不记录 planner 过滤过的 parent、不包含 raw identity/address/error,也不参与 route。Given Viewer 请求房间级 snapshot,Then 服务端拒绝。 +- Given 一个 Host root 退出并释放 Host sender slot,When 新 Viewer 加入或一个失父 child 进入同一 reconcile,Then 候选按结果深度优先,选择 Host 而不是另一 root 的更深空槽;已健康挂在另一 root 下的旧 child 不被周期性搬动。连接详情只显示当前页面本地观测值;Host 与 Viewer 页面均不提供诊断文件下载。Given authenticated Host test harness 请求 bounded route snapshot,Then 服务端只返回现有 privacy-safe current snapshot;Viewer 请求仍拒绝,snapshot 不参与 route authority。 - Given 一名分享者和 20 名观看者,When 运行 bounded browser smoke,Then 全部 Viewer 取得明确媒体或 bounded wait/failure,非服务器端点不超过 `C`,并记录 route depth、首帧时间、实际 sender 数、应用服务器 CPU/内存以及 SFU 用量。诊断汇总报告 20 个 current-child 样本数、pending 数、每项原始排序时长和 nearest-rank p50/p95/max,不设通过阈值;confirmed departure 和 room delete 后对应 timing 必须消失。该 smoke 验证 admission、控制面效能和资源上界,不单独证明任意公网画质或 30 分钟 endurance。 - Given candidate 改变房间 admission 默认值或上限,When 在目标数量与代表性路径上运行规定时长并注入 relay departure、重连和一个弱路径,Then 每名观看者持续解码、每个非服务器端点不超过配置容量、深度与故障半径有界、未受影响 subtree 不迁移,并记录端点与中央服务的 CPU、GPU、内存、上行、出口、帧率、丢包率、冻结和恢复;任一门槛失败时不得切换默认值。 - Given peer-assisted host 鉴权或直播中连续修改质量设置,When 服务端校验其分辨率、FPS、码率和偏好,Then 只有 host 的完整有界设置被接受,last-wins 地应用到当前和以后建立的 relay child sender 以及启用的 SFU publisher;越界、缺字段和多余字段被拒绝,普通 P2P 的鉴权 wire 不新增该字段且不接受同步消息。 @@ -159,7 +159,7 @@ SFU 的 publication、subscription、server-resource accounting、临时 overlap - Given 同一时刻采集、host outbound 与 viewer inbound 证据,When 进行根因分类,Then 关联派生后的实际协商视频 codec/profile/parameters、适用时的 `scalabilityMode` 与真实解码表现,依次区分 Capture 低、Capture 高但 outbound 因 CPU 低、Capture 高但 outbound 因 bandwidth 低、outbound 正常但 inbound 低、以及 inbound 正常但肉眼模糊五类;不用 UA 或设备型号代替运行时证据,也不上传 raw SDP。 - Given Viewer 发送低档请求,When 请求未鉴权、过频、重复、过期或没有匹配的 sender/viewer 统计,Then 请求不改变表示或路径状态;有效请求也只能作为交叉验证输入。 - Given 受控 RTT/丢包测试条件,When 连续分享高动态游戏画面 30 分钟,Then 延迟、帧率与丢帧达到上述目标,且没有不可恢复的冻结。 -- Given Windows Chrome/Edge Browser Host 开始或继续分享,When 浏览器返回 display video track,Then 应用不赋任何视频 `contentHint`,并按浏览器 stats 如实显示实际 RTP codec 与实现字段。Given 当前 `screener-v11` 建立 direct 或 browser-relay sender,Then offer 的唯一视频媒体 codec 是 VP8;RTX/RED/FEC 只可作为修复格式,不能引入另一媒体 codec。若浏览器缺少 VP8 capability、`setCodecPreferences()` 或应用失败,Then 在创建 offer 前明确失败而不是静默协商其他 codec。Given 建立 SFU publication,Then LiveKit 固定使用 VP8 且不启用 backup codec。 +- Given Windows Chrome/Edge Browser Host 开始或继续分享,When 浏览器返回 display video track,Then 应用不赋任何视频 `contentHint`,并按浏览器 stats 如实显示实际 RTP codec 与实现字段。Given 已接受的 `screener-v12` 建立 direct 或 browser-relay sender,Then offer 的唯一视频媒体 codec 是 VP8;RTX/RED/FEC 只可作为修复格式,不能引入另一媒体 codec。若浏览器缺少 VP8 capability、`setCodecPreferences()` 或应用失败,Then 在创建 offer 前明确失败而不是静默协商其他 codec。Given 建立 SFU publication,Then LiveKit 固定使用 VP8 且不启用 backup codec。 - Given 分享过程中切换 Wi-Fi/有线网络,When ICE 连接失败,Then 客户端触发 ICE restart,并在成功或超时后给出明确状态。 - Given 当前路径仍可播放且控制器准备迁移 logical parent 或替换 transport,When 候选路径拥有模型授权的 overlap 资源,Then 旧路径保持权威,直到 exact candidate child 解码出第一帧新画面后才原子提交;ICE `connected` 单独不能触发切换。 - Given 候选没有准入资源,或 provisional 路径失败、超时、代次过期、参与者变化、房间停止、权限撤销或 session 被替换,When 本次机会结束,Then 原路径和未受影响 branches 继续工作,临时资源被销毁,重试遵守模型定义的有界策略。 @@ -169,7 +169,7 @@ SFU 的 publication、subscription、server-resource accounting、临时 overlap - Given Web Host 或浏览器 relay 建立或替换分享音轨,When 对实际音频 sender 应用发送参数,Then 从当次 `getParameters()` 派生所选 `encodings[0].maxBitrate = 64000 | 128000 | 256000`、调用 `setParameters()` 并立即读回,旧配置缺失时按 128000 处理;Given Web Viewer 为含唯一有效 audio/Opus payload 的 offer 生成 answer,Then 用结构化 SDP parser 只 upsert 该 payload 的 `stereo=1` 与 `maxaveragebitrate=256000`,并把同一个结果用于 `setLocalDescription()` 和 signaling。首次连接、ICE restart、同 parent rebuild 和 relay child 都适用;空 SDP、无/歧义 Opus、多个有效 audio、重复目标参数、畸形 fmtp 或 parse/write 异常必须按对象 identity 返回浏览器原 answer 并继续,不能发送部分修改或静默冒充 stereo/实际码率。只有实际 `setLocalDescription()` 失败才进入既有连接错误路径。`sprop-stereo` 与 `usedtx` 不新增,其他 fmtp/FEC/session/media 字段逐项保留。 - Given 通过 SFU 发布分享音轨,When 构建 LiveKit 2.22.0 publish options,Then 64 kbps 使用 `AudioPresets.musicStereo`、128 kbps 使用 `AudioPresets.musicHighQualityStereo`、256 kbps 使用同等结构的显式 sender ceiling,三档都设置 `forceStereo = true` 与 `dtx = false`,且不覆盖 pinned room default `red = true`;验收同时核对 SDK 选项、applied ceiling、当前 RTP codec/fmtp 与 stats 实际码率,不能从 preset 名称或 `opus/48000/2` 单独推断实际声道内容。 - Given 分享正在直播或暂停且 Host 选择新的 64/128/256 kbps 音频档位,When 当前 profile mutation 串行执行,Then 服务端以 last-wins 保存并广播最新 desired profile,每个当前 Host、relay 与 SFU audio sender 从 fresh `getParameters()` 派生 `setParameters()` 并立即 readback,过期 completion 不得覆盖新值;以后创建的 sender 使用最新 desired profile。单个端点应用失败时保留媒体与其旧 applied 值、显示可见告警并允许后续更新或重建重试,不得谎称全房已收敛。当前 applied readback 保持 endpoint-local;没有显式回执 wire 就不宣称跨端点原子提交。该操作不改变 Opus、route revision、media-binding generation 或 publication generation,也不创建第二个音频控制环。当前 screen-audio Host 基线是 Chrome/Edge;其他浏览器只有实际 sender readback 成功时才算支持。 -- Given Host 打开当前 `screener-v11` 分享设置或更新房间画质,When 查看 UI 或校验消息,Then 不存在 codec selector、codec quality-state/wire 字段或分享生命周期 codec 分支;Browser direct、browser relay 与 SFU publication 始终使用固定 VP8。实际协商 codec 与 encoder 只作为只读诊断显示。 +- Given Host 打开已接受的 `screener-v12` 分享设置或更新房间画质,When 查看 UI 或校验消息,Then 不存在 codec selector、codec quality-state/wire 字段或分享生命周期 codec 分支;Browser direct、browser relay 与 SFU publication 始终使用固定 VP8。实际协商 codec 与 encoder 只作为只读诊断显示。 - Given 浏览器返回屏幕音轨,When 产品展示音频状态或未来连接详情,Then 只能把存在性、浏览器实际返回的 track settings、实际协商 codec/fmtp 和实际流量分别标为捕获、协商或观测事实;字段缺失必须显示为未知,且不得从 Opus 的 `48000/2` RTP 声明推断源采样率或实际 stereo。 - Given Web 或后续 native sender 分享电影/视频音频,When 进行产品验收,Then 必须用独立 A/B/C 证据关联 capture audio track/settings、实际 negotiated codec/fmtp、outbound/inbound 实际码率、loss、jitter、concealment、jitter-buffer 与 A/V sync;P2P、浏览器 relay 与 SFU route switch 保持同一持久 Viewer `MediaStream`/媒体元素和 stereo 协商,不新增独立 audio element 或 Web Audio 时钟边界。native sender 必须通过该音质和同步矩阵,且不得把 audio `maxBitrate` ceiling 当作提质证明。 - Given 分享者同时运行语音软件,When 选择仅分享游戏声音,Then supported Windows 11 native sender 只保留目标游戏及其子进程的渲染音频,观众、通知和其他应用声音不得进入分享;浏览器 system audio 无法满足时必须允许 video-only 或明确告知其可能包含语音/通知,不能用 AEC、降噪或来源标签伪称已隔离。