diff --git a/docs/adr/0005-automatic-hybrid-media-routing.md b/docs/adr/0005-automatic-hybrid-media-routing.md index 079209e..cdf9b2a 100644 --- a/docs/adr/0005-automatic-hybrid-media-routing.md +++ b/docs/adr/0005-automatic-hybrid-media-routing.md @@ -152,13 +152,13 @@ A `prepare` route update names that operation's exact child, route kind, and server-issued candidate connection identity. Parent and child therefore prepare the same connection; neither endpoint infers candidate authority from an assignment-list difference. -The current Browser runtime uses one internal wire. On each WebSocket, the +The accepted next Browser runtime contract uses the single `screener-v10` wire. On each WebSocket, the server sends the exact prepare before its SFU configuration; the candidate child is queued before a peer parent is allowed to start its offer. WebSocket ordering is the companion-delivery contract, so clients keep no reordering inbox. -Duplicate current companions are idempotent and stale ones are ignored. Native -and executable senders are outside this release and fail the protocol boundary -rather than receiving a compatibility path. +Duplicate current companions are idempotent and stale ones are ignored. Native, +v9, and executable senders are outside this release and fail the protocol +boundary before room authority rather than receiving a compatibility path. A stale or mismatched asynchronous result fails closed and cannot revive an old edge. Successful candidate `P` is broadcast as active revision `P`. Failure, timeout, @@ -260,10 +260,9 @@ second mutable graph. authority. - Authoritative pause aborts the pending child operation, including its current candidate and reservations, keeps the active graph, suppresses decoded-frame- - stall decisions, and leaves new participants waiting. Resume normally wakes a - fresh reconciliation; when the accepted codec transaction owns a prepared - generation, that same room-serial owner handles Resume and settles first. - Healthy unaffected edges remain sticky. + stall decisions, and leaves new participants waiting. Resume always wakes a + fresh reconciliation from the committed graph. Healthy unaffected edges remain + sticky. SFU remains a bounded fallback resource with independent deployment-wide admission. Resource exhaustion produces an explicit wait or failure; it never @@ -273,7 +272,7 @@ one waiter set. An actual SFU usage decrease drains that set once, advances each waiting controller's external fact, and schedules normal reconciliation; there is no periodic capacity poll or resource-specific route controller. -The current v9 Viewer wire includes one strict, revision-fenced `route-status` +The accepted v10 Viewer wire includes one strict, revision-fenced `route-status` union only for states that the existing prepare/active `route-update` cannot express: `{ state: "waiting", reason: "sfu-admission" }` while this Viewer is waiting on central admission, or `{ state: "failed", reason: @@ -359,7 +358,10 @@ SFU/UDP routes, including the route, first-frame, typed-status, and Host-only diagnostic contract described above. The atomic deployment postflight passed; its rollback boundary is `/opt/screener/backups/39fcf93-pre-v9-20260824T004257Z`. Exact configuration and operational evidence are owned by the deployment -document; real heterogeneous-network and SFU media validation remains open. +document; real heterogeneous-network and SFU media validation remains open. In +the accepted next contract this route model is unchanged, the operation owner is +only `route`, and the boundary ships only with the next atomic `screener-v10` +release; production has not crossed it. ## Acceptance Boundary diff --git a/docs/project-memory.md b/docs/project-memory.md index 58bfaec..0d52fc5 100644 --- a/docs/project-memory.md +++ b/docs/project-memory.md @@ -13,12 +13,12 @@ Last updated: 2026-08-24 - Keep routing automatic and media distributed. Direct/peer UDP is preferred; the dedicated LiveKit SFU/UDP path is the only application fallback and is not the default topology. Screener configures no TURN, ICE/TCP, media TCP, or TLS-relayed media path. - Every non-server endpoint uses one server-authoritative steady outbound media-copy capacity: default `2`, statically configurable as `1`, `2`, or `3`. A peer child or the Host publication consumes one slot; upstream receive is free, and SFU subscriber egress is accounted at the server. Browser role/UA/visibility does not create a separate tier, and clients cannot raise the deployment value. - The stable routing invariants are one authoritative upstream per Viewer, an acyclic source-reachable active graph, exact authenticated route attempts, endpoint and server admission, one room-serial child operation, and bounded success, wait, or failure. -- One room controller reconciles allocation, child reparenting, and relay drain through the same operation. It finds one waiting Viewer or invalid edge and builds one deterministic tuple list. The operation owns its cursor, current candidate and reservations, fact version, plus one total deadline; only its current candidate may produce media. A `screener-v9` prepare names that exact child, selected transport, and server-issued candidate connection ID. A usable old edge remains until the exact candidate child decodes its first new video frame. `settleReady` synchronously commits admission before graph promotion; success commits prepare revision `P`, while failure releases resources, broadcasts rollback `R > P` from one monotonic allocator, and advances the cursor without resetting the deadline. Exhaustion retires hard-invalid, departed-parent, or overflow edges into explicit waiting but retains a healthy bootstrap edge. Bounded-gap preflights server resources, replans the untried suffix after retirement, and appends one ordinary restore candidate when it cut healthy media. A relay with a bad ingress reparents itself as a child and retains its subtree. A disconnected endpoint, or one whose effective downstream capacity falls below its current child count, accepts no excess children; its direct or overflow children enter the same operation and a disconnected endpoint is removed when childless. Authoritative pause aborts the operation, suppresses decoded-progress stall decisions, and resumes from a fresh reconciliation. +- One room controller reconciles allocation, child reparenting, and relay drain through the same operation. It finds one waiting Viewer or invalid edge and builds one deterministic tuple list. The operation owns its cursor, current candidate and reservations, fact version, plus one total deadline; only its current candidate may produce media. A `screener-v10` prepare names that exact child, selected transport, and server-issued candidate connection ID. A usable old edge remains until the exact candidate child decodes its first new video frame. `settleReady` synchronously commits admission before graph promotion; success commits prepare revision `P`, while failure releases resources, broadcasts rollback `R > P` from one monotonic allocator, and advances the cursor without resetting the deadline. Exhaustion retires hard-invalid, departed-parent, or overflow edges into explicit waiting but retains a healthy bootstrap edge. Bounded-gap preflights server resources, replans the untried suffix after retirement, and appends one ordinary restore candidate when it cut healthy media. A relay with a bad ingress reparents itself as a child and retains its subtree. A disconnected endpoint, or one whose effective downstream capacity falls below its current child count, accepts no excess children; its direct or overflow children enter the same operation and a disconnected endpoint is removed when childless. Authoritative pause aborts the operation, suppresses decoded-progress stall decisions, and resumes from a fresh reconciliation. - One Host publication may serve all SFU subscriptions. Its exact generation owns one ingress handle and exact Viewer subscription handles own egress; later SFU children reuse the publication, while reserved, committed, and draining handles remain charged until the managed room is deleted and proven absent. Actual SFU usage release wakes only rooms that previously lost a candidate to global admission, and SFU-fed and peer-fed endpoints use the same provisional-child transaction. Parent choice hard-filters authority, reachability, cycles, effective capacity, and producer transition, then orders by shallowest result, remaining steady sender capacity, stable join order, and peer identity; server admission is checked at the current cursor. Acyclicity and room admission bound maximum depth; depth is observed but has no separate hard cap. Healthy committed edges remain sticky: the controller repairs waiting, invalid, departed-parent, and overflow children but does not periodically rebalance the graph. Raw IP, claimed NAT type, UA, geography, all-pairs probes, weighted scores, and periodic room rebalancing are not route inputs. - Ordinary peer ICE is STUN-only, and current LiveKit media is UDP-only. Credentials, room secrets, candidate details, and diagnostic data remain private and narrowly scoped. A future strict-firewall transport requires separate evidence and must remain inside LiveKit rather than becoming another Screener route candidate. - Browser peers use existing periodic WebRTC/LiveKit stats to maintain one exact-route decoded-progress timestamp. New frames or route/connection changes reset it; a non-paused deadline reports the edge once. Track availability, bitrate, FPS, resolution, freeze, RTT, jitter, loss, sender stats, and SFU layer describe only the current path: they remain framework or diagnostic inputs and cannot prove another parent would be better. - Browser capture requests available share audio by default on initial capture and source switch, with an explicit video-only warning when no audio track is returned. Its screen-audio control offers live-switchable 64/128/256 kbps sender ceilings and defaults to 128. The last-wins quality wire owns desired state; each endpoint serially applies and reads back current Host/relay/SFU audio senders, exposes local failures, and gives future senders the latest desired value. Opus remains fixed, and without an applied-ack wire the Host does not claim room-wide atomic convergence. These are configuration ceilings, not fidelity claims. -- Video codec selection remains `automatic | H.264 | VP8`. Its first switching boundary is a generation-fenced renegotiation/republish transaction while sharing is explicitly paused. Preparation never resumes a user-paused share: it stops its deadline but retains the single room-serial codec owner as one bounded prepared record with no timer. The user's normal Resume atomically revalidates those exact bindings, allocates the next server-monotonic resume attempt, and starts one bounded resume/proof deadline. The server keeps authoritative pause until the Host enables its source and returns the exact attempt acknowledgement; only then does it broadcast Resume and issue proof requests. Authorization, source acknowledgement, proof request, and proof response echo that attempt, and Pause invalidates it before another Resume can allocate the next one. Proof failure first restores authoritative pause and immediately runs bounded rollback preparation; the resulting prepared rollback has no timer and waits for the user's next normal Resume before revalidation, a fresh resume attempt, proof, and source enable. Unpaused hot switching is later reuse of that transaction, not an independent mechanism. +- Browser sharing defaults to VP8. The advanced `automatic | H.264 | VP8` selector is a pre-share diagnostic control: the selected preference is applied when the share's peer offers and optional SFU publication are first created, then remains fixed until that share ends. Ordinary Pause/Resume changes source enablement under the current `shareGeneration`; it does not renegotiate or republish media. H.264 remains non-default until a controlled root-cause fix and physical direct, relay, and SFU evidence justify a separate decision to make it default and remove the diagnostic selector. - Recommended quality remains exactly `1080p60`, `1080p30`, and `720p30`, with the middle `1080p30` as the accepted default. `480p` means only an advanced `854x480` resolution choice whose FPS and bitrate remain independently selected; it is not a fourth recommended profile. - Two distribution artifacts are accepted only after the current feature, NAT, and real-network ledger is complete. A public-server one-click deployment package installs the current application plus STUN/SFU and proxy components on a user-owned server. A Windows/macOS/Linux local package runs the Host, application server, and local state on the broadcaster's machine without source, Node, or any external Screener/network service; it keeps workable direct P2P but reports the reachability loss imposed by public ingress, TLS, gateway, NAT, or firewall limits rather than promising universal connectivity. - Current UI, presence, route labels, and diagnostics describe observed state only; they do not create route authority. @@ -27,11 +27,11 @@ Last updated: 2026-08-24 ## Current Source And Production -- Production runs Browser runtime `39fcf93bae057fcbb1002702c3be6b90bac9027f`, release `39fcf93`, on `screener-v9`. Canonical root `main` contains that runtime plus the docs-only postflight truth; auxiliary branches and older worktrees do not supersede it. +- Production runs Browser runtime `39fcf93bae057fcbb1002702c3be6b90bac9027f`, release `39fcf93`, on `screener-v9`. Canonical root `main` contains that runtime plus the current docs-only truth checkpoints; auxiliary branches and older worktrees do not supersede it. - Integrated 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. - Integrated 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 runs from `/opt/screener/releases/39fcf93`. Its immutable runtime ZIP SHA-256 is `28190c3a69ec937d39ab5d49fdbc8db6a07e6013c2ddd5f5590bf8249bf6bce6`; `/opt/screener/backups/39fcf93-pre-v9-20260824T004257Z` is the verified rollback boundary and its `SHA256SUMS` hash is `1f741f29022f5eb1eb311a24f87f5f48598dca7b1879b88f9033c331fa06bd37`. -- Production implements live 64/128/256 kbps audio-ceiling mutation; generation-fenced paused codec prepare, Resume source acknowledgement, decoded-progress proof, and rollback; typed first-frame Viewer presentation and privacy-safe on-demand route diagnostics; neutral room-code denial and responsive entry controls; exactly three recommended profiles with `1080p30` as default and advanced-only `854x480`; and no parent-edge quality-proof wire. +- Production implements live 64/128/256 kbps audio-ceiling mutation; generation-fenced paused codec prepare, Resume source acknowledgement, decoded-progress proof, and rollback; typed first-frame Viewer presentation and privacy-safe on-demand route diagnostics; neutral room-code denial and responsive entry controls; exactly three recommended profiles with `1080p30` as default and advanced-only `854x480`; and no parent-edge quality-proof wire. The accepted codec contract above is not yet implemented or deployed: the next atomic Browser release removes that transaction, defaults new shares to VP8, restores ordinary `shareGeneration`-fenced Pause/Resume, and uses one new wire version that rejects v9 without aliases or dual parsing. - 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. - Exact runtime source `d543f38aacad3df5ef65fde1055cc8e733972afe`, integrated without runtime changes by main `39fcf93bae057fcbb1002702c3be6b90bac9027f`, passed 670 Web tests in 48 files, TypeScript typecheck, client/server production builds, the access/privacy gate, production-dependency audit, and repository hygiene. Its one-Host/20-Viewer Chrome loopback decoded every Viewer and kept endpoint fanout within cap `2`, but used direct local peer routes and observed no SFU publication; it is not public-network, SFU, mobile, audio-quality, codec-switch, H.264 performance, or background-lifecycle evidence. - Postflight found `screener`, LiveKit, coturn, and nginx active with `NRestarts=0`; local and public `/` and `/healthz` return 200, every served client file matches the immutable release, the public Browser bundle contains `screener-v9` and no `screener-v8`, the release shares no regular-file inode with its rollback predecessor, and the accepted socket/firewall boundary is intact. No current v9 media room was created, so deployment evidence is not route or physical-media acceptance. Detail is owned by [deployment](./deployment.md) and [verification status](./verification-status.md). @@ -39,7 +39,7 @@ Last updated: 2026-08-24 ## Current Priority -1. Close the [current evidence ledger](./todo.md) with physical codec and live-audio validation, controlled H.264 and Host-background diagnosis, and representative direct/peer-relay/SFU network runs. Browser loopback, state-machine gates, and deployment health do not substitute for those paths. +1. Implement, integrate, and atomically deploy the accepted codec simplification, then close the [current evidence ledger](./todo.md) with physical live-audio validation, controlled Host-background diagnosis, and representative direct/peer-relay/SFU network runs. Browser loopback, state-machine gates, and deployment health do not substitute for those paths. 2. Run the mobile lifecycle matrix and later mechanism-simplification audit only at their stated boundaries; Native/executable and distribution work remain later product phases. ## Working Rules diff --git a/docs/research/browser-background-capture.md b/docs/research/browser-background-capture.md index 6f5f8fc..43b051e 100644 --- a/docs/research/browser-background-capture.md +++ b/docs/research/browser-background-capture.md @@ -36,10 +36,11 @@ does not say that minimizing the separate capturing page must reduce capture. Capture constraints are post-selection preferences and allow frame decimation, so requested `maxFramerate` is not proof of actual source or encoded frame rate. -The current default video preference is `automatic`, not a contractual VP8 -selection. Every run must record the actual negotiated codec and encoder. -H.264, VP8, and encoder implementation are diagnostic variables only after a -stable baseline exists; none is a page-keepalive mechanism. +The accepted default video preference is VP8. The deployed v9 runtime still +defaults to `automatic` until the next atomic Browser cutover. Every run must +record the actual negotiated codec and encoder rather than infer either from +the preference. H.264, VP8, and encoder implementation are diagnostic variables +only after a stable baseline exists; none is a page-keepalive mechanism. ## Current Source Boundary @@ -167,8 +168,8 @@ Use equal 60-second measurement windows, restore the focused baseline between states, randomize state order, and repeat three fresh controlled runs. These window and repeat counts define a small comparable sample; they are not product timeouts, quality thresholds, or proof of statistical significance. Record the -actual initial negotiated codec and encoder with `automatic`; do not switch -codec inside a run. +actual initial negotiated codec and encoder for the chosen pre-share preference; +do not switch codec inside a run. For each exact overlapping window, correlate: @@ -194,7 +195,8 @@ Change one variable per fresh run: 2. Compare local preview playing and paused while capture and sender identities stay fixed. 3. Run separate new Host/share/peer-connection sessions for `automatic`, H.264, - and VP8, and verify the actual codec and encoder in each session. + and VP8 through the pre-share diagnostic selector, and verify the actual codec + and encoder in each session. 4. When investigating the 2026-08-19 production report, compare exact release `769de201f7cc` with the then-current exact `main` commit on the same machine, browser, driver, game scene, and direct wired Viewer. diff --git a/docs/research/realtime-quality-adaptation.md b/docs/research/realtime-quality-adaptation.md index 6fa907b..c3763d0 100644 --- a/docs/research/realtime-quality-adaptation.md +++ b/docs/research/realtime-quality-adaptation.md @@ -188,8 +188,8 @@ A 2026-08-21 report says that selecting fluid preference on an SFU path could retain low received FPS without reducing the visible resolution. This is not proof that the preference was ignored: `maintain-framerate` is a degradation tradeoff rather than an FPS target. It is also not evidence of SFU temporal -downlayering: the current codec is left to automatic negotiation unless the Host -explicitly selects one. Pinned LiveKit server 1.13.5 installs a temporal selector +downlayering: the accepted v10 Browser default prefers VP8 unless the Host explicitly +selects another value before starting the share. Pinned LiveKit server 1.13.5 installs a temporal selector for VP8, but its H.264/H.265 path installs only the simulcast spatial selector. With H.264 selected, a `HIGH` ceiling may therefore let BWE choose the lower-resolution `q` representation, not a lower temporal layer at the same @@ -229,54 +229,50 @@ physical evidence. RFC 7742 requires WebRTC browsers to implement both VP8 and H.264 Constrained Baseline, while W3C `setCodecPreferences()` lets the application reorder the -browser's negotiated codec set. The smallest Web policy is automatic by default: -do not override browser/LiveKit negotiation. Advanced settings can explicitly -place every advertised H.264 or VP8 entry first while retaining the complete +browser's negotiated codec set. The accepted v10 Browser default places VP8 first. +Advanced settings expose Automatic/H.264/VP8 only before sharing starts so a +fresh share can provide a controlled H.264 comparison. Explicit H.264 or VP8 +places every advertised entry for that codec first while retaining the complete repair and fallback list. A missing capability or rejected preference leaves the -browser default unchanged. LiveKit likewise keeps its default codec for -`automatic`, overrides it only for an explicit selection, and retains -`backupCodec=false`; there is no parallel backup-codec publication. +browser default unchanged. `automatic` passes an empty preference list and does +not override the LiveKit default; an explicit selection sets the initial LiveKit +publication codec and retains `backupCodec=false`, so there is no parallel +backup-codec publication. Changing `RTCRtpTransceiver.setCodecPreferences()` affects later negotiation; it does not itself switch an established sender, and `replaceTrack()` only replaces -a same-kind source. Mature WebRTC code such as Jitsi follows a codec preference -change with renegotiation, while pinned LiveKit changes publication codec through -its publish/republish lifecycle. Screener therefore starts with a paused-share -transaction: fence the request by codec/media generation, serialize it with -route mutation, boundedly renegotiate the frozen still-current peer/relay -targets, and republish the SFU generation when present. Preparation never -resumes an explicitly paused share. When preparation settles, it stops only the -running preparation deadline: one room-serial codec mutation owner remains as -bounded in-memory prepared state, holds no timer while paused, and excludes -route mutation. The user's ordinary Resume is handled inside that same owner. It -atomically revalidates the exact frozen bindings, starts the decoded-progress -proof deadline, and only then enables the source. A zero-target share saves the -future preference and commits without media proof. If forward proof fails, the -owner first restores authoritative paused state and then runs bounded -old-preference rollback preparation. A prepared rollback again holds no timer -and waits for the user's next ordinary Resume to start its decoded proof; no -transaction path resumes the share automatically. Passing an empty codec list -restores default browser preferences. -This is a deliberate paused bounded-gap path. Unpaused make-before-break is a -later extension rather than a second codec mechanism. +a same-kind source. Pinned LiveKit likewise fixes the publication codec through +its publish lifecycle. Screener therefore applies the selected preference only +to the first offer/publication of a new share and to later senders created for +that same share. Once sharing starts, the codec control is disabled in both live +and paused states. Changing codec requires stopping and starting a fresh share; +ordinary Pause and Resume only toggle the same capture tracks and one +share-generation-fenced paused state. -H.264 remains an explicit option because mature cross-device hardware -encode/decode paths may avoid an observed VP8 software-path cost. A controlled -Web comparison also found that forcing H.264 could lower downstream FPS with the -current browser configuration, so the product does not force either codec by -default. Neither observation proves an intrinsic bitrate or efficiency win for -every implementation or scene; actual outbound codec/profile, encoder -implementation and decoded stats remain the result truth. A codec choice also -does not prove shared encode: separate browser PeerConnections may construct -separate encoders, and the SFU still publishes only the configured `q,h` -representations. +This boundary needs no codec generation, per-Viewer preparation/proof, Resume +attempt, rollback, or SFU publication replacement. Its wire surface ships +atomically as the single `screener-v10` contract and rejects v9 before room +authority; there is no v9 alias, parser, writer, or translator. + +H.264 remains a temporary explicit comparison option because mature cross-device +hardware encode/decode paths may avoid an observed VP8 software-path cost. A +controlled Web comparison also found that preferring H.264 could lower downstream +FPS with the current browser configuration. Neither observation proves an +intrinsic bitrate or efficiency win for every implementation or scene; actual +outbound codec/profile, encoder implementation and decoded stats remain the +result truth. A codec choice also does not prove shared encode: separate browser +PeerConnections may construct separate encoders, and the SFU still publishes +only the configured `q,h` representations. Only after the H.264 root cause is +fixed and fresh-share physical direct, browser-relay, and SFU runs pass may the +product separately decide to make H.264 the default; that decision also removes +the selector instead of retaining a permanent codec control. The repository diagnostic is a mechanical single-machine preflight, not a -quality runner. Lifecycle runs fix `automatic`; each fresh run performs exactly +quality runner. Lifecycle runs fix the accepted v10 VP8 default; each fresh run performs exactly one of observe, preview cycle, Host-peer rebuild, capture replacement, or Host reload, keeps the actual codec/profile/parameters stable across that action, and rechecks the action budget after its two current-generation evidence windows. -Codec preflight uses fresh Host/share/PeerConnection sessions for automatic, +Codec preflight uses fresh Host/share/PeerConnection sessions for Automatic, H.264, and VP8 and rejects an actual-codec mismatch. Reports retain only allowlisted failure stages and sanitized identifiers. @@ -299,16 +295,16 @@ ships no H.264 codec binary or new codec dependency, so licensing uncertainty is not a runtime blocker for the preference. Bundling a codec implementation or changing the distribution/service model still requires a separate license review. -These reversible standard preferences are gated by focused ordering, fallback and -interoperability tests, not by an exhaustive CPU/GPU/game matrix on one ordinary -PC. Primary specifications, maintained implementation behavior, representative -target-device observations and sanitized production stats drive product choices. -Synthetic local runs may verify negotiation, decode, cleanup and edge bounds; -they must not calibrate capacity, claim performance, or block a standard feature. -Hardware attribution and controlled performance evidence remain required before -making efficiency claims or introducing dynamic codec selection. AV1 remains -outside the default because compression efficiency alone does not establish a -power-efficient WebRTC encoder on the target cohort. +These pre-share standard preferences are gated by focused initial-ordering, +fallback, and interoperability tests, not by an exhaustive CPU/GPU/game matrix +on one ordinary PC. Primary specifications, maintained implementation behavior, +representative target-device observations and sanitized production stats drive +product choices. Synthetic local runs may verify negotiation, decode, cleanup +and edge bounds; they must not calibrate capacity or claim performance. Hardware +attribution and controlled physical evidence remain required before changing the +default or removing the comparison selector. AV1 remains outside the default +because compression efficiency alone does not establish a power-efficient +WebRTC encoder on the target cohort. Discord's published Go Live material is a useful architecture comparison, not a preset to copy. It describes native OS/driver-integrated capture and encoding, @@ -737,15 +733,14 @@ guarantees the emitted resolution, frame rate, or bitrate. and `maintain-framerate` choices. None promises an emitted resolution or rate. - `maxBitrate` and `maxFramerate` are ceilings. They are neither minimums nor target guarantees, and the project does not use SDP bitrate hacks. -- Exact v9 source's Share advanced settings panel accepts +- The accepted `screener-v10` Share advanced settings panel accepts 480p/720p/1080p/1440p, integer 15-60 fps, 2-12 Mbps, the three preferences, and - Automatic/H.264/VP8. The `480p` choice is only advanced `854x480`, not a + Automatic/H.264/VP8 before sharing starts and defaults to VP8. The `480p` choice is only advanced `854x480`, not a fourth recommended profile. Its 64/128/256 kbps audio ceiling, default 128, is live-switchable on the existing Opus path. Production deploys both the - advanced 480p resolution and live audio mutation. The first - codec-switch product boundary is a generation-fenced renegotiation and SFU - republish while sharing is explicitly paused; pause or `replaceTrack()` alone - does not switch codec. Display capture does not standardize channel-count or + advanced 480p resolution and live audio mutation. A share keeps its initial + codec preference until stopped; live and paused states expose no codec + mutation. Display capture does not standardize channel-count or sample-rate control. The peer receive contract permits Opus `stereo=1;maxaveragebitrate=256000`, paired with pinned LiveKit's explicit high-quality stereo/forceStereo option; the selected sender @@ -786,8 +781,9 @@ CPU/GPU cost, public networks, or sustained behavior. - No canvas pixel-difference detector, machine-learned rate controller, or periodic profile switching. - No copied x264 CRF/preset recipe in the browser path. -- No forced codec order until target hardware measurements identify the actual - power-efficient encoder. +- No runtime or automatic codec switching. VP8 remains the static default while + fresh-share Automatic/H.264/VP8 comparisons identify the actual encoder and + physical result. - No channel-count, sample-rate, codec, arbitrary bitrate, stereo, DTX, RED or FEC control, and no inference of actual stereo or sample rate from `opus/48000/2`. Screen media uses one route-consistent stereo contract; diff --git a/docs/session-handoff.md b/docs/session-handoff.md index 738d6a1..fa27e02 100644 --- a/docs/session-handoff.md +++ b/docs/session-handoff.md @@ -81,11 +81,19 @@ Current high-signal boundary: material. Do not merge or delete them until their unique changes, references, and reparse/junction safety are checked against current main. -Current TODO priority is physical codec and audio validation, controlled H.264 -and Host-background diagnosis, and representative direct/peer-relay/SFU network -acceptance. Mobile lifecycle, distribution/native -packages, retained product decisions, and the read-only simplification audit stay -at their later boundaries. +The accepted codec contract now defaults new shares to VP8, keeps the codec +selector pre-share only, fixes the preference for the lifetime of a share, and +uses ordinary `shareGeneration`-fenced Pause/Resume. Current source and production +still run the v9 codec transaction described above. The next Browser cutover must +delete that transaction and its messages, use one new wire version, and reject v9 +without compatibility aliases before this simplification is reported as implemented +or deployed. + +Current TODO priority is that codec simplification and atomic deployment, followed +by physical audio validation, controlled Host-background diagnosis, and representative +direct/peer-relay/SFU network acceptance. H.264 diagnosis/default reconsideration, +mobile lifecycle, distribution/native packages, retained product decisions, and the +read-only simplification audit stay at their later boundaries. Follow the current ledger in order and report each verified physical-evidence boundary without reviving stale agent or branch facts. diff --git a/docs/status.md b/docs/status.md index 126b9dd..11aed4d 100644 --- a/docs/status.md +++ b/docs/status.md @@ -15,7 +15,7 @@ This is the current execution index. Git history owns completed timelines; [veri ## Current Source -- Canonical root `main` is the source and integration truth. It contains the deployed Browser runtime checkpoint `39fcf93bae057fcbb1002702c3be6b90bac9027f` plus this docs-only postflight truth; auxiliary branches and older worktrees do not supersede it. +- Canonical root `main` is the source and integration truth. It contains the deployed Browser runtime checkpoint `39fcf93bae057fcbb1002702c3be6b90bac9027f` plus the current docs-only truth checkpoints; auxiliary branches and older worktrees do not supersede it. - The single Browser `screener-v9` boundary rejects stale clients before room authority. 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. @@ -26,8 +26,8 @@ This is the current execution index. Git history owns completed timelines; [veri ## Current Milestone -1. Validate codec switching and live audio ceilings on physical direct, peer-relay, and SFU media paths, including actual codec/bitrate, first-frame, source authority, rollback, audible quality, and A/V synchronization. -2. Reproduce the dated H.264/blur and Host background/minimized reports under controlled conditions. +1. Replace the current codec transaction with the accepted pre-share-only selector, VP8 default, fixed per-share codec preference, and ordinary `shareGeneration`-fenced Pause/Resume. Integrate and deploy it atomically on one new Browser wire version with no v9 compatibility path. +2. Validate live audio ceilings on physical direct, peer-relay, and SFU paths, then reproduce the Host background/minimized report under controlled conditions while recording the actual negotiated codec. 3. 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,6 +42,8 @@ This is the current execution index. Git history owns completed timelines; [veri ## Current Hold There is no active P0/P1 source or deployment hold. Browser v9 is integrated, locally gated, atomically -deployed, and postflight-clean. Physical codec/audio, heterogeneous-network, SFU, H.264/background, -and mobile evidence remains open. Native/executable work and broad repository cleanup remain outside -the current evidence boundary. +deployed, and postflight-clean, but its codec transaction is no longer the accepted product contract. +The accepted simplification is truth-only until the next atomic Browser release; source and production +must not be reported as simplified before that cutover. Physical audio, heterogeneous-network, SFU, +background, and mobile evidence remains open. Native/executable work and broad repository cleanup +remain outside the current evidence boundary. diff --git a/docs/todo.md b/docs/todo.md index 7f0f683..517db49 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -6,23 +6,23 @@ Only items in **Now** are executable after their stated decision gate. A branch ## Now -1. **Physically validate paused codec switching.** On current Chrome and Edge, exercise `automatic | H.264 | VP8` across a direct Viewer, a browser-relay descendant, and an SFU subscriber. Capture the exact negotiated outbound/inbound codec, source-enabled acknowledgement boundary, newly decoded progress, and first presented frame after each normal Resume. Exercise a real preparation or proof failure and prove authoritative re-pause plus old-preference rollback on the next user Resume; local state-machine tests alone do not close this item. +1. **Simplify Browser codec ownership and deploy it atomically.** Default new shares to VP8, keep `automatic | H.264 | VP8` only as a pre-share diagnostic choice, and keep that choice fixed after sharing starts. Delete the codec generation, prepared binding, `resumeAttempt`, source acknowledgement, proof, rollback, and SFU replacement transaction; ordinary Pause/Resume remains one `shareGeneration`-fenced source-state change. Remove the replaced v9 messages instead of preserving aliases or dual parsers, cut one new wire version that rejects stale v9 clients before room authority, run the proportionate Browser gates, then deploy and record the exact release boundary. 2. **Physically validate live screen-audio ceilings.** With audible game/music capture, switch 64/128/256 kbps while sharing across direct, browser-relay, and SFU paths, then create a new edge and switch source. Record sender readback, observed bitrate, route continuity, audible result, and A/V synchronization. Treat every setting as a ceiling, and do not infer fidelity or room-wide atomic convergence from configuration alone. -3. **Diagnose the dated H.264/blur report.** Reproduce the 2026-08-19 then-production release `769de201f7cc` against current source on the same Host/browser/driver/game and one wired direct Viewer, changing one variable at a time. Implement a codec-specific repair only if codec/encoder evidence identifies it; otherwise fix the proven capture, sender, BWE, or lifecycle cause. -4. **Diagnose desktop Host background/minimized performance.** On the same machine, dynamic scene, direct Viewer, browser, and fresh share, compare foreground, occluded, minimized, and hidden states while separately verifying actual negotiated VP8/H.264. Correlate capture settings/FPS, frames captured/encoded/sent/decoded, bitrate, `qualityLimitationReason`, encode time, browser/game CPU and GPU, and the existing local-preview pause boundary. Treat browser scheduling/throttling and codec effects as hypotheses until measured. Do not spoof page activity, add keepalive timers, or misuse Wake Lock; if current Chrome/Edge cannot preserve the accepted Host behavior through standard Web APIs, record that capability boundary before considering the packaged/native Host. -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. +3. **Diagnose desktop Host background/minimized performance.** On the same machine, dynamic scene, direct Viewer, browser, and fresh share, compare foreground, occluded, minimized, and hidden states while recording the actual negotiated codec. Correlate capture settings/FPS, frames captured/encoded/sent/decoded, bitrate, `qualityLimitationReason`, encode time, browser/game CPU and GPU, and the existing local-preview pause boundary. Treat browser scheduling/throttling and codec effects as hypotheses until measured. Do not spoof page activity, add keepalive timers, or misuse Wake Lock; if current Chrome/Edge cannot preserve the accepted Host behavior through standard Web APIs, record that capability boundary before considering the packaged/native Host. +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 These items are real product work, ordered after the current physical-media, real-network, and product-decision gates; they are not permission to resume an old branch wholesale. -1. **Mobile Viewer lifecycle.** Run Android Chrome and iOS Safari device matrices for autoplay gesture, foreground/background audio, foreground video recovery, lock/page reclamation, rotation, Wi-Fi/cellular migration, and an assigned relay's outbound-media survival or controller failover across those transitions. Preserve the same media element and current reconnect path; do not promise background video composition, background relay continuity, or unsupported lock-screen behavior. -2. **Platform output only when real.** Revisit AirPlay/Cast only when a target browser and physical receiver prove the live `MediaStream` contract. System mirroring remains external; no placeholder button, custom receiver, or server transcode is scheduled. -3. **Public-server one-click deployment package.** After every current functional and real-network item is settled, package the exact application, STUN/SFU, reverse proxy, secrets, health checks and rollback flow for a user-owned public server. Require real domain/TLS/firewall/capacity inputs and never ship credentials or call a partial deployment ready. -4. **Fully local one-click Host-server package.** Package Windows/macOS/Linux Host capture, the exact application server and local state without requiring source, Node or any external Screener/STUN/SFU/tunnel/relay service. Preserve usable direct P2P and use the completed NAT/network evidence to maximize reachability, while reporting public-origin, gateway, NAT and firewall limits truthfully; loopback/LAN success is not universal Internet success. -5. **Native Host capture backend.** At lowest priority, productize a proven Windows native capture path before expanding capture/audio backends to macOS and Linux. Its capture, hardware encode, audio, identity and lifecycle evidence becomes an input to later packages; a source checkout, helper executable, or evaluation ZIP is not a distributable product. -6. **Native shared encode, Windows first.** At lowest priority, prove one hardware encoder output can serve two independent Viewer transports and the Host-SFU publication while each connection retains its own RTP/RTCP, pacing, encryption, feedback, and upload accounting. Productize only after actual access-unit identity, Viewer2/FIFO, heterogeneous feedback, game-resource, browser/hardware, A/V, packaging, and licensing gates pass. Web Host/relay remains standard per-`RTCPeerConnection` encoding because no portable shared-encoder contract exists. -7. **Repository simplification audit.** After the current Browser, physical-media, route, and room checkpoints settle, inventory components, configuration, migrations, gates, timers, compatibility paths, and tests using the [maintenance review](./maintenance.md#机制减负审查). Begin read-only: identify the user behavior each mechanism preserves, quantify the impact of removing it, and compare that value with its whole-system cost. Any accepted feature tradeoff updates its owning contract before complete unused surfaces are removed. This later audit must not become a broad refactor or block current product work. +1. **Revisit H.264 only after evidence changes the decision.** Reproduce the dated blur report under controlled conditions and implement a codec-specific repair only if actual codec/encoder evidence identifies one. If the repaired H.264 path then passes physical direct, browser-relay, and SFU validation, make a separate decision about defaulting to H.264 and deleting the diagnostic selector. Otherwise keep VP8 as the default; do not rebuild live codec switching. +2. **Mobile Viewer lifecycle.** Run Android Chrome and iOS Safari device matrices for autoplay gesture, foreground/background audio, foreground video recovery, lock/page reclamation, rotation, Wi-Fi/cellular migration, and an assigned relay's outbound-media survival or controller failover across those transitions. Preserve the same media element and current reconnect path; do not promise background video composition, background relay continuity, or unsupported lock-screen behavior. +3. **Platform output only when real.** Revisit AirPlay/Cast only when a target browser and physical receiver prove the live `MediaStream` contract. System mirroring remains external; no placeholder button, custom receiver, or server transcode is scheduled. +4. **Public-server one-click deployment package.** After every current functional and real-network item is settled, package the exact application, STUN/SFU, reverse proxy, secrets, health checks and rollback flow for a user-owned public server. Require real domain/TLS/firewall/capacity inputs and never ship credentials or call a partial deployment ready. +5. **Fully local one-click Host-server package.** Package Windows/macOS/Linux Host capture, the exact application server and local state without requiring source, Node or any external Screener/STUN/SFU/tunnel/relay service. Preserve usable direct P2P and use the completed NAT/network evidence to maximize reachability, while reporting public-origin, gateway, NAT and firewall limits truthfully; loopback/LAN success is not universal Internet success. +6. **Native Host capture backend.** At lowest priority, productize a proven Windows native capture path before expanding capture/audio backends to macOS and Linux. Its capture, hardware encode, audio, identity and lifecycle evidence becomes an input to later packages; a source checkout, helper executable, or evaluation ZIP is not a distributable product. +7. **Native shared encode, Windows first.** At lowest priority, prove one hardware encoder output can serve two independent Viewer transports and the Host-SFU publication while each connection retains its own RTP/RTCP, pacing, encryption, feedback, and upload accounting. Productize only after actual access-unit identity, Viewer2/FIFO, heterogeneous feedback, game-resource, browser/hardware, A/V, packaging, and licensing gates pass. Web Host/relay remains standard per-`RTCPeerConnection` encoding because no portable shared-encoder contract exists. +8. **Repository simplification audit.** After the current Browser, physical-media, route, and room checkpoints settle, inventory components, configuration, migrations, gates, timers, compatibility paths, and tests using the [maintenance review](./maintenance.md#机制减负审查). Begin read-only: identify the user behavior each mechanism preserves, quantify the impact of removing it, and compare that value with its whole-system cost. Any accepted feature tradeoff updates its owning contract before complete unused surfaces are removed. This later audit must not become a broad refactor or block current product work. ## Product Decisions Needed diff --git a/docs/方案设计.md b/docs/方案设计.md index 7dfc0c3..ea947e8 100644 --- a/docs/方案设计.md +++ b/docs/方案设计.md @@ -2,7 +2,7 @@ ## 2026-08-18 14:45 -状态:production Browser runtime 为 exact `39fcf93bae057fcbb1002702c3be6b90bac9027f`、release `39fcf93`、wire `screener-v9`;canonical main 包含该 runtime 和其上的 docs-only postflight truth。当前 Browser 合同包括 live-media mutation、Viewer presentation/diagnostics、无枚举房间准入和质量选择,并已通过原子部署 postflight;精确 deployed behavior 只由 deployment 文档记录。 +状态:production Browser runtime 为 exact `39fcf93bae057fcbb1002702c3be6b90bac9027f`、release `39fcf93`、wire `screener-v9`;canonical main 包含该 runtime 和其上的当前 docs-only truth checkpoints。当前 Browser 合同包括 live-media mutation、Viewer presentation/diagnostics、无枚举房间准入和质量选择,并已通过原子部署 postflight;精确 deployed behavior 只由 deployment 文档记录。 ## 目标与边界 @@ -100,7 +100,7 @@ rotate/revoke 是 grant 路径的强撤销:服务端先同步提交新 digest/ 显示名是独立的本地偏好:同源 `localStorage` 只保存浏览器输入,存储不可用时静默退回内存 fallback;服务端只把参与者的规范化名称挂在当前 authenticated socket state,断线即丢弃,不进入 RoomStore durable state 或日志。shared schema 先做 NFC、合并 Unicode space separators、去除边缘空格,再限制为 1 至 24 个 Unicode code points,并拒绝控制字符、行/段分隔符、BOM、双向控制符和孤立 surrogate;未设置时 Viewer 使用通用“访客”,Web Host 可由 room-scoped stable client ID 生成本地 fallback。普通多语言文本、组合字符和 emoji 仍可用。 -Web Host 在 `authenticate` 中显式发送 `viewerPresence: true` capability,Viewer 也可显式订阅同一 participant snapshot;Native sender 不发送,因此继续只收到原有 exact server wire。两端可在 `authenticate.displayName` 发送名称,并以 `set-display-name` 更新当前会话。服务端只向已订阅的 Web participant 发送完整 `viewer-presence` snapshot,列表严格来自当前带 sessionId 的在线参与者,不含五秒媒体恢复 grace 成员;条目用 `role` discriminator 区分 Host/Viewer。加入、离线、会话替换、改名和 active route 改变都会重发快照;该只读广播不调用 connect/reconcile,也不建立任何媒体边。Host 的“在线”Viewer 总数只取 Viewer 条目数;最多两条本机媒体快照只补充当前以 Host 为 active upstream 的连接指标。 +Web Host 在 `authenticate` 中显式发送 `viewerPresence: true` capability,Viewer 也可显式订阅同一 participant snapshot;Native sender 不在 accepted Browser wire 内并在 authority 前拒绝。两端可在 `authenticate.displayName` 发送名称,并以 `set-display-name` 更新当前会话。服务端只向已订阅的 Web participant 发送完整 `viewer-presence` snapshot,列表严格来自当前带 sessionId 的在线参与者,不含五秒媒体恢复 grace 成员;条目用 `role` discriminator 区分 Host/Viewer。加入、离线、会话替换、改名和 active route 改变都会重发快照;该只读广播不调用 connect/reconcile,也不建立任何媒体边。Host 的“在线”Viewer 总数只取 Viewer 条目数;最多两条本机媒体快照只补充当前以 Host 为 active upstream 的连接指标。 每个 presence 条目包含 `role`、`peerId`、`displayName` 和服务端 active assignment 的精确 `upstream`:Host 为 `none`,Viewer 为 `none | peer(peerId) | sfu`。UI 默认只展示名称;房内出现同名时,仅为同名项追加 peerId 尾部至少六字符短后缀,并只在同名项后缀碰撞时逐字符有界延长至完整 peerId。卡片状态只依据本机连接或当前 Viewer 接收证据显示“已连接”,否则保持中性的“线路分配中”或具体连接状态;active assignment 本身不是连接成功证据。连接详情同样等待当前媒体证据,再显示绿色 `P2P` 或黄色 `SFU fallback`。Host 观看者卡片采用两级指标披露:一级固定保留 route/transport、协议、帧率、分辨率、码率、丢包、RTT 和发送质量限制,codec/fmtp、编解码深指标、sender 参数与音频细节由卡片内的二级按钮按需展开;Viewer 自身的独立详情页保持完整指标。Host 与 Viewer 都可通过独立拓扑按钮从 authoritative assignment snapshot 展示 Host、实际存在的 SFU 节点及精确上下游;Viewer 默认收起并放在视频下方,没有当前 Host presence 时不生成占位拓扑。拓扑不把计划线路当作连接成功,也不显示 IP 或唯一名称对应的 peerId。peerId 不因此成为授权身份或路由输入;名称、presence 与拓扑视图不改变授权、媒体或路由状态机。 @@ -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。 -下表描述当前 source 的单一 `screener-v9` wire;server 与 Browser assets 必须作为一个完整合同原子发布,不存在 v8 parser、writer、translator 或中间协议子集: +下表描述 accepted Browser 的单一 `screener-v10` wire;server 与 Browser assets 必须作为一个完整合同原子发布,v9 和更早客户端在取得房间 authority 前拒绝,不存在旧 parser、writer、translator 或中间协议子集: | 方向 | 消息 | 作用 | | --- | --- | --- | @@ -124,11 +124,11 @@ WebSocket 使用无 query/fragment 的同源 `/signal`。upgrade 只记录 site- | 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/viewer -> server | codec transition apply/proof | 精确绑定 share/codec generation、`resumeAttempt` 与 frozen media binding,独立于 `route-ready` 和 Viewer quality evidence | +| host -> server -> viewers | `set-sharing-paused` / `host-status` | 当前 Host session 与 `shareGeneration` 约束一个 paused boolean;Resume 使用同一状态更新,不建立第二次授权、source acknowledgement 或媒体 proof | Zod 在服务边界拒绝未知类型、超长字符串、非法 SDP/candidate 结构和越权目标。服务端根据已认证会话决定路由:观看者不能给另一名观看者发信令,分享者也不能向本房间以外的 peerId 发送。 -当前唯一 source wire 是 `screener-v9`,不做版本协商、旧 parser、双写或 translator。任何旧页面或 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-v10`,不做版本协商、v9 parser、双写或 translator。任何 v9 或更早页面及 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`。 ## WebRTC 协商 @@ -156,25 +156,25 @@ Zod 在服务边界拒绝未知类型、超长字符串、非法 SDP/candidate | 1080p30 | 1920x1080、30 fps | 5 Mbps/peer | | 720p30 | 1280x720、30 fps | 3 Mbps/peer | -浏览器可返回更低设置,界面以 `track.getSettings()` 显示实际捕获结果。推荐区始终只有表中的三档并默认选择中档 `1080p30`;高级分辨率额外提供 `480p`(854x480)、720p、1080p 与 1440p,FPS 继续独立接受 15 至 60 整数,码率继续独立接受 2 至 12 Mbps,因此不建立第四个推荐档或 `480p` preset ID。视频 track 使用 `contentHint = "motion"`,返回的音频 track 使用 `contentHint = "music"`;二者都是内容提示而不是 codec 或质量档位。三档推荐质量与折叠高级区初始值统一采用 `degradationPreference = "balanced"`,并允许显式选择 clarity-first 或 fluid。“分享高级设置”同时提供 `自动 | H.264 | VP8` 和离散的 `64 kbps 省流 | 128 kbps 音乐默认 | 256 kbps 极高音质` 屏幕音频 sender ceiling。音频 ceiling 可在直播中串行更新;视频 codec 首先支持在分享显式暂停时事务化切换。它们只是 ceiling 和浏览器偏好,不保证实际分辨率、帧率、码率、声道或硬编。 +浏览器可返回更低设置,界面以 `track.getSettings()` 显示实际捕获结果。推荐区始终只有表中的三档并默认选择中档 `1080p30`;高级分辨率额外提供 `480p`(854x480)、720p、1080p 与 1440p,FPS 继续独立接受 15 至 60 整数,码率继续独立接受 2 至 12 Mbps,因此不建立第四个推荐档或 `480p` preset ID。视频 track 使用 `contentHint = "motion"`,返回的音频 track 使用 `contentHint = "music"`;二者都是内容提示而不是 codec 或质量档位。三档推荐质量与折叠高级区初始值统一采用 `degradationPreference = "balanced"` 与 `VP8`,并允许显式选择 clarity-first 或 fluid。“分享高级设置”在开始分享前提供 `自动 | H.264 | VP8` 作为 H.264 对照测试入口,并提供离散的 `64 kbps 省流 | 128 kbps 音乐默认 | 256 kbps 极高音质` 屏幕音频 sender ceiling。音频 ceiling 可在直播中串行更新;视频 codec 在分享开始后固定,直播和暂停期间均禁用 selector,改变 codec 需要停止并重新开始分享。它们只是 ceiling 和浏览器偏好,不保证实际分辨率、帧率、码率、声道或硬编。 每次 `setParameters()` 都必须从当次 `getParameters()` 派生更新,然后读回 requested/applied `maxBitrate`、`maxFramerate`、`scaleResolutionDownBy` 和 degradation preference。浏览器负责具体降级,实际结果由 stats 观测;拒绝或浏览器改写必须进入可见状态,不能只写 `console.warn`。 -Web 视频端不做视频 SDP munging。`automatic` 以空 `setCodecPreferences([])` 恢复 P2P 浏览器默认且不覆盖 LiveKit 默认 codec;显式 H.264/VP8 在首个 offer/publication 前应用,P2P 与 browser relay 仍保留完整 browser codec/repair 回退列表,SFU 始终保持 `backupCodec: false`。暂停时的 codec 切换使用单调 codec-negotiation generation,并和 route mutation 串行:冻结 exact current route/media binding target,新 join 按 pause 语义等待、leave 从 target 移除;P2P/relay 在暂停中以独立总 deadline 显式重新 offer/answer,SFU 以新 publication generation republish。SFU bounded-gap 先以 active revision 权威退休旧 generation,并等待该 managed room delete-plus-absence 完成,之后才创建 room、签发 token 或连接新 generation;全局 SFU 尚有余量也不能形成未获准的双 publication overlap。Preparation 成功后只停止 preparation deadline;同一个 room-serial codec mutation owner 保留一个内存有界的 prepared generation,暂停期间无 timer并排斥 route mutation,绝不擅自恢复用户主动暂停的分享。Host 的正常 Resume 动作先在该 owner 内原子重验 frozen bindings,由服务端为本次窗口分配进程内严格递增的 `resumeAttempt` 并启动同一个有界 resume/proof deadline;服务端向 Host 发恢复授权,但继续保持 authoritative pause。Host 成功启用 source 后回发精确绑定 room/share、Host session、codec generation(可空)与 `resumeAttempt` 的 source-enabled acknowledgement;只有服务端接受该 ack 后才清除 authoritative pause、先向 Viewer 广播 Resume,再下发 codec proof request。每名 still-current target 从 ack 后的实际 inbound RTP stats 与 decoded-frame counter 生成专用 codec-transition proof;proof 精确绑定 room/share、Viewer session、assignment/connection/media-binding/publication generation、codec-negotiation generation 与当前 `resumeAttempt`,request 和 response 必须回显同一 attempt。Pause 清空当前 proof batch并使该 attempt 失效;下一次 Resume 分配新 attempt,因此任何较早窗口延迟到达的 ack 或 proof 都不能提交当前窗口。不复用 Viewer quality evidence,也不改变 route authority。leave、hard failure 或 binding generation 变化会立即使 target 退出 proof 并把仍在线 Viewer置为 `route-wait`;在 codec owner settle 前,其旧物理 edge 与资源仍按实际占用保守计数,commit/abort 用同一个下一 active revision 统一 teardown 和释放,不能以未广播的逻辑失效低估 endpoint 或 SFU 用量。事务以剩余 target settle 后释放 owner,controller 再使用最终 committed preference 恢复该 Viewer。服务端只在全部 still-current target 的 actual codec 与 decoded progress 成立后提交 desired codec。Preparation failure 保持暂停并运行有界 old-preference rollback preparation;forward proof failure 先重新进入权威 paused,再运行 rollback preparation。Rollback prepared state 保留同一 owner且无 timer,等待用户下一次正常 Resume 后重验、分配新 resume attempt、完成 source-enabled ack、启动 rollback proof并启用 source;proof 证明 P2P 空/有序 preference 或 SFU fresh publish options 已恢复,并从新协商代次取得 actual inbound codec 与 decoded progress,不要求 actual codec 与旧观测字面相同。回滚失败保持暂停和明确错误,任何路径都不自动 Resume。stop/share replacement/room delete 清除 prepared state;零 target 只更新 future-edge desired codec。普通无 codec 的 Resume 也使用同一个 bounded `resumeAttempt` authorization/ack boundary,ack 前不向 Viewer宣告恢复,attempt timeout、Pause、stop、share replacement 或 Host session replacement 都保持 authoritative pause并允许用户重试。该 slice 是 paused bounded-gap mutation,不声称 make-before-break;`replaceTrack()` 或暂停本身不改变 codec,未暂停热切换后续复用同一事务。实际 outbound codec/profile、encoder implementation 与 stats 是结果真相;浏览器支持 H.264 不证明命中硬编。AV1 仍不得仅凭压缩率推断默认启用。唯一获准的 Web SDP 变换是下述 Viewer answer 的 Opus stereo receive preference,不得借此扩展视频码率、codec 或通用 munging。 +Web 视频端不做视频 SDP munging。当前默认 `VP8`;`automatic` 以空 `setCodecPreferences([])` 恢复 P2P 浏览器默认且不覆盖 LiveKit 默认 codec,显式 H.264/VP8 在本轮分享的首个 offer/publication 前应用。P2P 与 browser relay 始终保留完整 browser codec/repair 回退列表,SFU 始终保持 `backupCodec: false`。分享开始后 codec preference 固定;现有 peer 不因暂停而重协商,SFU 不因暂停而 republish,v10 wire 不包含 codec generation、preparation、proof、rollback 或 replacement 消息。普通 Pause/Resume 只以当前 Host session 和 `shareGeneration` 更新一个 paused boolean、切换同一批 capture tracks并唤醒普通 route reconciliation。实际 outbound codec/profile、encoder implementation 与 stats 是结果真相;浏览器支持 H.264 不证明命中硬编。H.264 根因修复并通过物理 direct、browser relay 与 SFU 验证后,另行决定是否将默认改为 H.264;若改为 H.264,同时删除 selector。AV1 仍不得仅凭压缩率推断默认启用。唯一获准的 Web SDP 变换是下述 Viewer answer 的 Opus stereo receive preference,不得借此扩展视频码率、codec 或通用 munging。 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 始终可用。 当前 Windows Native sender 用本地 opaque ID 保存所选窗口的 HWND、PID 与进程创建时间;helper 在使用前重新验证三者绑定,并以 WASAPI process loopback 的 include-target-process-tree 捕获游戏及其子进程。目标进程无渲染流、退出或捕获失败时显示无声/未知,不回退为整机音频,也不上传 PID、路径、窗口标题、设备标识或原始音频。该路径只支持 Windows 11;Windows 10 game-only 音频保持明确不支持。 -同一 helper 的显式 `native-window-h264` source 用 `CreateForWindow` 建立 WGC item,在所选 adapter 的 free-threaded D3D11 frame pool 接收 BGRA surface;D3D11 VideoProcessor 做固定 1280x720/30 aspect-fit NV12 转换,随后进入 adapter-LUID-bound、hardware-only asynchronous Media Foundation H.264 MFT。输出必须是 Annex-B `42c01f`,首次、周期和反馈恢复帧携带 SPS、PPS 与 IDR。PCM、H.264 access unit 和 local-only hardware status 通过一个有界 helper protocol 进入 generation-bound Go session;H.264 直接写现有 shared fanout,UI 只收一份单向预览副本,PCM 保持现有本地 WebCodecs Opus bridge。任一 Native capture/encode 失败都终止该 source,不静默改用软件 MFT、其他 codec、browser capture、monitor capture 或 system mix;普通 Web Host 的 `automatic` 默认与显式 H.264/VP8 选择是独立标准 WebRTC 路径。一名普通 Chrome Viewer 已 decoded/rendered 203 帧并收到 500 Opus packets;同一 helper PID 与 adapter LUID 的 `VideoEncode` 观测匹配。 +同一 helper 的显式 `native-window-h264` source 用 `CreateForWindow` 建立 WGC item,在所选 adapter 的 free-threaded D3D11 frame pool 接收 BGRA surface;D3D11 VideoProcessor 做固定 1280x720/30 aspect-fit NV12 转换,随后进入 adapter-LUID-bound、hardware-only asynchronous Media Foundation H.264 MFT。输出必须是 Annex-B `42c01f`,首次、周期和反馈恢复帧携带 SPS、PPS 与 IDR。PCM、H.264 access unit 和 local-only hardware status 通过一个有界 helper protocol 进入 generation-bound Go session;H.264 直接写现有 shared fanout,UI 只收一份单向预览副本,PCM 保持现有本地 WebCodecs Opus bridge。任一 Native capture/encode 失败都终止该 source,不静默改用软件 MFT、其他 codec、browser capture、monitor capture 或 system mix;普通 Web Host 的默认 VP8 与开始分享前可选的 Automatic/H.264/VP8 是独立标准 WebRTC 路径。一名普通 Chrome Viewer 已 decoded/rendered 203 帧并收到 500 Opus packets;同一 helper PID/adapter 的 `VideoEncode` 与 LUID 匹配。 直播中切换来源仍由明确的按钮手势重新调用 `getDisplayMedia()`,不改变房间、host generation 或媒体拓扑。选源取消或失败时继续发送旧流;成功后为每个现有 sender 调用 `replaceTrack()`,健康连接不重新协商。初始 offer 预留 send-only audio transceiver,使音频可以在 `null -> track -> null` 间切换。若某个 peer 的替换或回滚失败,只重建该 peer;新流提交后才停止旧流,并以当前 stream 身份保护旧 track 的迟到 `ended` 事件。统计码率累加器在换源后重置,避免把不同 track 的计数器混算。 -直播中切换质量不重新打开选源器:对当前视频 track 调用 `applyConstraints()`,再更新所有现有 sender 的参数;新加入、重建的 relay sender 和启用的 SFU publisher 使用同一权威设置。失败时保留当前分享并给出状态,不以重建整个房间补偿。临时暂停禁用当前 capture stream 的全部音视频 tracks;Host 通过当前 share generation 更新现有 `host-status`,`authenticated` 快照携带当前暂停位,Viewer 在舞台显示“分享者已暂停”。晚加入和信令重连从快照恢复,恢复、停止、新分享或会话替换清除旧状态;不得把 track mute、黑帧或网络停顿推断成主动暂停。房间与连接保持不变,恢复时重新启用同一批 tracks。 +直播中切换质量不重新打开选源器:对当前视频 track 调用 `applyConstraints()`,再更新所有现有 sender 的参数;该 mutation 不包含 codec,新加入、重建的 relay sender 和启用的 SFU publisher 使用本轮分享开始时固定的 codec 与最新其他权威设置。失败时保留当前分享并给出状态,不以重建整个房间补偿。临时暂停禁用当前 capture stream 的全部音视频 tracks;Host 通过当前 session 与 `shareGeneration` 发送一个 paused boolean,服务端同步 `host-status`、route pause 与 `authenticated` 快照,Viewer 在舞台显示“分享者已暂停”。Resume 使用同一状态更新并重新启用同一批 tracks,不另设 authorization、source acknowledgement 或 proof。晚加入和信令重连从快照恢复,恢复、停止、新分享或会话替换清除旧状态;不得把 track mute、黑帧或网络停顿推断成主动暂停。房间与连接保持不变。 Host 本地预览直接绑定现有 capture stream,不创建 viewer、`RTCPeerConnection` 或额外媒体边,也不产生 SFU/应用服务器出口。预览仍可能有本地合成、缩放和 GPU 显示成本;在同一动态场景对预览开/关采集 Host CPU、GPU、encode time 和实际 FPS。后台性能诊断必须把 Host 页面前台/遮挡/最小化、被捕获 source 前台/遮挡/最小化以及 preview 播放/暂停分成独立变量,并关联 document visibility/focus、track mute/readyState 与 capture/outbound/inbound stats。页面 timer、伪活跃和 Screen Wake Lock 都不构成屏幕捕获保活方案;若标准浏览器无法维持目标行为,应记录平台能力边界而不是制造第二套 lifecycle。只有稳定收益且捕获、编码和远端画面不变时,才把直播后的默认动态预览改为静止缩略图或隐藏并允许按需展开;隐藏预览绝不停止 capture track 或 sender encode。 -2026-08-19 当时生产 release `769de201f7cc` 的模糊、游戏低帧、持续 `bandwidth` 提示和 Host 刷新恢复都只作为该 release 的用户报告;当前生产已是 `39fcf93`。首轮复现必须在相同机器、Chrome/Edge、驱动、游戏场景和单个有线 direct Viewer 上对照该历史 release 与最新 `main`。生命周期实验固定 `automatic`,每个独立 affected baseline 只运行 observe、受影响 `HostPeer` rebuild、capture replace、Host reload 或 preview cycle 中一个动作;codec A/B 另以全新 Host/share/PeerConnection 会话分别请求 automatic/H.264/VP8 并校验 actual codec。质量判断只使用同一 generation 且时间重叠的 capture A、sender B、Viewer C 窗口;stats identity poll 不推进采样 accumulator,可选 encoder 字段缺失保持 unknown。若实际 codec/encoder 不变而 BWE/发送恢复,则下调 codec 根因优先级;只有编码成本异常时才进入受控 codec repair。 +2026-08-19 当时生产 release `769de201f7cc` 的模糊、游戏低帧、持续 `bandwidth` 提示和 Host 刷新恢复都只作为该 release 的用户报告;当前生产已是 `39fcf93`。首轮复现必须在相同机器、Chrome/Edge、驱动、游戏场景和单个有线 direct Viewer 上对照该历史 release 与最新 `main`。生命周期实验固定当前默认 VP8,每个独立 affected baseline 只运行 observe、受影响 `HostPeer` rebuild、capture replace、Host reload 或 preview cycle 中一个动作;codec A/B 另以全新 Host/share/PeerConnection 会话分别请求 Automatic/H.264/VP8 并校验 actual codec。质量判断只使用同一 generation 且时间重叠的 capture A、sender B、Viewer C 窗口;stats identity poll 不推进采样 accumulator,可选 encoder 字段缺失保持 unknown。若实际 codec/encoder 不变而 BWE/发送恢复,则下调 codec 根因优先级;只有编码成本异常时才进入受控 codec repair。 `track.getSettings()` 中存在的 EC/NS/AGC/voice-isolation、`channelCount` 和 `sampleRate` 只能作为当前捕获观测;缺失字段保持未知,Opus 协商仍是独立事实。每个 Web Host 或浏览器 relay 的实际音频 sender 在首次绑定和换源后通过标准 `RTCRtpSender.setParameters()` 请求并读回当前 64/128/256 kbps ceiling;这只是上限,浏览器 WebRTC 拥塞控制仍可降低实际流量,不新增应用层音频降级器。128 kbps 保持默认,因为 RFC 7587 将 20 ms full-band stereo music 的建议区间列为 64--128 kbps,pinned LiveKit 2.22.0 也把 128 kbps 命名为 `musicHighQualityStereo`;256 kbps 是用户显式选择的较高上限,不是质量保证。Peer answer 声明 `stereo=1;maxaveragebitrate=256000`,让同一接收协商覆盖所有三档,实际 sender ceiling 仍由所选档位约束;普通 PC synthetic 不用于标定参数、听感或性能。 @@ -270,7 +270,7 @@ media-proof 和 bounded-recovery 不变量的 scoped code 与 tests。 每两秒在本地采集一次 `getStats()`。首版不上传原始统计。折叠详情可在产生该连接的本机显示 selected local/remote candidate 的 `address`/`port`,但浏览器未暴露时保持未知;地址不得进入信令、Viewer C、日志、诊断导出、持久化或路由/质量决策。在 peer 边中它只是当前连接端点,在 SFU 边中通常是 SFU 端点,不能标成参与者的权威公网 IP。 -当前 `screener-v9` 在 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-v10` 在 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。 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。 @@ -336,7 +336,7 @@ Token-free SFU standby prewarm 保持可选且不创建 participant、publicatio - RoomStore 的四位空闲码随机分配/释放、活动分享免过期、dormant lease/续租、Host token/Viewer grant 摘要验证、`open/password/disabled` 正交 policy、grant 到期/跨房拒绝、rotate/revoke 强撤销、Viewer 上限和超额拒绝; - 进程重启后 room state 全空、旧 Host/grant/password fail closed、释放号码复用时旧 grant 仍失败;`ROOM_LEASE_SECONDS` 的默认/边界与 `MAX_ROOMS <= 9000` 校验,已删除的 `ROOM_DATABASE_PATH`/`ROOM_TTL_SECONDS` fail-fast; - STUN UDP URL 严格校验、生产 STUN 必需、LiveKit tuple 完整性/TTL/secret 独立性、当前 route/session/revision/connection generation guard,以及 ordinary peer、未授权房间和 Native 的 STUN-only isolation;LiveKit `sfu-config` 保持独立边界; -- 当前 source 的单一 `screener-v9` 信令 literal、任意旧客户端 mismatch 的一次性刷新终态,以及协议非法 JSON、未知消息、错误方向、跨房间目标和 payload 限制;v9 不含无消费者的 parent-edge quality proof,包含 paused codec transaction proof、`ROOM_ACCESS_DENIED`、typed Viewer route failure 与 Host-only 按需脱敏 route snapshot,不保留 v8 parser、writer 或别名;`qualityProfileIdSchema` 保持三档不变,只有 `qualityResolutionSchema` 包含 `480p`,`DEFAULT_QUALITY_SETTINGS` 为 1080p30; +- accepted next source contract 的单一 `screener-v10` 信令 literal、v9 及更早客户端 mismatch 的一次性刷新终态,以及协议非法 JSON、未知消息、错误方向、跨房间目标和 payload 限制;v10 不含 parent-edge quality proof 或 codec transaction/resume handshake,只保留 shareGeneration-fenced paused boolean、`ROOM_ACCESS_DENIED`、typed Viewer route failure 与 Host-only 按需脱敏 route snapshot,不保留旧 parser、writer 或别名;`qualityProfileIdSchema` 保持三档不变,只有 `qualityResolutionSchema` 包含 `480p`,`DEFAULT_QUALITY_SETTINGS` 为 1080p30、VP8; - 质量协议的 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 拒绝; - 客户端策略与 stats 解析:会话替换不重连,ordinary ICE 与 LiveKit protocol 分离; @@ -359,7 +359,7 @@ Token-free SFU standby prewarm 保持可选且不创建 participant、publicatio ## 当前交付边界 -- Production 当前运行 exact `39fcf93bae057fcbb1002702c3be6b90bac9027f`、release `39fcf93`、wire `screener-v9`。Live audio ceiling、paused codec transaction、first-frame Viewer presentation、typed failure、按需 route timing、无枚举房间准入、1080p30 默认值和 advanced 480p 已随同一 wire 原子部署;该 release 的 deployment postflight 已通过,回滚边界为 `/opt/screener/backups/39fcf93-pre-v9-20260824T004257Z`。 +- Production 当前运行 exact `39fcf93bae057fcbb1002702c3be6b90bac9027f`、release `39fcf93`、wire `screener-v9`,因此仍是 automatic 默认、paused codec transaction 与 Resume source-ack/proof 行为;该 release 的 deployment postflight 已通过,回滚边界为 `/opt/screener/backups/39fcf93-pre-v9-20260824T004257Z`。本设计正文的 VP8 默认、pre-share-only selector、简单 Pause/Resume 与 `screener-v10` 是下一次原子实现/部署边界,尚未进入当前 source 或 production;v10 上线时拒绝 v9,不保留 alias 或兼容 parser。 - H.264/模糊与 Host 后台性能继续按受控根因证据诊断;标准 ICE/SFU 继续按真实异构网络验收,不增加 guessed candidate、NAT classifier、TCP probe 或 quality-driven reparent。 - 当前发布面只包含 Web Host、Viewer 和 browser relay。Native sender、capture helper、shared-encode executable 与两个分发包仍由各自后续决策和物理证据驱动,不属于当前 Browser release。 @@ -379,7 +379,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-v9` 的 `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-v10` 的 `authenticated.endpointMediaCopyCapacity` 把部署 `C` 交给 Host 与 Viewer;两端用该值校验稳态 assignment,并让 active+pending 的物理 copy 只在 controller 已授权 overlap 时达到 `min(C + 1, 3)`。代码通过一个 shared accounting guard 执行,不重复 magic number;v9、其他旧 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 5df1ddd..854a952 100644 --- a/docs/需求理解.md +++ b/docs/需求理解.md @@ -27,7 +27,7 @@ SFU 的 publication、subscription、server-resource accounting、临时 overlap - 端点发送容量:所有非服务器端点使用同一个服务端权威稳态 outbound media-copy capacity,由唯一部署项 `ENDPOINT_MEDIA_COPY_CAPACITY` 配置,默认 `2` 且只接受 `1`、`2` 或 `3`;不读取旧键或保留第二套 tier。peer child 或 Host 的唯一 SFU publication 各消耗一份,上游接收不消耗该预算。客户端只能声明不高于部署值的可用容量,角色、浏览器、UA、设备类型和页面可见性不产生不同上限。服务端在每次 `authenticated` 权威快照中返回最终 `endpointMediaCopyCapacity`;普通非 peer-assisted 模式也只授权稳定加入顺序中的至多 `C` 个 Host child,超额 Viewer 保持入房等待并在槽位释放后提升,信令不能绕过该集合。Browser Host 与 Viewer 严格校验该值并用它约束稳态 assignment;并行产生媒体的 candidate 必须持有唯一 `TransitionOverlapSlot`,active+pending 物理 copy 只可达到 `min(C + 1, 3)` 并在 commit/abort 后释放。`C=3` 满载时必须先收到 active bounded-gap retirement 再 prepare,绝不产生第四份 copy。SFU subscriber egress 由独立 server admission 记账。当前不兼容房间合同以单一 release 原子升级;缺失/非法 cap、旧 wire 与 executable sender都在取得room/route authority前fail closed。 - P2P 优先:每条普通逻辑媒体边先尝试 direct/peer UDP,服务器不得默认中转可直连媒体。更多观看者优先沿满足统一端点容量的无环 peer 图分布;每名 Viewer 同时至多一个 active upstream,失败时只影响需要重选的 child。任何 source-reachable endpoint 在 effective capacity 有空位时都可作为 parent candidate;Peer-fed 与 SFU-fed endpoint 使用同一个 provisional-child transaction,并在 candidate child 解码出第一帧新画面后独立提交该 edge。Host 已满且首次需要 SFU 时,同一 reconcile 将 affected Host direct child(若无则稳定顺序中最新的 connected Host direct child)作为本次 child operation,创建唯一 Host publication 并把该 child 变为首个 SFU subscriber,subtree 不动;publication 取代释放的 Host peer slot,下一次扫描再处理原受限 Viewer,不建立 bootstrap 状态机。 - NAT 与传输边界:STUN 必须可用,所有 ordinary peer `RTCPeerConnection` 只接收 STUN ICE servers。浏览器 ICE connectivity checks 是 direct UDP 可达性的权威;Screener 不合成 remote candidate、不预测端口、不分类 NAT,也不使用 TCP 探测推断 UDP 媒体可达性。所有恢复和候选授权必须绑定 authenticated room/endpoint session、share generation、base/pending route revision 与 candidate connection identity;成功即停止,失败、超时或绑定失效即撤销,direct 阶段耗尽后在同一 operation deadline 内进入 SFU/UDP,全部可用路径耗尽后明确等待或失败。已接受的完整顺序只有保持或尝试 direct/STUN peers,再复用或建立 direct-ingress Host publication/SFU subscription。Screener 不配置 TURN、ICE/TCP、media TCP 或 TLS-relayed media;当前 LiveKit 也保持 UDP-only。未来若真实严格防火墙矩阵证明需要另一种传输,必须把它作为 LiveKit 内部能力重新决策,不能增加第三种应用 route candidate。LiveKit join JWT 仅按协议进入不得记录的 WebSocket request target;HTTPS/WSS 继续独立使用 TLS/TCP。 -- 质量控制:推荐档位始终只有 1080p60(8 Mbps)、1080p30(5 Mbps)、720p30(3 Mbps)三档,默认选择中档 `1080p30`。高级设置的分辨率选项增加 `480p`(854x480),其 FPS 与码率仍由高级控件独立选择;不增加 `480p` 推荐档、preset ID 或固定的 480p FPS/码率组合。三档与高级设置初始值统一采用 `balanced`(平衡)与 `automatic`(自动编码),`maintain-resolution`(清晰优先)、`maintain-framerate`(流畅优先)和 H.264/VP8 保留为显式选择。分享期间可直接换视频档位和 64/128/256 kbps 音频 sender ceiling,不重新选择来源或重建健康连接;视频 codec 首个可交付切换边界是在分享已显式暂停时执行有界重协商,未暂停热切换是后续复用同一事务的扩展。Peer-assisted relay 与已配置的 SFU 必须接收同一个严格校验的房间级权威设置,覆盖当前和以后建立的 sender;普通 P2P 信令结构保持不变。任何设置都只是浏览器上限和偏好,具体降级由浏览器决定;必须读回实际 sender 参数并通过 stats 观测结果、显示参数拒绝、改写和持续受限原因。不做自定义动态 FPS、综合评分或周期性全房间调档。 +- 质量控制:推荐档位始终只有 1080p60(8 Mbps)、1080p30(5 Mbps)、720p30(3 Mbps)三档,默认选择中档 `1080p30`。高级设置的分辨率选项增加 `480p`(854x480),其 FPS 与码率仍由高级控件独立选择;不增加 `480p` 推荐档、preset ID 或固定的 480p FPS/码率组合。三档与高级设置初始值统一采用 `balanced`(平衡)与 `VP8`,`maintain-resolution`(清晰优先)、`maintain-framerate`(流畅优先)保留为显式选择;`自动 | H.264 | VP8` 只在开始分享前作为 H.264 对照测试入口。分享期间可直接换视频档位和 64/128/256 kbps 音频 sender ceiling,不重新选择来源或重建健康连接;一次分享的 video codec 从开始到停止保持固定,直播中和暂停中都不能切换,改变 codec 必须停止后重新开始分享。Peer-assisted relay 与已配置的 SFU 必须接收同一个严格校验的房间级权威设置,覆盖当前和以后建立的 sender;普通 P2P 信令结构保持不变。任何设置都只是浏览器上限和偏好,具体降级由浏览器决定;必须读回实际 sender 参数并通过 stats 观测结果、显示参数拒绝、改写和持续受限原因。不做自定义动态 FPS、综合评分或周期性全房间调档。 - 路径隔离双表示:direct/peer 继续使用每条 PeerConnection 独立的 stock WebRTC GCC,不把 viewer 反馈合成全房目标。每条 SFU path 从 `HIGH` ceiling 开始;优先使用 pinned LiveKit 的标准两层 screen-share simulcast,让内建 per-subscriber BWE 独立降层和恢复,Screener 不实现媒体层 selector。应用只诊断实际接收层、硬件编码、游戏性能、资源和上传;SFU 自动降层不改变拓扑。`LOW` 停编只是资源优化,active representation/layer 硬上限为二且不随 viewer 增长。当前 Web P2P simulcast 与 Web/LiveKit SVC 不能证明跨 direct/peer 共享编码或可移植硬件路径;全部标准路径失败后才考虑 custom/native dual encode。 - 路由失效输入:应用不推断 parent-wide 质量,也不计算综合分数。当前 authenticated child 只在 PeerConnection `failed/closed`,或非权威暂停时现有 periodic stats 在统一 deadline 内没有新 decoded frame 时,使自己的 exact edge 失效并唤醒同一个 reconcile。decoded progress 只保存 route-keyed `lastDecodedAt`,route/connection变化或新帧重置、暂停抑制,同一 edge只报告一次,不增加轮询器。该 operation 把失败的 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。 @@ -45,8 +45,8 @@ SFU 的 publication、subscription、server-resource accounting、临时 overlap - 独立房间入口:合法 `/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 行为。 - 统一自动路由:旗舰部署开启 `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-v9` 服务端补充消息是 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、清晰/平衡/流畅三种偏好、`自动 | H.264 | VP8` 编码选择,以及离散的 `64 kbps 省流 | 128 kbps 音乐默认 | 256 kbps 极高音质` 屏幕音频 sender ceiling;480p 不形成推荐档或固定 FPS/码率组合,初始值采用 `balanced`、`automatic` 与 128 kbps。视频档位和音频档位在直播中显式应用;音频变更只更新固定 Opus 路径的 sender ceiling,不是 codec 切换。所有当前及以后建立的 Web Host、Browser relay 与 SFU sender 都读取最新 desired 档位,并各自在 fresh sender parameters 上应用和读回 endpoint-local applied 值;实际码率仍由浏览器拥塞控制和 stats 决定。Peer 只在唯一明确的 active Opus audio 上做结构化、幂等的 Viewer answer `stereo=1;maxaveragebitrate=256000` 变换,使同一接收上限覆盖三档;非目标形态或 parser/writer 异常必须原样保留浏览器 answer,不得阻断可继续的 mono/video edge。不得用宽泛字符串/正则改 SDP,也不得改写浏览器协商的 FEC。SFU 将三档映射到 pinned SDK 的 music stereo preset 或显式 256 kbps ceiling,统一保留 `forceStereo = true`、`dtx = false` 与 pinned room default `red = true`。不得暴露任意码率、声道、采样率、audio codec、stereo、DTX、RED 或 FEC 控件;256 kbps 只是显式较高上限,不是无损、高保真或最低码率保证。实际质量持续低于所选上限时必须显示可解释的限制原因;在真实拥塞、编码器和接收端证据完成前,不把任一组合称为“完美平衡”。未来 microphone voice 若进入范围,必须是独立 track/处理链,不能把 AEC/降噪/DTX 套到电影或游戏音频。 +- 观看端连接反馈:Viewer 舞台从鉴权到首帧使用一个由现有权威状态推导的稳定阶段反馈,至少区分等待分享、分配线路、建立 P2P、连接备用线路、等待首帧、浏览器阻止自动播放、恢复连接和有界失败。阶段不是进度百分比,也不产生 route authority;只有当前 video element 实际提交首帧后才显示正在播放。播放按钮只在 `play()` 因自动播放策略被拒绝时稳定出现,连接中不得忽隐忽现;明确失败后才提供一次受现有 controller 约束的重试入口。make-before-break 或仍有可见旧帧时保留画面并叠加恢复状态,不为显示反馈主动清空健康 media element。Host 离线本身不使仍健康的媒体失效;exact upstream 硬失败或 route terminal failure 必须使 current frame proof 失效,已证明画面至多降为 retained background,不能遮蔽离线或有界失败。accepted `screener-v10` 服务端补充消息是 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、清晰/平衡/流畅三种偏好、开始分享前可选的 `自动 | H.264 | VP8` 编码,以及离散的 `64 kbps 省流 | 128 kbps 音乐默认 | 256 kbps 极高音质` 屏幕音频 sender ceiling;480p 不形成推荐档或固定 FPS/码率组合,初始值采用 `balanced`、`VP8` 与 128 kbps。视频档位和音频档位可在直播中显式应用;codec 控件在分享开始后保持禁用,暂停不改变该边界。音频变更只更新固定 Opus 路径的 sender ceiling,不是 codec 切换。所有当前及以后建立的 Web Host、Browser relay 与 SFU sender 都读取本次分享开始时已确定的 codec 和最新其他 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。只有稳定收益且捕获、编码和远端画面不受影响时,才允许直播开始后默认显示静止缩略图或隐藏动态预览并提供按需展开;未测前不宣称收益。 - Peer-assisted 媒体路径:所有非服务器 endpoint 都受同一个服务端权威稳态 outbound media-copy capacity 约束,默认 `2` 且可静态配置为 `1`、`2` 或 `3`;peer child 与 Host publication 都按实际发送副本计数。每名 Viewer 任一时刻只有一个 active upstream,活动媒体图保持无环;只在离散事件中做有界分配或换父。浏览器把收到的标准 remote track 加入独立下游 `RTCPeerConnection` 时会逐跳解码并分别重编码,必须测量而不能称为 shared encode。服务端只在有界房间内存中保存严格校验的质量设置,不写入持久存储。 @@ -58,12 +58,12 @@ SFU 的 publication、subscription、server-resource accounting、临时 overlap - 分享端桌面封装:用 Electron 固定 Chromium 版本、改善选源/热键/系统音频体验,同时保留 Web 观看端。 - Windows 应用音频:Windows 11 Native sender 使用本地 opaque window target 绑定 HWND、PID 与进程创建时间,只捕获所选游戏及其子进程的音频,避免把语音聊天或通知声一并分享;“全部系统声音”只能由分享者显式选择。普通 Windows 10 不具备当前 process-loopback API 门槛,必须明确标为未解决/不支持而不是伪兼容。浏览器无法证明或隔离任意进程音频时必须如实提示,原生捕获失败、无声或进程退出不得静默扩大为整机捕获。 - 受限网络边界:当前媒体只使用 ordinary direct/STUN UDP 与 LiveKit SFU/UDP;全 UDP 封锁、只允许标准 HTTPS 出站等网络在有界窗口后明确失败。不得用 TCP 探测、客户端声称的 NAT 类型或另一套应用 route candidate 猜测可达性。若真实矩阵证明该失败边界需要扩展,先比较 LiveKit 内建 ICE/TCP 或其内部安全 relay transport,并另行接受安全、端口、延迟和部署合同。 -- Web 编码选择:分享高级设置提供 `自动 | H.264 | VP8`,默认自动使用浏览器/LiveKit 协商;首次分享前直接应用,分享中只在 Host 已显式暂停后允许切换。暂停切换与 route mutation 串行,使用 generation-fenced P2P/relay 重协商和 SFU republish;preparation 不得自动恢复用户暂停,只有用户正常 Resume 后才以实际 codec 与 decoded-frame progress 验证,失败保持暂停并有界回滚。P2P/浏览器 relay 保留完整 repair 与其他 codec 回退列表,SFU 保持 `backupCodec = false`,不维护第二套应用编码链。实际 codec/profile、encoder implementation 与 stats 是结果真相;H.264 可选不等于浏览器必走硬编,AV1 仍不得仅凭压缩率推断默认启用。 +- Web 编码选择:分享高级设置在开始分享前提供 `自动 | H.264 | VP8`,当前 Browser 默认 `VP8`;该 selector 只用于建立 H.264 对照测试,不是长期动态调节功能。选定 preference 在本次分享的首个 P2P/relay offer 和 SFU publication 前应用,并对之后为该分享新建的 sender 保持一致;`automatic` 恢复浏览器/LiveKit 默认,显式 H.264/VP8 仍保留完整 repair 与其他 codec 回退列表,SFU 保持 `backupCodec = false`。分享开始后 codec 固定,直播中和暂停中都不提供 codec 切换;需要改变时停止并重新开始分享。实际 codec/profile、encoder implementation 与 stats 是结果真相;H.264 可选不等于浏览器必走硬编。只有 H.264 根因修复且在物理 direct、browser relay 与 SFU 路径验证成功后,才另行决定是否把默认改为 H.264;若改为 H.264,同时删除 selector。AV1 仍不得仅凭压缩率推断默认启用。 ### P2 - 可选功能 - 运行时有效容量:部署的 steady cap `C` 仍只接受 `1`、`2` 或 `3` 且默认 `2`,不按角色、设备或 UA 分类。服务端把 authenticated endpoint 当前广告的 `0..C` 可用值 clamp 成唯一 `effectiveDownstreamCapacity`;`0` 表示当前不承担下游,不是另一套 release tier。controller 通过同一个 reconcile 迁移 deterministic overflow children。H.264、hardware encode 或 shared encode 可以降低编码成本或同观感码率,但每个 ordinary child 仍消耗一份网络副本。 -- 原生 Windows 捕获辅助程序:显式 `native-window-h264` 模式使用同一 bound window target 的 WGC/D3D11 画面与 WASAPI process-tree PCM,并用 adapter-bound、hardware-only Media Foundation H.264 `42c01f` 直接进入现有 Go fanout;普通 Web Host 独立采用默认 `automatic`、可显式选择 H.264/VP8 的标准协商路径,不允许运行时静默改用软件编码、显示器画面或系统混音。一名普通 Chrome Viewer 已 decoded/rendered 203 帧并收到 500 Opus packets,同一 helper PID/adapter 的 `VideoEncode` 与 LUID 匹配。 +- 原生 Windows 捕获辅助程序:显式 `native-window-h264` 模式使用同一 bound window target 的 WGC/D3D11 画面与 WASAPI process-tree PCM,并用 adapter-bound、hardware-only Media Foundation H.264 `42c01f` 直接进入现有 Go fanout;普通 Web Host 独立采用默认 `VP8`、只在开始分享前可显式选择 Automatic/H.264/VP8 的标准协商路径,不允许运行时静默改用软件编码、显示器画面或系统混音。一名普通 Chrome Viewer 已 decoded/rendered 203 帧并收到 500 Opus packets,同一 helper PID/adapter 的 `VideoEncode` 与 LUID 匹配。 - Viewer 向电视输出:系统级屏幕镜像现在可直接使用;产品只复用平台 AirPlay/Cast,不自造投屏协议。截至 2026-08-21,Chromium 的 Remote Playback URL 路径和 WebKit 明确拒绝 wireless target 的 live `MediaStream` player 均未给当前 WebRTC `srcObject` 提供可用契约,因此不显示伪可用入口。只在浏览器实现明确支持并由实体 Apple TV 或 Chromecast/Google TV 证明当前 live 画面与音频后,才按通过的平台加入 Viewer 本地图标按钮。投屏不能改变 room、SFU/peer route、Host fanout 或服务端媒体分发;Google Cast receiver 需要可获取的媒体 URL,当前 P2P `MediaStream` 不满足该输入,因此不加入 Cast SDK/receiver。 - 多地区 server-assisted ingress/egress 调度及 SFU 的按地区、运营商可达率。 @@ -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。客户端、服务端和部署配置按同一 release 原子迁移;旧客户端在 authority 前拒绝,替代后立即删除旧配置、wire、parser、双写、translator、deprecated alias、migration shim 与仅服务旧版本的测试,不保留第二套架构。Git 历史承担旧实现追溯;公开发布后才按明确的新要求重新评估兼容策略。 +- 迁移完整性:项目拥有者明确宣布公开发布前,只维护当前内部产品合同,不承担 backward/forward compatibility。客户端、服务端和部署配置按同一 release 原子迁移;本次 Browser codec 合同使用单一 `screener-v10`,v9 及更早客户端在 authority 前拒绝。替代后立即删除旧配置、wire、parser、双写、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。 @@ -102,7 +102,7 @@ SFU 的 publication、subscription、server-resource accounting、临时 overlap - 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 本地输入不是 `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 首条信令鉴权到达,Then 服务端返回当前 `screener-v9` 唯一 `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 尝试对应未知/过期房间、disabled policy、缺失/错误密码、满房或其他已知有界 admission refusal,When `screener-v10` 首条信令鉴权到达,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 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 或恢复旧号码。 @@ -133,7 +133,7 @@ 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 后释放。When 当前房间合同上线,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 后释放。When `screener-v10` 房间合同上线,Then server 与 Browser assets 在同一 release 原子升级,缺失或越界的 `C`、v9/其他旧 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 一名分享者和 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。 @@ -153,7 +153,7 @@ SFU 的 publication、subscription、server-resource accounting、临时 overlap - Given 当前 Viewer 的 exact edge 进入 `failed/closed`,或在非权威暂停时超过统一 deadline 没有新 decoded frame,When 服务端确认 authenticated child、share generation、active revision 与 connection identity 仍匹配,Then 只把该 child 送入同一个 reserve/prepare/first-frame/commit-or-abort 事务;低码率、低 FPS、模糊和 SFU layer 只进入诊断。 - Given 当前 edge 仍在解码但分辨率、FPS、码率、freeze ratio、RTT、jitter、loss 或 limitation evidence 持续影响观感,When 评估是否自动换父,Then 当前产品只诊断而不迁移:现有路径证据不能证明未连接 parent 的反事实质量,WebRTC/LiveKit 继续拥有拥塞适应,应用不建立 alternative-parent probe、hysteresis/cooldown、加权分数或周期性重排。 - Given 同一 Viewer 同时是某人的 child 和其他人的 parent,When 自身 ingress 失效,Then 它作为 child 换父、在新 ingress 首帧提交前保留可用旧 edge,并保持 subtree。Given endpoint disconnect 或 `effectiveDownstreamCapacity` 降低,Then controller 只保留稳定顺序内可容纳的 children,并将 deterministic overflow children 逐个送入同一事务;childless 后删除 disconnected relay。 -- Given 当前分享权威 paused,When 新 Viewer 加入、edge 失效或已有 child operation 尚未提交,Then abort 整个 pending route operation 及其 current candidate/预留、保留 active graph、让新 Viewer waiting,并且不因没有 decoded frame 触发恢复。普通 Resume 和 prepared codec Resume 都先分配一个服务端单调 `resumeAttempt` 并保持 authoritative pause,直到当前 Host 启用 source 后回发 exact acknowledgement;attempt 超时、再次 Pause、stop、share replacement 或 Host session replacement 都使其失效并保持暂停。普通 ack 后从当前 graph 重新 reconcile;若存在 prepared codec owner,则 ack 后才开始该唯一 room-serial owner 的 frame proof,codec settle 后才处理积压 route reconcile。 +- Given 当前分享权威 paused,When 新 Viewer 加入、edge 失效或已有 child operation 尚未提交,Then abort 整个 pending route operation 及其 current candidate/预留、保留 active graph、让新 Viewer waiting,并且不因没有 decoded frame 触发恢复。普通 Pause/Resume 只通过当前 Host session 与 `shareGeneration` 约束的一次 paused-state 更新同步本地 capture tracks、服务端快照、`host-status` 和 route pause;Resume 重新启用同一批 tracks并从当前 graph 唤醒 fresh reconciliation,不建立第二次授权、source acknowledgement、媒体 proof 或回滚状态。较早 share generation、已替换 Host session、stop 或新分享的迟到更新不得改变当前状态。 - Given exactly-two publication 的实测 encoder/CPU/GPU 或上传成本超过游戏预算,或没有合格 hardware/power-efficient 路径,When 评估该候选,Then 保留健康路径并明确显示弱路径降级或不可用,不得退回全房间最低目标或静默软件编码;若 supported sender cohort 无法以常驻或按需方式提供共享 `LOW`,Then 质量验收失败,必须优化编码路径或明确标记不支持。 - 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 请求不改变表示或路径状态;有效请求也只能作为交叉验证输入。 @@ -168,7 +168,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 选择 `自动 | H.264 | VP8` 的不同值,When 执行首个视频 codec 切换闭环,Then 为该请求分配单调 codec-negotiation generation,并与 room route mutation 串行;事务开始时冻结 exact current route/media binding target,离开的 Viewer 从 target 移除,新加入者按既有 pause 语义等待。暂停期间在独立有界 preparation deadline 内对 target 的 P2P/relay path 完成显式 offer/answer 重协商,并对 SFU 路径完成新的 publication generation/republish;选择 `automatic` 时以空 codec preference 恢复浏览器默认。SFU republish 必须先用 active revision 权威退休旧 generation并完成 managed room delete-plus-absence,之后才创建、签发或连接 fresh generation;不得因全局 ingress 尚有余量形成双 publication。Preparation 完成不得擅自恢复用户主动暂停的分享:停止 preparation deadline,但继续由同一个 room-serial codec mutation owner 持有一个 bounded prepared generation,暂停期间不运行 timer并排斥 route mutation。Host 的正常 Resume 必须先在该 owner 内原子重验全部 frozen binding,由服务端分配进程内严格递增的 `resumeAttempt`、启动同一个有界 resume/proof deadline并发送恢复授权,同时保持 authoritative pause。Host 成功启用 source 后回发精确绑定 room/share、Host session、codec generation 与 `resumeAttempt` 的 acknowledgement;服务端接受后才清除 authoritative pause、先向 Viewer 广播 Resume、再发送 codec proof request。暂停帧不能充当媒体证明:ack 后每名 still-current target 发送一个只属于 codec transaction 的 proof,精确绑定 room/share、Viewer session、assignment/connection/media-binding/publication generation、codec-negotiation generation、当前 `resumeAttempt`、实际 inbound codec 与 decoded-frame baseline/delta;request 和 response 必须回显同一 attempt,Pause 立即使当前 attempt 失效,因此旧 Resume 窗口的延迟 ack 或 proof 不能提交新窗口。该 proof 不进入路由失效判断。若 leave、hard failure 或任何 exact binding 变化使 target 失效,服务端立即把它移出本事务并标为 `route-wait`;codec owner settle 前仍按物理 edge 和资源的实际占用保守计数,并在 commit/abort 的下一 active revision统一 teardown和释放。codec 事务以剩余 target settle 后释放 owner,controller 再用最终 committed preference 为仍在线 Viewer 恢复路径。只有服务端校验全部 still-current proof 后才提交 desired codec。Preparation failure 保持暂停并开启有界 old-preference rollback preparation;forward proof failure 则先重新进入服务端权威 paused,再执行同一 rollback preparation。Rollback prepared state继续占有同一 owner、无 timer,并等待用户下一次正常 Resume 后重验 binding、分配新 resume attempt、完成 source-enabled ack并启动 rollback proof;rollback 证明旧 preference 已重新应用(P2P 空/有序 preference 或 SFU fresh publish options)以及新协商代次的 actual inbound codec 与 decoded progress,不要求 actual codec 字面等于历史值。回滚失败保持暂停并显示明确失败;任何路径都不自动 Resume,不允许 stale generation 或 stale resume attempt 恢复播放。停止分享、替换 share generation 或删除房间清除 prepared state。零 current Viewer 或事务中全部离开时直接提交 future-edge desired codec,无需伪造 frame proof。`pause`、`replaceTrack()` 或仅调用 `setCodecPreferences()` 均不能冒充已完成切换;本 slice 是明确的 paused bounded-gap 事务,未暂停 MBB 热切换留作后续扩展。 +- Given 分享尚未开始且 Host 选择 `自动 | H.264 | VP8`,When 开始一轮新分享,Then 默认值为 VP8,所选 preference 在该分享的首个 P2P/relay offer 与 SFU publication 前应用,并由实际 RTP stats 验证结果。Given 分享已经开始,When 分享处于直播或暂停状态,Then codec selector 保持禁用且不发送 codec mutation 消息;本轮分享的现有和以后新建 sender 使用开始时确定的 codec preference。Host 只有停止当前分享并开始新分享后才能改变 codec。 - 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、降噪或来源标签伪称已隔离。