diff --git a/AGENTS.md b/AGENTS.md index 054ab8c..0258c06 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ - Keep media distributed and automatic. Use centralized services for rooms, authentication, signaling, STUN, observability, and bounded SFU-root capacity. TURN is an optional authenticated selected-edge compatibility transport, disabled unless explicitly configured; it is not a media topology. - Let viewers join from a normal desktop or mobile browser without installing the sender application. - STUN is required. The target ladder is direct/peer UDP, then an SFU/UDP virtual parent feeding at most two roots, then optional authenticated selected-edge TURN, followed by a clear bounded failure. Ordinary peer edges must not receive TURN candidates by default. HTTPS/WSS remains TLS/TCP and is independent of this media-transport policy. -- Every non-server endpoint stays within its advertised relay capacity and the deployment hard ceiling; an upstream receive edge does not consume this upload budget. The deployment cap defaults to two and may be explicitly set from one through three without UA- or visibility-based policy; actual edge failure returns to the bounded route-reselection path. Dynamic evidence-driven capacity remains future work and must not infer capacity from device identity. Central media normally serves at most two roots; extra server-fed exceptional viewers require an explicit deployment egress/admission cap and must never create unbounded fanout. +- Every non-server endpoint stays within its advertised relay capacity and the deployment hard ceiling; an upstream receive edge does not consume this upload budget. The current release policy permits at most two downstream physical media edges for non-server endpoints, while ordinary Browser Viewer relay is strictly limited to one until the re-encode/resource gates pass. Deployment may tighten these limits but never raise them; UA/visibility do not change capacity, and actual edge failure returns to the bounded route-reselection path. Dynamic evidence-driven capacity remains future work and must not infer capacity from device identity. Central media normally serves at most two roots; extra server-fed exceptional viewers require an explicit deployment egress/admission cap and must never create unbounded fanout. - Treat a web client as the initial delivery target. A packaged desktop sender or native capture helper is a later optimization, not an assumed requirement. - Do not silently change the product into an always-SFU conferencing system. An SFU is a virtual parent for one or two necessary roots whose peer descendants remain distributed; record any broader topology change as an ADR with measured justification. diff --git a/docs/adr/0005-automatic-hybrid-media-routing.md b/docs/adr/0005-automatic-hybrid-media-routing.md index dd8008d..aee9568 100644 --- a/docs/adr/0005-automatic-hybrid-media-routing.md +++ b/docs/adr/0005-automatic-hybrid-media-routing.md @@ -367,7 +367,10 @@ change or the current share stops. A new sharing generation starts from the cheapest available UDP route. The controller does not continuously rebalance healthy media. The bounded quality candidate changes only the Viewer-rooted subtree whose current parent-to-child edge produced three consecutive hard-bad -windows. One confirmed child remains edge-local. Two distinct confirmed +windows. One confirmed child remains edge-local. Two-distinct-child +corroboration is retained only for a future accepted Viewer-capacity tier; the +current ordinary Browser Viewer1 release makes it unreachable and does not +authorize a second downstream child. Two distinct confirmed children under the same Viewer parent session and share generation within five seconds temporarily make that parent relay-ineligible (effective capacity zero) for 30 seconds; its advertised capacity is unchanged. The triggering edge still diff --git a/docs/方案设计.md b/docs/方案设计.md index c43da4a..47c08f8 100644 --- a/docs/方案设计.md +++ b/docs/方案设计.md @@ -188,6 +188,8 @@ Viewer 本地连接详情只从当前 route 的唯一 inbound screen video/audio 单个三窗确认的 C+B 事件仍是 edge-local,`severe` 与 `relative-fps` 都先且只尝试一个 active upstream 为 peer 或 SFU 的 Viewer;Host 排除,候选的 session/share/revision 必须当前,且 active children 加一不得超过服务端有效 relay capacity。服务端在现有 route prepare 上保留旧 active edge 与 connection generation;新 parent 在 active child IDs/map 外只建立一个 provisional `HostPeer`,answer/candidate 按 exact child/connection/revision 优先路由到该 PC。Viewer 以当前 prepare revision 的正 RTP、正 decoded frames 和 live video track 证明后,matching active revision 才把同一个 PC 原子移入 active map,再执行普通 child reconcile 并一次性提交拓扑、route 与候选 connection ID;provisional 禁止用新 connection ID 自动重试。prepare-ready 后的候选失败必须保留精确 revision/parent/connection identity 到 matching active 或 rollback;候选 parent 对同 revision 的重复 matching active 持续 fail-closed,不 promotion、不建新 PC,也不越权发送 `route-failed`,恢复只由 target Viewer probe 发起。其他 edge 的已鉴权 hard `route-failed` 先 abort soft probe 并让 target/candidate 回到 current authority,再立即进入原故障梯级。候选失败、超时、session/share 变化或无候选均回滚/保留旧边,不创建 SFU/TURN intent。同房同时最多一个 peer migration。若同一 Viewer parent peer/session/share 在五秒内由两个不同 child 分别确认任一类型,服务端把该 parent 的独立 `relayEligible` 暂停 30 秒,使拓扑有效容量为零但保留客户端广告值;后续 capacity 广播不能绕过。Host 不参与,parent 自身 upstream/观看不变;本 slice 只保证触发 child 的 MBB,其他 current children 暂沿既有有界重选,完整 sibling MBB 后续实现。到期惰性恢复,parent 新 session 或新分享立即恢复,stop/delete/disconnect 清理状态。该离散资格不命令 LiveKit/WebRTC 媒体层,也不把 RTT、CPU、码率、设备信息压成综合分数;窗口仍需真实质量矩阵校准。 +上述 two-distinct-child corroboration 仅在未来独立验收并接受更高 relay-capacity tier 后有效;当前 Browser1 release 使其不可达,且不授权第二 child。 + 状态决策必须关联同一时间区间和媒体代次的三段证据: | 阶段 | 必要证据 | diff --git a/docs/需求理解.md b/docs/需求理解.md index 39e7ec9..3ef1444 100644 --- a/docs/需求理解.md +++ b/docs/需求理解.md @@ -41,7 +41,7 @@ - [ ] 私密房间双入口:private room 接受现有 `/r/{code}#v={grant}` 合法邀请直接进入,或“站点访问 + 房间号 + 该房间观看密码”。没有 grant 的 `/join` 或 `/r/{code}` 先显示明确标为“站点访问”的整站验证,通过后才显示明确标为“加入房间”的当前房间密码输入;有效 site-access cookie 直接跳过第一步,但两类密码不得复用、代填或互相替代。Host 可设置、轮换或移除房间观看密码。密码接受 1 至 64 个可见 ASCII 字符;异步 scrypt 全局最多执行 2 个、等待 16 个派生。站点口令和房间密码都不得进入 URL/query/storage/log/SQLite;不存在、错误密码、队满和正确密码后的满房都返回通用结果。 -- [ ] 统一自动路由:旗舰部署开启 `PEER_ASSISTED_MEDIA` 后,所有正常房间都进入同一个 direct/peer -> SFU -> selected-edge controller;每个房间独立保存 assignment/session/revision/connection generation 和容量状态。合格父节点构成网状候选关系,但实际媒体保持无环且每个 Viewer 只有一个 active upstream;加入、容量变化、掉线和当前边失败触发有界重选,切换重叠必须受现有边预算与当前代媒体证据约束,不做常驻多源分片。一个 child 的已确认 C+B 质量事件仍只处理该 edge:候选只允许 active upstream 为 peer 或 SFU、session/share/revision 当前且有效 relay capacity 严格多一个空槽的 Viewer,Host 排除;保留旧边并在候选 parent 的 active child map 外建立至多一个 provisional `HostPeer`,只有候选在当前 prepare revision 收到 RTP、解码出帧且持有 live video track 后才把同一个 PC 原子 promotion 并换父。prepare-ready 后候选失败时,精确 revision/parent/connection identity 必须保留到 matching active 或 rollback;候选 parent 对同 revision 的重复 matching active 持续 fail-closed,不 promotion、不建新 PC,也不越权发送 `route-failed`,恢复只由 target Viewer probe 发起。其他 edge 的真实 `route-failed` 必须先抢占 soft probe、恢复 target/candidate current authority,再进入原故障梯级。候选失败、超时、session/share 变化或无候选都保留旧边,质量事件本身不得进入 SFU/TURN。只有同一 Viewer parent peer/session/share 在五秒内收到两个不同 child 各自三窗确认的 `severe` 或 `relative-fps`,服务端才把该 parent 的独立 relay 资格暂停 30 秒,使有效容量为零但不覆盖客户端广告。Host 不退位,parent 的上游观看不变;其他当前 children 暂沿既有有界重选,完整 sibling MBB 是后续 slice。新 parent session、新分享或 30 秒到期立即/惰性恢复资格。`PEER_ASSISTED_ROOM_IDS` 已退役,room `1` 只作为历史 smoke fixture,不能作为功能开关。未启用总开关的开发/轻量部署仍可保留 ordinary P2P;普通 peer 始终 STUN-only。selected-edge TURN 只能复用当前 controller 选中的异常 edge,不能向普通 peer edge 预发 candidates。该能力不增加 UI、用户选择、百分比发布或第二套路由器。 +- [ ] 统一自动路由:旗舰部署开启 `PEER_ASSISTED_MEDIA` 后,所有正常房间都进入同一个 direct/peer -> SFU -> selected-edge controller;每个房间独立保存 assignment/session/revision/connection generation 和容量状态。合格父节点构成网状候选关系,但实际媒体保持无环且每个 Viewer 只有一个 active upstream;加入、容量变化、掉线和当前边失败触发有界重选,切换重叠必须受现有边预算与当前代媒体证据约束,不做常驻多源分片。一个 child 的已确认 C+B 质量事件仍只处理该 edge:候选只允许 active upstream 为 peer 或 SFU、session/share/revision 当前且有效 relay capacity 严格多一个空槽的 Viewer,Host 排除;保留旧边并在候选 parent 的 active child map 外建立至多一个 provisional `HostPeer`,只有候选在当前 prepare revision 收到 RTP、解码出帧且持有 live video track 后才把同一个 PC 原子 promotion 并换父。prepare-ready 后候选失败时,精确 revision/parent/connection identity 必须保留到 matching active 或 rollback;候选 parent 对同 revision 的重复 matching active 持续 fail-closed,不 promotion、不建新 PC,也不越权发送 `route-failed`,恢复只由 target Viewer probe 发起。其他 edge 的真实 `route-failed` 必须先抢占 soft probe、恢复 target/candidate current authority,再进入原故障梯级。候选失败、超时、session/share 变化或无候选都保留旧边,质量事件本身不得进入 SFU/TURN。只有在未来独立验收并接受更高 relay-capacity tier(当前 Browser1 release 不可达,且不授权第二 child)后,同一 Viewer parent peer/session/share 才可在五秒内收到两个不同 child 各自三窗确认的 `severe` 或 `relative-fps`,服务端才把该 parent 的独立 relay 资格暂停 30 秒,使有效容量为零但不覆盖客户端广告。Host 不退位,parent 的上游观看不变;其他当前 children 暂沿既有有界重选,完整 sibling MBB 是后续 slice。新 parent session、新分享或 30 秒到期立即/惰性恢复资格。`PEER_ASSISTED_ROOM_IDS` 已退役,room `1` 只作为历史 smoke fixture,不能作为功能开关。未启用总开关的开发/轻量部署仍可保留 ordinary P2P;普通 peer 始终 STUN-only。selected-edge TURN 只能复用当前 controller 选中的异常 edge,不能向普通 peer edge 预发 candidates。该能力不增加 UI、用户选择、百分比发布或第二套路由器。 - [ ] 质量控制扩展:推荐档位仍是默认入口;“分享高级设置”只允许 720p/1080p/1440p 分辨率 ceiling、15 至 60 的整数 FPS ceiling、2 至 12 Mbps 视频码率 ceiling、清晰/平衡/流畅三种偏好、`自动 | H.264 | VP8` 编码选择,以及离散的 `64 kbps 省流 | 128 kbps 音乐默认 | 256 kbps 极高音质` 屏幕音频 sender ceiling;初始值采用 `balanced`、`automatic` 与 128 kbps。视频档位在直播中显式应用;codec 与音频档位在直播中锁定并于下一次分享生效,避免控件产生连续控制流量。所有当前及以后建立的 Web Host、Browser relay 与 SFU sender 必须请求并读回同一房间锁定的 64/128/256 kbps 上限,实际码率仍由浏览器拥塞控制和 stats 决定。Peer 只在唯一明确的 active Opus audio 上做结构化、幂等的 Viewer answer `stereo=1;maxaveragebitrate=256000` 变换,使同一接收上限覆盖三档;非目标形态或 parser/writer 异常必须原样保留浏览器 answer,不得阻断可继续的 mono/video edge。不得用宽泛字符串/正则改 SDP,也不得改写浏览器协商的 FEC。SFU 将三档映射到 pinned SDK 的 music stereo preset 或显式 256 kbps ceiling,统一保留 `forceStereo = true`、`dtx = false` 与 pinned room default `red = true`。不得暴露任意码率、声道、采样率、audio codec、stereo、DTX、RED 或 FEC 控件;256 kbps 只是显式较高上限,不是无损、高保真或最低码率保证。实际质量持续低于所选上限时必须显示可解释的限制原因;在真实拥塞、编码器和接收端证据完成前,不把任一组合称为“完美平衡”。未来 microphone voice 若进入范围,必须是独立 track/处理链,不能把 AEC/降噪/DTX 套到电影或游戏音频。 - [ ] SFU 质量设置生命周期:直播中修改档位时,浏览器 sender 的 applied readback、LiveKit `LocalVideoTrack` 保存的 degradation preference、track restart options 与 `LocalTrackPublication` 的 republish options 必须作为同一次串行操作更新;任一步失败恢复旧 sender 与旧 SDK 保存态,旧 publication generation 不得回写当前状态。LiveKit full reconnect/republish 不得恢复初次发布的旧偏好。该修复不改变 codec、码率上限、层数或路由,也不能单独解释即时低帧。 - [ ] 本地预览性能候选:在相同动态场景中 A/B 测量 Host 动态预览开/关的 CPU、GPU、encode time 和实际 FPS。只有稳定收益且捕获、编码和远端画面不受影响时,才允许直播开始后默认显示静止缩略图或隐藏动态预览并提供按需展开;未测前不宣称收益。 @@ -76,7 +76,7 @@ - 可维护性:代码、需求、调研、项目记忆、`AGENTS.md` 和 `.codex/` 均在 Git 中跟踪;开发配置不得依赖单一操作系统的绝对路径。 - 迁移完整性:同一 release 的客户端、服务端和部署配置原子迁移;canary 结束后删除被替代的配置、wire、parser、双写与测试,不为无当前消费者的旧版本保留兼容层或第二套架构。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 统一使用当前 relay capacity `2`;移动真机表现是兼容观察,不建立单独 route class。 +- 兼容性:分享端首个目标矩阵为 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 统一使用当前 relay capacity `1`,直到逐跳重编码与资源门禁通过;移动真机表现是兼容观察,不建立单独 route class。 ## 边界与约束