From 815568bb49e1fdb8924488f0d7ef20b985c2b037 Mon Sep 17 00:00:00 2001 From: AAA <35992542+TNTcraftHIM@users.noreply.github.com> Date: Mon, 24 Aug 2026 12:10:17 +0800 Subject: [PATCH] docs(media): select fixed Browser VP8 Select the fixed Browser VP8 contract and record the H.264 encoder-selection evidence. --- docs/README.md | 2 +- .../0005-automatic-hybrid-media-routing.md | 2 +- ...mand-driven-dual-representation-quality.md | 4 +- docs/adr/0008-window-scoped-audio-capture.md | 8 +- docs/project-memory.md | 6 +- docs/research/browser-background-capture.md | 11 +- docs/research/native-h264-opt-in-path.md | 22 +-- docs/research/realtime-quality-adaptation.md | 151 +++++++----------- docs/session-handoff.md | 9 +- docs/status.md | 4 +- docs/todo.md | 3 +- docs/verification-status.md | 2 +- docs/方案设计.md | 20 +-- docs/需求理解.md | 20 +-- 14 files changed, 109 insertions(+), 155 deletions(-) diff --git a/docs/README.md b/docs/README.md index c724e17..9f14590 100644 --- a/docs/README.md +++ b/docs/README.md @@ -25,7 +25,7 @@ - [ADR-0001](./adr/0001-p2p-first-media-topology.md): historical P2P-first baseline; later access and automatic-routing decisions supersede its stale current-state details. - [ADR-0002](./adr/0002-memory-resident-protected-rooms.md): four-digit leased in-memory rooms, local Host defaults, scoped Viewer grants, and orthogonal code entry. - [ADR-0004](./adr/0004-peer-assisted-media-experiment.md): historical standard-WebRTC peer-assisted experiment and reusable evidence. -- [ADR-0005](./adr/0005-automatic-hybrid-media-routing.md): accepted and deployed v10 P2P-first single-reconcile route model; remaining real-network validation is tracked in verification status. +- [ADR-0005](./adr/0005-automatic-hybrid-media-routing.md): accepted and deployed P2P-first single-reconcile route model; remaining real-network validation is tracked in verification status. - [ADR-0006](./adr/0006-fixed-high-native-sender-canary.md): proposed fixed-`HIGH` native canary, current no-go result, and staged revalidation boundary. - [ADR-0007](./adr/0007-demand-driven-dual-representation-quality.md): accepted path-isolated `HIGH + at most one LOW` policy, optional idle-layer stop, evidence contract, and SVC boundary. - [ADR-0008](./adr/0008-window-scoped-audio-capture.md): browser window-audio hint and the Windows WASAPI process-loopback boundary. diff --git a/docs/adr/0005-automatic-hybrid-media-routing.md b/docs/adr/0005-automatic-hybrid-media-routing.md index 9b9b187..603d16c 100644 --- a/docs/adr/0005-automatic-hybrid-media-routing.md +++ b/docs/adr/0005-automatic-hybrid-media-routing.md @@ -272,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 accepted v10 Viewer wire includes one strict, revision-fenced `route-status` +The accepted 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: diff --git a/docs/adr/0007-demand-driven-dual-representation-quality.md b/docs/adr/0007-demand-driven-dual-representation-quality.md index a5d52b5..71e6920 100644 --- a/docs/adr/0007-demand-driven-dual-representation-quality.md +++ b/docs/adr/0007-demand-driven-dual-representation-quality.md @@ -139,8 +139,8 @@ results do not create an application media-layer selector or route authority: spatial-quality ceiling. Server 1.13.5 derives requested spatial/temporal maxima from quality, dimensions and FPS, but actual selection is codec-specific: VP8 has temporal selection, while H.264/H.265 simulcast is - spatial-only. The current H.264 candidate can adapt each SFU downtrack - between `q,h` spatial representations and recover it independently. Server + spatial-only. The fixed VP8 publication can adapt each SFU downtrack between + `q,h` spatial representations and recover it independently. Server Dynacast takes the maximum requested quality and enables every quality at or below it, so any `HIGH` root keeps `LOW` active. That cumulative behavior prevents dynamic `LOW` stop while `HIGH` is subscribed, but dynamic stop is diff --git a/docs/adr/0008-window-scoped-audio-capture.md b/docs/adr/0008-window-scoped-audio-capture.md index 1baf865..4b57fd9 100644 --- a/docs/adr/0008-window-scoped-audio-capture.md +++ b/docs/adr/0008-window-scoped-audio-capture.md @@ -33,7 +33,7 @@ Windows 11 desktop (build 22000 or newer) and is not a Windows 10 fallback. this does not force a codec, bitrate, channel count, or processing mode. No Web Audio mixer, SDP rewrite, or app-owned audio bitrate knob is introduced. 3. The native sender has an explicit Windows 11 local window target. Browser - VP8/H.264 may use that target only for `window-process-audio`; the separate + VP8 may use that target only for `window-process-audio`; the separate `native-window-h264` source uses the same bound HWND/PID/creation time for WGC/DXGI video and WASAPI process-tree audio. Process IDs, HWNDs, creation tokens, titles, paths, device identity, PCM, and local hardware status stay @@ -130,9 +130,9 @@ The helper multiplexes process-tree PCM, H.264 access units, and bounded local status. Go writes H.264 directly to the existing shared fanout; the browser UI receives only a one-way local copy for `VideoDecoder` preview. PCM retains the existing WebCodecs Opus bridge. PLI/FIR recovery is generation-bound and -coalesced into a helper key-frame command. Normal browser VP8/H.264 sources -remain explicit alternatives, with VP8 as the default; neither is an implicit -runtime downgrade. +coalesced into a helper key-frame command. The normal browser source uses the +fixed VP8 contract; the native H.264 source remains an explicit mode and is not +an implicit runtime downgrade. ## Consequences diff --git a/docs/project-memory.md b/docs/project-memory.md index 9812126..782f1a1 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-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 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 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. -- Browser sharing defaults to VP8. The advanced `automatic | H.264 | VP8` selector is temporary pre-share diagnostic input: 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`. Controlled H.264 diagnosis and physical direct, relay, and SFU evidence choose one final fixed Browser codec; the final product exposes only that codec. +- The accepted Browser codec is fixed VP8 with no codec/profile control or quality-state field. Current Windows evidence found Chrome's H.264 performance depended on an OS-selected AMD versus NVIDIA Media Foundation encoder that a website cannot choose; profile ordering did not repair it, and the dated production A/B recovered after forced H.264 was removed. Actual codec/profile/encoder stats remain diagnostic only. Current v10 source and production retain the temporary selector until the atomic v11 implementation and deployment replace it. - 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. @@ -39,7 +39,7 @@ Last updated: 2026-08-24 ## Current Priority -1. Use a physical direct comparison to select the fixed Browser codec; validate relay and SFU only if H.264 is selected. +1. Implement the accepted fixed-VP8 Browser v11 contract, delete the temporary codec selector and wire field, and deploy it atomically; no H.264 relay or SFU validation is required. 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 20c855f..aad6797 100644 --- a/docs/research/browser-background-capture.md +++ b/docs/research/browser-background-capture.md @@ -68,7 +68,7 @@ physical background-capture evidence. be inferred as an equal media-rate drop. - `src/client/lib/diagnostic-export.ts` exports only the latest bounded sample. It is a useful cross-check, not a historical trace. -- V9's short-lived pending-candidate decoded-frame observer reads cumulative RTP +- The short-lived pending-candidate decoded-frame observer reads cumulative RTP progress only for the exact pending route. It neither drives capture nor proves document activity, and it stops when that operation settles. - `scripts/peer-assisted-benchmark.ts` is not evidence for this issue: its @@ -167,8 +167,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 for the chosen pre-share preference; -do not switch codec inside a run. +actual initial negotiated VP8 codec and encoder; do not switch codec inside a +run. For each exact overlapping window, correlate: @@ -193,10 +193,7 @@ Change one variable per fresh run: page state stays fixed. 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 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 +3. 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/native-h264-opt-in-path.md b/docs/research/native-h264-opt-in-path.md index 3430ea1..1d809ad 100644 --- a/docs/research/native-h264-opt-in-path.md +++ b/docs/research/native-h264-opt-in-path.md @@ -2,9 +2,9 @@ ## 2026-08-21 Native Window Source -The Native sender has two explicit local video sources. The Web source now -prefers H.264 through standard negotiation and keeps the browser codec fallback -list; the separate `native-window-h264` source is Windows 11-only and requires +The Native sender has two explicit local video sources. The Web source uses the +fixed Browser VP8 contract; the separate `native-window-h264` source is Windows +11-only and requires the user to select one local opaque window target. It never silently replaces a browser source or falls back to software encode, another codec, monitor capture, or system audio. @@ -34,8 +34,8 @@ local WebCodecs Opus bridge. PLI/FIR and fanout recovery requests enter the generation-bound helper stdin as one coalesced `K` command. The helper forces an in-band SPS/PPS/IDR recovery unit, while the existing Pion `H264Payloader`, exact `42c01f` MediaEngine, one -shared track, and two-edge cap remain unchanged. Browser VP8/H.264 use their -existing `getDisplayMedia` and `VideoEncoder` path. +shared track, and two-edge cap remain unchanged. Browser VP8 uses the existing +`getDisplayMedia` and `VideoEncoder` path. Both the product helper and offline fixture compile the same MF encoder source. The product build and the fixture build pass MSVC `/W4 /WX`; focused Go tests @@ -101,17 +101,17 @@ codec benchmark. ## Hardware Boundary The Media Foundation/NVIDIA fixture established the physical hardware gate and -the exact `42c01f` in-band recovery contract. Browser H.264 still has only a -hardware preference. The native-window source instead enumerates only an -adapter-LUID-bound hardware MFT, requires D3D11 awareness and exact codec -readback, and has no software retry. The Native acceptance run above adds the +the exact `42c01f` in-band recovery contract. Browser H.264 cannot select a +specific adapter or Media Foundation encoder. The native-window source instead +enumerates only an adapter-LUID-bound hardware MFT, requires D3D11 awareness and +exact codec readback, and has no software retry. The Native acceptance run above adds the required product-process `VideoEncode` attribution; fixture evidence remains a separate encoder-contract measurement. ## Scope -- The Web source prefers H.264 through standard negotiation; production rollout - remains tracked separately from this Native research path. +- The Web source uses fixed VP8; production rollout remains tracked separately + from this Native research path. - No codec matrix, benchmark, second viewer, or endurance run is required for this landing slice. - `npm run probe:native-one-viewer` remains optional Native research and does diff --git a/docs/research/realtime-quality-adaptation.md b/docs/research/realtime-quality-adaptation.md index e1f2d20..f925ec3 100644 --- a/docs/research/realtime-quality-adaptation.md +++ b/docs/research/realtime-quality-adaptation.md @@ -1,6 +1,6 @@ # Realtime Screen-Share Quality Adaptation -- Research date: 2026-08-22 +- Research date: 2026-08-24 - Scope: realtime game screen sharing in the browser - Status: implementation input; real-device quality remains unverified @@ -187,8 +187,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 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 +downlayering: the current deployed v10 Browser defaults to VP8 but still lets the +Host select 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 @@ -224,102 +224,60 @@ wire, server telemetry, global score, selector or new UI framework. Production deploys this v10 publisher view; its target-browser fields and values still need physical evidence. -## Codec Preference And Evidence Boundary +## Fixed Browser Codec Decision -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 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. `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. +The Browser product uses one fixed VP8 path and exposes no codec or profile +control. Direct and browser-relay offers put VP8 first while retaining its +browser-advertised repair codecs; the SFU publication explicitly requests VP8 +with `backupCodec=false`. Actual codec/profile and encoder fields remain +diagnostic results. They do not authorize codec switching, route changes, or a +second encoder policy. -Changing `RTCRtpTransceiver.setCodecPreferences()` affects later negotiation; it -does not itself switch an established sender, and `replaceTrack()` only replaces -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. +The decision combines dated production behavior with an exact target-machine +probe. Production first forced H.264 through standard codec ordering and a +single LiveKit H.264 publication; removing that preference restored the reported +frame rate. On 2026-08-24, fresh Chrome/Edge 151 sessions then used real +`getDisplayMedia()` tab capture, `1920x1080@30`, a 5 Mbps ceiling, `balanced`, +`motion`, ten seconds of warm-up, and a fifteen-second wall-clock sample: -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. +| Browser / requested format | Actual encoder | Encoded FPS | Encode time/frame | End resolution | Limitation | +| --- | --- | ---: | ---: | --- | --- | +| Chrome VP8 | libvpx, software | 15.00 | 2.69 ms | 402x214 | `bandwidth` | +| Chrome H.264 Baseline `42001f` | AMD Media Foundation, hardware | 11.87 | 12.10 ms | 1920x1024 | `none` | +| Chrome H.264 CBP `42e01f` | OpenH264, software | 15.00 | 1.33 ms | 402x214 | `bandwidth` | +| Chrome H.264 Main `4d001f` | AMD Media Foundation, hardware | 11.86 | 12.61 ms | 1920x1024 | `none` | +| Chrome H.264 High, negotiated `64001f` | AMD Media Foundation, hardware | 12.60 | 12.53 ms | 1920x1024 | `none` | +| Edge H.264 Baseline `42001f` | NVIDIA Media Foundation, hardware | 28.98 | 5.67 ms | 1396x732 | `bandwidth` | -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. A fresh-share physical direct -comparison chooses the fixed Browser codec; relay and SFU validate H.264 only -when that comparison selects it. +The synthetic content, headless capture, loopback path, and BWE-driven resolution +changes prevent a physical-game image-quality or intrinsic codec-efficiency +ranking. They do establish the first bottleneck: Windows had an explicit +minimum-power preference for Chrome (`GpuPreference=1`), Chrome selected the AMD +H.264 MFT, and Baseline/Main/High all stayed near 12 fps. Edge had no explicit +preference, selected the NVIDIA MFT, and approached 29 fps. Chrome's +`--force-high-performance-gpu` flag still selected the AMD MFT and produced +12.66 fps, so that ANGLE/EGL switch does not override Media Foundation selection. -Current Chromium source makes the negotiated H.264 format a material Windows -runtime input. `kPlatformH264CbpEncoding` is disabled by default on Windows; -the Media Foundation path skips the NVIDIA MFT for constrained-baseline H.264, -and the WebRTC hardware encoder returns software fallback for unsupported -formats such as odd dimensions. Ordinary H.264 simulcast streams are initialized -as separate encoder streams. The target run must therefore join each outbound -SSRC/RID to its exact `RTCCodecStats.sdpFmtpLine` and record the exposed encoder -implementation, power-efficiency flag, interval encode cost, and process GPU -video-encode activity. These fields distinguish format and encoder paths; their -presence alone does not establish the Screener root cause. +Chromium 151 enumerates Windows Media Foundation encoders in adapter/MFT order +and activates the first compatible result. Its preferred adapter LUID is used to +handle cross-GPU resources, not to let a website select a vendor. The WebRTC API +can reorder codec/profile formats but has no API to select NVIDIA, AMD, or a GPU +adapter. Windows Graphics preference, disabling an iGPU, or launching a whole +browser differently are host diagnostics, not a Screener product mechanism. +See Chromium's [MFT enumeration](https://chromium.googlesource.com/chromium/src/+/refs/tags/151.0.7922.174/media/gpu/windows/mf_video_encoder_util.cc#347), +[encoder activation](https://chromium.googlesource.com/chromium/src/+/refs/tags/151.0.7922.174/media/gpu/windows/media_foundation_video_encode_accelerator_win.cc#1422), +and [GPU switch scope](https://chromium.googlesource.com/chromium/src/+/refs/tags/151.0.7922.174/gpu/ipc/service/gpu_init.cc#291), plus the +[WebRTC Stats](https://www.w3.org/TR/webrtc-stats/) and +[Media Capabilities](https://www.w3.org/TR/media-capabilities/) specifications. -Pinned LiveKit server `1.13.5` gives VP8 a temporal layer selector and gives -H.264/H.265 the generic simulcast spatial selector. H.264 SFU evidence therefore -starts with every Host publication RID and the Viewer-selected dimensions/FPS: -layer changes are spatial, while same-dimension FPS changes remain attributable -to publisher, transport, or Viewer decode evidence. - -The repository diagnostic is a mechanical single-machine preflight, not a -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, -H.264, and VP8 and rejects an actual-codec mismatch. Reports retain only -allowlisted failure stages and sanitized identifiers. - -Quality diagnosis uses a headed matrix with the same real game, Browser build, -driver/GPU, capture surface, `1080p30 / 5 Mbps / balanced`, foreground state, -and one external wired direct Viewer. Fresh H.264, VP8, and H.264 confirmation -shares each run for 60 to 90 seconds. Capture A, sender B, and Viewer C samples -share the current generation and overlapping windows; identity polling does not -advance their accumulators. Each window records actual codec/profile, -configured/source/send/receive FPS, interval encoded frames/encode time, -limitation reason, game FPS, and process-scoped CPU/GPU evidence. Capture input, -sender output, transport limits, and Viewer decode evidence classify the first -bottleneck. A direct H.264 result selected by that matrix is then checked once -through browser relay and once through SFU. Missing implementation or -power-efficiency fields remain unknown. - -Open-source distribution is not itself a patent-license exemption. This Web -change only requests a codec already implemented by the browser/LiveKit path and -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 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. +SDP level rewriting is not a repair: Chromium matches H.264 hardware formats by +profile and packetization mode and lets the encoder choose the output level from +the real dimensions and rate. Reordering High only changes the requested profile +and still selected the same AMD MFT. CBP deliberately fell back to OpenH264 and +lost resolution under the same budget. Pinned LiveKit 1.13.5/Pion 4.2.17 also +preserves Chrome's H.264 offer order, whose first compatible entry is Baseline +`42001f/mode1`, not CBP. No profile special case, SDP munging, GPU selector, +codec controller, or H.264 relay/SFU follow-up enters the Browser product. 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, @@ -748,7 +706,7 @@ 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. -- The accepted `screener-v10` Share advanced settings panel accepts +- The current deployed `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 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, @@ -796,9 +754,8 @@ 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 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 runtime or automatic codec switching. VP8 is the one Browser product codec; + there is no codec selector or H.264 product fallback. - 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 9a9eebf..0d0e397 100644 --- a/docs/session-handoff.md +++ b/docs/session-handoff.md @@ -44,11 +44,10 @@ Current high-signal boundary: loss, local Host preference replay, an independent expiring Viewer grant, and open/password/disabled code entry. Neither source nor production uses SQLite; exact rollback artifacts are owned by deployment. -- Browser sharing defaults to VP8, keeps the diagnostic codec selector pre-share - only, fixes codec preference for the share lifetime, and uses - `shareGeneration`-fenced Pause/Resume. Exact source gates and production - artifacts are owned by verification status and deployment; physical H.264, - heterogeneous-network, and SFU media evidence remains open. +- The accepted Browser v11 contract fixes VP8 and removes the diagnostic codec + selector and quality-wire field. Current source and production remain v10 + until that client/server change is integrated and deployed atomically; + heterogeneous-network and SFU media evidence remains open. - Ordinary browser ICE owns direct reachability. Do not build port prediction, guessed candidates, NAT classification, TCP probes, quality scores, or quality-driven reparenting. Current-path quality is diagnostic; a healthy diff --git a/docs/status.md b/docs/status.md index b057661..9001298 100644 --- a/docs/status.md +++ b/docs/status.md @@ -26,7 +26,7 @@ This is the current execution index. Git history owns completed timelines; [veri ## Current Milestone -1. Use a physical direct comparison to choose the fixed Browser codec; validate relay and SFU only if H.264 is selected. +1. Implement and atomically deploy the accepted fixed-VP8 Browser v11 contract, removing the temporary codec selector and quality-wire field. Current source and production remain v10 until that cutover; no H.264 relay or SFU validation is scheduled. 2. Diagnose the reported screen-audio `1 kbps` readout and validate live audio ceilings, then refine the Host invitation controls without changing grant semantics implicitly. 3. Reproduce the Host background/minimized report under controlled conditions while recording the actual negotiated codec. 4. Finish representative ICE/STUN/SFU acceptance including mobile networks. Browser port prediction, NAT classification, TCP probing, fake page keepalive, and quality-driven reparenting remain outside the accepted model. @@ -42,6 +42,6 @@ This is the current execution index. Git history owns completed timelines; [veri ## Current Hold There is no active P0/P1 source or deployment hold. Current source and production run the postflight-clean -Browser v10 contract. Physical H.264 quality, audio, heterogeneous-network, SFU, +Browser v10 contract while the accepted fixed-VP8 v11 change is pending. 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 2edb94b..6eb75a4 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -6,7 +6,7 @@ Only items in **Now** are executable after their stated decision gate. A branch ## Now -1. **Classify H.264 and select one fixed Browser codec.** Reproduce the dated blur/performance report on the same Host, browser, driver, game scene, and wired direct Viewer while changing one variable at a time. Choose VP8 or H.264 from fresh direct-share codec/profile/encoder and capture/sender/Viewer evidence, and make that codec the only Browser product choice. Validate browser relay and SFU only if the direct comparison selects H.264. +1. **Ship the fixed-VP8 Browser contract.** Remove the temporary `automatic | H.264 | VP8` UI, quality-state/wire field, and share-lifetime codec branches; use VP8 for direct, browser-relay, and SFU senders, bump the private Browser wire once, and deploy client/server atomically. Keep actual codec/profile/encoder stats as diagnostics. Do not add GPU selection, SDP munging, profile fallback, or H.264 relay/SFU work. 2. **Diagnose and physically validate live screen audio.** First reproduce the Host diagnostic remaining at `1 kbps` after switching the shared source, and distinguish stale/reset statistics from an actual RTP bitrate problem. The app already requests audio and supplies system/window audio hints; record the selected surface, returned audio track, sender/receiver counters, and audible result because the browser owns the picker and may still return video only. Then switch the 64/128/256 kbps ceilings across direct, browser-relay, and SFU paths, create a new edge, and switch source while checking route continuity and A/V synchronization. Do not infer fidelity or room-wide atomic convergence from configuration alone. 3. **Redesign the Host invitation controls.** Replace the current raw-link-heavy block with a compact copy-first layout while preserving the accepted copy-link, copy-room-code, code-entry policy, rotate, and revoke actions. Keep the credential out of logs and request URLs; changing the copied credential length is blocked on the explicit product decision below. 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 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. @@ -32,6 +32,7 @@ These items are real product work, ordered after the current physical-media, rea | #181 selected-pair response counter | Keep as local observation or remove; it has no route authority. | | #182 signaling watchdog | Confirm Host and mobile/background session semantics before calling it complete. | | Viewer invitation credential length | The current grant is about 62 characters because its 43-character random suffix is the shortest base64url encoding of the accepted 256-bit secret. Decide whether a shorter copied URL justifies changing that security requirement or adding server-side indirection; do not truncate it ad hoc. | +| Viewer invitation lifetime | Align a grant with the exact in-memory room incarnation instead of an independent seven-day clock: it remains usable while that room exists and ends on rotate, revoke, process restart, or room reclamation. This deferred room change needs its own truth checkpoint before implementation. | | Project license and closed-commercial boundary | Decide the repository and distribution license before any public release or package distribution. GPL/AGPL implementations remain research-only until then. | These decisions are not permission to continue old branches, and current deployment remains unchanged until a scoped decision is made. diff --git a/docs/verification-status.md b/docs/verification-status.md index c199a04..584656c 100644 --- a/docs/verification-status.md +++ b/docs/verification-status.md @@ -23,7 +23,7 @@ Capacity and routing entries below describe the exact tested source or release o - Chrome 151 synthetic `1/3/5/8` and 720p30 runs preserved bounded fanout and decoding. A `9461e20`-era three-Viewer rerun kept Host2/Browser Viewer1, decoded every Viewer, and recovered a hard first-level relay departure in 5,380 ms; it did not exercise quality-triggered MBB. Sixteen-Viewer cap2/cap3 loopbacks decoded all Viewers and a ten-second follow-up recorded guarded sender and aggregate Chromium CPU evidence, but resolution/FPS remained low and the ordinary-PC data cannot rank caps, select product policy, or prove sustainable heterogeneous-network quality. See [peer-assisted media](./research/peer-assisted-media.md). - Chrome 151 with local LiveKit 1.13.5/client 2.22.0 proved one SFU/UDP root after `q,f` -> `q,h`: decoded frames advanced 3 -> 23, rendered frames reached 26, Host used one edge, and leaves were clean. This is functional evidence only. See [low-server media routes](./research/low-server-media-routes.md). - Host A+B and authenticated P2P Viewer C evidence is sanitized, generation-bound, diagnostic-only, and closed for stale or ambiguous reports. Production can expose a Host-local current SFU `h` snapshot and clears it on publisher identity changes; raw media metadata is not retained. See [realtime quality adaptation](./research/realtime-quality-adaptation.md). -- Chrome H.264 loopback proves negotiation and decode interoperability, not encoder performance or hardware attribution. Native WGC/MF has a one-Viewer Windows proof with 203 rendered 1280x720 frames, 500 Opus packets, and nonzero process/LUID-correlated `VideoEncode`. See [Native H.264](./research/native-h264-hardware-decision.md) and [Native shared encode](./research/native-shared-encode-sender.md). +- A fresh Chrome/Edge 151 tab-capture probe identified the current Browser H.264 boundary: Chrome, configured by Windows for minimum-power graphics, used `MediaFoundation AMDh264Encoder` for Baseline/Main/High and sustained about 12 fps; the same-machine Edge Baseline used the NVIDIA MFT and sustained about 29 fps. The Chromium high-performance GPU flag did not change Chrome's MF encoder. This is strong encoder-selection and wall-clock throughput evidence, not a physical-game image-quality ranking. The Web platform cannot select the adapter/MFT, so the accepted Browser product codec is fixed VP8 and H.264 relay/SFU validation is not scheduled. Native WGC/MF remains a separate later path with one-Viewer hardware evidence. See [realtime quality adaptation](./research/realtime-quality-adaptation.md), [Native H.264](./research/native-h264-hardware-decision.md), and [Native shared encode](./research/native-shared-encode-sender.md). - Native Windows 11 process audio is source-only and default-off. A retained run isolated the target by about 4018x and delivered 495 Opus packets to one Viewer; package download, game sync, Windows 10, and other routes remain open. See [ADR-0008](./adr/0008-window-scoped-audio-capture.md). ## Open Proof Boundaries diff --git a/docs/方案设计.md b/docs/方案设计.md index dd29e33..bce52da 100644 --- a/docs/方案设计.md +++ b/docs/方案设计.md @@ -2,7 +2,7 @@ ## 2026-08-18 14:45 -状态:Current source 与 production 使用单一 `screener-v10` Browser wire,默认 VP8,codec selector 仅在分享前用于诊断且结果在 share lifetime 内固定;Pause/Resume 由当前 Host session 和 `shareGeneration` 约束。精确 deployed behavior 只由 deployment 文档记录。 +状态:已接受的下一 Browser 合同固定 VP8 并移除 codec selector/wire 字段,以单一 `screener-v11` 原子替换 v10;Current source 与 production 在该实现合并和部署前仍是 `screener-v10`。Pause/Resume 继续由当前 Host session 和 `shareGeneration` 约束。精确 deployed behavior 只由 deployment 文档记录。 ## 目标与边界 @@ -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。 -下表描述 accepted Browser 的单一 `screener-v10` wire;server 与 Browser assets 必须作为一个完整合同原子发布,v9 和更早客户端在取得房间 authority 前拒绝,不存在旧 parser、writer、translator 或中间协议子集: +下表描述 accepted Browser 的单一 `screener-v11` wire;server 与 Browser assets 必须作为一个完整合同原子发布,v10 和更早客户端在取得房间 authority 前拒绝,不存在旧 parser、writer、translator 或中间协议子集: | 方向 | 消息 | 作用 | | --- | --- | --- | @@ -128,7 +128,7 @@ WebSocket 使用无 query/fragment 的同源 `/signal`。upgrade 只记录 site- Zod 在服务边界拒绝未知类型、超长字符串、非法 SDP/candidate 结构和越权目标。服务端根据已认证会话决定路由:观看者不能给另一名观看者发信令,分享者也不能向本房间以外的 peerId 发送。 -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`。 +accepted 的唯一 wire 是 `screener-v11`,不做版本协商、v10 parser、双写或 translator。任何 v10 或更早页面及 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,17 +156,17 @@ accepted 的唯一 wire 是 `screener-v10`,不做版本协商、v9 parser、 | 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"` 与 `VP8`,并允许显式选择 clarity-first 或 fluid。“分享高级设置”在开始分享前提供 `自动 | H.264 | VP8` 作为 H.264 对照测试入口,并提供离散的 `64 kbps 省流 | 128 kbps 音乐默认 | 256 kbps 极高音质` 屏幕音频 sender ceiling。音频 ceiling 可在直播中串行更新;视频 codec 在分享开始后固定,直播和暂停期间均禁用 selector,改变 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"`,并允许显式选择 clarity-first 或 fluid。“分享高级设置”不提供 codec 控件,只提供离散的 `64 kbps 省流 | 128 kbps 音乐默认 | 256 kbps 极高音质` 屏幕音频 sender ceiling。音频 ceiling 可在直播中串行更新。这些设置只是 ceiling 和浏览器偏好,不保证实际分辨率、帧率、码率或声道。 每次 `setParameters()` 都必须从当次 `getParameters()` 派生更新,然后读回 requested/applied `maxBitrate`、`maxFramerate`、`scaleResolutionDownBy` 和 degradation preference。浏览器负责具体降级,实际结果由 stats 观测;拒绝或浏览器改写必须进入可见状态,不能只写 `console.warn`。 -Web 视频端当前默认 `VP8`。诊断期内,`automatic` 以空 `setCodecPreferences([])` 使用 P2P 浏览器和 LiveKit 默认,显式 H.264/VP8 在本轮分享的首个 offer/publication 前应用;P2P 与 browser relay 保留浏览器公布的完整 codec/repair 列表,SFU 使用 `backupCodec: false` 的单一 publication codec。每次分享的 codec preference 保持固定,普通 Pause/Resume 只以当前 Host session 和 `shareGeneration` 更新 paused 状态并切换同一批 capture tracks。实际 outbound codec/profile、encoder implementation 与 stats 是结果真相。物理 direct 对照选择一个固定 Browser codec,browser relay 与 SFU 随后验证该选择,最终产品静态配置这一 codec。Viewer answer 的 Opus stereo receive preference 是当前 Web SDP 变换边界。 +Web 视频端固定使用 `VP8`。P2P 与 browser relay 在首个 offer 前只保留 VP8 及其浏览器公布的 repair codecs,SFU publication 显式使用 VP8 并保持 `backupCodec: false`;无法采用 VP8 时明确失败,不静默协商另一种产品 codec。质量设置、房间 state、Pause/Resume、换源和 sender mutation 都不保存或改变 codec。实际 outbound codec/profile、encoder implementation 与 stats 仍是只读结果真相。Viewer answer 的 Opus stereo receive preference 是当前唯一应用 SDP 变换边界。 Web display-audio 请求显式关闭 `echoCancellation`、`noiseSuppression`、`autoGainControl` 和 `voiceIsolation`,并以 `channelCount: { ideal: 2 }` 偏好双声道;这是当前 Chrome/Edge Host 基线的实现契约,用于避开网页 `getDisplayMedia()` 默认的语音处理和单声道 APM 候选,不宣称跨浏览器保证,也不固定采样率。所有值都是非强制 post-selection preference;不使用 `exact`、`min` 或 `advanced`。若 `getAudioTracks()` 为空,分享者界面必须显示“当前来源没有可共享音频”。Web 端同时传入 `windowAudio: "window"` 与 `systemAudio: "include"`:窗口选项请求该窗口/应用音频,整个屏幕选项允许用户勾选系统音频;标签页使用浏览器自身的标签页音频选择。浏览器可以忽略这些约束或 picker hints,且没有对应的标准来源类型读回,因此 UI 不额外推断或标注来源。系统音频由用户显式选择,可能包含语音或通知;video-only 始终可用。 当前 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 的默认 VP8 与开始分享前可选的 Automatic/H.264/VP8 是独立标准 WebRTC 路径。一名普通 Chrome Viewer 已 decoded/rendered 203 帧并收到 500 Opus packets;同一 helper PID/adapter 的 `VideoEncode` 与 LUID 匹配。 +同一 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。一名普通 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 的计数器混算。 @@ -174,7 +174,7 @@ Web display-audio 请求显式关闭 `echoCancellation`、`noiseSuppression`、` 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。 -H.264 对照固定同一 Host、Chrome/Edge 版本、驱动、游戏场景、`1080p30 / 5 Mbps / balanced` 和单个有线 direct Viewer,并为 H.264、VP8 与复核用 H.264 分别创建全新 Host/share/PeerConnection 会话。质量判断只使用同一 generation 且时间重叠的 capture A、sender B、Viewer C 窗口,并同步记录游戏 FPS 与 Host 浏览器进程 CPU/GPU;stats identity poll 不推进采样 accumulator,可选 encoder 字段缺失保持 unknown。Capture、sender 与 Viewer 的实际 FPS/码率、codec/profile、encode/decode cost、`qualityLimitationReason`、丢帧、冻结和传输指标共同归类采集、编码、带宽或解码瓶颈。 +固定 VP8 的依据由研究文档持有:同机 Chrome 的 Windows minimum-power GPU preference 选择 AMD H.264 MFT并稳定低帧,Edge 选择 NVIDIA MFT 后恢复,而网页没有选择 GPU adapter/MFT 的标准 API;H.264 profile 重排和 Chromium high-performance GPU flag 都不能修复 Chrome 的 MF encoder 选择。该结论不把合成 loopback 当作游戏画质排名,而是拒绝一个依赖宿主系统配置、应用无法约束的固定 Browser codec。实际 codec 与 encoder 字段继续用于诊断,不触发 codec 切换。 `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。 -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。 +accepted `screener-v11` 在 authenticated Host 点击下载诊断时发送一次 `request-route-diagnostic`;服务端当场返回严格 Host-only 的 `route-diagnostic-snapshot`,不连续推送。每个 current child 只保留 latest demand timestamp、operation/candidate start、first-frame/final outcome;新的 route demand 覆盖该 child 的旧 record,confirmed departure 与 room delete 立即删除,且不增加 timer、事件环或第二张 graph。`queueWaitMs` 只从同一个 current demand 到其 room-serial operation start计算;没有权威 demand timestamp 就是 `null`。Serializer 为本次导出分配 snapshot-local opaque ordinal,用它表达 current parent/SFU 关系、effective capacity、child count、current operation reason/stage/cursor/candidate count与相对 timing;`finalRoute` 只允许 `direct | sfu | waiting | failed`,`rejectionBucket` 只允许 `none | stale | endpoint-capacity | sfu-admission | candidate-failed | first-frame-timeout | operation-deadline | aborted`。Ordinal 不是身份或稳定 ID。Snapshot 不包含 room code、display name、真实 peer/session/connection/generation、地址、URL、SDP、token、raw error或 planner 曾过滤的 parent,不形成路由输入或前台第二套状态。Viewer 的 schema v2 只携带自身 stage、粗粒度 revision、typed failure、signal/Host state、route kind 与 current-frame proof,即使 connection list为空也可下载;未鉴权 `ROOM_ACCESS_DENIED` 分支只含 stage/code,不含 Host、route、revision或timing。 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。 @@ -360,7 +360,7 @@ Token-free SFU standby prewarm 保持可选且不创建 participant、publicatio ## 当前交付边界 - Current source 与 production 使用单一 `screener-v10` wire,默认 VP8,codec selector 只在分享前可用且对本次分享固定,Pause/Resume 由当前 Host session 和 `shareGeneration` 约束。 -- 物理 direct 证据选定最终固定 Browser codec;若选择 H.264,再由 browser relay 与 SFU 验证。Host 后台性能按受控根因证据诊断;标准 ICE/SFU 按真实异构网络验收。 +- 已接受的 Browser v11 固定 VP8 并删除 codec selector/wire 字段;H.264 不进入 browser relay 或 SFU 验证。Host 后台性能按受控根因证据诊断;标准 ICE/SFU 按真实异构网络验收。 - 当前发布面只包含 Web Host、Viewer 和 browser relay。Native sender、capture helper、shared-encode executable 与两个分发包仍由各自后续决策和物理证据驱动,不属于当前 Browser release。 任何一步只实现当前消费者需要的接口。不得为未来账号、第三种媒体拓扑或原生 sender 预建抽象层。 @@ -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-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都不得签发。 +所有非服务器端点共享一个 server-authoritative steady outbound media-copy cap `C`:唯一配置 `ENDPOINT_MEDIA_COPY_CAPACITY` 默认 `2`,只接受 `1`、`2` 或 `3`,不兼容旧键。服务端将 endpoint 当前广告的 `0..C` 可用值 clamp 为唯一 `effectiveDownstreamCapacity`;该值只表示当前可用 sender slots,不建立角色、浏览器、UA、设备或页面可见性 tier。peer child 或 Host publication 各占一份,upstream receive 免费;SFU subscriber egress 使用独立 server admission。Browser `screener-v11` 的 `authenticated.endpointMediaCopyCapacity` 把部署 `C` 交给 Host 与 Viewer;两端用该值校验稳态 assignment,并让 active+pending 的物理 copy 只在 controller 已授权 overlap 时达到 `min(C + 1, 3)`。代码通过一个 shared accounting guard 执行,不重复 magic number;v10 及更早 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 c92a7d8..bcd3d90 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`(平衡)与 `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、综合评分或周期性全房间调档。 +- 质量控制:推荐档位始终只有 1080p60(8 Mbps)、1080p30(5 Mbps)、720p30(3 Mbps)三档,默认选择中档 `1080p30`。高级设置的分辨率选项增加 `480p`(854x480),其 FPS 与码率仍由高级控件独立选择;不增加 `480p` 推荐档、preset ID 或固定的 480p FPS/码率组合。三档与高级设置初始值统一采用 `balanced`(平衡),`maintain-resolution`(清晰优先)、`maintain-framerate`(流畅优先)保留为显式选择。Browser 视频固定使用 VP8,不提供 codec 选择、切换或 profile 控件。分享期间可直接换视频档位和 64/128/256 kbps 音频 sender ceiling,不重新选择来源或重建健康连接。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,不能遮蔽离线或有界失败。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 套到电影或游戏音频。 +- 观看端连接反馈: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-v11` 服务端补充消息是 strict、current-revision-fenced `route-status` union,且只允许 `waiting/sfu-admission` 与 `failed/route-exhausted` 两种配对;它不携带 raw text、身份、candidate 或 retry hint,prepare/active、access、Host、signal、media 与 autoplay 继续由各自权威输入派生。 +- 质量控制扩展:推荐档位仍是默认入口且只保留 1080p60/1080p30/720p30 三档,当前页面初始 desired state 选择 1080p30 且不写 `localStorage`;“分享高级设置”允许 480p(854x480)/720p/1080p/1440p 分辨率 ceiling、15 至 60 的整数 FPS ceiling、2 至 12 Mbps 视频码率 ceiling、清晰/平衡/流畅三种偏好,以及离散的 `64 kbps 省流 | 128 kbps 音乐默认 | 256 kbps 极高音质` 屏幕音频 sender ceiling;480p 不形成推荐档或固定 FPS/码率组合,初始值采用 `balanced` 与 128 kbps。视频档位和音频档位可在直播中显式应用。所有当前及以后建立的 Web Host、Browser relay 与 SFU sender 都读取最新 desired 档位,并各自在 fresh sender parameters 上应用和读回 endpoint-local applied 值;实际码率仍由浏览器拥塞控制和 stats 决定。Peer 只在唯一明确的 active Opus audio 上做结构化、幂等的 Viewer answer `stereo=1;maxaveragebitrate=256000` 变换,使同一接收上限覆盖三档;非目标形态或 parser/writer 异常必须原样保留浏览器 answer,不得阻断可继续的 mono/video edge。不得用宽泛字符串/正则改 SDP,也不得改写浏览器协商的 FEC。SFU 将三档映射到 pinned SDK 的 music stereo preset 或显式 256 kbps ceiling,统一保留 `forceStereo = true`、`dtx = false` 与 pinned room default `red = true`。不得暴露任意码率、声道、采样率、audio codec、stereo、DTX、RED 或 FEC 控件;256 kbps 只是显式较高上限,不是无损、高保真或最低码率保证。实际质量持续低于所选上限时必须显示可解释的限制原因;在真实拥塞、编码器和接收端证据完成前,不把任一组合称为“完美平衡”。未来 microphone voice 若进入范围,必须是独立 track/处理链,不能把 AEC/降噪/DTX 套到电影或游戏音频。 - SFU 质量设置生命周期:直播中修改档位时,浏览器 sender 的 applied readback、LiveKit `LocalVideoTrack` 保存的 degradation preference、track restart options 与 `LocalTrackPublication` 的 republish options 必须作为同一次串行操作更新;任一步失败恢复旧 sender 与旧 SDK 保存态,旧 publication generation 不得回写当前状态。LiveKit full reconnect/republish 不得恢复初次发布的旧偏好。该修复不改变 codec、码率上限、层数或路由,也不能单独解释即时低帧。 - 本地预览性能候选:在相同动态场景中 A/B 测量 Host 动态预览开/关的 CPU、GPU、encode time 和实际 FPS。只有稳定收益且捕获、编码和远端画面不受影响时,才允许直播开始后默认显示静止缩略图或隐藏动态预览并提供按需展开;未测前不宣称收益。 - 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`,当前 Browser 默认 `VP8`。选定 preference 在本次分享的首个 P2P/relay offer 和 SFU publication 前应用,并对该分享的后续 sender 保持固定;`automatic` 使用浏览器/LiveKit 默认,显式 H.264/VP8 优先使用浏览器公布的对应 codec,SFU 使用单一 publication codec。实际 codec/profile、encoder implementation 与 stats 是结果真相。相同 Host、浏览器、驱动、游戏场景和有线 direct Viewer 的物理对照先选择一个固定 Browser codec,再由 browser relay 与 SFU 验证;最终产品只暴露该固定 codec。 +- Web 编码:Browser Host、browser relay 与 SFU publication 固定使用 VP8,产品不提供 codec 或 profile 控件;无法采用 VP8 时明确失败,不保留另一种产品 codec。实际 codec/profile、encoder implementation 与 stats 继续作为只读诊断事实,不产生自动切换或路由 authority;选择依据由 [realtime quality research](./research/realtime-quality-adaptation.md#fixed-browser-codec-decision) 持有。 ### 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 独立采用默认 `VP8`、只在开始分享前可显式选择 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,不允许运行时静默改用软件编码、显示器画面或系统混音。一名普通 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 原子迁移;本次 Browser codec 合同使用单一 `screener-v10`,v9 及更早客户端在 authority 前拒绝。替代后立即删除旧配置、wire、parser、双写、translator、deprecated alias、migration shim 与仅服务旧版本的测试,不保留第二套架构。Git 历史承担旧实现追溯;公开发布后才按明确的新要求重新评估兼容策略。 +- 迁移完整性:项目拥有者明确宣布公开发布前,只维护当前内部产品合同,不承担 backward/forward compatibility。客户端、服务端和部署配置按同一 release 原子迁移;移除 Browser codec 选择的合同使用单一 `screener-v11`,v10 及更早客户端在 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 `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 已通过 site access 的 well-formed code-only 尝试对应未知/过期房间、disabled policy、缺失/错误密码、满房或其他已知有界 admission refusal,When `screener-v11` 首条信令鉴权到达,Then 服务端返回唯一 `ROOM_ACCESS_DENIED`,并使用共享的 authentication-failed close code;同一公开文案和 close 行为不得泄露房间存在性或 policy。UI 不自动断言“需要密码”,只允许可选密码重试并提示使用 Host 提供的邀请链接,不向服务器索取链接。Given grant 无效,Then 继续使用通用 `INVALID_TOKEN`;Host 鉴权保持独立错误,unexpected internal failure 使用不含 raw message 的 `SERVER_ERROR`。Given Viewer 持另一个房间的 grant、缺失/过期 grant 或自行把 role 改为 Host,Then 不能建房、发布、跨房或绕过 Host token。 - Given 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 后释放。Given 当前 `screener-v10` 房间合同,When Host 或 Viewer 取得房间 authority,Then server 与 Browser assets 属于同一 release,缺失或越界的 `C`、其他 wire 与 executable sender 得到明确 mismatch,且 `C=3` 满载 Host 在取得会产生第四份 endpoint media copy 的 publication/token 前明确等待或失败。 +- Given `ENDPOINT_MEDIA_COPY_CAPACITY=C`,其中 `C` 只能是 `1`、`2` 或 `3` 且默认 `2`,When Browser Host 或任一 Browser Viewer 建立、恢复、重鉴权或迁移媒体路径,Then peer child 与 Host publication 的稳态实际发送副本总数不超过 `C`,upstream receive 不计数,角色、浏览器和 UA 不改变 `C`;每个 `authenticated` 快照都携带同一个 `C`,并行候选只有持有 `TransitionOverlapSlot` 时才可达到 `min(C + 1, 3)`,并在 commit/abort 后释放。Given 当前 `screener-v11` 房间合同,When Host 或 Viewer 取得房间 authority,Then server 与 Browser assets 属于同一 release,缺失或越界的 `C`、其他 wire 与 executable sender 得到明确 mismatch,且 `C=3` 满载 Host 在取得会产生第四份 endpoint media copy 的 publication/token 前明确等待或失败。 - Given peer-assisted 路径分别运行 1、5、10、20 名观看者,When 用户加入、容量释放、节点离开、重鉴权或当前 edge 失效,Then 服务端只为新 Viewer、等待 Viewer 或失去 parent 的 subtree root 选择一个 active parent;每名 Viewer 同时至多一个 active upstream,活动图无环,其余健康边保持 sticky。没有可用 parent 时明确等待或拒绝,不主动重平衡或引入常驻多源媒体。 - Given 一个 Host root 退出并释放 Host sender slot,When 新 Viewer 加入或一个失父 child 进入同一 reconcile,Then 候选按结果深度优先,选择 Host 而不是另一 root 的更深空槽;已健康挂在另一 root 下的旧 child 不被周期性搬动。Given authenticated Host 点击下载诊断,Then 服务端按需生成一份 current route snapshot,用 snapshot-local opaque ordinal 表达当前 parent 关系,并只包含 current operation reason/stage/cursor、每个 current child 的 latest demand/queue/candidate/first-frame timing 与封闭 outcome enum;该 ordinal 不是稳定身份,snapshot 不反推丢失事件、不记录 planner 过滤过的 parent、不包含 raw identity/address/error,也不参与 route。Given Viewer 请求房间级 snapshot,Then 服务端拒绝。 - Given 一名分享者和 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。 @@ -158,7 +158,7 @@ SFU 的 publication、subscription、server-resource accounting、临时 overlap - Given 同一时刻采集、host outbound 与 viewer inbound 证据,When 进行根因分类,Then 关联派生后的实际协商视频 codec/profile/parameters、适用时的 `scalabilityMode` 与真实解码表现,依次区分 Capture 低、Capture 高但 outbound 因 CPU 低、Capture 高但 outbound 因 bandwidth 低、outbound 正常但 inbound 低、以及 inbound 正常但肉眼模糊五类;不用 UA 或设备型号代替运行时证据,也不上传 raw SDP。 - Given Viewer 发送低档请求,When 请求未鉴权、过频、重复、过期或没有匹配的 sender/viewer 统计,Then 请求不改变表示或路径状态;有效请求也只能作为交叉验证输入。 - Given 受控 RTT/丢包测试条件,When 连续分享高动态游戏画面 30 分钟,Then 延迟、帧率与丢帧达到上述目标,且没有不可恢复的冻结。 -- Given 2026-08-19 当时生产 release `769de201f7cc` 出现游戏低帧、持续 `bandwidth` 限制或只有 Host 刷新才能恢复,When 归因该历史报告,Then 用同一机器、浏览器、驱动、游戏场景和单个有线 direct Viewer 对照该精确 release 与最新 `main`。每次从独立重新确认的 affected state 只执行 `observe`、重建该 `HostPeer`、替换 capture、Host reload 或本地预览 cycle 中一个动作;codec A/B 另用新的 Host/share/PeerConnection 分别请求 `automatic/H.264/VP8` 并校验实际 codec,不把 codec 变更混入生命周期动作。记录 capture A、sender B、Viewer C 的同代重叠窗口、游戏 FPS、CPU 与进程级 GPU encode;可选 encoder implementation/power-efficient 字段缺失只记 unknown,不能把当时生产报告直接归为当前代码或 H.264 缺陷。 +- Given Windows Chrome/Edge Browser Host 开始或继续分享,When 建立 direct、browser-relay 或 SFU sender,Then 应用固定请求 VP8 且不暴露 codec/profile 选择;实际 RTP codec 与实现字段仍按浏览器 stats 如实显示。Given 浏览器未能采用 VP8,Then 该路径明确失败而不是静默切换为另一套产品 codec 或保留旧 selector/wire 字段。 - Given 分享过程中切换 Wi-Fi/有线网络,When ICE 连接失败,Then 客户端触发 ICE restart,并在成功或超时后给出明确状态。 - Given 当前路径仍可播放且控制器准备迁移 logical parent 或替换 transport,When 候选路径拥有模型授权的 overlap 资源,Then 旧路径保持权威,直到 exact candidate child 解码出第一帧新画面后才原子提交;ICE `connected` 单独不能触发切换。 - Given 候选没有准入资源,或 provisional 路径失败、超时、代次过期、参与者变化、房间停止、权限撤销或 session 被替换,When 本次机会结束,Then 原路径和未受影响 branches 继续工作,临时资源被销毁,重试遵守模型定义的有界策略。 @@ -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 开始一轮新分享,Then 默认值为 VP8,所选 preference 在该分享的首个 P2P/relay offer 与 SFU publication 前应用,并由实际 RTP stats 验证结果。Given 分享已经开始,When 分享处于直播或暂停状态,Then codec selector 保持禁用且不发送 codec mutation 消息;本轮分享的现有和以后新建 sender 使用开始时确定的 codec preference。Host 只有停止当前分享并开始新分享后才能改变 codec。 +- Given Host 打开分享设置,When 查看推荐档或高级设置,Then 不显示 codec 控件,也不把 codec 写入房间 quality state。Given 新建当前或未来 sender,Then direct、browser relay 与 SFU publication 使用同一固定 VP8 合同;Pause/Resume、换源和直播中质量更新都不携带或改变 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、降噪或来源标签伪称已隔离。