docs: accept stable room authority and codec controls

This commit is contained in:
AAA committed 2026-08-26 19:27:33 +08:00
1 parent 3b2e79bda6
commit 1035d4e6bd
12 files changed
+296 -337

No files matched your search

+1 -1
View File
@@ -24,7 +24,7 @@
- [Display name and presence boundaries](./research/display-name-presence.md): browser-local storage failure behavior, Unicode normalization, control-character policy, and the Web-only presence capability boundary.
- [Agent context governance research](./research/agent-context-governance.md): official Codex, Claude Code, Hermes Agent, and GitHub practices adopted by this repository.
- [ADR-0001](./adr/0001-p2p-first-media-topology.md): historical P2P-first baseline; later access and automatic-routing decisions supersede its stale current-state details.
- [ADR-0002](./adr/0002-memory-resident-protected-rooms.md): four-digit leased in-memory rooms, local Host defaults, accepted room-lived short Viewer grants, explicit missing-room entry, and orthogonal code entry.
- [ADR-0002](./adr/0002-memory-resident-protected-rooms.md): four-digit leased room authority with lightweight memory and optional SQLite stable modes, local Host defaults, scoped Viewer grants, and explicit room replacement.
- [ADR-0004](./adr/0004-peer-assisted-media-experiment.md): historical standard-WebRTC peer-assisted experiment and reusable evidence.
- [ADR-0005](./adr/0005-automatic-hybrid-media-routing.md): accepted P2P-first single-reconcile route model, including deployed Browser SFU ICE-server isolation; remaining real-network validation is tracked in verification status.
- [ADR-0006](./adr/0006-fixed-high-native-sender-canary.md): proposed fixed-`HIGH` native canary, current no-go result, and staged revalidation boundary.
+108 -159
View File
@@ -1,195 +1,144 @@
# ADR-0002: Memory-Resident Rooms And Scoped Viewer Access
# ADR-0002: Optional Durable Room Authority And Scoped Viewer Access
- Status: Accepted and deployed on strict v12
- Date: 2026-08-23
- Status: Accepted; implementation pending
- Date: 2026-08-26
## Context
Screener serves one Host and at most 20 trusted Viewers. The media plane stays
distributed and P2P-first, while the application server owns the small room,
authorization, signaling, and route-control state. An application restart already
ends every active media session. Removing durable room storage additionally loses
the current code, invitation, and dormant lease, while same-browser creation
preferences can still be replayed locally. For this private, bounded product that
is a small and accepted recovery cost.
Screener serves one Host and at most 20 trusted Viewers. The application server
owns a small room and authorization aggregate while WebRTC and LiveKit own live
media sessions. Frequent application releases should not force the Host to
create another room or redistribute invitations, but a database must not become
a second owner for transient signaling, routes, media, or diagnostics.
SQLite would preserve those identifiers across restart, but it also keeps a
schema, migrations, path and writable-directory configuration, backup/restore
operations, persistence tests, and cross-restart state semantics. That ongoing
surface is not justified by the limited recovery benefit and works against the
small, ephemeral control plane. The product therefore accepts restart-wide room
loss and does not include cross-restart room recovery in its current contract.
The same product therefore needs two deployment choices over one room model:
The room experience needs a memorable code, a direct invitation, optional
code-entry protection, and short-term reuse by the same browser. It does not need
accounts, browser fingerprinting, a user database, permanent room ownership,
schema migration, or high-availability coordination.
- lightweight mode keeps no server-side room data after process exit;
- stable mode keeps only room authority in one local SQLite file so surviving
pages can reauthenticate and rebuild fresh media after an application restart.
## Decision
### One Memory-Resident Room Model
### One Room Authority Model
All room state lives in the authoritative application process:
Every room owns:
- a random four-digit room code from `1000` through `9999`;
- the room lease and current Host/Viewer membership;
- SHA-256 digests of the Host token and current Viewer grant;
- the current code-entry policy and, when enabled, a salted room-password
verifier;
- authorization generations and the existing signaling/route state.
- one random free code from `1000` through `9999`;
- a SHA-256 Host-token digest and Viewer-grant digest;
- a monotonically increasing Viewer authorization generation;
- `open | private` code entry and optional salted scrypt password material;
- one absolute dormant lease deadline, or an active-Host marker.
The free-code pool contains every currently unallocated four-digit code. A room
creation request may name one locally preferred code: the store takes it only if
it is still in the free pool, otherwise it selects uniformly from the current
pool. Room release returns the code to that pool. The complete `1000..9999`
space fixes active room capacity at 9,000; there is no separate room-limit
configuration. A room code is a locator and best-effort preference, not a
secret, reservation, or permanent identity.
`ROOM_LEASE_SECONDS` defaults to 86,400 seconds. An authenticated Host keeps the
room active; stop or disconnect starts the dormant lease. The exact Host token
may resume before expiry. Viewer activity never renews the room. Expiry or
explicit replacement invalidates every credential and releases the code.
`ROOM_LEASE_SECONDS` is the single room-lifetime setting and defaults to 86,400
seconds. An authenticated, actively sharing Host prevents expiry. When sharing
stops or the Host is no longer connected, the room remains dormant until the
lease deadline. The same Host token may resume sharing before that deadline and
renew the lease. Viewer presence never renews ownership. Expiry closes the room,
invalidates its credentials, and releases its code.
### Lightweight And Stable Storage
Process restart deliberately clears every room, lease, credential digest,
password verifier, and participant. Old Host records and invitations then fail
closed, and the next Host share creates a new room incarnation. There is no room database,
`ROOM_DATABASE_PATH`, persistent-room mode, schema migration, or compatibility
reader. The site-access secret remains deployment configuration rather than room
state.
`ROOM_DATABASE_PATH` is optional and absent by default:
### Ownership And Local Host Defaults
- when absent, the bounded RoomStore is process memory and restart loses every
room and credential;
- when present, the same RoomStore persists its authority aggregate in one
SQLite file. The production target enables this stable mode after its
persistent-state deployment and recovery checks pass.
Room ownership uses the existing random Host bearer token, not an IP address,
user agent, hardware property, or browser fingerprint. The Host browser keeps the
raw token in same-origin `localStorage`; the server keeps only its digest in the
current room. Clearing local data or changing browser/device loses ownership.
Stable mode stores only `roomId`, Host-token digest, Viewer-grant digest,
authorization generation, code-entry policy, optional password verifier, and
dormant lease deadline. It never stores raw tokens, grants, or passwords.
The Host browser also keeps one local creation profile containing its display
name, code-entry policy, and optional room password, plus one independent
non-secret preferred room code. The preference has no client-side lease, timer,
or expiry authority. When old ownership no longer works, the next explicit
share always requests that code and atomically reapplies the creation profile.
The server remains the only owner of room expiry and allocates the requested code
only when free; otherwise it selects another free code and the browser remembers
that result. Every allocation creates a new Host token, Viewer grant, password
material, server lease, and room incarnation. The site-access password is never
part of this profile. A Host room password may be stored locally
as a convenience for this private product; the server receives it only over the
authenticated creation or update path, derives the verifier, and never stores or
logs the plaintext.
The schema is one exact current version using built-in `node:sqlite`, one
connection, one writer, and transactional room mutations. An unknown schema,
wrong application identity, corrupt row, inaccessible path, or second owner
fails startup before signaling or LiveKit mutation. This private pre-release
contract has no legacy reader or migration chain; an incompatible database must
be explicitly replaced from a verified recovery boundary.
### Orthogonal Viewer Access
On restart, expired rows are deleted. A saved dormant deadline remains exact. A
row last observed with an active Host becomes dormant until
`startup time + ROOM_LEASE_SECONDS`; restart never claims that the old Host,
socket, share, or media route is still online. Successful Host reauthentication
marks it active again.
Every room has one room-scoped Viewer grant independent of code entry. The grant
is 16 cryptographically random bytes encoded as 22 base64url characters; its
SHA-256 digest is stored directly on that exact in-memory room incarnation. It
has no separate expiry clock or embedded room metadata. It remains usable only
while that room exists, and therefore ends on room reclamation or process
restart as well as explicit rotation or revocation. Reusing the same four-digit
code creates a new digest and never revives an old invitation.
Participants, display names, client/peer/session IDs, share generation, pause,
quality settings, codec decisions, route graph, pending operations, first-frame
proof, SFU tokens/resources/rooms, RTCStats, diagnostics, and logs remain
process-only. Every restart creates fresh session and route authority and media
must reconnect and recommit on a newly decoded frame. SQLite does not make a
LiveKit restart seamless or remove the browser gesture required after capture
itself ends.
The primary share action copies `/r/{code}#v={grant}`. A valid grant directly
authorizes only the Viewer role in that room. The browser consumes it into
room-scoped `sessionStorage`, immediately clears the fragment with
`history.replaceState`, and never places it in a cookie, query, log, error, or
`localStorage`.
### Host Ownership And Preferred Code
Code entry is a separate two-state policy:
The Host browser keeps the raw Host token and creation profile in same-origin
local storage. The server keeps only the token digest. Clearing browser data or
changing device loses ownership; IP, UA, hardware and browser fingerprints are
never identity.
- `open` is the default: after site access, the four-digit code admits a Viewer
without a room password;
- `private` disables passwordless code entry. Without a room password the room
is invitation-only; with one configured, matching code-and-password entry is
also admitted.
The browser also remembers one non-secret preferred room code with no local
expiry or renewal timer. It is only a future allocation hint. The server remains
the sole owner of room existence, expiry and code allocation: it reuses the hint
only when free, otherwise allocates another random free code and the browser
replaces its preference.
There is no third code-entry-disabled state; invitation access remains owned by
the independent Viewer grant.
### Explicit Room Replacement
The Host may rotate or revoke the grant without changing code-entry policy, and
may change code-entry policy without changing a healthy media route. Strong
grant revocation advances the authorization generation, clears Viewer grace and
route state, closes the old receive and send edges, and disconnects the affected
Viewers before they can reconnect with the old grant.
The Host room-code control includes a refresh action immediately before Copy.
With the exact Host token, one server operation allocates a guaranteed different
free code and retires the old room through the normal delete lifecycle. Failure
before commit leaves the old room unchanged. Success invalidates the old Host
token, Viewer grant, password, authorization generation, sessions, routes and
SFU resources, ends any active share, returns fresh room credentials, and updates
the local preferred code. No old room or media state is migrated.
`SITE_ACCESS_PASSWORD` continues to protect room creation, Host admission, and
all code-only Viewer attempts. A valid room grant may bypass that site gate only
for the exact Viewer role and room. Neither a code, room password, nor Viewer
grant can create a room or become Host authority.
### Viewer Access
After site access, a well-formed code-only attempt for an unallocated, reclaimed,
or expired room returns `ROOM_NOT_FOUND`; the Viewer shows "房间不存在或已过期"
and does not offer a room-password input. Other expected code-only admission
failures retain `ROOM_ACCESS_DENIED` and the existing optional password retry.
This intentionally reveals only that no current room owns the submitted code;
it does not expose the policy or password state of an existing room. The UI does
not request, discover, or mint an invitation through the server.
Each room has one 128-bit, 22-character base64url Viewer grant independent of
code entry. A valid grant authorizes only the Viewer role for that exact current
room. The browser consumes it into room-scoped session storage and removes the
URL fragment. Stable mode preserves only its digest so an existing invitation
continues across application restart; rotation or revocation atomically updates
the digest and authorization generation.
Exact-room grant failures remain `INVALID_TOKEN`, Host authentication retains
its independent role-specific outcomes, and unexpected internal faults remain
the generic `SERVER_ERROR`. None of those paths is folded into
`ROOM_ACCESS_DENIED`.
`open` admits site-authorized code entry without a room password. `private`
disables passwordless code entry; without a password it is invitation-only, and
with a password it additionally admits a matching code-and-password attempt.
Missing or expired codes return `ROOM_NOT_FOUND`; expected existing-room denial
returns `ROOM_ACCESS_DENIED`.
## Consequences
- Normal operation and the 24-hour renewable-room experience require only one
bounded in-memory room map and no writable database directory.
- The central control plane stays small and ephemeral; media distribution and
bandwidth remain governed by the P2P-first route model rather than moved to the
application server.
- A deployment, crash, or restart invalidates 100 percent of current rooms and
invitations. Active media already disconnects at that boundary; the additional
cost is a new room code and invitation. Same-browser Host preferences are
reapplied automatically on the next explicit share.
- A room unused beyond the server lease is released. The local preferred code
remains only a future allocation hint and never extends or proves that lease.
- Recycled code-only bookmarks may eventually identify a different room. A stale
grant remains unusable because the new room has a different digest.
- Multi-process room coordination, seamless restart, and horizontal scaling are
outside the current contract. They require a new accepted storage decision if
they become real requirements.
- Lightweight mode remains the smallest complete server and leaves no durable
room authority after process exit.
- Stable mode preserves room code, Host ownership, invitations, revocation,
password policy and lease across application releases without persisting live
topology or media state.
- Application restart in stable mode is a bounded reauthentication and media
rebuild, not uninterrupted playback. LiveKit high availability remains a
separate infrastructure decision.
- Production activation changes persistent state and therefore requires a
verified writable directory, configuration backup, database backup/restore
boundary and rollback procedure; it is not an ordinary app-only cutover.
## Acceptance Gates
- Allocation uses only `1000` through `9999`, never duplicates an active code,
admits at most the fixed 9,000-code capacity, and returns released codes to
the free pool.
- An active Host is not expired. Stop/disconnect starts the configured dormant
lease; the exact Host token resumes before expiry; Viewer activity does not.
- Expiry and process restart reject the old Host token, Viewer grant, room
password, and code-bound room state. Reusing a released code never accepts the
old grant.
- Default creation atomically produces `open` code entry with no room password
and an independent 22-character token-bearing invitation. The grant remains
valid for exactly the current room incarnation and has no independent expiry.
`open` and `private` code entry, including private rooms with and without a
password, are covered independently from grant rotate/revoke.
- Same-browser recreation reapplies the Host profile and always requests its
preferred code; an occupied code uses random allocation and replaces the local
preference. Another browser or cleared storage has no preference. No
fingerprint, client clock, or server user record participates.
- Raw site passwords, Host tokens, Viewer grants, and room passwords remain out
of application/proxy logs and server durable storage.
- On the accepted `screener-v12` Browser wire, a well-formed, site-authorized
code-only attempt for an unallocated, reclaimed, or expired room yields
`ROOM_NOT_FOUND` and no
password prompt. Existing-room policy, password, full, and bounded admission
failures retain `ROOM_ACCESS_DENIED`; exact-room grant, Host-authentication,
and unexpected-server-fault paths keep their independent typed outcomes.
## Implementation Status
Current source and production use the strict `screener-v12` wire and implement the
22-character room-incarnation grant, exact digest validation, rotate/revoke,
browser-storage privacy, `open | private` code entry, and the `ROOM_NOT_FOUND`
split without a v11 parser.
Exact release and operational evidence are owned by deployment, status, and
verification status.
- Both modes pass the same creation, ownership, admission, password,
rotate/revoke, expiry, replacement and 9,000-code-capacity behavior.
- Stable restart preserves exact room authority and dormant deadlines, converts
crash-active rooms to one configured dormant lease, and restores no participant
or media authority.
- A surviving Host and Viewer can reauthenticate after restart, receive fresh
sessions/routes, and recommit media; an old revoked grant stays revoked.
- Replacement never returns the old code, never partially retires the old room,
and invalidates all old access before the new credentials are exposed.
- Raw credentials and private media/network identifiers never enter SQLite or
logs. Corrupt, mismatched or multiply owned databases fail before service
readiness.
## Decision Sources
- [Node.js `crypto.randomBytes()`](https://nodejs.org/docs/latest-v24.x/api/crypto.html#cryptorandombytessize-callback)
- [NIST SP 800-57 Part 1 Rev. 5](https://csrc.nist.gov/pubs/sp/800/57/pt1/r5/final)
- [Node.js SQLite](https://nodejs.org/docs/latest-v24.x/api/sqlite.html)
- [SQLite transactional guarantees](https://www.sqlite.org/transactional.html)
- [SQLite locking mode](https://sqlite.org/pragma.html#pragma_locking_mode)
- [LiveKit reconnect behavior](https://docs.livekit.io/intro/basics/connect/#network-changes-and-reconnection)
@@ -1,7 +1,6 @@
# ADR-0007: Framework-Owned Media Quality Adaptation
- Status: Accepted; deployed candidate under production acceptance, canonical
integration pending
- Status: Accepted; adaptive gate deployed, local selector implementation pending
- Date: 2026-08-19
- Last reviewed: 2026-08-26
@@ -31,9 +30,12 @@ it does not need a second bitrate, resolution, FPS, or layer-control system.
but its current motion does not decide encoder capability. A proved sender
prefers H.264 and retains VP8 as
the native negotiation fallback; an unsupported, inconclusive, slow, or
failed preflight uses VP8 only. The result is not persisted or inferred from
UA, GPU, capability advertisement, or `powerEfficientEncoder` alone.
Codec UI/state/wire and active-edge codec switching are not product features.
failed preflight uses VP8 only. The Host advanced settings expose one local
`VP8 | Auto | H264` selector with Auto default: it is editable only before a
share, strict H264/VP8 bypass the gate, and share start locks it. Viewer relay
senders remain Auto. The selection is not persisted or inferred from UA, GPU,
capability advertisement, or `powerEfficientEncoder` alone; it creates no
room state, wire field, or active-edge codec switching.
2. Display video uses the standard `contentHint = "motion"`; display audio uses
`contentHint = "music"`. The hint expresses content intent and does not
promise a resolution, frame rate, bitrate, encoder, or hardware path.
@@ -44,11 +46,12 @@ it does not need a second bitrate, resolution, FPS, or layer-control system.
After accepting an answer, the Host reapplies the current video profile to
the negotiated sender because the browser may replace or rewrite encoding
parameters during negotiation.
5. Direct and Browser-relay offers with a proved share source order H.264 before VP8
in one standard codec-preference list; both endpoints select their first
common codec without a parallel media connection. A failed source gate
offers VP8 only. Existing edges are not renegotiated when a later relay gate
completes; its result applies to future children.
5. Auto direct and Browser-relay offers with a proved sender order H.264 before
VP8 in one standard codec-preference list; endpoints select their first common
codec without a parallel media connection. Failed Auto or manual VP8 offers
VP8 only; manual H264 offers H.264 mode 1 only. Existing edges are not
renegotiated when a later relay gate completes; its result applies to future
children.
6. The SFU publisher uses the Host source decision for its one publication,
disables backup codec, sets the selected HIGH ceiling and degradation
preference, but does not set `screenShareSimulcastLayers`, mutate lower
@@ -98,7 +101,8 @@ LiveKit HIGH+LOW publication without backup codec.
- Screener owns fewer media mechanisms and follows the pinned frameworks'
supported control surfaces.
- A capable Browser Host or relay can use H.264 hardware encoding while another
source remains on VP8 without a user setting or room-protocol branch.
sender remains on VP8 without a room-protocol branch; the Host can explicitly
override one future share for diagnosis or preference.
- A constrained Viewer may receive a lower LiveKit representation without
lowering every subscriber, subject to the publisher and network actually
sustaining the framework contract.
@@ -112,8 +116,9 @@ LiveKit HIGH+LOW publication without backup codec.
## Stop Lines
- No Screener resolution/FPS/bitrate ladder or representation formula.
- No codec selector, GPU/MFT allowlist, persistent codec cache, parallel
VP8/H.264 route, active-edge codec churn, or LiveKit backup publication.
- No codec wire/state, GPU/MFT allowlist, persistent codec cache, parallel
VP8/H.264 route, active-edge codec churn, or LiveKit backup publication. The
local pre-share Host selector is the only manual codec surface.
- No manual SFU layer selector, forced single HIGH publication, or per-Viewer
encoder.
- No quality score, all-pairs probing, periodic rebalancing, or speculative
+12 -9
View File
@@ -280,8 +280,9 @@ key, even blank, fails startup.
`ROOM_LEASE_SECONDS` defaults to 86,400 seconds and owns the dormant room
lifetime. An actively connected Host prevents expiry; explicit stop or Host
disconnect starts the lease, and only the exact Host token renews it before
expiry. Viewer activity never renews ownership. `ROOM_DATABASE_PATH` and
`ROOM_TTL_SECONDS` fail startup even when blank.
expiry. Viewer activity never renews ownership. Optional `ROOM_DATABASE_PATH`
enables the exact-schema SQLite stable room-authority mode; unset keeps the
lightweight memory mode. `ROOM_TTL_SECONDS` remains invalid.
Production accepts 1 through 20 and explicitly selects 20. This is an
admission limit, not evidence that every publisher, network, or quality profile
can sustain that many streams.
@@ -382,14 +383,16 @@ Room allocation and lifetime use one deliberately small model:
- only the exact Host token resumes and renews before expiry, Viewer activity
cannot, and expiry invalidates the code, credentials, verifier, participants,
and routes; and
- process restart has the same fail-closed effect.
- lightweight-mode process restart has the same fail-closed effect; stable mode
restores only unexpired room authority and requires fresh sessions/routes.
The server keeps only process-memory digests/verifiers for Host tokens, Viewer
grants, and optional room passwords. There is no room database, schema,
migration, writable room directory, or backup/restore step. The same browser may
replay its Host display-name, code-entry, and optional-password creation profile
when it explicitly creates a replacement room; this is local convenience, not
server-side identity or cross-restart recovery.
The server keeps only digests/verifiers for Host tokens, Viewer grants, and
optional room passwords. In stable mode those room-authority values, policy,
authorization generation and lease use one exact SQLite schema; live members,
routes and SFU state remain memory-only. The same browser keeps its raw Host token,
creation profile and non-expiring preferred code locally. Production stable-mode
activation requires a separately verified writable directory, configuration and
database backup/restore boundary before setting `ROOM_DATABASE_PATH`.
Production startup requires one to eight syntactically valid `stun:` URLs
before the server listens. This validates shape only; it does not prove DNS,
+1 -1
View File
@@ -39,7 +39,7 @@
机制减负遵循 [`stop-that-shit`](../.agents/skills/stop-that-shit/SKILL.md) 的 scope 和 smallest-correct-result 规则。本仓库的审查先只读清点机制服务的用户行为,量化移除后的真实退让,并与代码、配置、迁移、测试、运行时状态、部署和故障面的整体成本比较。文档也是需要同步和审查的机制:长期真相只保留产品合同、非显然不变量、算法/模型、关键取舍和外部证据,普通 UI 与实现细节由代码、测试和 PR 持有。若 owner 接受有界退让,先更新产品合同再删除失去必要性的完整表面;否则保留合同并寻找更小实现。审查不以行数为目标,也不授权无边界重构。
内存房间与应用层 TURN 分别是 [ADR-0002](./adr/0002-memory-resident-protected-rooms.md) 和 [ADR-0005](./adr/0005-automatic-hybrid-media-routing.md) 已接受的机制减负实例;TURN 的完整成本证据由 [ICE/TURN 研究](./research/built-in-peer-ice-turn.md)维护。具体取舍和当前边界不在本文件复制,也不推广为对 SQLite 或 TURN 的通用结论。
应用层 TURN 清理是 [ADR-0005](./adr/0005-automatic-hybrid-media-routing.md) 的机制减负实例;TURN 的完整成本证据由 [ICE/TURN 研究](./research/built-in-peer-ice-turn.md)维护。内存房间曾以有限恢复收益换取更小机制,但真实开发发布已证明旧邀请、撤销高水位和房间权威连续性构成一个内聚 SQLite 消费者,因此 [ADR-0002](./adr/0002-memory-resident-protected-rooms.md) 现在保留轻量与稳定两种模式。该变化说明减负结论必须随可达需求重审,而不是把一次删除推广为永不恢复该机制。
## 检查点与恢复
+24 -19
View File
@@ -9,20 +9,22 @@ Last updated: 2026-08-26
- Web Host, Viewer, and Browser relay are the current delivery surface. Native
capture, shared encoding, and distributable server/local packages remain
later work.
- Rooms are bounded process memory: random free `1000..9999` code, default
- Rooms use one bounded authority model: random free `1000..9999` code, default
24-hour dormant lease, active sharing never expires, exact Host token resume,
Viewer activity does not renew, and process restart loses every room and
credential. A Host may request its last locally remembered active code when
creating a new room; the server uses it only while free and always creates a
new room incarnation. There is no database or migration path.
and Viewer activity never renews. `ROOM_DATABASE_PATH` absent is the default
lightweight process-memory mode; configured is the optional SQLite stable
mode that persists only room authority across application restart. Production
will enable stable mode after its state/recovery gate passes.
- Every room has one 128-bit/22-character Viewer grant bound to that exact room
incarnation and independent `open | private` code entry. Private without a
password is invitation-only; adding a password also permits matching code
entry. Reclamation, restart, rotate, or revoke ends the grant. Missing codes
return `ROOM_NOT_FOUND` without a password prompt.
- Host creation preferences and raw ownership token stay local to the Host
browser. Server authority stores only current-process digests/verifiers and
never uses IP, UA, device, or browser fingerprint as identity.
entry. Reclamation, explicit replacement, lightweight restart, rotate, or
revoke ends the grant; stable restart retains its digest and generation.
Missing codes return `ROOM_NOT_FOUND` without a password prompt.
- Host creation preferences, raw ownership token and one non-expiring preferred
code stay local to the Host browser. Server authority stores only
digests/verifiers, optionally in SQLite stable mode, and never uses IP, UA,
device, or browser fingerprint as identity.
## Media And Routing
@@ -69,15 +71,16 @@ Last updated: 2026-08-26
## Media Quality
- Production candidate `5551177` runs one sender-scoped actual-sender preflight
- Canonical source and production run one sender-scoped actual-sender preflight
with a deterministic moving probe track sized to the current share target, so
captured-content motion cannot decide encoder capability. Proved senders prefer
H.264 with native VP8 fallback, while failed/inconclusive senders remain
VP8-only. Codec UI/state/wire, persistent codec cache, parallel codec media,
backup publication, and active-edge codec switching remain absent. Display
video uses `contentHint = "motion"` and display audio uses `contentHint = "music"`.
Canonical `main` has accepted this contract but its application tree remains
fixed VP8 until production acceptance and integration.
VP8-only. The accepted next Host UI adds a local pre-share
`VP8 | Auto | H264` selector with Auto default; it locks while sharing, is not
persisted, and creates no codec wire/state or active-edge switching. Viewer
relays remain Auto. Current production does not yet include that selector.
Display video uses `contentHint = "motion"` and display audio uses
`contentHint = "music"`.
- Windows Chrome 151 uses software VP8 and exposes no Web control for selecting
NVENC, AMF, QSV, a GPU, or an MFT. Edge, non-Windows paths, and real-game CPU
contention remain evidence-specific.
@@ -132,9 +135,11 @@ Last updated: 2026-08-26
## Current Priority
1. Validate and integrate production candidate `5551177`, then simplify the
local preferred-room record while retaining the server lease.
2. Complete the mobile Viewer lifecycle matrix and representative real-network
1. Implement optional SQLite stable room authority, remove the client preferred-
room expiry/timer, and add exact Host room replacement.
2. Add the locked pre-share Host codec selector and resolved Host codec display
without codec wire state.
3. Complete the mobile Viewer lifecycle matrix and representative real-network
route/media acceptance.
Quality-driven parent selection, Native sender work, distribution packages,
+79 -87
View File
@@ -1,110 +1,102 @@
# Cross-Restart Room Recovery
Status: Later optional persistence research. ADR-0002 continues to own the
default behavior: application restart loses every room, credential, lease,
invitation, route, and media authority. The owner accepts that default and does
not reopen persistence merely to preserve a preferred room code.
- Research date: 2026-08-26
- Status: SQLite stable room-authority mode accepted; implementation pending
## Current Boundary
## Current Conclusion
The application process owns the complete `RoomStore`, while the Host browser
keeps its raw Host token, creation profile, and best-effort preferred room code.
After restart the old token and Viewer grant cannot authenticate; replaying the
preferred code and profile creates a new room incarnation. LiveKit rooms and the
Screener SFU resource ledger are also generation-scoped and process-resident.
SQLite has one cohesive current consumer: the room authorization aggregate. It
is justified when application releases should retain not only a preferred code,
but the exact Host ownership, existing Viewer invitation, revocation high-water,
code-entry policy, password verifier and dormant lease. It is not a generic
store for future features.
LiveKit resume covers a transient signaling or network interruption while the
same server-side session exists. A full reconnect rebuilds participants, tracks,
publications, and subscriptions. The current deployment additionally deletes
managed LiveKit rooms before admitting a new application generation. Neither
WebRTC nor LiveKit can restore application authorization or route ownership from
surviving media packets.
`ROOM_DATABASE_PATH` absent remains the default lightweight mode. Configuring an
exact file enables stable mode over the same RoomStore contract. Production will
use stable mode after persistent-state deployment and recovery acceptance.
## Minimal Recoverable Model
## Stored Authority
The smallest candidate is Host-led lineage recovery, not persistent room-state
reconstruction:
| Field | Restart value |
| --- | --- |
| `roomId` | Retains the exact four-digit allocation. |
| Host-token SHA-256 | Lets a browser that still holds the raw token reclaim Host authority. |
| Viewer-grant SHA-256 | Keeps an existing invitation usable without persisting its raw grant. |
| Viewer authorization generation | Prevents rotate/revoke from rolling back across restart. |
| `open | private` | Preserves code-entry policy. |
| 48-byte scrypt material | Preserves optional password admission without plaintext. |
| Dormant lease deadline or active marker | Retains an exact dormant deadline; crash-active becomes dormant for one configured lease from startup. |
- a random stable `lineageId` identifies the Host-owned room across application
generations;
- every successful creation or recovery allocates a fresh random
`authorityEpoch`;
- the server signs a bounded Host recovery capsule containing the lineage,
Host-token digest, preferred code hint, server-owned expiry, and purpose;
- the Host retains the capsule and raw Host token locally; a Viewer can never
create, recover, reserve, or renew a room;
- recovery atomically verifies site access, capsule, Host token, expiry, code
availability, and replayed policy/password before publishing the new room;
- session, share, connection, route, authorization, and SFU generations are
fenced by the fresh authority epoch. Old asynchronous results fail closed.
These values form one atomic row. Create, password/policy update, rotate/revoke,
lease transition, expiry, abandon and room replacement update the row in the same
RoomStore mutation that changes memory authority.
The room code remains a best-effort locator. If another lineage already owns it,
recovery must not evict that Host; the recovering lineage receives another free
code or fails according to the future accepted contract. Client clocks, local
lease extensions, roster, graph, first-frame proof, and resource counts never
become server truth.
## Deliberately Ephemeral State
## Invitation And Revocation Boundary
SQLite does not store raw Host tokens, raw Viewer grants, room passwords, site
access credentials, participants, display names, client/peer/session IDs,
sharing generation, pause, quality settings, codec decisions, route graph,
pending operation, connection/revision, first-frame proof, SFU token/resource or
LiveKit room, SDP, ICE candidate/address, RTCStats, quality evidence, diagnostic
snapshot or log event.
A signature proves that the server issued a capsule; it does not prove that the
capsule is the newest one. Restoring the old Viewer-grant digest and authorization
generation from a still-valid capsule can replay a grant that the Host rotated or
revoked before the crash. Rejecting that stale checkpoint requires a server-side
latest-generation high-water mark or accepting replay until the capsule expires.
There is no stateless third option.
Those facts describe a current process or media generation. Restoring them would
grant stale sockets or media false authority and could undercount physical sender
or SFU resources.
An old invitation must always bind to its lineage and authorization generation.
It cannot fall through to code-only admission, enter a different lineage that
reused the same four-digit code, or create a missing room. A private room whose
local password material is unavailable must fail closed rather than become open.
## Restart Behavior
The lowest-risk first phase would recover only Host lineage, control, lease
ceiling, and a free preferred code, then issue a new invitation. Stable old
invitations are a separate decision that must settle revocation high-water and
code relocation first.
After an application restart, the database restores room authorization only.
Surviving Host and Viewer pages reconnect with their existing raw credentials,
receive fresh session/peer/connection/route generations, and recommit media on a
newly decoded frame. A surviving Host capture track may therefore recover with a
bounded interruption; a page/browser restart still requires a new capture gesture.
## Media Boundary
The application continues to clear and rebuild LiveKit rooms and the process
resource ledger. SQLite does not make a LiveKit process restart seamless. True
LiveKit failover remains its separate Redis/distributed/draining architecture.
Control recovery can be automatic while the Host tab and capture track remain
alive, but a page or browser restart still requires a new user gesture for screen
capture. Every recovered route must close or fence old P2P/SFU senders, rebuild
the graph and resource ledger, and recommit on a newly decoded frame. Media may
briefly survive a process crash, but it has no current authority and cannot be
advertised as uninterrupted service.
## Why Not A Signed Recovery Capsule
True multi-node LiveKit continuity requires its distributed Redis deployment and
draining model. That is a different infrastructure and availability decision,
not a consequence of Host lineage recovery.
A signed Host capsule is smaller only when recovery may issue a new invitation.
It cannot prove that an older capsule is not replaying a Viewer grant revoked by
a newer one. Stable old invitations plus strong rotate/revoke therefore require
a durable latest-authorization-generation high-water mark. Once that durable
owner exists, keeping the code, credential digests, policy, verifier and lease in
the same SQLite row is simpler than a client capsule, signing key and second
truth path.
## Decision Needed
## Storage And Deployment Boundary
1. Is Host lineage/control recovery sufficient, with new invitations and a
bounded media gap?
2. If old invitations must survive, does the product accept bounded stale replay
or one small durable latest-authorization-generation owner?
3. May a recovered lineage move to another room code when its hint is occupied,
and should a lineage-bound invitation resolve that new code?
4. Is a same-browser bearer recovery capsule an acceptable ownership boundary?
5. Are application and LiveKit restart targets both bounded rebuild, or is
LiveKit high availability a separately funded requirement?
The accepted implementation uses built-in Node `node:sqlite`, one exact schema,
one process connection and transactional low-frequency room writes. No ORM,
event table, stats writer, compatibility reader or migration chain is added.
Unknown schema, wrong application identity, corrupt row, inaccessible file or a
second owner fails before signaling or LiveKit mutation.
An optional SQLite mode may reopen this decision only as one complete durable
room-authority contract, disabled by default. It would own room lineage, code,
credential digests, authorization generation, policy/password verifier and a
bounded inactive-retention fact atomically; live participants, routes, stats and
SFU resources still rebuild. Unknown future data is not justification for a
generic database. SQLite can make room identity and authorization robust across
an application restart, but it cannot preserve WebSocket, WebRTC or LiveKit
process state without client reconnect and media recommit.
Production activation is not an app-only cutover. It requires an access-restricted
writable directory, configuration backup, database backup/restore procedure and
rollback that removes the new environment setting before starting a release that
does not understand it. Old database backups can revive expired credentials, so
restoring one later requires an explicit credential-invalidating decision.
## Acceptance Matrix
1. Both storage modes pass identical room behavior within one process.
2. Stable restart preserves code, token/grant validation, password, policy,
generation and lease while restoring zero participants or media state.
3. A grant revoked before restart remains revoked afterward.
4. Active-at-crash rows become dormant once; repeated restart does not repeatedly
extend an already stored dormant deadline.
5. Expired rooms are deleted and their codes become free before readiness.
6. Corrupt/mismatched/multiply owned state fails before accepting traffic.
7. Surviving Host/Viewer tabs reauthenticate and rebuild a fresh route; no test
labels that bounded rebuild as uninterrupted media.
## Primary Sources
- [W3C Capability URLs](https://www.w3.org/TR/capability-urls/)
- [RFC 6750 bearer-token threats](https://www.rfc-editor.org/rfc/rfc6750.html#section-5.2)
- [RFC 7519 expiration and token IDs](https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1)
- [RFC 9246 signed-URI replay boundary](https://www.rfc-editor.org/rfc/rfc9246.html)
- [Kubernetes names and UIDs](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/)
- [The Chubby lock service](https://storage.googleapis.com/gweb-research2023-media/pubtools/4444.pdf)
- [WebRTC ICE restart](https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-restartice)
- [Node.js SQLite](https://nodejs.org/docs/latest-v24.x/api/sqlite.html)
- [SQLite transactional guarantees](https://www.sqlite.org/transactional.html)
- [SQLite locking mode](https://sqlite.org/pragma.html#pragma_locking_mode)
- [LiveKit reconnect behavior](https://docs.livekit.io/intro/basics/connect/#network-changes-and-reconnection)
- [LiveKit distributed deployment and draining](https://docs.livekit.io/transport/self-hosting/distributed/)
- [LiveKit distributed deployment](https://docs.livekit.io/transport/self-hosting/distributed/)
+6 -4
View File
@@ -7,8 +7,9 @@
## Current Conclusion
Production candidate `5551177` uses a content-independent H.264 sender gate with
VP8 fallback; canonical source remains fixed VP8 until acceptance and integration.
Production `5551177` and canonical source use a content-independent H.264 sender
gate with VP8 fallback. A local pre-share Host `VP8 | Auto | H264` selector is
accepted but not yet implemented; Viewer relays remain automatic.
Display video uses `contentHint = "motion"`, and display audio uses
`contentHint = "music"`. The three recommended profiles and advanced settings
are ceilings, not delivery guarantees. WebRTC owns direct and peer congestion
@@ -291,8 +292,9 @@ otherwise it retains VP8, and already active edges are not churned. A single SFU
publication still cannot provide per-subscriber H.264 and VP8 without a second
encoded publication.
LiveKit backup codec is therefore not accepted. The Host source decision owns
that single publication: proved H.264 or VP8 fallback. No VP9, AV1, H.265,
product codec selector, or deployment is accepted by this evidence.
that single publication: resolved H.264 or VP8 with no backup codec. No VP9,
AV1, H.265, codec wire/cache, parallel publication, or active-edge codec switch
is accepted by this evidence.
HEVC, AV1, custom WebCodecs pipelines, and application packetization do not
replace the browser WebRTC sender without a new capture, RTP/RTCP, feedback,
+9 -9
View File
@@ -38,10 +38,9 @@ This is the current execution index. Git history owns completed timelines;
## Current Source
- Canonical root `main` is clean at `376be27`; its application/runtime tree still
matches `b6a8a5f`. Production is the separately verified rapid-iteration
candidate `5551177`; that candidate does not supersede canonical truth and is
not yet integrated.
- Canonical `main` integrated the production-tested adaptive H.264 application at
merge `636e0ac`. Production remains exact runtime `5551177`; their application
trees match while production room storage remains process memory.
- One event-driven controller owns the committed graph and one room-serial child
operation. Initial direct acquisition uses a five-second foreground window;
exact transport-connected progress may retain that candidate through the
@@ -59,11 +58,12 @@ This is the current execution index. Git history owns completed timelines;
## Current Milestone
1. Validate production candidate `5551177` across real Host, Browser relay and
SFU paths, then integrate the accepted content-independent H.264 gate and VP8
fallback into canonical `main`.
2. Remove the client-side preferred-room expiry/renewal timer while keeping the
server dormant lease; the explicit replace-room UI follows the codec work.
1. Implement and verify optional SQLite stable room authority; lightweight mode
remains default in config, while production activation follows as a separate
persistent-state deployment with recovery.
2. Remove the client preferred-room expiry/renewal timer, add atomic room
replacement, and add the locked pre-share `VP8 | Auto | H264` Host selector
with resolved codec display.
3. Finish the Android Chrome and iOS Safari Viewer lifecycle matrix.
4. Finish representative public-network direct, peer-relay, SFU, recovery,
Pause/Resume, screen-audio, real-game A/V, and all-UDP-blocked acceptance
+14 -14
View File
@@ -7,19 +7,20 @@ later topic is not implementation authority by itself.
## Now
1. **Adaptive Browser H.264 acceptance and integration.** Validate deployed
candidate `5551177` with real Host, Browser-relay and SFU paths, then integrate
it into canonical `main`. The content-independent sender probe prefers H.264
only when target cadence is proved and otherwise uses VP8. Keep codec selector,
wire state, persistent cache, parallel codec route, backup publication and
active-edge churn absent; connection details may display the actual negotiated
codec.
2. **Preferred room simplification and replacement.** Keep the server dormant
lease unchanged, but store the latest preferred code without a client expiry
or renewal timer. After H.264, add the Host refresh-icon action immediately
left of copy: retire the old room through the normal lifecycle so its
credentials, password, invitation and routes become invalid, then allocate a
new room and update the local preference.
1. **Stable room authority and room controls.** Add optional
`ROOM_DATABASE_PATH` SQLite stable mode over the existing RoomStore aggregate;
production will enable it through a separate persistent-state cutover after
restart/recovery acceptance. Keep lightweight mode default when unset. Remove
the client preferred-code expiry and renewal timer. Add the Host refresh action
immediately before Copy: atomically allocate a different room, retire all old
authority/media state, end an active share, and remember the new code.
2. **Host codec selector.** In advanced settings, add the local
`VP8 | Auto | H264` selector with Auto centered/default. It is editable only
before sharing and locked for the whole share generation. Manual modes are
strict, Auto runs the current gate, Viewer relays remain Auto, and no codec
wire/cache/parallel publication or active-edge switching is added. Two-line
subtitles are `兼容优先 | 自动选择/实际 codec | 硬件优先`; connection details
show the resolved Host codec and each edge's actual negotiated codec.
3. **Mobile Viewer lifecycle.** Run Android Chrome and iOS Safari matrices for
autoplay gesture, foreground/background audio, foreground video recovery,
lock/page reclamation, rotation, Wi-Fi/cellular migration, and assigned-relay
@@ -70,7 +71,6 @@ later topic is not implementation authority by itself.
| Item | Decision |
| --- | --- |
| Repository and distribution license | Decide before public release or package distribution. GPL/AGPL implementations remain research-only until then. |
| Optional SQLite persistence | Default memory rooms remain accepted. Define the complete opt-in durable authority, stored fields, inactive retention, credential/grant rotation and recovery before implementation. It may restore rooms and authorization across restart, but media still reconnects and recommits; unspecified future data is not scope. |
| Quality-driven route exploration | The intended model includes exact-child active reparenting, relay ingress reparent with subtree retention, relay egress abdication/drain, and Host egress convergence onto one SFU publication while SFU-fed Viewers may still relay. Research must first unify their evidence, reservations, probation/restore and recovery under the existing single graph/operation; no active quality route change is authorized yet. |
## Evidence Boundaries
+11 -9
View File
@@ -2,7 +2,7 @@
## 2026-08-18 14:45
状态:production 与 canonical source 使用严格 `screener-v12`。Canonical source 已实现每分享代次的 H.264 实际 sender gate 与 VP8 fallback,production 仍是待切换的固定 VP8 artifact;两者都使用 video `contentHint = "motion"` 和 audio `music`,且 codec UI/state/wire、自定义 SFU layer list与应用媒体质量 controller 均不存在。SFU 由 pinned LiveKit 默认 representations、Dynacast、server send-side BWE 和 disabled AdaptiveStream 拥有质量适配;screen audio 使用 96/128/192 kbps ceiling、stereo、DTX off 与 RED off。精确执行状态由 `docs/status.md` 持有。
状态:production 与 canonical source 使用严格 `screener-v12`,均已实现每分享代次的 H.264 sender gate 与 VP8 fallback;两者都使用 video `contentHint = "motion"` 和 audio `music`。本阶段接受但尚未实现 Host 本地 pre-share `VP8 | 自动 | H264` selector,以及 SQLite stable room authority、无期限 preferred code 和换房控制;这些不新增 codec wire、自定义 SFU layer list或应用媒体质量 controller。SFU 由 pinned LiveKit 默认 representations、Dynacast、server send-side BWE 和 disabled AdaptiveStream 拥有质量适配;screen audio 使用 64/128/192 kbps ceiling、stereo、DTX off 与 RED off。精确执行状态由 `docs/status.md` 持有。
## 目标与边界
@@ -80,15 +80,17 @@ code-only policy 使用单一 `open | private` 状态:`open` 是无房间密
房间准入管理统一使用同源 `POST /api/rooms/{roomId}/access`:site-access cookie 与 `Authorization: Bearer {hostToken}` 共同授权一个受 schema 约束的 policy、password、rotate 或 revoke 动作。该控制面在分享活动和休眠期间语义相同,不建立 Host participant/WebSocket/media route,不改变 `sharingActive` 或 dormant lease;rotate/revoke 的现有强撤销副作用保持不变。
Host 的同源 `localStorage` creation profile 保存显示名、code-only policy 和可选房间密码,使进程重启或租约过期后的下一次显式分享可原子应用同一偏好。建房响应仍返回实际 `roomLeaseSeconds` 并仅由 Host ownership/服务端房间生命周期使用;独立 preferred-room 记录只保存上次实际分配的四位号码,不保存时间、不运行续写 interval,也不证明或延长服务端租期。旧 ownership 过期/失效后始终请求该号码,服务端只在空闲时采用,否则随机分配并让客户端覆盖本地偏好。该密码只保存在 Host 浏览器;不保存在 Viewer 浏览器、URL/query/fragment、cookie、日志、错误或服务器持久存储。建房/更新请求通过 HTTPS 把明文短暂交给服务端,服务端使用 Node 标准异步 `scrypt` 固定 `N=16384,r=8,p=1`、16-byte 随机 salt 和 32-byte verifier,只在当前房间内存保存 48-byte material。进程级 gate 最多执行 2 个、等待 16 个派生,队满即有界失败;任务开始和提交前都复核当前 Host authority 与 room material。不存在、已回收或已过期房间使用 `ROOM_NOT_FOUND`;现存房间的未配置/错误密码和正确密码后的满房使用 `ROOM_ACCESS_DENIED`。
Host 的同源 `localStorage` creation profile 保存显示名、code-only policy 和可选房间密码,使 ownership 失效或租约过期后的下一次显式分享可原子应用同一偏好。建房响应仍返回实际 `roomLeaseSeconds` 并仅由 Host ownership/服务端房间生命周期使用;独立 preferred-room 记录只保存上次实际分配的四位号码,不保存时间、不运行续写 interval,也不证明或延长服务端租期。旧 ownership 过期/失效后始终请求该号码,服务端只在空闲时采用,否则随机分配并让客户端覆盖本地偏好。该密码只保存在 Host 浏览器;不进入 Viewer 浏览器、URL/query/fragment、cookie、日志或错误。建房/更新请求通过 HTTPS 把明文短暂交给服务端,服务端使用 Node 标准异步 `scrypt` 固定 `N=16384,r=8,p=1`、16-byte 随机 salt 和 32-byte verifier;稳定模式只持久化这 48-byte verifier material,轻量模式只保存在当前 RoomStore。进程级 gate 最多执行 2 个、等待 16 个派生,队满即有界失败;任务开始和提交前都复核当前 Host authority 与 room material。不存在、已回收或已过期房间使用 `ROOM_NOT_FOUND`;现存房间的未配置/错误密码和正确密码后的满房使用 `ROOM_ACCESS_DENIED`。
邀请使用 `/r/{roomId}#v={grant}`。RFC 3986 要求 fragment 在 dereference 前由 user agent 分离,因此 HTTP request-target 与反向代理 access log 看不到它;WHATWG WebSocket 还明确拒绝含 fragment 的 URL。客户端只从页面 URL 严格解析一次,把 grant 写入 `screener:viewer-grant:{roomId}` 的 `sessionStorage`,随即以 `history.replaceState` 把地址栏替换为 canonical `/r/{roomId}`;刷新和页面内 WSS 重连继续使用该页面会话的值。Host 建房/rotate 收到的 raw grant 同样只在内存和 room-scoped `sessionStorage` 中保留,用它临时构造复制链接;`localStorage` 的 room ownership 只包含 canonical URL、room ID、Host token 和期限。grant 不进入 cookie、query、WebSocket URL、Referrer、日志或错误。生产继续发送 `Referrer-Policy: no-referrer`;fragment 与该 header 降低传输泄漏,但不能防止原邀请消息、浏览器扩展、截图或用户转发,UI 必须把链接称为访问凭据。
房间只有一种易失模型。进程启动时建立 `1000..9999` 的空闲码池;创建房间可携带一个严格四位 `preferredRoomId`,若它仍在空闲池则以同一个 swap-remove 取走,否则用加密随机索引从当前空闲集合分配,释放时放回。偏好请求不预占、不替换现存房间,也不改变新房凭据生成。这样在固定 9000 个活动房间容量内没有碰撞重试、递增身份、独立房间上限或数据库序列。
房间只有一个权威模型和两种存储模式。进程启动时以 `1000..9999` 建立空闲码池,并先扣除稳定模式恢复的未过期房间;创建房间可携带一个严格四位 `preferredRoomId`,若它仍在空闲池则以同一个 swap-remove 取走,否则用加密随机索引分配,释放时放回。偏好请求不预占、不替换现存房间,也不改变新房凭据生成。固定 9000 个活动房间容量不需要碰撞重试、递增身份、独立房间上限或数据库序列。
`ROOM_LEASE_SECONDS` 默认 86400。RoomStore 对连接中的 Host 不执行到期;Host 明确停止或连接消失时将 `expiresAt` 设为 `now + lease`,同一 Host token 在期限内重新鉴权并分享时续租,Viewer 活动不修改期限。清理循环删除 dormant expired room、关闭其 session/route/resource 并把号码归还空闲池。应用进程重启直接清空全部房间;配置不再包含 `ROOM_DATABASE_PATH`、`ROOM_TTL_SECONDS` 或任何 schema/migration。所有房间继续生成 256-bit Host token,Host 浏览器保存 raw token,服务器当前进程只保存摘要;不使用 IP、UA、设备信息或浏览器指纹认领。
`ROOM_LEASE_SECONDS` 默认 86400。RoomStore 对连接中的 Host 不执行到期;Host 明确停止或连接消失时将 `expiresAt` 设为 `now + lease`,同一 Host token 在期限内重新鉴权并分享时续租,Viewer 活动不修改期限。清理循环删除 dormant expired room、关闭其 session/route/resource 并把号码归还空闲池。`ROOM_DATABASE_PATH` 缺失时全部 authority 只在内存;配置绝对文件路径时,一个 exact-version `node:sqlite` owner 事务持久化 roomId、Host/grant digest、authorization generation、policy、可选 verifier 和 lease。启动删除过期行,保留 dormant absolute deadline,并把 crash-active 行转换为 `startup now + lease`;participant/session/share/pause/quality/codec/route/SFU/stats 一律不恢复。错误 application id、schema、row、路径或第二 owner 在 signaling/LiveKit 前 fail closed,不维护旧 schema reader 或 migration chain。所有房间继续生成 256-bit Host token,Host 浏览器保存 raw token,服务器只保存摘要;不使用 IP、UA、设备信息或浏览器指纹认领。
rotate/revoke 是 grant 路径的强撤销:服务端先同步提交新 digest/authorization generation,再清除 Viewer grace、连接代次与路由状态,向 Viewer 发送通用访问已撤销终态使其立即关闭接收 `RTCPeerConnection`/媒体并停止重连,同时让每个 Host/relay/SFU upstream owner 关闭旧代次发送边,最后关闭 Viewer socket。不能只断 WSS,因为健康 P2P 可在信令断开后继续播放。rotate 生成并仅返回一次新 grant;revoke 写入没有对应 raw grant 的 fresh random locked digest。code-only policy 和密码保持独立;其变更只影响以后 code-only admission,除非 Host 明确执行强撤销。服务端永不返回现有 grant 明文;丢失 Host 标签页中的 grant 时只能 rotate。
rotate/revoke 是 grant 路径的强撤销:服务端先在 RoomStore 中原子提交新 digest/authorization generation(稳定模式同一 SQLite transaction),再清除 Viewer grace、连接代次与路由状态,向 Viewer 发送通用访问已撤销终态使其立即关闭接收 `RTCPeerConnection`/媒体并停止重连,同时让每个 Host/relay/SFU upstream owner 关闭旧代次发送边,最后关闭 Viewer socket。不能只断 WSS,因为健康 P2P 可在信令断开后继续播放。rotate 生成并仅返回一次新 grant;revoke 写入没有对应 raw grant 的 fresh random locked digest。code-only policy 和密码保持独立;其变更只影响以后 code-only admission,除非 Host 明确执行强撤销。服务端永不返回现有 grant 明文;丢失 Host 标签页中的 grant 时只能 rotate。
房间号控件在复制按钮左侧提供换房 icon。它携带 exact Host token 与当前 creation profile 请求一个保证不同的新房间;服务端先完成新密码 material 与凭据生成,再以一个 RoomStore transaction 提交新 room 并删除旧 room,随后复用正常 abandon lifecycle 清除旧 session、route 与 SFU resource。提交前失败保持旧房不变;成功后活动分享结束、所有旧访问失效,客户端写入新 ownership、邀请和无期限 preferred code,不迁移旧媒体或自动重开 capture。
房间规则:
@@ -104,7 +106,7 @@ rotate/revoke 是 grant 路径的强撤销:服务端先同步提交新 digest/
Web Host 在 `authenticate` 中显式发送 `viewerPresence: true` capability,Viewer 也可显式订阅同一 participant snapshot;Native sender 不在 accepted Browser wire 内并在 authority 前拒绝。两端可在 `authenticate.displayName` 发送名称,并以 `set-display-name` 更新当前会话。服务端只向已订阅的 Web participant 发送完整 `viewer-presence` snapshot,列表严格来自当前带 sessionId 的在线参与者,不含五秒媒体恢复 grace 成员;条目用 `role` discriminator 区分 Host/Viewer。加入、离线、会话替换、改名和 active route 改变都会重发快照;该只读广播不调用 connect/reconcile,也不建立任何媒体边。Host 的“在线”Viewer 总数只取 Viewer 条目数;最多两条本机媒体快照只补充当前以 Host 为 active upstream 的连接指标。
每个 presence 条目包含 `role`、`peerId`、`displayName` 和服务端 active assignment 的精确 `upstream`:Host 为 `none`,Viewer 为 `none | peer(peerId) | sfu`。UI 默认只展示名称;房内出现同名时,仅为同名项追加 peerId 尾部至少六字符短后缀,并只在同名项后缀碰撞时逐字符有界延长至完整 peerId。卡片状态只依据本机连接或当前 Viewer 接收证据显示“已连接”,否则保持中性的“线路分配中”或具体连接状态;active assignment 本身不是连接成功证据。Host 对 direct、Peer 转发和 SFU Viewer 使用完全相同的卡片结构,不生成 SFU 分组或 Host publication 伪 Viewer。连接详情同样等待当前媒体证据,再显示绿色 `P2P` 或黄色 `SFU fallback`;每张卡优先使用该 Viewer 回报的实际接收指标,Host 直连发送快照只在尚无回报时提供同结构回退。一级固定为分辨率、帧率、码率和丢包,二级按方向只渲染当前有值的 RTT、jitter、冻结/丢帧、解码耗时、音频播放质量,或捕获/编码输入、编码耗时、编码器与质量限制。正常协商的 H.264/VP8 与 Opus 不显示;实际 codec 超出 accepted contract 时在卡片内提示。候选地址、候选对 ID、STUN 计数、RID、fmtp、常规 sender 请求/读回和其他内部标识不渲染。Viewer 接收证据由服务端绑定当前 `viewer/upstream/connection/route revision`:Peer 子边只送达其直接父节点及 Host,SFU 接收只送达 Host;因此 SFU Viewer 继续带 Peer 子节点时,两名 Viewer 的证据仍各自归属,不逐层转发或互相冒充。Host 与 Viewer 都可通过独立拓扑按钮从 authoritative assignment snapshot 展示 Host、实际存在的 SFU 节点及精确上下游;Viewer 默认收起并放在视频下方,没有当前 Host presence 时不生成占位拓扑。拓扑不把计划线路当作连接成功,也不显示 IP 或唯一名称对应的 peerId。peerId 不因此成为授权身份或路由输入;名称、presence 与拓扑视图不改变授权、媒体或路由状态机。
每个 presence 条目包含 `role`、`peerId`、`displayName` 和服务端 active assignment 的精确 `upstream`:Host 为 `none`,Viewer 为 `none | peer(peerId) | sfu`。UI 默认只展示名称;房内出现同名时,仅为同名项追加 peerId 尾部至少六字符短后缀,并只在同名项后缀碰撞时逐字符有界延长至完整 peerId。卡片状态只依据本机连接或当前 Viewer 接收证据显示“已连接”,否则保持中性的“线路分配中”或具体连接状态;active assignment 本身不是连接成功证据。Host 对 direct、Peer 转发和 SFU Viewer 使用完全相同的卡片结构,不生成 SFU 分组或 Host publication 伪 Viewer。连接详情同样等待当前媒体证据,再显示绿色 `P2P` 或黄色 `SFU fallback`;每张卡优先使用该 Viewer 回报的实际接收指标,Host 直连发送快照只在尚无回报时提供同结构回退。一级固定为分辨率、帧率、码率和丢包,二级按方向只渲染当前有值的 RTT、实际 H264/VP8 codec、jitter、冻结/丢帧、解码耗时、音频播放质量,或捕获/编码输入、编码耗时、编码器与质量限制;超出 accepted codec contract 时另行提示。Host capture strip 在详情开启时追加 share-generation resolved codec,不冒充逐 edge 协商。候选地址、候选对 ID、STUN 计数、RID、fmtp、常规 sender 请求/读回和其他内部标识不渲染。Viewer 接收证据由服务端绑定当前 `viewer/upstream/connection/route revision`:Peer 子边只送达其直接父节点及 Host,SFU 接收只送达 Host;因此 SFU Viewer 继续带 Peer 子节点时,两名 Viewer 的证据仍各自归属,不逐层转发或互相冒充。Host 与 Viewer 都可通过独立拓扑按钮从 authoritative assignment snapshot 展示 Host、实际存在的 SFU 节点及精确上下游;Viewer 默认收起并放在视频下方,没有当前 Host presence 时不生成占位拓扑。拓扑不把计划线路当作连接成功,也不显示 IP 或唯一名称对应的 peerId。peerId 不因此成为授权身份或路由输入;名称、presence 与拓扑视图不改变授权、媒体或路由状态机。
## 信令协议
@@ -162,7 +164,7 @@ accepted 的唯一 wire 是 `screener-v12`,不做版本协商、旧 parser、
每次 `setParameters()` 都必须从当次 `getParameters()` 派生更新,然后读回 requested/applied `maxBitrate`、`maxFramerate`、`scaleResolutionDownBy` 和 degradation preference。浏览器负责具体降级,实际结果由 stats 观测;拒绝或浏览器改写必须进入可见状态,不能只写 `console.warn`。
Accepted `screener-v12` Web 视频使用页面内 share-generation-scoped H.264/VP8 决策。初始 Host capture 在取得 room/route authority 前,以一对无 ICE server 的本地标准 `RTCPeerConnection` 运行一个有界 H.264-only sender probe。Probe 使用按当前分享 effective target dimensions/frame rate 生成的短时确定性运动 Canvas track,并以 `motion` 表达同一内容意图;真实 capture/upstream track 只提供目标规格,其当前内容是否变化不参与 encoder capability 判断。Gate 只用实际协商 codec、同一身份的 probe media-source/encoded progress 与 bounded completion 判断,不读取 UA/GPU/MFT allowlist,也不单独相信 capability 或 `powerEfficientEncoder`;标准 Canvas capture 不可用、失败或 inconclusive 时使用 VP8。成功结果让后续 P2P/browser-relay offer 按浏览器原生 H.264 capability 顺序列出 H.264,再列 VP8 与 RTX/RED/FEC repair capabilities;失败或 inconclusive 只列 VP8 与 repair。Relay 在该分享代首次收到 upstream video 且没有 active/prepared child 时运行同一 gate;若 child prepare 先到,则等待已经在运行的 bounded gate 结束后只创建一份真实 child sender。质量设置、换源和换父不重复 gate。无法应用 codec preferences 或缺少 VP8 时明确失败。SFU publication 使用 Host share generation 的同一 H.264/VP8 决定并关闭 backup codec。所有新 Web peer sender 与 SFU publication 保留 `motion` 和用户选择的分辨率、帧率及码率 ceiling;当 desired degradation preference 允许分辨率下降时,启动阶段的 effective preference 暂为 `maintain-resolution`,当前 sender 累计编码至少五帧后由既有 stats 路径一次性应用 desired preference。该切换使用标准 `setParameters()` 进入或离开 balanced 时的原生 restriction reset,不增加周期重写、质量评分或应用层调档器。UI 和 quality state/wire 没有 codec 字段;gate 结果不持久化、不改写已连接 edge,实际 outbound codec/profile、encoder implementation 与 stats 是只读结果真相。应用 SDP 变换边界仍只有 Viewer answer 的 Opus stereo receive preference。
Accepted `screener-v12` Web 视频使用页面内 share-generation-scoped H.264/VP8 决策。Host 高级设置在未分享时按从低到高显示 `VP8 | 自动 | H264`,默认自动,分享开始后锁定。自动模式在取得 room/route authority 前,以一对无 ICE server 的本地标准 `RTCPeerConnection` 运行一个有界 H.264-only sender probe;手动 VP8 或 H264 跳过 gate 并严格使用所选 primary codec。Probe 使用按当前分享 effective target dimensions/frame rate 生成的短时确定性运动 Canvas track,并以 `motion` 表达同一内容意图;真实 capture/upstream track 只提供目标规格,其当前内容是否变化不参与 encoder capability 判断。Gate 只用实际协商 codec、同一身份的 probe media-source/encoded progress 与 bounded completion 判断,不读取 UA/GPU/MFT allowlist,也不单独相信 capability 或 `powerEfficientEncoder`;标准 Canvas capture 不可用、失败或 inconclusive 时使用 VP8。自动成功让后续 offer 按浏览器原生 H.264 capability 顺序列出 H.264,再列 VP8 与 repair formats;自动失败或手动 VP8 只列 VP8,手动 H264 只列 H.264 mode 1。Viewer relay 始终自动,并在该分享代首次收到 upstream video 且没有 active/prepared child 时运行同一 gate;若 child prepare 先到,则等待已经在运行的 bounded gate 结束后只创建一份真实 child sender。质量设置、换源、换父和分享暂停/继续不重复决定;只有停止后开始新的分享代才按当前 selector 重新判断。Viewer 原生播放器暂停不改变 remote track 或 relay gate。无法应用 codec preferences 或缺少所选 codec 时明确失败。SFU publication 使用 Host share generation 的同一 resolved H.264/VP8 决定并关闭 backup codec。所有新 Web peer sender 与 SFU publication 保留 `motion` 和用户选择的分辨率、帧率及码率 ceiling;当 desired degradation preference 允许分辨率下降时,启动阶段的 effective preference 暂为 `maintain-resolution`,当前 sender 累计编码至少五帧后由既有 stats 路径一次性应用 desired preference。该切换使用标准 `setParameters()` 进入或离开 balanced 时的原生 restriction reset,不增加周期重写、质量评分或应用层调档器。Host selector 只在页面内存,resolved 结果也不持久化、不进入 quality state/wire、不改写已连接 edge;实际 outbound codec/profile、encoder implementation 与 stats 是只读结果真相。应用 SDP 变换边界仍只有 Viewer answer 的 Opus stereo receive preference。
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 始终可用。
@@ -337,7 +339,7 @@ Token-free SFU standby prewarm 保持可选且不创建 participant、publicatio
- SiteAccess 的 production 必配、8 至 128 visible-ASCII bytes/独立 secret、local-dev 显式关闭、12 小时签名 cookie、过期、篡改、cookie 属性、已移除 access key fail-fast 和无服务端 session 状态;
- RoomStore 的四位空闲码随机分配/释放、活动分享免过期、dormant lease/续租、Host token/Viewer grant 摘要验证、22 字符 128-bit grant 与 exact room incarnation 同寿、`open/private` 正交 policy、私密无密码仅邀请可入、跨房/同号重建拒绝、rotate/revoke 强撤销、Viewer 上限和超额拒绝;
- 进程重启后 room state 全空、旧 Host/grant/password fail closed、释放号码复用时旧 grant 仍失败;`ROOM_LEASE_SECONDS` 的默认/边界与固定 9000 房间码池,已删除的 `ROOM_DATABASE_PATH`/`ROOM_TTL_SECONDS` fail-fast;
- RoomStore 的轻量/SQLite 稳定双模式等价行为、exact schema/application identity/单 owner fail-fast、稳定重启只恢复 authority、crash-active 转 dormant、旧 revoked grant 仍拒绝,以及 `ROOM_LEASE_SECONDS` 默认/边界与固定 9000 房间码池;`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` 保持独立边界;
- Accepted v12 contract 使用单一 `screener-v12` 信令 literal、严格 JSON schema、方向与房间授权校验、payload 上限和 client-mismatch 刷新终态;分享状态由 `shareGeneration`-fenced paused boolean 表达,missing room 使用 `ROOM_NOT_FOUND`,现存房间准入失败使用 `ROOM_ACCESS_DENIED`,Viewer route failure 为 typed terminal,Host-only bounded route snapshot 保留给自动验收但不进入产品 UI。`qualityProfileIdSchema` 保持三档,`qualityResolutionSchema` 包含 `480p`,`DEFAULT_QUALITY_SETTINGS` 为 1080p30,quality schema 严格拒绝 codec 字段;
- 质量协议的 host 授权、严格字段与数值范围、last-wins、当前/未来 relay、已配置 SFU 和普通 P2P wire 兼容;sender 的四字段 requested/applied 读回、拒绝与持续受限提示;screen audio SDP helper 覆盖 CRLF、existing/missing fmtp、unknown key preservation、幂等、payload number、rejected/non-audio,以及 empty/no-Opus/ambiguous/parser 异常原对象 fallback,`ViewerPeer` 覆盖 initial/restart/rebuild 时同一结果的 local/signaling 一致性,SFU publisher 精确断言 high-quality stereo/forceStereo/DTX/RED-off;
@@ -363,7 +365,7 @@ Token-free SFU standby prewarm 保持可选且不创建 participant、publicatio
## 当前交付边界
- Production 与 canonical source 使用严格单一 `screener-v12` wire。Canonical source 的每分享代次 actual-sender gate 在证据通过时让未来 direct/relay 和 Host 单一 SFU publication 使用 H.264,否则使用 VP8;production 仍是固定 VP8 artifact。两者都使用 video `contentHint = "motion"`、audio `music`,不存在 codec selector、quality-state/wire 字段、自定义 SFU layer list、backup publication或健康 edge codec 切换。Browser SFU 使用 pinned LiveKit 默认 representations、Dynacast、server send-side BWE、disabled AdaptiveStream 与 RED off;精确执行状态由 `docs/status.md` 持有。
- Production 与 canonical source 使用严格单一 `screener-v12` wire。每分享代 actual-sender gate 在证据通过时让未来 direct/relay 和 Host 单一 SFU publication 使用 H.264,否则使用 VP8。当前 production 尚无手动 selector;本阶段实现本地 pre-share `VP8 | 自动 | H264` 并保持 quality-state/wire、自定义 SFU layer list、backup publication与健康 edge codec 切换不存在。Browser SFU 使用 pinned LiveKit 默认 representations、Dynacast、server send-side BWE、disabled AdaptiveStream 与 RED off;精确执行状态由 `docs/status.md` 持有。
- 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。
+13 -12
View File
@@ -33,11 +33,11 @@ SFU 的 publication、subscription、server-resource accounting、临时 overlap
- 断线恢复:临时网络变化先交给 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。
- 每名 child 只有一个有界 operation deadline;foreground direct 预算、SFU 可用性优先和后台 direct 收敛由 ADR-0005 的同一 cursor 模型定义,不配置逐 candidate timeout。Exact direct 的标准WebRTC transport-connected只把当前wake延长到该总deadline,不提交graph;第一解码帧仍是唯一commit证据。20 名 Viewer 同时加入时,单一 room-serial operation 仍可能形成线性排队尾延迟;在接受另一种并发模型前,只测量 queue wait、candidate start、first decoded frame 与 final route 的 p50/p95/max,不用 NAT 猜测、TCP probe 或 magic timeout 绕过排队。
- 可观测性:按目标 RTP transport 与不透明连接/ICE 代次关联 selected path、`iceGatheringState`/ICE/connection state、事件推导的建立时长、generation-baselined pair-change delta,以及 RTT、RTP 丢包率、抖动、码率、帧率、编码耗时和受限原因。候选、route decision、队列时序与错误的 wire 和日志使用封闭 schema 和 code bucket,禁止 raw SDP、candidate、地址、URL、`errorText`、真实 peer/parent/session/connection identity 或网络标识。Host 与 Viewer 产品 UI 不提供连接诊断文件下载;本地折叠详情只显示当前页面直接观测到的连接事实。现有 Host-only bounded route snapshot 保留给自动验收,不连续推送、不进入产品 UI、也不形成 route authority。只有创建该 `RTCPeerConnection` 的本机可显示浏览器对 selected local/remote candidate 实际暴露的地址与端口;该值不上传、不持久化、不用于地理/NAT 推断,缺失或被隐私保护时显示未知。
- 渐进式诊断:分享者和观看者默认只看到房间、画面、核心状态和常用控制;Host 对每名 Viewer 使用同一种卡片,不按 direct、Peer 转发或 SFU 分组。卡片只把本机连接或当前 Viewer 接收证据称为“已连接”,其余状态保持中性的“线路分配中”或具体连接状态,不把 active assignment 当作连接成功。展开连接详情也只在当前媒体证据成立后显示绿色 `P2P` 或黄色 `SFU fallback`;一级只保留分辨率、帧率、码率和丢包等一眼可读的实际画面结果,二级只显示当前确有值且能解释网络、编解码或播放质量的指标。正常协商的 H.264/VP8 与 Opus 不常驻展示,只在实际 codec 超出 accepted H.264/VP8 contract 时提示;候选地址、候选对 ID、STUN 计数、RID、fmtp 和其他浏览器内部标识不进入产品 UI。Host/Peer/SFU 的精确上下游与分支结构只由独立权威拓扑视图表达,不在卡片内重复。远端 Viewer evidence 的详情 shell 在同一 viewer/upstream/connection/route identity 内保持稳定并保留每个字段最后一次真实非空观测;五秒 freshness 只决定 evidence 能否继续证明“已连接”,`connecting`/`disconnected` 立即降为非 fresh,`failed`/`closed`、identity/session/share/route 变化或非单调 sequence 则清空/换代。隐藏诊断不能隐藏失败原因、音频缺失、持续画质受限、编码合同偏离或需要用户操作的告警。
- 渐进式诊断:分享者和观看者默认只看到房间、画面、核心状态和常用控制;Host 对每名 Viewer 使用同一种卡片,不按 direct、Peer 转发或 SFU 分组。卡片只把本机连接或当前 Viewer 接收证据称为“已连接”,其余状态保持中性的“线路分配中”或具体连接状态,不把 active assignment 当作连接成功。展开连接详情也只在当前媒体证据成立后显示绿色 `P2P` 或黄色 `SFU fallback`;一级只保留分辨率、帧率、码率和丢包等一眼可读的实际画面结果,二级只显示当前确有值且能解释网络、编解码或播放质量的指标,并显示实际 H264/VP8 codec。Host capture strip 在详情开启时显示本分享代 resolved codec;这不是每条 edge 的协商替代品。实际 codec 超出 accepted H264/VP8 contract 时继续提示;候选地址、候选对 ID、STUN 计数、RID、fmtp 和其他浏览器内部标识不进入产品 UI。Host/Peer/SFU 的精确上下游与分支结构只由独立权威拓扑视图表达,不在卡片内重复。远端 Viewer evidence 的详情 shell 在同一 viewer/upstream/connection/route identity 内保持稳定并保留每个字段最后一次真实非空观测;五秒 freshness 只决定 evidence 能否继续证明“已连接”,`connecting`/`disconnected` 立即降为非 fresh,`failed`/`closed`、identity/session/share/route 变化或非单调 sequence 则清空/换代。隐藏诊断不能隐藏失败原因、音频缺失、持续画质受限、编码合同偏离或需要用户操作的告警。
- 站点访问:生产部署必须配置独立的 `SITE_ACCESS_PASSWORD` 控制建房、Host 信令角色和所有 code-only Viewer 入口;只有本地开发/测试可显式关闭。非空值接受 8 至 128 个可见 ASCII bytes,不能复用 LiveKit、TLS 或其他部署 secret。`POST /api/site-access` 验证成功签发 12 小时、无服务端会话状态的 HttpOnly、`SameSite=Strict` site-access cookie。有效 room-scoped fragment grant 可绕过站点 gate 直接进入对应房间;其余 `/r/{code}` 和 `/join` 必须先持有效 site-access cookie,之后才检查房间是否存在与 `open | private` code-only policy。未分配、已回收或已过期的 code 返回 `ROOM_NOT_FOUND`;现存私密房间的无密码、缺失或错误密码及容量拒绝继续使用 `ROOM_ACCESS_DENIED`。建房 HTTP 只认该 cookie。已移除的旧环境变量、endpoint 和 cookie 不保留兼容。不引入账号、JWT、服务端 session Map 或通用 logout。
- 房间级观看授权:新房间总是生成一个 128-bit 随机、22 字符 base64url、可 rotate/revoke 的 room-scoped Viewer grant;服务端内存只在该 exact room incarnation 保存完整 grant 的 SHA-256 摘要。grant 没有独立到期时间或内嵌 room/expiry 字段,与房间同寿,并在房间回收、进程重启、rotate 或 revoke 时失效;同号新房间使用新摘要,旧链接不能进入。浏览器从 URL fragment 严格读取 grant 后,只写该房间的 `sessionStorage` 并立即用 `history.replaceState` 清除 fragment;刷新和页面内重连可继续。code-only 入口独立采用 `open | private`,默认 `open` 且无房间密码;`private` 可不设密码并拒绝全部 code-only 尝试,设置密码后只接受匹配值。所有 code-only 尝试先通过站点访问。grant 不得进入 `localStorage`、cookie、query、WebSocket URL、Referrer、日志或错误。Host 可独立 rotate/revoke grant、改变 code-only policy 或设置/移除密码;授权变化必须先提交新的内存代次,再清除 Viewer grace/路由、终止旧代次信令和媒体并断开受影响 Viewer。
- 易失房间状态:服务器只在一个有界内存 RoomStore 中保存房间码、租期、Host token/Viewer grant 摘要、code-only policy、可选 48-byte 房间密码校验材料、参与者和控制状态。`ROOM_LEASE_SECONDS` 默认 `86400`;Host 正在分享时不回收,停止分享或 Host 断开后开始 dormant lease,同一 Host token 在期限内重新分享即续租,Viewer 活动不续租,过期后释放号码。应用进程重启时全部房间、租约、邀请和密码状态直接失效;不使用 SQLite、`ROOM_DATABASE_PATH`、schema migration、账号、用户表或服务器 session 表。
- 本地 Host 偏好:房间所有权继续使用随机 Host token,不使用 IP、UA、硬件信息或浏览器指纹。Host 浏览器在同源 `localStorage` 保存 raw Host token,以及昵称、code-only policy 和可选房间密码组成的创建偏好;另以无秘密记录只保存上次实际分配的四位号码。该 preferred room 没有本地租期、倒计时或续写 timer,也不证明或延长服务端 `ROOM_LEASE_SECONDS`。服务器只在当前进程内保存 token 摘要和密码 verifier。旧 ownership 过期/失效后的下一次显式分享始终请求原号码;服务端只在号码空闲时分配,否则随机分配并返回新的本地偏好。两种结果都创建全新 room incarnation、Host token 和 Viewer grant,不恢复旧租约、密码材料、成员或路由。换设备、换浏览器或清理站点数据即没有该偏好。
- 房间级观看授权:新房间总是生成一个 128-bit 随机、22 字符 base64url、可 rotate/revoke 的 room-scoped Viewer grant;服务端只在该 exact room incarnation 保存完整 grant 的 SHA-256 摘要,稳定模式可持久化该摘要与 authorization generation。grant 没有独立到期时间或内嵌 room/expiry 字段,与房间同寿,并在房间回收、显式换房、轻量模式重启、rotate 或 revoke 时失效;同号新房间使用新摘要,旧链接不能进入。浏览器从 URL fragment 严格读取 grant 后,只写该房间的 `sessionStorage` 并立即用 `history.replaceState` 清除 fragment;刷新和页面内重连可继续。code-only 入口独立采用 `open | private`,默认 `open` 且无房间密码;`private` 可不设密码并拒绝全部 code-only 尝试,设置密码后只接受匹配值。所有 code-only 尝试先通过站点访问。raw grant 不得进入 `localStorage`、cookie、query、WebSocket URL、Referrer、SQLite、日志或错误。Host 可独立 rotate/revoke grant、改变 code-only policy 或设置/移除密码;授权变化必须先提交新的权威代次,再清除 Viewer grace/路由、终止旧代次信令和媒体并断开受影响 Viewer。
- 房间权威状态:服务器只在一个有界 RoomStore 中保存房间码、租期、Host token/Viewer grant 摘要、authorization generation、code-only policy、可选 48-byte 房间密码校验材料、参与者和控制状态。`ROOM_LEASE_SECONDS` 默认 `86400`;Host 正在分享时不回收,停止分享或 Host 断开后开始 dormant lease,同一 Host token 在期限内重新分享即续租,Viewer 活动不续租,过期后释放号码。`ROOM_DATABASE_PATH` 未配置时全部状态随进程退出;配置时 SQLite 只保存房间权威 aggregate,参与者和媒体控制仍随进程退出。不存在账号、用户表、持久服务器 session、旧 schema reader 或 migration chain。
- 本地 Host 偏好:房间所有权继续使用随机 Host token,不使用 IP、UA、硬件信息或浏览器指纹。Host 浏览器在同源 `localStorage` 保存 raw Host token,以及昵称、code-only policy 和可选房间密码组成的创建偏好;另以无秘密记录只保存上次实际分配的四位号码。该 preferred room 没有本地租期、倒计时或续写 timer,也不证明或延长服务端 `ROOM_LEASE_SECONDS`。服务器保存 token 摘要和密码 verifier,稳定模式可将其持久化。旧 ownership 过期/失效后的下一次显式分享始终请求原号码;服务端只在号码空闲时分配,否则随机分配并返回新的本地偏好。两种结果都创建全新 room incarnation、Host token 和 Viewer grant,不恢复旧成员或路由。换设备、换浏览器或清理站点数据即没有该偏好。
### P1 - 重要功能
- 公网服务器一键部署包:在用户自有的公网服务器上一键部署当前 Web/信令应用、易失房间状态和所需 STUN/SFU/反向代理组件,复用唯一配置、secret、路由、健康检查和恢复合同。普通 app-only 更新先验证新的不可变 artifact 再原子切换,切换前 application release 只保证保留到有限 health/postflight 完成;之后没有保留合同,也不作为维护中的备份。只有实际改动基础设施、配置、secret、持久或不可逆状态时,才为被触及表面定义恢复并预检。它不是托管服务,不能内置真实凭据或跳过域名、TLS、防火墙、端口、容量和适用的恢复预检。
@@ -76,7 +76,7 @@ SFU 的 publication、subscription、server-resource accounting、临时 overlap
- 安全:应用控制面全程 HTTPS/WSS;site access、Host token 与 room-scoped Viewer grant 权限正交,任何一个 Viewer grant 只授予一个房间的观看角色。P2P 边在 WebRTC 端点间使用 DTLS-SRTP,普通 SFU 是逐跳 DTLS-SRTP;只有另行实现并验收应用层 E2EE 才能对 SFU operator 保持媒体端到端机密。仓库、服务端持久存储、日志和错误不得包含明文密码或 grant。已鉴权 Host 与 shipped clients 是当前私密房间中的可信媒体参与者;越权房间、角色和未授权 subscription 必须拒绝。旧 generation 不得重新取得 route authority,并在有界 drain 后由 exact-room 删除阻止再次加入。
- 隐私:P2P 会向房间内好友暴露网络地址,首版威胁模型仅覆盖可信熟人;若要求隐藏 IP,必须切换为服务器转发。
- 可维护性:代码、需求、调研、项目记忆、`AGENTS.md` 和 `.codex/` 均在 Git 中跟踪;开发配置不得依赖单一操作系统的绝对路径。
- 迁移完整性:项目拥有者明确宣布公开发布前,只维护当前内部产品合同,不承担 backward/forward compatibility。Browser 客户端、服务端和部署配置使用单一 `screener-v12` wire 原子发布,旧客户端在 authority 前拒绝;codec selector、quality-state/wire 字段、旧 parser/writer、双写、translator、deprecated alias、migration shim 与仅服务旧版本的测试均不存在。Git 历史承担旧实现追溯;公开发布后才按明确的新要求重新评估兼容策略。
- 迁移完整性:项目拥有者明确宣布公开发布前,只维护当前内部产品合同,不承担 backward/forward compatibility。Browser 客户端、服务端和部署配置使用单一 `screener-v12` wire 原子发布,旧客户端在 authority 前拒绝;Host codec selector 只属于页面本地 pre-share 设置,不进入 quality-state/wire。旧 parser/writer、双写、translator、deprecated alias、migration shim 与仅服务旧版本的测试均不存在;SQLite 只接受 exact current schema。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。
@@ -84,7 +84,7 @@ SFU 的 publication、subscription、server-resource accounting、临时 overlap
- 首版不包含远程键鼠控制、公开/大规模直播、录制、社区系统或完整语音聊天;大规模分享直接使用现有直播服务。
- `open` code-only 入口只省略房间密码,不省略站点访问;`private` 在没有房间密码时只允许 grant 邀请,在设置密码后才允许匹配的 code-only 请求。四位房间码只定位,不提供隐私保证、目录或防转发承诺。现有每房 Viewer 上限、全局/未鉴权连接上限、房间上限和中央 egress/admission cap 仍适用,任何访问方式都不能创建无界服务器 fanout。
- 房间控制面是单进程、单实例、易失内存状态;不得持久保存房间身份、租约、Host/Viewer credential、房间密码 verifier、成员、`clientId`、`peerId`、SDP、ICE candidate、IP 地址或媒体。进程重启是明确的全部房间失效边界,而不是恢复或迁移事件。
- 房间控制面保持单进程、单实例和一个 RoomStore 合同。`ROOM_DATABASE_PATH` 未配置时使用默认轻量模式,进程重启清空房间;配置时使用 SQLite 稳定模式,只持久化房间号、租约、Host/Viewer credential digest、Viewer authorization generation、`open | private` policy 和可选密码 verifier。成员、`clientId`、`peerId`、session/share generation、暂停、质量、codec、route、SFU、SDP、ICE candidate、IP、stats 与媒体始终不持久化,重启后必须重新鉴权、建图并以新首帧提交。
- 20-Viewer admission 是产品边界,不承诺任意机器、档位、网络或跨洲路径都达到相同画质;endpoint cap、SFU admission 和明确 wait/failure 继续保护资源。
- 浏览器规范要求每次由用户主动选择分享源,权限不能永久保存;纯网页不能保证指定游戏窗口或始终获得系统音频。
- 1080p60 是能力目标,实际分辨率、帧率和硬件编码由浏览器、GPU、驱动、画面复杂度及网络共同决定。
@@ -96,17 +96,18 @@ SFU 的 publication、subscription、server-resource accounting、临时 overlap
### 房间模式与生命周期
- Given 当前空闲房间码池,When 连续创建房间,Then 每间房从 `1000..9999` 的未占用集合随机取得一个四位码,不重复活动号码,活动房间容量固定为 9000;释放房间后号码返回同一空闲池。
- Given Host 正在分享,When `ROOM_LEASE_SECONDS` 到达,Then 活动房间不被回收。Given 分享者明确停止、捕获轨结束或 Host 断开,When 同一 Host token 在默认 86400 秒内重新分享,Then 同一进程内恢复原房间并重新续租;超过期限则关闭房间、释放号码并拒绝旧 Host token、grant 和房间密码。
- Given Host 正在分享,When `ROOM_LEASE_SECONDS` 到达,Then 活动房间不被回收。Given 分享者明确停止、捕获轨结束或 Host 断开,When 同一 Host token 在默认 86400 秒内重新分享,Then 恢复原房间并重新续租;轻量模式限当前进程,稳定模式可跨应用重启。超过期限则关闭房间、释放号码并拒绝旧 Host token、grant 和房间密码。
- Given 分享期间只有 host 信令短暂断开,When 现有 P2P 媒体仍健康,Then 客户端继续播放媒体并恢复信令,而不是错误显示“已停止分享”。
- Given 应用以 production 模式启动但 `SITE_ACCESS_PASSWORD` 缺失,或配置仍提供已删除的 `ACCESS_PASSWORD`、`ROOM_DATABASE_PATH`、`ROOM_TTL_SECONDS`,When 应用启动,Then 配置校验明确失败,而不是兼容旧 gate、旧持久化模式或公开建房。
- Given 应用以 production 模式启动但 `SITE_ACCESS_PASSWORD` 缺失,或配置仍提供已删除的 `ACCESS_PASSWORD`、`ROOM_TTL_SECONDS`,When 应用启动,Then 配置校验明确失败。Given 配置 `ROOM_DATABASE_PATH`,When 路径、应用标识、schema、行数据或独占 owner 任一无效,Then 在 signaling 与 LiveKit mutation 前 fail closed;未配置时不创建数据库或写目录。
- 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 是 128-bit 随机量的 22 字符 base64url,只授权该 exact room incarnation 的 Viewer,并只在该页面会话的 room-scoped `sessionStorage` 留存且在首次读取后从地址栏移除;HTTP request-target、WebSocket URL、Referrer、cookie、`localStorage`、服务端持久存储、日志和错误均不包含 grant。Given 房间仍存在且未 rotate/revoke,Then grant 保持有效;Given 房间回收、进程重启、rotate/revoke 或同号新房间,Then 旧 grant 失效。
- Given 新建房间,When 服务端返回邀请并由 Host/Viewer 客户端读取,Then grant 是 128-bit 随机量的 22 字符 base64url,只授权该 exact room incarnation 的 Viewer,并只在该页面会话的 room-scoped `sessionStorage` 留存且在首次读取后从地址栏移除;HTTP request-target、WebSocket URL、Referrer、cookie、`localStorage`、服务端持久存储、日志和错误均不包含 raw grant。Given 房间仍存在且未 rotate/revoke,Then grant 保持有效;稳定模式应用重启只恢复 digest 和 authorization generation,存活页面可用原 grant 重新鉴权。Given 房间回收、显式换房、轻量模式重启、rotate/revoke 或同号新房间,Then 旧 grant 失效。
- Given 本地输入不是 `1000..9999` 的四位房间码,When 用户提交或直接访问 malformed `/r/...`,Then 客户端保留原输入并直接提示“四位数字”,不发起房间鉴权、不静默删除字符。Given 默认新房,When 已通过站点访问的 Viewer 只输入合法房间码,Then 无房间密码即可在现有 Viewer/连接/egress 上限内加入,同时 Host 的主分享动作仍复制独立的 token-bearing link。
- Given 已通过 site access 的 well-formed code-only 尝试对应未分配、已回收或已过期房间,When `screener-v12` 首条信令鉴权到达,Then 服务端返回 `ROOM_NOT_FOUND`,UI 显示“房间不存在或已过期”且不提供密码重试。Given code 属于现存房间但缺失/错误密码、满房或其他已知有界 admission refusal,Then 服务端继续返回 `ROOM_ACCESS_DENIED` 并使用共享 authentication-failed close code,UI 可提供现有可选密码重试和 Host 邀请提示。Given grant 无效,Then 继续使用通用 `INVALID_TOKEN`;Host 鉴权保持独立错误,unexpected internal failure 使用不含 raw message 的 `SERVER_ERROR`。Given Viewer 持另一个房间的 grant、缺失 grant 或自行把 role 改为 Host,Then 不能建房、发布、跨房或绕过 Host token。
- Given Host rotate/revoke Viewer grant,When 新内存摘要和 authorization generation 提交,Then 服务端清除 Viewer grace/路由,命令 Viewer 关闭接收 PC/媒体并让每个 Host/relay/SFU upstream owner 关闭对应旧代次媒体边,最后关闭旧 Viewer socket;rotate 返回一次新 grant,revoke 锁定 grant 路径,code-only policy 保持独立。
- Given Host 浏览器仍持有现存房间的 ownership,When 分享活动或休眠期间更新邀请、code-only policy 或房间密码,Then 同源 HTTP 管理只接受有效 site access 与该房间精确 Host token;操作不建立 Host 媒体会话、不把房间标为分享中、不申请 route/resource,也不续租 dormant lease。rotate/revoke 继续执行同一强撤销,服务端只在 rotate 成功时返回新的 raw grant。
- Given Host 同源本地偏好保存了 `open | private`、可选房间密码和上次实际分配的房间号,When 服务器报告旧 ownership 已过期/失效或重启后该 Host 下一次显式开始分享,Then 客户端清除失效 ownership、始终请求该 preferred room 并原子重放创建偏好;号码空闲时分配给新的 room incarnation,已占用时随机分配并替换本地偏好。两者都生成新的 Host token 和 Viewer grant。该本地号码没有租期、倒计时或续租 authority;换浏览器、换设备或清理站点数据时没有该偏好。
- Given 应用进程重启,When 旧 Host、Viewer 或 code-only 链接再次访问,Then 所有旧房间、租约、摘要、密码 verifier 和参与者均已丢弃并 fail closed;运行时不读取数据库、迁移旧 schema 或恢复旧房间。后续同号分配只能创建凭据完全独立的新 incarnation。
- Given 应用进程重启,When 运行轻量模式,Then 所有旧房间、租约、摘要、密码 verifier 和参与者均已丢弃并 fail closed。Given 运行稳定模式,Then 未过期房间的号码、credential digest、authorization generation、policy/password verifier 和 dormant deadline 从 exact current schema 恢复;crash 时 active 的房间从启动时刻进入一个配置租期,所有 participant/session/share/route/SFU 状态仍为空,旧页面必须重新鉴权并重新提交媒体。
- Given Host 点击房间号旁的换房按钮,When exact Host token 仍有效,Then 服务端以一个原子操作分配保证不同的新号码并正常销毁旧 room incarnation;成功后旧 Host token、grant、密码、authorization、session、route 与 SFU 资源全部失效,活动分享结束,客户端保存新房间和无期限 preferred code。Given 新号码或凭据无法在提交前生成,Then 旧房间保持不变。
- Given 部署切换了不兼容的当前协议,When 仍打开的旧页面连接新服务端,Then 单一 release/protocol mismatch 边界终止该 session、停止自动重连并明确提示刷新;不得通过旧 parser、双写或双协议继续假连接。
### P2P 与 SFU 兜底
@@ -161,7 +162,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 报告被丢弃;有效报告也只能用于诊断,不能改变 Host 表示或路径状态。
- Given 受控 RTT/丢包测试条件,When 连续分享高动态游戏画面 30 分钟,Then 延迟、帧率与丢帧达到上述目标,且没有不可恢复的冻结。
- Given Windows Chrome/Edge Browser Host 开始或继续分享,When 浏览器返回 display video track,Then 应用设置 `contentHint = "motion"`,并按浏览器 stats 如实显示实际 RTP codec 与实现字段。Given 当前分享代次的有界 H.264 预检证明实际 `video/H264` sender 的源帧/编码帧进度达标,Then 后续 direct 或 browser-relay offer 按浏览器原生 H.264 capability 顺序列出 H.264,再列 VP8 与修复格式;双方不支持 H.264 时同一次协商选择 VP8。Given 预检不支持、证据不足、超时、失败或不达标,Then offer 只列 VP8 与修复格式。缺少 VP8、`setCodecPreferences()` 或应用失败仍在创建 offer 前明确失败。Given 建立 SFU publication,Then 使用 Host share generation 的同一 H.264/VP8 决定、禁用 backup codec,且 Screener 不设置自定义 simulcast layer list。
- Given Windows Chrome/Edge Browser Host 开始分享,When 浏览器返回 display video track,Then 应用设置 `contentHint = "motion"`,并按浏览器 stats 如实显示实际 RTP codec 与实现字段。Host 高级设置在未分享时按从低到高显示 `VP8 | 自动 | H264` 且默认自动,分享开始后锁定;自动模式以有界 H.264 sender preflight 决定 H.264-first 或 VP8-only,手动 H264 只允许 H.264 mode 1,手动 VP8 只允许 VP8。Viewer relay 始终自动。换源、换父、质量更新与暂停/继续沿用当前分享代决定;停止后开始新分享才按当前 selector 重新决定。缺少所选 codec、`setCodecPreferences()` 或应用失败在创建 offer 前明确失败。Given 建立 SFU publication,Then 使用 Host share generation 的同一 resolved H.264/VP8 决定、禁用 backup codec,且 Screener 不设置自定义 simulcast layer list。
- 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 继续工作,临时资源被销毁,重试遵守模型定义的有界策略。
@@ -171,7 +172,7 @@ SFU 的 publication、subscription、server-resource accounting、临时 overlap
- Given Web Host 或浏览器 relay 建立或替换分享音轨,When 对实际音频 sender 应用发送参数,Then 从当次 `getParameters()` 派生所选 `encodings[0].maxBitrate = 64000 | 128000 | 192000`、调用 `setParameters()` 并立即读回,档位缺失时按 128000 处理;Given Web Viewer 为含唯一有效 audio/Opus payload 的 offer 生成 answer,Then 用结构化 SDP parser 只 upsert 该 payload 的 `stereo=1` 与 `maxaveragebitrate=192000`,并把同一个结果用于 `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`、192 kbps 使用同等结构的显式 sender ceiling,三档都设置 `forceStereo = true`、`dtx = false` 与 `red = false`;验收同时核对 SDK 选项、applied ceiling、当前 RTP codec/fmtp 与 stats 实际码率,不能从 preset 名称或 `opus/48000/2` 单独推断实际声道内容。
- Given 分享正在直播或暂停且 Host 选择新的 64/128/192 kbps 音频档位,When 当前 profile mutation 串行执行,Then 服务端以 last-wins 保存并广播最新 desired profile,每个当前 Host、relay 与 SFU audio sender 从 fresh `getParameters()` 派生 `setParameters()` 并立即 readback,过期 completion 不得覆盖新值;以后创建的 sender 使用最新 desired profile。单个端点应用失败时保留媒体与其旧 applied 值、显示可见告警并允许后续更新或重建重试,不得谎称全房已收敛。当前 applied readback 保持 endpoint-local;没有显式回执 wire 就不宣称跨端点原子提交。该操作不改变 Opus、route revision、media-binding generation 或 publication generation,也不创建第二个音频控制环。当前 screen-audio Host 基线是 Chrome/Edge;其他浏览器只有实际 sender readback 成功时才算支持。
- Given Host 打开已接受的 `screener-v12` 分享设置或更新房间画质,When 查看 UI 或校验消息,Then 不存在 codec selector 或 codec quality-state/wire 字段。share-generation-scoped H.264 预检只产生当前页面内存中的内部发送偏好;结果不持久化、不重新协商健康 edge,实际协商 codec 与 encoder 只作为只读诊断。
- Given Host 打开分享高级设置,When 尚未开始分享,Then 本地三段 codec selector 可编辑且默认自动;Given 分享已开始或暂停,Then selector 锁定且不触发重协商。该选择和 share-generation-scoped 自动结果均不持久化、不进入 quality state/wire、不改写健康 edge;实际协商 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、降噪或来源标签伪称已隔离。