diff --git a/README.md b/README.md index f567526..f0b44f2 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # Screener -Screener is a private, low-latency screen-sharing project for one game player and a small group of friends. Its accepted media target is WebRTC P2P-first, with an SFU virtual parent feeding only the necessary roots and optional authenticated TURN for explicitly supported restrictive networks. Roots continue distributing to bounded peer descendants; this is not an always-SFU conferencing design. Viewers should be able to join from a desktop or mobile browser without installing the sender application. +Screener is a private, low-latency screen-sharing project for one game player and a small group of friends. Its accepted media target is WebRTC P2P-first, with a dedicated LiveKit SFU/UDP virtual parent feeding only the necessary roots when direct/STUN peer paths cannot serve them. Roots continue distributing to bounded peer descendants; this is not an always-SFU conferencing design. Viewers should be able to join from a desktop or mobile browser without installing the sender application. -The first measurable Web proof of concept is implemented. A host captures a screen, window, or browser tab and shares a random four-digit room code plus an independent fragment invitation. During a share, the host can change between three ceiling profiles without reopening the source picker, pause and switch sources, and switch video codec while paused. Code-only entry defaults to `open` after site access and may instead require a room password or be disabled; a valid grant enters directly. Rooms are process-memory state with a configurable dormant lease and disappear on restart. Lightweight deployments may keep independent direct WebRTC connections. The flagship deployment enables the all-room controller with bounded peer descendants and one LiveKit Host publication serving admitted Viewer subscriptions; short-lived TURN may be authorized only for a controller-selected exceptional media edge. Production admits 20 viewers and deployments may configure 1 through 20, but that admission limit is not a performance promise. +The first measurable Web proof of concept is implemented. A host captures a screen, window, or browser tab and shares a random four-digit room code plus an independent fragment invitation. During a share, the host can change between three ceiling profiles without reopening the source picker, pause, and switch sources. Browser video is fixed VP8 and leaves the video content hint unset. Code-only entry defaults to `open` after site access and may instead require a room password or be disabled; a valid grant enters directly. Rooms are process-memory state with a configurable dormant lease and disappear on restart. Lightweight deployments may keep independent direct WebRTC connections. The flagship deployment enables the all-room controller with bounded peer descendants and one LiveKit Host publication serving admitted Viewer subscriptions. Production admits 20 viewers; that admission limit is not a performance promise. -This is not yet a broadly validated product release; [Current status](./docs/status.md) owns the deployed revision and validation evidence. The deployed flagship boundary is the all-room bounded peer/SFU-UDP controller plus the memory-resident room/access model on `screener-v8`, with ordinary peer ICE kept STUN-only and short-lived TURN limited to controller-selected exceptional media edges. The optional Windows native sender remains source-only and is not a production runtime. Mobile devices are Web viewers only; ordinary Android and iOS browsers do not expose reliable screen capture for a Web Host. Real cross-network SFU/TURN media, game audio, a sustained public-network 1:20 session, mobile browser lifecycle behavior, and latency targets still require the documented manual test matrix. +This is not yet a broadly validated product release; [Current status](./docs/status.md) owns the deployed revision and validation evidence. The deployed flagship boundary is the all-room bounded peer/SFU-UDP controller plus the memory-resident room/access model on `screener-v10`, with ordinary peer ICE kept STUN-only and no TURN or TCP media route. The optional Windows native sender remains source-only and is not a production runtime. Mobile devices are Web viewers only; ordinary Android and iOS browsers do not expose reliable screen capture for a Web Host. Real cross-network SFU media, game audio, a sustained public-network 1:20 session, mobile browser lifecycle behavior, and latency targets still require the documented manual test matrix. ## Run locally diff --git a/docs/README.md b/docs/README.md index 9f14590..89b0728 100644 --- a/docs/README.md +++ b/docs/README.md @@ -17,7 +17,7 @@ - [Native shared-encode sender](./research/native-shared-encode-sender.md): consolidated Draft #16/#18/#22/#23/#25/#28 ladder, unclassified product-gate failure, staged revalidation, and stop line. - [Native H.264 hardware decision](./research/native-h264-hardware-decision.md): bounded WebCodecs no-go, proved Media Foundation/NVIDIA hardware encode, the default-fmtp `42c01f` stop, and the retained interop boundary. - [Native H.264 opt-in path](./research/native-h264-opt-in-path.md): explicit H.264 sender mode, exact Pion profile registration, and one bounded Host/Viewer loopback with hardware caveats. -- [Realtime quality adaptation](./research/realtime-quality-adaptation.md): current v10 manual profile/codec boundaries, diagnostic-only capture/send/receive correlation, path-isolated dual representations, and bounded simulcast/Dynacast/SVC checks. +- [Realtime quality adaptation](./research/realtime-quality-adaptation.md): fixed Browser VP8/no-video-hint evidence, manual quality ceilings, diagnostic-only capture/send/receive correlation, path-isolated dual representations, and bounded simulcast/Dynacast/SVC checks. - [Browser background capture diagnostics](./research/browser-background-capture.md): browser lifecycle evidence, a privacy-bounded capture/send/receive experiment, interpretation gates, and non-goal keepalive mechanisms. - [Browser screen-audio quality](./research/browser-screen-audio-quality.md): capture/source compatibility, v10 live sender-ceiling readback, Opus negotiation boundaries, and remaining audible/device evidence. - [Display name and presence boundaries](./research/display-name-presence.md): browser-local storage failure behavior, Unicode normalization, control-character policy, and the Web-only presence capability boundary. diff --git a/docs/project-memory.md b/docs/project-memory.md index b2f6ef4..4b8b7ea 100644 --- a/docs/project-memory.md +++ b/docs/project-memory.md @@ -18,7 +18,7 @@ Last updated: 2026-08-24 - 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 display video leaves `contentHint` unset in current source so Chromium retains its screen-capture classification; returned audio tracks keep `contentHint = "music"`. Production still sets video `motion` until this source change is deployed. The final Browser video codec remains undecided; v10 defaults to VP8 and retains the pre-share diagnostic selector, fixed for each share, until controlled game-load evidence supports one codec and the selector/wire can then be removed atomically. Actual codec/profile/encoder stats remain diagnostic only. +- Browser display video leaves `contentHint` unset so Chromium retains its screen-capture classification; returned audio tracks keep `contentHint = "music"`. Browser video is fixed to VP8 across direct, browser-relay, and SFU paths; codec selection, fallback to another media codec, quality-state/wire fields, and share-lifetime codec branches are not product features. Actual codec/profile/encoder stats remain diagnostic only. Production still sets video `motion` and exposes the v10 selector until the accepted v11 contract is implemented and deployed atomically. - 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. @@ -31,7 +31,7 @@ Last updated: 2026-08-24 - Current source and production admit up to `20` Viewers and implement the uniform `1/2/3` endpoint cap, exact prepare candidate, first-decoded-frame commit, strict rollback revision, decoded-progress stall, a two-stage direct/SFU operation deadline, and one event-driven room controller. A short-lived exact-generation decoded-frame observer proves only the pending candidate inside the existing total deadline; the periodic stats sampler remains responsible for active-path diagnostics and stalls. Ordinary peer ICE is STUN-only and the sole application fallback is SFU/UDP; stale TURN environment keys fail startup. SFU admission uses exact publication and Viewer-subscription handles with explicit no-default ingress/egress capacities and a listener-fenced dedicated LiveKit owner. - Current source 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. - Exact production artifacts, rollback boundary, services, and ports are owned by [deployment](./deployment.md) and indexed by [status](./status.md). -- Source and production default new shares to VP8, keep the diagnostic selector pre-share only and fixed for the share lifetime, use `shareGeneration`-fenced Pause/Resume, and require the v10 wire. They implement live 64/128/256 kbps audio-ceiling mutation, typed first-frame Viewer presentation, privacy-safe on-demand route diagnostics, neutral room-code denial, responsive entry controls, exactly three recommended profiles with `1080p30` as default and advanced-only `854x480`. +- Current source and production default new shares to VP8, keep the diagnostic selector pre-share only and fixed for the share lifetime, use `shareGeneration`-fenced Pause/Resume, and require the v10 wire. The accepted v11 replacement removes the selector and codec state, constrains Browser media to VP8, and preserves the no-video-hint source behavior. Current source and production implement live 64/128/256 kbps audio-ceiling mutation, typed first-frame Viewer presentation, privacy-safe on-demand route diagnostics, neutral room-code denial, responsive entry controls, exactly three recommended profiles with `1080p30` as default and advanced-only `854x480`. - Browser SFU subscribers reconcile eligible Host screen publications across connect/activate event ordering, participant/track arrival, and reconnect. Production LiveKit logs only warn/error, and the application service has a bounded same-host LiveKit readiness preflight. - Exact v10 runtime source `fdd5a4a529ff297f41c05ea3388bf484d76afe8f` passed 610 Web tests in 45 files, TypeScript typecheck, client/server production builds, access/privacy, the 257-test SFU admission gate, dependency audit, and repository hygiene. Its one-Host/20-Viewer Chrome 151 loopback decoded every Viewer, propagated the selected VP8 settings, bounded Host/relay fanout at `2`, captured all current-child timing distributions, and observed no SFU publication. It is not public-network, SFU, mobile, physical audio/codec, 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-v10` and no `screener-v9`, the release shares no regular-file inode with its predecessor, and the accepted socket/firewall boundary is intact. The LiveKit namespace remained empty, so deployment evidence is not route or physical-media acceptance. Detail is owned by [deployment](./deployment.md) and [verification status](./verification-status.md). @@ -39,8 +39,8 @@ Last updated: 2026-08-24 ## Current Priority -1. Deploy and physically validate the no-video-hint Browser source change, and keep the final codec decision open until controlled game-load evidence is sufficient. -2. Diagnose screen audio and invitation UI, then finish background/mobile/network evidence at their stated boundaries; Native/executable, distribution, and repository-wide simplification remain later phases. +1. Implement the accepted single-wire Browser v11 contract with fixed VP8 and no video hint, then deploy and physically validate exact direct, browser-relay, and SFU codec behavior. +2. Queue Browser VP8 hardware-acceleration evidence separately; do not add another codec, custom encoder, or Native helper without a new accepted decision. Diagnose screen audio and invitation UI, then finish background/mobile/network evidence at their stated boundaries; Native/executable, distribution, and repository-wide simplification remain later phases. ## Working Rules diff --git a/docs/research/browser-background-capture.md b/docs/research/browser-background-capture.md index 8189c55..49b3f14 100644 --- a/docs/research/browser-background-capture.md +++ b/docs/research/browser-background-capture.md @@ -36,18 +36,18 @@ 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 deployed default video preference is VP8. Every run records -the actual negotiated codec and encoder rather than inferring 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. +The accepted Browser video codec is fixed VP8. Every run still records the +actual negotiated codec and encoder rather than inferring either from +configuration. Encoder implementation is a diagnostic variable only after a +stable baseline exists; neither codec nor encoder choice is a page-keepalive +mechanism. ## Current Source Boundary -The source review covers exact Browser v10 source -`fdd5a4a529ff297f41c05ea3388bf484d76afe8f`, integrated and deployed by exact -main `2726edde9b87f31fd76e749de47972ef817a9bd5`. The current path does not claim or -implement a page-keepalive mechanism, and deployment health supplies no -physical background-capture evidence. +The source review covers canonical source `a26eb39dc677003110787b0ed1581c208f894fd7`; +production remains exact main `2726edde9b87f31fd76e749de47972ef817a9bd5`. +The current path does not claim or implement a page-keepalive mechanism, and +deployment health supplies no physical background-capture evidence. - Current `src/client/media/quality.ts` obtains one `getDisplayMedia()` stream, applies ideal/max capture constraints, leaves the video hint unset, and @@ -265,7 +265,7 @@ The diagnostic does not adopt: service-worker activity as a liveness proof; - the Web Screen Wake Lock API as a capture-priority control; - product launch flags that disable browser background policies; -- visibility-triggered H.264/VP8 switching, forced H.264, extra sender +- visibility-triggered codec switching, another Browser media codec, extra sender generations, or automatic capture replacement; - server-side media telemetry, raw diagnostic upload, periodic probes, routing changes, quality-driven reparenting, or magic quality thresholds; or diff --git a/docs/research/native-h264-opt-in-path.md b/docs/research/native-h264-opt-in-path.md index 75ea7d8..080f408 100644 --- a/docs/research/native-h264-opt-in-path.md +++ b/docs/research/native-h264-opt-in-path.md @@ -70,12 +70,12 @@ Primary sources checked 2026-08-21: - [`ID3D11VideoDevice::CreateVideoProcessorInputView`](https://learn.microsoft.com/en-us/windows/win32/api/d3d11/nf-d3d11-id3d11videodevice-createvideoprocessorinputview) - [`ID3D11VideoContext::VideoProcessorSetOutputBackgroundColor`](https://learn.microsoft.com/en-us/windows/win32/api/d3d11/nf-d3d11-id3d11videocontext-videoprocessorsetoutputbackgroundcolor) -## 2026-08-21 Web H.264 Source +## 2026-08-21 Browser-To-Native H.264 Evidence -The Web sender's standard WebRTC path now prefers H.264 while preserving the -browser's negotiated fallback list. The separate browser-to-Native bridge -loopback remains Chrome WebCodecs Annex-B -> the local envelope -> the same -Go/Pion fanout. +The ordinary Browser WebRTC path is fixed VP8 and is independent of this Native +research. The browser-to-Native bridge loopback below uses Chrome WebCodecs +H.264 Annex-B -> the local envelope -> the same Go/Pion fanout only as bounded +Native-path evidence; it does not add a Browser codec option or fallback. ## Bounded Loopback Evidence diff --git a/docs/research/realtime-quality-adaptation.md b/docs/research/realtime-quality-adaptation.md index aed8ae1..1e84a58 100644 --- a/docs/research/realtime-quality-adaptation.md +++ b/docs/research/realtime-quality-adaptation.md @@ -226,12 +226,12 @@ physical evidence. ## Browser Codec And Content-Hint Evidence -The accepted change is narrow: Browser display video does not set -`contentHint`; returned audio keeps `contentHint = "music"`. The final Browser -video codec is not decided. V10 continues to default to VP8 and retains the -pre-share diagnostic selector, fixed for one share, until a controlled real-game -comparison supports one codec. Codec/profile/encoder stats remain diagnostic -and do not authorize automatic switching, route changes, or another controller. +The accepted Browser contract is fixed VP8 with no video `contentHint`; returned +audio keeps `contentHint = "music"`. Direct and browser-relay offers contain VP8 +as their only video media codec, while RTX/RED/FEC may remain repair formats; +SFU publication explicitly uses VP8 with no backup codec. The UI and quality +wire expose no codec choice. Codec/profile/encoder stats remain diagnostic and +do not authorize automatic switching, route changes, or another controller. Chromium maps video `contentHint = "motion"` to libwebrtc `kFluid`, and libwebrtc clears `is_screencast` for that mode. Screener was therefore replacing @@ -279,14 +279,29 @@ hardware selection across Screener's browser targets. Capability advertisement alone is not acceptance; an actual sender must prove codec, implementation, power efficiency, rate control, and game-load behavior. +These results select VP8 for the ordinary Browser path: no-hint VP8 met the +current controlled quality baseline, has the required WebRTC interoperability, +and does not depend on a page choosing a specific Windows encoder. Current +accepted implementation uses `RTCRtpTransceiver.setCodecPreferences()` before +the first offer and fails the edge if the API, a VP8 capability, or the +preference application is missing; it does not leave another video media codec +as fallback. Browser VP8 +hardware acceleration is a separate later evidence question. It must be proven +on the supported browser/platform matrix from the actual encoder and game-load +measurements before it can change the implementation; custom codecs, another +codec ladder, and a Native helper are not implied by this decision. + Comparable native products do not resolve this browser boundary. Discord negotiates VP8/H.264 and selected-platform HEVC/AV1 through its own capture and hardware pipeline, and documents an AMD rate-control/frame-dropper repair. Parsec, Steam Remote Play, and Moonlight/Sunshine primarily use controlled -hardware H.264/HEVC/AV1 paths. Oopz only publicly identifies Agora as its screen -sharing SDK; Agora may use VP8/H.264/H.265 or automatic selection, so Oopz's -actual session codec is unknown without runtime stats. These native designs do -not prove that an ordinary web page can force the same encoder path. +hardware H.264/HEVC/AV1 paths. A read-only inspection of the installed Oopz +0.87.425 package found Agora screen sharing, an H.264 Web-viewer configuration, +and native NVENC/QSV/AMF plus software codec paths; it did not prove the codec or +encoder active in a real sender session. These native designs do not prove that +an ordinary web page can force the same encoder path. Reproducibility hashes and +the exact static boundary are retained in +[Native shared encode](./native-shared-encode-sender.md#2026-08-20-windows-codec-comparison). Primary implementation evidence: Chromium's [content-hint bridge](https://chromium.googlesource.com/chromium/src/+/refs/tags/151.0.7922.174/third_party/blink/renderer/modules/peerconnection/media_stream_video_webrtc_sink.cc#36), @@ -706,7 +721,7 @@ guarantees the emitted resolution, frame rate, or bitrate. - A live profile change uses `track.applyConstraints()` and updates every current sender with `RTCRtpSender.setParameters()`. It does not reopen the source picker or renegotiate healthy peer connections. -- Current v10 source leaves the video hint unset so display capture retains +- Current source leaves the video hint unset so display capture retains browser screen semantics; production still sets `contentHint = "motion"` until deployment. Recommended profiles and the advanced initial value use `balanced`, with explicit `maintain-resolution` and @@ -718,9 +733,9 @@ guarantees the emitted resolution, frame rate, or bitrate. 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. 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 + advanced 480p resolution and live audio mutation. The accepted v11 replacement + removes that codec selector and the corresponding quality-state/wire field; + Browser media is fixed VP8. 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 @@ -731,7 +746,8 @@ guarantees the emitted resolution, frame rate, or bitrate. - Every sender update derives from `getParameters()`, calls `setParameters()`, then reads requested/applied bitrate, frame rate, scale, and preference. Rejection or browser rewriting is visible rather than console-only. -- A P2P sender receives its profile before the first offer. Explicit source or +- A P2P or browser-relay sender is constrained to VP8 before the first offer and + receives its quality profile before that offer. Explicit source or profile changes use the existing serialized sender-mutation path; accepting an answer does not trigger an extra whole-profile write. - One strict room setting is last-wins for current/future peer relays and the @@ -761,8 +777,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 runtime or automatic codec switching. The current v10 pre-share selector - remains diagnostic and fixed for one share until the final codec decision. +- No runtime, automatic, or user-selected Browser codec switching. The accepted + v11 replacement fixes VP8; implementation and production cutover are tracked + separately. - 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/research/webrtc-p2p-screen-sharing.md b/docs/research/webrtc-p2p-screen-sharing.md index 11f28bc..0f9d777 100644 --- a/docs/research/webrtc-p2p-screen-sharing.md +++ b/docs/research/webrtc-p2p-screen-sharing.md @@ -294,9 +294,8 @@ WebRTC 媒体本身使用 DTLS-SRTP 加密,但 direct P2P 仍可能让这组 ## 编解码策略 -- 所有合规 WebRTC 浏览器必须支持 VP8 和 H.264 Constrained Baseline;VP9/AV1 是能力协商项。 -- 游戏场景首版通常优先 H.264,因为硬件编码覆盖广;保留 VP8 回退。 -- 不要仅凭 codec 名称判断性能。通过 `RTCRtpSender.getCapabilities()` 协商,通过统计确认 `encoderImplementation`、`powerEfficientEncoder`、`totalEncodeTime/framesEncoded` 和 `qualityLimitationReason`。 +- 当前 Browser 路径固定 VP8,因为它满足 WebRTC 基线互通,且无视频 hint 的受控证据恢复了接近 30 fps、完整分辨率和正常码控;direct、browser relay 与 SFU 不保留另一视频媒体 codec 作为 fallback。 +- 不要仅凭 codec 名称判断性能。通过实际协商与统计确认 `encoderImplementation`、`powerEfficientEncoder`、`totalEncodeTime/framesEncoded` 和 `qualityLimitationReason`;VP8 hardware acceleration 仍需目标平台实测。 - 软件 VP9/AV1 可能抢占游戏 CPU;只有端点支持且实测硬件高效时才启用。 - Simulcast/SVC 主要帮助 SFU 按观看者网络选择层。它们不会消除 P2P fan-out 的 `N * B` 上行,还可能增加编码实例。 diff --git a/docs/session-handoff.md b/docs/session-handoff.md index 1dc2cf6..7f94fad 100644 --- a/docs/session-handoff.md +++ b/docs/session-handoff.md @@ -45,10 +45,11 @@ Current high-signal boundary: open/password/disabled code entry. Neither source nor production uses SQLite; exact rollback artifacts are owned by deployment. - Browser display video must leave `contentHint` unset; audio keeps `music`. - Current source implements that behavior; production still sets video `motion`. - The final Browser video codec is not decided. Both remain v10, default VP8, - with the diagnostic selector fixed for each share; heterogeneous-network and - SFU media evidence remains open. + Browser video is fixed VP8 across direct, browser-relay, and SFU paths; the + accepted strict v11 contract removes codec UI/state/wire and fallback media + codecs. Current source and production remain v10 with the selector, and + production still sets video `motion`, until implementation and atomic deploy. + 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 4182ae4..96ba65b 100644 --- a/docs/status.md +++ b/docs/status.md @@ -16,7 +16,7 @@ This is the current execution index. Git history owns completed timelines; [veri ## Current Source -- Canonical root `main` is the source and integration truth. Current source and production use the single Browser `screener-v10` boundary and reject every other wire before room authority; auxiliary branches and older worktrees do not supersede it. +- Canonical root `main` is the source and integration truth. Current source and production use the single Browser `screener-v10` boundary and reject every other wire before room authority; auxiliary branches and older worktrees do not supersede it. The accepted replacement is one atomic `screener-v11` contract with fixed Browser VP8 and no codec quality field; it is not yet implemented or deployed. - 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. @@ -27,7 +27,7 @@ This is the current execution index. Git history owns completed timelines; [veri ## Current Milestone -1. Deploy and physically validate the no-video-hint Browser source change. The final Browser codec remains undecided; source and production remain v10, default VP8, with the pre-share diagnostic selector fixed for each share. +1. Implement and validate the accepted `screener-v11` fixed-VP8/no-video-hint Browser contract, then deploy it atomically and verify direct, browser-relay, and SFU paths from actual codec stats. Current source and production remain v10 with the pre-share selector; production also remains on video `motion`. 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. @@ -43,6 +43,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 while the no-video-hint deployment and final codec evidence remain pending. Audio, heterogeneous-network, SFU, +Browser v10 contract while the accepted v11 fixed-VP8 implementation and no-video-hint deployment remain 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 61e2c56..14c8a68 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. **Deploy and physically validate the no-video-hint source.** Current source leaves video `contentHint` unset and retains audio `contentHint = "music"`; production still sets video `motion`. Deploy the source-only v10 change, then confirm real `getDisplayMedia()` capture and outbound stats on the production path. This does not decide the final codec. +1. **Implement, deploy, and physically validate fixed Browser VP8 with no video hint.** Replace the current v10 selector/state with one strict v11 wire: direct and browser-relay offers keep VP8 as the only media codec, SFU publication uses VP8 with no backup codec, video leaves `contentHint` unset, and audio keeps `contentHint = "music"`. Validate the source first, then deploy server/assets atomically and confirm actual codec/capture/outbound/decoded stats on direct, browser-relay, and SFU paths. Production remains v10 with video `motion` until that cutover. 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. @@ -16,13 +16,14 @@ Only items in **Now** are executable after their stated decision gate. A branch 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. **Browser VP8 hardware-acceleration evidence.** After the fixed-v11 source and physical codec gate, measure supported Chrome/Edge platform combinations under real game load using actual negotiated codec, encoder implementation, power-efficient status, encode time, frame rate, and CPU/GPU attribution. Capability advertisement alone is not proof. Do not add another codec, a custom encoder, GPU-selection workaround, codec ladder, or Native helper without a new accepted decision. +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 @@ -33,7 +34,6 @@ These items are real product work, ordered after the current physical-media, rea | #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. | -| Final Browser video codec | Current v10 defaults to VP8 and retains the pre-share diagnostic selector. Compare no-hint VP8 and available hardware paths under real game load, then choose one codec and remove the selector, quality-state field, and share-lifetime codec branches atomically. Do not add application codec switching, GPU selection, SDP munging, or a codec ladder. | | 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 35ec41f..2262cfa 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). -- Fresh Chrome 151 tab-capture probes identified two separate Browser boundaries. Removing video `contentHint = "motion"` changed VP8 from 14.93 fps at 428x208 to 29.73 fps at 1904x928 under the same 5 Mbps balanced setup. No-hint AMD H.264 remained about 14--16 fps with hundreds of outer rate-limiter drops, while the same MFT reached 29.93 fps under diagnostic Desktop SW BRC; OpenH264 was slower. The page cannot enable that Chromium field trial or select an adapter/MFT. No-hint VP8 used libvpx software and held about 30 fps in one- and two-sender loopback on the test machine, but the process CPU totals include capture and local decode and are not real-game or weaker-device evidence. Current source now leaves the video hint unset; production still sets `motion`. The final Browser codec remains undecided. 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). +- Fresh Chrome 151 tab-capture probes identified two separate Browser boundaries. Removing video `contentHint = "motion"` changed VP8 from 14.93 fps at 428x208 to 29.73 fps at 1904x928 under the same 5 Mbps balanced setup. No-hint AMD H.264 remained about 14--16 fps with hundreds of outer rate-limiter drops, while the same MFT reached 29.93 fps under diagnostic Desktop SW BRC; OpenH264 was slower. The page cannot enable that Chromium field trial or select an adapter/MFT. No-hint VP8 used libvpx software and held about 30 fps in one- and two-sender loopback on the test machine, but the process CPU totals include capture and local decode and are not real-game or weaker-device evidence. This evidence selects fixed VP8 for the Browser contract; current source leaves the video hint unset, while production still sets `motion` and the v11 codec cleanup remains pending implementation. 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 1bb0c09..2dcf020 100644 --- a/docs/方案设计.md +++ b/docs/方案设计.md @@ -2,7 +2,7 @@ ## 2026-08-18 14:45 -状态:已接受 Browser display video 不设置 `contentHint`,audio 保留 `music`;最终 Browser codec 尚未决定。Current source 与 production 仍是 `screener-v10`、默认 VP8,并保留对每次分享固定的临时诊断 selector。Pause/Resume 继续由当前 Host session 和 `shareGeneration` 约束。精确 deployed behavior 只由 deployment 文档记录。 +状态:已接受 Browser display video 不设置 `contentHint`、audio 保留 `music`,且 Browser 唯一视频媒体 codec 为 VP8。下一原子 Browser 合同是严格 `screener-v11`,不含 codec selector、quality-state/wire 字段或分享生命周期 codec 分支;current source 与 production 仍是 `screener-v10`,production 还设置视频 `motion`。Pause/Resume 继续由当前 Host session 和 `shareGeneration` 约束。精确 source/deployed behavior 只由 status 与 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。 -下表描述 current Browser 的单一 `screener-v10` wire;server 与 Browser assets 必须作为一个完整合同原子发布,v9 和更早客户端在取得房间 authority 前拒绝,不存在旧 parser、writer、translator 或中间协议子集: +下表描述 current Browser 的单一 `screener-v11` wire;server 与 Browser assets 必须作为一个完整合同原子发布,其他 wire 在取得房间 authority 前拒绝,不存在旧 parser、writer、translator 或中间协议子集: | 方向 | 消息 | 作用 | | --- | --- | --- | @@ -128,7 +128,7 @@ WebSocket 使用无 query/fragment 的同源 `/signal`。upgrade 只记录 site- Zod 在服务边界拒绝未知类型、超长字符串、非法 SDP/candidate 结构和越权目标。服务端根据已认证会话决定路由:观看者不能给另一名观看者发信令,分享者也不能向本房间以外的 peerId 发送。 -current 的唯一 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`。 +current 的唯一 wire 是 `screener-v11`,不做版本协商、旧 parser、双写或 translator。任何其他 Browser wire 及 executable sender 的首条消息在角色进入 room lookup 前得到同一个 fatal refresh 终态并停止自动重连。Client 与 server 共用 `SIGNAL_CLOSE_CODES` 定义会话替换、鉴权失败和 Viewer access revoke 的终态语义,避免双方对重连边界产生分叉;非法协议消息使用 WebSocket policy-violation close code。缺失、过期、跨房、已 rotate 或 revoked 的当前 Viewer grant 使用同一个通用“邀请无效或已失效”结果,不区分房间不存在与凭据错误。已通过 site access 的 well-formed code-only 预期拒绝只发 `ROOM_ACCESS_DENIED`:unknown/expired room、disabled policy、absent/wrong password、room full 与其他已知有界 admission refusal 使用同一 code、公开文案和共享 authentication-failed close code。Grant 继续使用 `INVALID_TOKEN`,Host 鉴权保持独立,unexpected internal failure 使用不含 raw message 的 `SERVER_ERROR`。 ## WebRTC 协商 @@ -156,11 +156,11 @@ current 的唯一 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`,让 display capture 保留浏览器的 screen 分类;返回的音频 track 使用 `contentHint = "music"`。三档推荐质量与折叠高级区初始值统一采用 `degradationPreference = "balanced"`,并允许显式选择 clarity-first 或 fluid。当前 v10 的“分享高级设置”暂时保留仅分享前可用、对本次分享固定的诊断 codec selector,并提供离散的 `64 kbps 省流 | 128 kbps 音乐默认 | 256 kbps 极高音质` 屏幕音频 sender ceiling。音频 ceiling 可在直播中串行更新。这些设置只是 ceiling 和浏览器偏好,不保证实际分辨率、帧率、码率或声道。 +浏览器可返回更低设置,界面以 `track.getSettings()` 显示实际捕获结果。推荐区始终只有表中的三档并默认选择中档 `1080p30`;高级分辨率额外提供 `480p`(854x480)、720p、1080p 与 1440p,FPS 继续独立接受 15 至 60 整数,码率继续独立接受 2 至 12 Mbps,因此不建立第四个推荐档或 `480p` preset ID。视频 track 不赋任何 `contentHint`,让 display capture 保留浏览器的 screen 分类;返回的音频 track 使用 `contentHint = "music"`。三档推荐质量与折叠高级区初始值统一采用 `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`。 -当前 v10 Web 视频端默认使用 `VP8`。P2P、browser relay 与 SFU publication 使用分享开始时由临时诊断 selector 选定的同一 codec,且本轮分享内固定;不做运行时或自动 codec 切换。最终 codec 等待无 hint 的真实游戏负载证据后决定,再原子删除 selector、quality-state/wire 字段和 share-lifetime 分支。实际 outbound codec/profile、encoder implementation 与 stats 是只读结果真相。Viewer answer 的 Opus stereo receive preference 是当前唯一应用 SDP 变换边界。 +接受的 `screener-v11` Web 视频固定使用 `VP8`。P2P 与 browser relay 在首次 offer 前只保留 VP8 media capabilities 及 RTX/RED/FEC repair capabilities;无法应用该约束时明确失败,不静默协商另一媒体 codec。SFU publication 显式使用 VP8 且关闭 backup codec。UI、quality state/wire 和分享生命周期均没有 codec 分支;实际 outbound codec/profile、encoder implementation 与 stats 是只读结果真相。Viewer answer 的 Opus stereo receive preference 是当前唯一应用 SDP 变换边界。 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 始终可用。 @@ -170,11 +170,11 @@ Web display-audio 请求显式关闭 `echoCancellation`、`noiseSuppression`、` 直播中切换来源仍由明确的按钮手势重新调用 `getDisplayMedia()`,不改变房间、host generation 或媒体拓扑。选源取消或失败时继续发送旧流;成功后为每个现有 sender 调用 `replaceTrack()`,健康连接不重新协商。初始 offer 预留 send-only audio transceiver,使音频可以在 `null -> track -> null` 间切换。若某个 peer 的替换或回滚失败,只重建该 peer;新流提交后才停止旧流,并以当前 stream 身份保护旧 track 的迟到 `ended` 事件。统计码率累加器在换源后重置,避免把不同 track 的计数器混算。 -直播中切换质量不重新打开选源器:对当前视频 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、黑帧或网络停顿推断成主动暂停。房间与连接保持不变。 +直播中切换质量不重新打开选源器:对当前视频 track 调用 `applyConstraints()`,再更新所有现有 sender 的参数;quality mutation 不含 codec,新加入或重建的 Browser sender 仍使用固定 VP8 与最新其他权威设置。失败时保留当前分享并给出状态,不以重建整个房间补偿。临时暂停禁用当前 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。 -codec 与 content-hint 依据由研究文档持有。`motion` 会把 Chromium/libwebrtc 的 display capture 从 screen 改成 realtime,并在同一 VP8 条件下造成明显降帧和降分辨率,因此产品不设置视频 hint。无 hint 时,普通 AMD H.264 MFT 的浏览器码控仍丢帧,而诊断性 Desktop SW BRC 可恢复约 30 fps,说明问题不等于硬件编码器本身的吞吐上限;网页没有启用该浏览器进程开关或选择具体 GPU/MFT 的 API。最终 codec 仍需真实游戏画面、整机负载和接收质量证据,实际 codec 与 encoder 字段只用于诊断,不触发 codec 切换。 +codec 与 content-hint 依据由研究文档持有。`motion` 会把 Chromium/libwebrtc 的 display capture 从 screen 改成 realtime,并在同一 VP8 条件下造成明显降帧和降分辨率,因此产品不设置视频 hint。无 hint VP8 在当前受控证据中恢复接近 30 fps、完整分辨率和正常码控,具备 Browser 基线覆盖;它因此成为唯一 Browser codec。普通 AMD H.264 MFT 的浏览器码控仍丢帧,而诊断性 Desktop SW BRC 可恢复约 30 fps,说明问题不等于硬件编码器本身的吞吐上限;网页没有启用该浏览器进程开关或选择具体 GPU/MFT 的 API。实际 codec 与 encoder 字段只用于诊断,不触发 codec 切换。VP8 hardware acceleration 需要后续按目标浏览器和真实游戏负载单独取证;本设计不假定其存在,也不增加替代 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 不用于标定参数、听感或性能。 @@ -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` 保持独立边界; -- current source contract 使用单一 `screener-v10` 信令 literal、严格 JSON schema、方向与房间授权校验、payload 上限和 client-mismatch 刷新终态;分享状态由 `shareGeneration`-fenced paused boolean 表达,访问失败使用 `ROOM_ACCESS_DENIED`,Viewer route failure 为 typed terminal,Host 诊断按需返回脱敏 snapshot。`qualityProfileIdSchema` 保持三档,`qualityResolutionSchema` 包含 `480p`,`DEFAULT_QUALITY_SETTINGS` 为 1080p30、VP8; +- 接受的 source contract 使用单一 `screener-v11` 信令 literal、严格 JSON schema、方向与房间授权校验、payload 上限和 client-mismatch 刷新终态;分享状态由 `shareGeneration`-fenced paused boolean 表达,访问失败使用 `ROOM_ACCESS_DENIED`,Viewer route failure 为 typed terminal,Host 诊断按需返回脱敏 snapshot。`qualityProfileIdSchema` 保持三档,`qualityResolutionSchema` 包含 `480p`,`DEFAULT_QUALITY_SETTINGS` 为 1080p30,quality schema 严格拒绝 codec 字段; - 质量协议的 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,8 +359,8 @@ Token-free SFU standby prewarm 保持可选且不创建 participant、publicatio ## 当前交付边界 -- Current source 与 production 使用单一 `screener-v10` wire,默认 VP8,codec selector 只在分享前可用且对本次分享固定,Pause/Resume 由当前 Host session 和 `shareGeneration` 约束。 -- 已接受 Browser video 不设置 `contentHint`;最终 codec 未决,当前 v10 selector/wire 暂时保留。Host 后台性能按受控根因证据诊断;标准 ICE/SFU 按真实异构网络验收。 +- 接受的下一合同使用单一 `screener-v11` wire,Browser 固定 VP8 且 video 不设置 `contentHint`;codec selector、quality-state/wire 字段与分享生命周期 codec 分支不存在。Current source 与 production 仍是 `screener-v10`,production 还设置 video `motion`;实现与部署分别原子推进。 +- Pause/Resume 由当前 Host session 和 `shareGeneration` 约束。Host 后台性能和 VP8 hardware acceleration 分别按受控根因证据诊断;标准 ICE/SFU 按真实异构网络验收。 - 当前发布面只包含 Web Host、Viewer 和 browser relay。Native sender、capture helper、shared-encode executable 与两个分发包仍由各自后续决策和物理证据驱动,不属于当前 Browser release。 任何一步只实现当前消费者需要的接口。不得为未来账号、第三种媒体拓扑或原生 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;其他 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 1cb8617..5033dd1 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`(平衡),`maintain-resolution`(清晰优先)、`maintain-framerate`(流畅优先)保留为显式选择。当前 v10 默认 VP8,并暂时保留仅分享前可选、对本次分享固定的诊断 codec selector;不做运行时或自动 codec 切换。分享期间可直接换视频档位和 64/128/256 kbps 音频 sender ceiling,不重新选择来源或重建健康连接。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;界面、质量状态和 wire 不提供 codec 选择或切换。分享期间可直接换视频档位和 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,7 +45,7 @@ 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-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 继续由各自权威输入派生。 +- 观看端连接反馈:Viewer 舞台从鉴权到首帧使用一个由现有权威状态推导的稳定阶段反馈,至少区分等待分享、分配线路、建立 P2P、连接备用线路、等待首帧、浏览器阻止自动播放、恢复连接和有界失败。阶段不是进度百分比,也不产生 route authority;只有当前 video element 实际提交首帧后才显示正在播放。播放按钮只在 `play()` 因自动播放策略被拒绝时稳定出现,连接中不得忽隐忽现;明确失败后才提供一次受现有 controller 约束的重试入口。make-before-break 或仍有可见旧帧时保留画面并叠加恢复状态,不为显示反馈主动清空健康 media element。Host 离线本身不使仍健康的媒体失效;exact upstream 硬失败或 route terminal failure 必须使 current frame proof 失效,已证明画面至多降为 retained background,不能遮蔽离线或有界失败。当前 `screener-v11` 服务端补充消息是 strict、current-revision-fenced `route-status` union,且只允许 `waiting/sfu-admission` 与 `failed/route-exhausted` 两种配对;它不携带 raw text、身份、candidate 或 retry hint,prepare/active、access、Host、signal、media 与 autoplay 继续由各自权威输入派生。 - 质量控制扩展:推荐档位仍是默认入口且只保留 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。只有稳定收益且捕获、编码和远端画面不受影响时,才允许直播开始后默认显示静止缩略图或隐藏动态预览并提供按需展开;未测前不宣称收益。 @@ -58,7 +58,7 @@ 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 编码:视频 track 不设置任何 `contentHint`,让 display capture 保留浏览器的 screen 分类;音频 track 继续使用 `contentHint = "music"`。当前 v10 默认 VP8,并暂时保留仅分享前使用、对本次分享固定的诊断 selector;最终 codec 等待无 hint 的真实游戏负载证据后决定。实际 codec/profile、encoder implementation 与 stats 继续作为只读诊断事实,不产生自动切换或路由 authority;依据由 [realtime quality research](./research/realtime-quality-adaptation.md#browser-codec-and-content-hint-evidence) 持有。 +- Web 编码:视频 track 不设置任何 `contentHint`,让 display capture 保留浏览器的 screen 分类;音频 track 继续使用 `contentHint = "music"`。Browser direct、browser relay 与 SFU publication 的唯一视频媒体 codec 是 VP8,不提供 Automatic/H.264/其他 codec 选择、fallback 或运行时切换。实际 codec/profile、encoder implementation 与 stats 继续作为只读诊断事实,不产生自动切换或路由 authority;依据由 [realtime quality research](./research/realtime-quality-adaptation.md#browser-codec-and-content-hint-evidence) 持有。 ### P2 - 可选功能 @@ -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 原子迁移;若最终 codec 决策删除当前 selector/quality wire 字段,则使用一个新的单一 wire 原子替换 v10,旧客户端在 authority 前拒绝。替代后立即删除旧配置、wire、parser、双写、translator、deprecated alias、migration shim 与仅服务旧版本的测试,不保留第二套架构。Git 历史承担旧实现追溯;公开发布后才按明确的新要求重新评估兼容策略。 +- 迁移完整性:项目拥有者明确宣布公开发布前,只维护当前内部产品合同,不承担 backward/forward compatibility。Browser 客户端、服务端和部署配置使用单一 `screener-v11` wire 原子发布,旧客户端在 authority 前拒绝;codec selector、quality-state/wire 字段、旧 parser/writer、双写、translator、deprecated alias、migration shim 与仅服务旧版本的测试均不存在。Git 历史承担旧实现追溯;公开发布后才按明确的新要求重新评估兼容策略。 - Web 优先:每项用户能力先使用普通浏览器的标准 API 和正常权限流程;只有当前目标浏览器存在已证实的能力缺口且该功能仍属当前范围时,才允许独立、可选的 Sender。不得为假设中的未来能力预建客户端、兼容层或第二套会话架构。 - 兼容性:分享端首个目标矩阵为 Windows 10/11 的当前 Chrome/Edge;普通移动浏览器当前只属于 Viewer 矩阵。截止 2026-08-21,MDN Browser Compatibility Data 仍将 Chrome Android、Firefox Android 和 Safari iOS 的 `getDisplayMedia()` 标为不支持;Web Host 必须用运行时能力检测,API 缺失时明确显示不支持,API 存在但调用拒绝时区分用户拒绝/取消与 capture failure,不能用 UA/设备型号伪造兼容。观看端仍必须覆盖当前 Android Chrome 和 iOS Safari,并实测自动播放限制、横竖屏、锁屏恢复与移动网络切换。所有 Web Viewer 使用统一的 server-authoritative endpoint capacity;移动真机表现是兼容观察,不建立单独 route class。 @@ -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 Windows Chrome/Edge Browser Host 开始或继续分享,When 浏览器返回 display video track,Then 应用不赋任何视频 `contentHint`,并按浏览器 stats 如实显示实际 RTP codec 与实现字段。Given 当前 v10 建立 direct、browser-relay 或 SFU sender,Then 使用分享开始时选定的 codec;Pause/Resume、换源和直播中质量更新不改变它。 +- Given Windows Chrome/Edge Browser Host 开始或继续分享,When 浏览器返回 display video track,Then 应用不赋任何视频 `contentHint`,并按浏览器 stats 如实显示实际 RTP codec 与实现字段。Given 当前 `screener-v11` 建立 direct 或 browser-relay sender,Then offer 的唯一视频媒体 codec 是 VP8;RTX/RED/FEC 只可作为修复格式,不能引入另一媒体 codec。若浏览器缺少 VP8 capability、`setCodecPreferences()` 或应用失败,Then 在创建 offer 前明确失败而不是静默协商其他 codec。Given 建立 SFU publication,Then LiveKit 固定使用 VP8 且不启用 backup codec。 - Given 分享过程中切换 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 打开当前 v10 分享设置,When 查看高级设置,Then codec selector 只作为临时诊断输入且默认 VP8。Given 分享已开始,Then selector 保持可见但禁用并说明本轮已锁定,direct、browser relay 与 SFU publication 使用分享开始时的同一 codec;Pause/Resume、换源和直播中质量更新都不改变 codec。最终 codec 决定后,该 selector 与对应 quality-state/wire 字段一起删除,不保留运行时切换。 +- Given Host 打开当前 `screener-v11` 分享设置或更新房间画质,When 查看 UI 或校验消息,Then 不存在 codec selector、codec quality-state/wire 字段或分享生命周期 codec 分支;Browser direct、browser relay 与 SFU publication 始终使用固定 VP8。实际协商 codec 与 encoder 只作为只读诊断显示。 - Given 浏览器返回屏幕音轨,When 产品展示音频状态或未来连接详情,Then 只能把存在性、浏览器实际返回的 track settings、实际协商 codec/fmtp 和实际流量分别标为捕获、协商或观测事实;字段缺失必须显示为未知,且不得从 Opus 的 `48000/2` RTP 声明推断源采样率或实际 stereo。 - Given Web 或后续 native sender 分享电影/视频音频,When 进行产品验收,Then 必须用独立 A/B/C 证据关联 capture audio track/settings、实际 negotiated codec/fmtp、outbound/inbound 实际码率、loss、jitter、concealment、jitter-buffer 与 A/V sync;P2P、浏览器 relay 与 SFU route switch 保持同一持久 Viewer `MediaStream`/媒体元素和 stereo 协商,不新增独立 audio element 或 Web Audio 时钟边界。native sender 必须通过该音质和同步矩阵,且不得把 audio `maxBitrate` ceiling 当作提质证明。 - Given 分享者同时运行语音软件,When 选择仅分享游戏声音,Then supported Windows 11 native sender 只保留目标游戏及其子进程的渲染音频,观众、通知和其他应用声音不得进入分享;浏览器 system audio 无法满足时必须允许 video-only 或明确告知其可能包含语音/通知,不能用 AEC、降噪或来源标签伪称已隔离。