docs: accept strict v13 quality shadow wire (#286)

This commit is contained in:
AAA authored and GitHub committed 2026-08-27 00:18:45 +08:00
1 parent 1310c8137e
commit 98e481cea3
6 files changed
+30 -22

No files matched your search

@@ -1,11 +1,11 @@
# ADR-0005: Automatic Hybrid Media Routing
- Status: Accepted and deployed on strict v12, including Browser SFU ICE-server
isolation, no product diagnostic-download UI, and the retained Host-only
acceptance snapshot. Exact current identity is owned by
- Status: Base routing is deployed on strict v12. The evidence-only quality
shadow and atomic strict-v13 upgrade are accepted but not yet implemented or
deployed. Exact current identity is owned by
[status](../status.md).
- Date: 2026-08-20
- Last updated: 2026-08-25
- Last updated: 2026-08-27
## Context
@@ -177,7 +177,7 @@ prepare the same connection; neither endpoint infers candidate authority from
an assignment-list difference. The candidate child may report one exact
transport-connected progress event fenced by the same revision and connection;
only its later decoded-frame ready event can commit.
The current Browser runtime contract uses the single `screener-v12` wire. On each WebSocket, the
The accepted Browser runtime contract uses the single `screener-v13` wire. On each WebSocket, the
server sends the exact prepare before its SFU configuration; the candidate child
is queued before a peer parent is allowed to start its offer. WebSocket ordering
is the companion-delivery contract, so clients keep no reordering inbox.
@@ -420,8 +420,9 @@ deployment-wide.
## Current Source And Deployment Boundary
Production and canonical application/runtime source use strict `screener-v12`
with the direct/STUN peer plus LiveKit SFU/UDP route model. Exact current source
Production and canonical application/runtime source currently use strict
`screener-v12`; the accepted quality-shadow integration atomically replaces it
with strict `screener-v13`. Exact current source
and deployment identity is indexed by [status](../status.md).
Browser SFU PCs use empty external ICE-server lists. The controlled exact-
candidate rollback/SFU commit and active-SFU cadence gates are closed. The
+5 -1
View File
@@ -124,6 +124,9 @@ Last updated: 2026-08-26
application with adaptive H.264/VP8 selection, local Host codec override and
optional SQLite room authority. Exact identities are indexed by
[status](./status.md).
- The accepted quality-shadow integration atomically replaces v12 with strict
`screener-v13`; old Browser pages must fail before room authority. It adds no
active route decision until production shadow evidence is accepted.
- Production uses SQLite-backed random rooms, 20-Viewer admission, endpoint cap `2`,
fixed 9,000-publication ingress and `9000 * 20` subscription egress admission,
STUN UDP 3478, LiveKit media UDP 7882, and Web TCP 80/443. Node 8787 and
@@ -134,7 +137,8 @@ Last updated: 2026-08-26
## Current Priority
1. Complete the mobile Viewer lifecycle matrix and representative real-network
1. Complete the evidence-only quality-route shadow on strict `screener-v13`.
2. Complete the mobile Viewer lifecycle matrix and representative real-network
route/media acceptance.
Quality-driven parent selection, Native sender work, distribution packages,
+2 -1
View File
@@ -66,7 +66,8 @@ This is the current execution index. Git history owns completed timelines;
1. Implement presentation-authoritative freeze/pause evidence with decoded
progress and the controller-owned shadow aggregate; collect production
samples without active route changes.
samples without active route changes. This integration atomically replaces
current v12 with accepted strict `screener-v13`.
2. Finish the Android Chrome and iOS Safari Viewer lifecycle matrix.
3. Finish representative public-network direct, peer-relay, SFU, recovery,
Pause/Resume, screen-audio, real-game A/V, and all-UDP-blocked acceptance
+3 -1
View File
@@ -14,7 +14,9 @@ later topic is not implementation authority by itself.
current-epoch aggregate per exact child in the existing room controller and
expose it only through the Host acceptance snapshot. It must not alter facts,
capacity, routes or SFU usage. Collect annotated production samples before
accepting an active trigger, probation, restore or cooldown policy.
accepting an active trigger, probation, restore or cooldown policy. Ship it
only as the atomic strict `screener-v13` Browser/server contract; v12 gets no
compatibility path.
2. **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
+8 -8
View File
@@ -2,7 +2,7 @@
## 2026-08-18 14:45
状态:production 与 canonical source 使用严格 `screener-v12`,均已实现每分享代次的 H.264 sender gate 与 VP8 fallback、本地 pre-share `VP8 | 自动 | H264` selector、可选 SQLite stable room authority、无期限 preferred code 和换房控制;这些不新增 codec wire、自定义 SFU layer list或应用媒体质量 controller。Video 使用 `contentHint = "motion"`,audio 使用 `music`。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` 持有。
状态:production 与 canonical source 当前使用严格 `screener-v12`;已接受的 quality-shadow 集成会把 Browser/server 原子升级为唯一 `screener-v13`,在 authority 前拒绝旧页面,不保留 v12 parser。当前已实现每分享代次的 H.264 sender gate 与 VP8 fallback、本地 pre-share `VP8 | 自动 | H264` selector、可选 SQLite stable room authority、无期限 preferred code 和换房控制;这些不新增 codec wire、自定义 SFU layer list或应用媒体质量 controller。Video 使用 `contentHint = "motion"`,audio 使用 `music`。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` 持有。
## 目标与边界
@@ -112,7 +112,7 @@ Web Host 在 `authenticate` 中显式发送 `viewerPresence: true` capability,
WebSocket 使用无 query/fragment 的同源 `/signal`。upgrade 只记录 site-access cookie 是否有效;连接后五秒内第一条消息必须是当前单一 wire 的房间级 `authenticate`。Host 消息携带 room、role、内部 Host token 与 clientId,并由 socket state 复核 site access。Viewer 消息携带 room、role、clientId,以及 exact-room Viewer grant 或当前 code-only password attempt。成功的 `authenticated` 快照始终携带部署最终值 `endpointMediaCopyCapacity: 1|2|3`;普通模式的服务端 admission 只向 Host 激活稳定加入顺序中的至多该数量 Viewer,并在离开后提升等待者,双方信令只允许 active child。Browser Host 与 Viewer 严格校验该值,房间 `maxViewers` 只约束观看者总 admission。Native/executable sender 停留在旧 wire 并在 authority 前拒绝。服务端设置 64 KiB `maxPayload`、关闭 `perMessageDeflate`,并以 ping/pong 清理失效连接。单进程固定限制 2048 条总连接和 256 条未鉴权连接,容量在 upgrade 前预留,超限直接返回 HTTP 503 并销毁 socket。
下表描述 accepted Browser 的单一 `screener-v12` wire;server 与 Browser assets 必须作为一个完整合同原子发布,其他 wire 在取得房间 authority 前拒绝,不存在旧 parser、writer、translator 或中间协议子集:
下表描述 accepted Browser 的单一 `screener-v13` wire;server 与 Browser assets 必须作为一个完整合同原子发布,其他 wire 在取得房间 authority 前拒绝,不存在旧 parser、writer、translator 或中间协议子集:
| 方向 | 消息 | 作用 |
| --- | --- | --- |
@@ -132,7 +132,7 @@ WebSocket 使用无 query/fragment 的同源 `/signal`。upgrade 只记录 site-
Zod 在服务边界拒绝未知类型、超长字符串、非法 SDP/candidate 结构和越权目标。服务端根据已认证会话决定路由:观看者不能给另一名观看者发信令,分享者也不能向本房间以外的 peerId 发送。
accepted 的唯一 wire 是 `screener-v12`,不做版本协商、旧 parser、双写或 translator。任何其他 Browser wire 及 executable sender 的首条消息在角色进入 room lookup 前得到同一个 fatal refresh 终态并停止自动重连。Client 与 server 共用 `SIGNAL_CLOSE_CODES` 定义会话替换、鉴权失败和 Viewer access revoke 的终态语义,避免双方对重连边界产生分叉;非法协议消息使用 WebSocket policy-violation close code。缺失、跨房、已 rotate 或 revoked 的当前 Viewer grant 使用同一个通用“邀请无效或已失效”结果。已通过 site access 的 well-formed code-only attempt 在没有 current room 时发 `ROOM_NOT_FOUND`;现存房间的 absent/wrong password、room full 与其他已知有界 admission refusal 继续发 `ROOM_ACCESS_DENIED`。两者都使用共享 authentication-failed close code。Grant 继续使用 `INVALID_TOKEN`,Host 鉴权保持独立,unexpected internal failure 使用不含 raw message 的 `SERVER_ERROR`。
accepted 的唯一 wire 是 `screener-v13`,不做版本协商、旧 parser、双写或 translator。任何其他 Browser wire 及 executable sender 的首条消息在角色进入 room lookup 前得到同一个 fatal refresh 终态并停止自动重连。Client 与 server 共用 `SIGNAL_CLOSE_CODES` 定义会话替换、鉴权失败和 Viewer access revoke 的终态语义,避免双方对重连边界产生分叉;非法协议消息使用 WebSocket policy-violation close code。缺失、跨房、已 rotate 或 revoked 的当前 Viewer grant 使用同一个通用“邀请无效或已失效”结果。已通过 site access 的 well-formed code-only attempt 在没有 current room 时发 `ROOM_NOT_FOUND`;现存房间的 absent/wrong password、room full 与其他已知有界 admission refusal 继续发 `ROOM_ACCESS_DENIED`。两者都使用共享 authentication-failed close code。Grant 继续使用 `INVALID_TOKEN`,Host 鉴权保持独立,unexpected internal failure 使用不含 raw message 的 `SERVER_ERROR`。
## WebRTC 协商
@@ -164,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 高级设置在未分享时按从低到高显示 `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。
Accepted `screener-v13` 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 始终可用。
@@ -274,7 +274,7 @@ media-proof 和 bounded-recovery 不变量的 scoped code 与 tests。
每两秒在本地采集一次 `getStats()`。首版不上传原始统计。折叠详情可在产生该连接的本机显示 selected local/remote candidate 的 `address`/`port`,但浏览器未暴露时保持未知;地址不得进入信令、Viewer C、日志、诊断导出、持久化或路由/质量决策。在 peer 边中它只是当前连接端点,在 SFU 边中通常是 SFU 端点,不能标成参与者的权威公网 IP。
accepted `screener-v12` 的 Host/Viewer 产品 UI 不提供连接诊断文件下载。现有 periodic stats 只驱动当前页面的折叠连接详情、typed failure 与 accepted exact-edge stall;原始 stats、地址和身份不上传。authenticated Host 自动验收仍可按需请求一次 `route-diagnostic-snapshot`;服务端当场读取 current graph/operation 与 bounded latest timing records,不连续推送、不保留事件环、不建立第二张 graph。snapshot 使用临时 ordinal 和封闭 outcome bucket,不含 raw identity、address、candidate、credential 或 error,也不形成 route authority。
accepted `screener-v13` 的 Host/Viewer 产品 UI 不提供连接诊断文件下载。现有 periodic stats 只驱动当前页面的折叠连接详情、typed failure 与 accepted exact-edge stall;原始 stats、地址和身份不上传。authenticated Host 自动验收仍可按需请求一次 `route-diagnostic-snapshot`;服务端当场读取 current graph/operation 与 bounded latest timing records,不连续推送、不保留事件环、不建立第二张 graph。snapshot 使用临时 ordinal 和封闭 outcome bucket,不含 raw identity、address、candidate、credential 或 error,也不形成 route authority。
Host 与 relay Viewer 复用同一个纯 presentation reducer 保存远端 `ViewerQualityEvidence`。代际 identity 是 exact route identity(room/share、viewer/parent、assignment/connection、route revision 和当前可用的 media-binding generation,及适用时 publication generation);只有同 identity 且 sequence 严格递增才可承接每个字段最后一次真实非空观测,其他输入从新 evidence 重建。五秒 freshness 只控制 evidence 能否证明当前连接,采样缺字段或短暂超时不把展示字段归零。`new`/`connecting`/`disconnected` 保留同 identity shell 但立即标为非 fresh,`failed`/`closed` 或 route identity 变化清空。任何依赖当前状态的消费者只读取 fresh presentation;该层不新增 wire 字段或服务端 route authority。
@@ -341,7 +341,7 @@ Token-free SFU standby prewarm 保持可选且不创建 participant、publicatio
- RoomStore 的四位空闲码随机分配/释放、活动分享免过期、dormant lease/续租、Host token/Viewer grant 摘要验证、22 字符 128-bit grant 与 exact room incarnation 同寿、`open/private` 正交 policy、私密无密码仅邀请可入、跨房/同号重建拒绝、rotate/revoke 强撤销、Viewer 上限和超额拒绝;
- 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 字段;
- Accepted v13 contract 使用单一 `screener-v13` 信令 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;
- 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/private`。覆盖原子创建 profile、同浏览器重建偏好、fragment/sessionStorage/replaceState、无 site access 的 code-only 拒绝、unknown/reclaimed/expired room 使用 `ROOM_NOT_FOUND` 且不显示密码、私密无密码与 wrong-or-absent-password/full code-only 使用 `ROOM_ACCESS_DENIED`、grant 与 Host 的独立错误、raw grant/密码不进入 URL/日志/服务端持久状态,以及 rotate/revoke 后 grace/route 清理、旧 Viewer/上游媒体边关闭和 reconnect 拒绝;
- 客户端策略与 stats 解析:会话替换不重连,ordinary ICE 与 LiveKit protocol 分离;
@@ -365,7 +365,7 @@ Token-free SFU standby prewarm 保持可选且不创建 participant、publicatio
## 当前交付边界
- Production 与 canonical source 使用严格单一 `screener-v12` wire。每分享代 actual-sender gate 在证据通过时让未来 direct/relay 和 Host 单一 SFU publication 使用 H.264,否则使用 VP8。本地 pre-share `VP8 | 自动 | H264` 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;quality-shadow 集成边界会原子升级到 accepted `screener-v13`。每分享代 actual-sender gate 在证据通过时让未来 direct/relay 和 Host 单一 SFU publication 使用 H.264,否则使用 VP8。本地 pre-share `VP8 | 自动 | H264` 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` 持有。
- 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。
@@ -385,7 +385,7 @@ Pending candidate 的 receive-transport owner 只在当前 exact generation 下
首次需要 SFU 但 Host 已满时,controller保存原始demand/session和本fact已试carrier集合,依稳定顺序让一个健康Host-direct child用普通SFU candidate建立publication。该candidate必须持有既有`TransitionOverlapSlot`;需要先切断健康carrier的bounded-gap计划不具资格。失败保持carrier及其subtree并尝试下一carrier;全部耗尽时终态归原始demand,同一fact不为后续demand重复整套carrier。成功后原demand下一次先尝试SFU,失败仍可继续direct suffix;carrier自身也进入低优先级direct convergence,待容量允许时恢复原Host直连。
所有非服务器端点共享一个 server-authoritative steady outbound media-copy cap `C`:唯一配置 `ENDPOINT_MEDIA_COPY_CAPACITY` 默认 `2`,只接受 `1`、`2` 或 `3`,不兼容旧键。服务端将 endpoint 当前广告的 `0..C` 可用值 clamp 为唯一 `effectiveDownstreamCapacity`;该值只表示当前可用 sender slots,不建立角色、浏览器、UA、设备或页面可见性 tier。peer child 或 Host publication 各占一份,upstream receive 免费;SFU subscriber egress 使用独立 server admission。Browser `screener-v12` 的 `authenticated.endpointMediaCopyCapacity` 把部署 `C` 交给 Host 与 Viewer;两端用该值校验稳态 assignment,并让 active+pending 的物理 copy 只在 controller 已授权 overlap 时达到 `min(C + 1, 3)`。代码通过一个 shared accounting guard 执行,不重复 magic number;其他 wire 与 Native executable 在 authority 前拒绝,不保留兼容路径。任何会产生第四份 endpoint media copy 的 token都不得签发;`C=3`满载bootstrap保持健康树并明确失败。
所有非服务器端点共享一个 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-v13` 的 `authenticated.endpointMediaCopyCapacity` 把部署 `C` 交给 Host 与 Viewer;两端用该值校验稳态 assignment,并让 active+pending 的物理 copy 只在 controller 已授权 overlap 时达到 `min(C + 1, 3)`。代码通过一个 shared accounting guard 执行,不重复 magic number;其他 wire 与 Native executable 在 authority 前拒绝,不保留兼容路径。任何会产生第四份 endpoint media copy 的 token都不得签发;`C=3`满载bootstrap保持健康树并明确失败。
SFU admission 在应用单进程中只有一个注入实例。LiveKit credential tuple 完整时必须显式配置私有 `LIVEKIT_API_URL`;固定四位房号池给出 9000 个 publication ingress,上界 `9000 * MAX_VIEWERS_PER_ROOM` 给出 subscription egress,不存在独立 SFU 容量配置。专用 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。
+4 -4
View File
@@ -45,7 +45,7 @@ SFU 的 publication、subscription、server-resource accounting、临时 overlap
- 独立房间入口:合法 `/r/{code}#v={grant}` 邀请直接进入对应房间;没有 grant 的 `/join` 或 `/r/{code}` 先显示明确标为“站点访问”的整站验证,再提交 code-only 尝试。`open` 且可准入时直接进入;未分配、已回收或已过期的 code 明确显示“房间不存在或已过期”且不提供密码输入;现存房间的其他预期拒绝使用 `ROOM_ACCESS_DENIED`,不披露具体 policy。有效 site-access cookie 只跳过第一步,两类密码不得复用或互相替代。Host 可设置、轮换或移除房间观看密码;密码接受 1 至 64 个可见 ASCII 字符,异步 scrypt 全局最多执行 2 个、等待 16 个派生。Host 可把房间密码保存在同源本地创建偏好中;它不得进入 URL、cookie、日志或服务端持久存储。
- 统一自动路由:旗舰部署开启 `PEER_ASSISTED_MEDIA` 后,每个房间只有一个服务端 route controller、一个 committed graph、一个 pending child operation、一个串行 pump 和一个 operation deadline timer。Graph 必须无环、source-reachable,且每名 Viewer 同时至多一个 active upstream;加入、容量变化、离开、exact edge 失败与非暂停 decoded-frame stall 都只唤醒同一个 reconcile,其余健康分支保持 sticky。候选先按身份、source reachability、无环、effective capacity 与 sender transition 过滤,再按深度、剩余容量、稳定 child-parent hash 和稳定身份排序;server admission、exact transport progress、首帧 commit、Host-full bootstrap、后台 round-robin 与终态所有权统一由 ADR-0005 定义。普通 peer 始终 STUN-only,SFU 使用独立 admission;不建立第二 graph、周期性重平衡、质量评分或独立 depth cap。
- 观看端连接反馈:Viewer 舞台从鉴权到首帧使用一个由现有权威状态推导的稳定阶段反馈,至少区分等待分享、分配线路、建立 P2P、连接 SFU、等待首帧、浏览器阻止自动播放、恢复连接和有界失败。阶段不是进度百分比,也不产生 route authority;只有当前 video element 实际提交首帧后才显示正在播放。Autoplay rejection 通过原生媒体 controls 恢复;连接中不显示虚假的播放动作。手动“重新连接媒体”只恢复当前 exact route:P2P 直接重建到同一 parent 的连接,SFU 只断开并重连当前 publication 的 subscriber;两者都保留 Screener WebSocket、房间会话和 route revision,不触发主动换父、质量选优或 P2P/SFU 重选。真实恢复耗尽后才进入既有 route-failure operation。仍有已证明旧帧时保留为背景并显示恢复状态,不为显示反馈主动清空健康 media element。页面从冻结/隐藏恢复时先重置 decoded-stall wall-clock baseline 并重新证明当前代画面,冻结时间本身不得成为立即换路依据。Host 离线本身不使仍健康的媒体失效;exact upstream 硬失败或 route terminal failure 必须使 current frame proof 失效,已证明画面至多降为 retained background,不能遮蔽离线或有界失败。已接受的 `screener-v12` 补充消息只有 strict、current-candidate-fenced 的 transport-connected 进度,以及仅允许 `waiting/sfu-admission` 与 `failed/route-exhausted` 两种配对的 `route-status` union;两者都不携带 raw text、额外身份或 retry hint,且都不产生 route authority。
- 观看端连接反馈:Viewer 舞台从鉴权到首帧使用一个由现有权威状态推导的稳定阶段反馈,至少区分等待分享、分配线路、建立 P2P、连接 SFU、等待首帧、浏览器阻止自动播放、恢复连接和有界失败。阶段不是进度百分比,也不产生 route authority;只有当前 video element 实际提交首帧后才显示正在播放。Autoplay rejection 通过原生媒体 controls 恢复;连接中不显示虚假的播放动作。手动“重新连接媒体”只恢复当前 exact route:P2P 直接重建到同一 parent 的连接,SFU 只断开并重连当前 publication 的 subscriber;两者都保留 Screener WebSocket、房间会话和 route revision,不触发主动换父、质量选优或 P2P/SFU 重选。真实恢复耗尽后才进入既有 route-failure operation。仍有已证明旧帧时保留为背景并显示恢复状态,不为显示反馈主动清空健康 media element。页面从冻结/隐藏恢复时先重置 decoded-stall wall-clock baseline 并重新证明当前代画面,冻结时间本身不得成为立即换路依据。Host 离线本身不使仍健康的媒体失效;exact upstream 硬失败或 route terminal failure 必须使 current frame proof 失效,已证明画面至多降为 retained background,不能遮蔽离线或有界失败。已接受的 `screener-v13` 补充消息只有 strict、current-candidate-fenced 的 transport-connected 进度,以及仅允许 `waiting/sfu-admission` 与 `failed/route-exhausted` 两种配对的 `route-status` union;两者都不携带 raw text、额外身份或 retry hint,且都不产生 route authority。
- 质量控制扩展:推荐档位仍是默认入口且只保留 720p30/1080p30/1080p60 三档,当前页面初始 desired state 选择均衡的 1080p30 且不写 `localStorage`;“分享高级设置”允许 480p(854x480)/720p/1080p/1440p 分辨率 ceiling、15 至 60 的整数 FPS ceiling、2 至 12 Mbps 视频码率 ceiling、清晰/均衡/流畅三种偏好,以及离散的 `64 kbps 普通 | 128 kbps 音乐默认 | 192 kbps 保真` 屏幕音频 sender ceiling;480p 不形成推荐档或固定 FPS/码率组合,音频初始值为 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=192000` 变换,使同一接收上限覆盖三档;非目标形态或 parser/writer 异常必须原样保留浏览器 answer,不得阻断可继续的 mono/video edge。不得用宽泛字符串/正则改 SDP,也不得改写浏览器协商的 FEC。SFU 将 64/128 kbps 映射到 pinned SDK stereo music preset,192 kbps 使用显式 ceiling,统一设置 `forceStereo = true`、`dtx = false` 与 `red = false`。不得暴露任意码率、声道、采样率、audio codec、stereo、DTX、RED 或 FEC 控件;192 kbps 只是显式较高上限,不是无损、高保真或最低码率保证。实际质量持续低于所选上限时必须显示可解释的限制原因;在真实拥塞、编码器和接收端证据完成前,不把任一组合称为“完美平衡”。未来 microphone voice 若进入范围,必须是独立 track/处理链,不能把 AEC/降噪/DTX 套到电影或游戏音频。
- SFU 质量设置生命周期:直播中修改档位时,浏览器 sender 的 applied readback、LiveKit `LocalVideoTrack` 保存的 degradation preference、track restart options 与 `LocalTrackPublication` 的 republish options 必须作为同一次串行操作更新;任一步失败恢复旧 sender 与旧 SDK 保存态,旧 publication generation 不得回写当前状态。LiveKit full reconnect/republish 不得恢复初次发布的旧偏好。该修复不改变 codec、码率上限、层数或路由,也不能单独解释即时低帧。
- 本地预览性能候选:在相同动态场景中 A/B 测量 Host 动态预览开/关的 CPU、GPU、encode time 和实际 FPS。只有稳定收益且捕获、编码和远端画面不受影响时,才允许直播开始后默认显示静止缩略图或隐藏动态预览并提供按需展开;未测前不宣称收益。
@@ -76,7 +76,7 @@ SFU 的 publication、subscription、server-resource accounting、临时 overlap
- 安全:应用控制面全程 HTTPS/WSS;site access、Host token 与 room-scoped Viewer grant 权限正交,任何一个 Viewer grant 只授予一个房间的观看角色。P2P 边在 WebRTC 端点间使用 DTLS-SRTP,普通 SFU 是逐跳 DTLS-SRTP;只有另行实现并验收应用层 E2EE 才能对 SFU operator 保持媒体端到端机密。仓库、服务端持久存储、日志和错误不得包含明文密码或 grant。已鉴权 Host 与 shipped clients 是当前私密房间中的可信媒体参与者;越权房间、角色和未授权 subscription 必须拒绝。旧 generation 不得重新取得 route authority,并在有界 drain 后由 exact-room 删除阻止再次加入。
- 隐私:P2P 会向房间内好友暴露网络地址,首版威胁模型仅覆盖可信熟人;若要求隐藏 IP,必须切换为服务器转发。
- 可维护性:代码、需求、调研、项目记忆、`AGENTS.md` 和 `.codex/` 均在 Git 中跟踪;开发配置不得依赖单一操作系统的绝对路径。
- 迁移完整性:项目拥有者明确宣布公开发布前,只维护当前内部产品合同,不承担 backward/forward compatibility。Browser 客户端、服务端和部署配置使用单一 `screener-v12` wire 原子发布,旧客户端在 authority 前拒绝;Host codec selector 只属于页面本地 pre-share 设置,不进入 quality-state/wire。旧 parser/writer、双写、translator、deprecated alias、migration shim 与仅服务旧版本的测试均不存在;SQLite 只接受 exact current schema。Git 历史承担旧实现追溯;公开发布后才按明确的新要求重新评估兼容策略。
- 迁移完整性:项目拥有者明确宣布公开发布前,只维护当前内部产品合同,不承担 backward/forward compatibility。Browser 客户端、服务端和部署配置使用单一 `screener-v13` 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。
@@ -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 是 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 已通过 site access 的 well-formed code-only 尝试对应未分配、已回收或已过期房间,When `screener-v13` 首条信令鉴权到达,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;换浏览器、换设备或清理站点数据时没有该偏好。
@@ -137,7 +137,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-v12` 房间合同,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-v13` 房间合同,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 不被周期性搬动。连接详情只显示当前页面本地观测值;Host 与 Viewer 页面均不提供诊断文件下载。Given authenticated Host test harness 请求 bounded route snapshot,Then 服务端只返回现有 privacy-safe current snapshot;Viewer 请求仍拒绝,snapshot 不参与 route authority。
- Given 一名分享者和 20 名观看者,When 运行 bounded browser smoke,Then 全部 Viewer 取得明确媒体或 bounded wait/failure,非服务器端点不超过 `C`,并记录 route depth、首帧时间、实际 sender 数、应用服务器 CPU/内存以及 SFU 用量。诊断汇总报告 20 个 current-child 样本数、pending 数、每项原始排序时长和 nearest-rank p50/p95/max,不设通过阈值;confirmed departure 和 room delete 后对应 timing 必须消失。该 smoke 验证 admission、控制面效能和资源上界,不单独证明任意公网画质或 30 分钟 endurance。