From b7de6ed24f734d9f7259affc9729abebbb6bda34 Mon Sep 17 00:00:00 2001 From: AAA <35992542+TNTcraftHIM@users.noreply.github.com> Date: Sat, 22 Aug 2026 05:00:26 +0800 Subject: [PATCH] fix(media): restore peer-assisted 16-viewer admission (#170) Keep the default at eight while restoring the documented explicit 1-16 admission range and its cap2/cap3 functional gate. --- .../0004-peer-assisted-media-experiment.md | 21 +++++++-------- docs/project-memory.md | 2 +- docs/research/low-server-media-routes.md | 13 ++++----- docs/research/peer-assisted-media.md | 27 ++++++++++++++----- docs/status.md | 2 +- docs/verification-status.md | 2 +- docs/方案设计.md | 2 +- scripts/peer-assisted-benchmark.ts | 3 ++- src/server/config.ts | 7 ----- tests/peer-assisted-benchmark.test.ts | 6 ++++- tests/server-config.test.ts | 18 ++++++++++--- 11 files changed, 61 insertions(+), 42 deletions(-) diff --git a/docs/adr/0004-peer-assisted-media-experiment.md b/docs/adr/0004-peer-assisted-media-experiment.md index a7256fb..ccbe0d5 100644 --- a/docs/adr/0004-peer-assisted-media-experiment.md +++ b/docs/adr/0004-peer-assisted-media-experiment.md @@ -42,15 +42,11 @@ not be read as requiring users to select a topology or rejecting automatic cross-mode fallback as a product goal. The experiment is gated by `PEER_ASSISTED_MEDIA`, which defaults to `false`. -Enabling it requires `MAX_VIEWERS_PER_ROOM` at or below eight; larger configured -rooms fail configuration rather than silently running a different topology. -A required non-empty `PEER_ASSISTED_ROOM_IDS` deployment allowlist restricts -the experiment to exact valid room IDs. Non-allowlisted rooms use the current -ordinary P2P authenticated wire, signaling, quality behavior, and lifecycle and -must never enter `HybridMediaRouter`. Missing, blank, invalid, empty-list-entry, -or duplicate values fail startup when the experiment is enabled; there is no -all-room fail-open. This exact-room gate is temporary validation scope and adds -no UI, percentage rollout, or routing score. +When enabled, it applies to every room. `MAX_VIEWERS_PER_ROOM` remains one +shared admission limit: it defaults to eight and accepts explicit values from +one through sixteen. The upper admission bound is not a media-capacity or +performance claim. The removed `PEER_ASSISTED_ROOM_IDS` allowlist fails startup; +there is no second rollout mode, UI selector, percentage rollout, or route score. The signaling server assigns a sticky, bounded DAG with a deterministic breadth-first walk. The host and each ordinary Web viewer currently have @@ -127,9 +123,10 @@ Chrome and Edge form the controlled relay cohort; Android Chrome and iOS Safari remain compatibility observations. ADR-0005 now gives every ordinary Web Viewer the same two-edge capacity without UA or visibility detection. Mobile resource behavior remains unverified, but does not define a separate route -class. This depth-three gate remains an eight-viewer experiment; it cannot -authorize the accepted 20-viewer default. That later release requires a separate -20-viewer matrix or bounded central exceptions. +class. The resource/quality gate remains an eight-viewer experiment. A separate +sixteen-viewer loopback may prove only admission, bounded topology, and decode +function; neither result can authorize a 20-viewer default. That later release +requires a separate 20-viewer matrix or bounded central exceptions. The proposal advances only if every condition holds: diff --git a/docs/project-memory.md b/docs/project-memory.md index ca01ae4..65a609a 100644 --- a/docs/project-memory.md +++ b/docs/project-memory.md @@ -53,7 +53,7 @@ Last updated: 2026-08-22 - Production uses nginx, Node.js 24.19.0, coturn 4.17.2, and LiveKit 1.13.5 on UDP 7882 (TCP fallback off). Ordinary peer ICE receives no TURN; the selected-edge tuple is configured only for the current controller edge, with no credential pre-advertised to ordinary peers. Coturn retains the old authenticated-relay ports. Services are healthy with zero restarts. - ADR-0005 rejects `PEER_ASSISTED_ROOM_IDS`; `PEER_ASSISTED_MEDIA=true` enables every room and ordinary peers stay STUN-only. Initial or active Host ingress may consume one bound relay-only grant; ready/abort clears it and failure restores peer baseline. Stale room-ID config fails startup. Production and source-only Native use `screener-v5`; Native does not subscribe to Web presence. - Defaults are balanced with automatic codec and 128 kbps screen audio; clarity/fluid, H.264/VP8, and 64/128/256 kbps audio remain explicit. Codec and audio quality are locked during a share. LiveKit 2.22.0 SFU uses `q,h`; production exposes one Host-local current-`h` A+B snapshot and clears it on publisher identity changes. Target-browser field availability, actual media values, BWE, and route observation remain open. -- Chrome 151 five-Viewer 720p30 cap2 evidence: Host/relay fanout two; both relay children +80 decoded frames/~1.1 MB; every Viewer decoded; clean stop. Functional only; resource and heterogeneous-network behavior remain open. +- Chrome 151 local functional evidence includes five-Viewer cap2 and sixteen-Viewer cap2/cap3 runs. All Viewers decoded; the sixteen-Viewer runs bounded Host/relay edges at 2/2 and 3/3, but final samples were only 320x180 at 9-10 fps and omitted process resource totals. Resource, visual quality, endurance, and heterogeneous-network behavior remain open. ## Provisional Quality Targets diff --git a/docs/research/low-server-media-routes.md b/docs/research/low-server-media-routes.md index 9ccc87c..c573010 100644 --- a/docs/research/low-server-media-routes.md +++ b/docs/research/low-server-media-routes.md @@ -1,8 +1,8 @@ # Low-Server-Cost Media Routes - Research date: 2026-08-21 -- Scope: one broadcaster, at most eight trusted viewers, low latency, and host - media fanout at most two +- Scope: one broadcaster, explicit admission up to sixteen trusted viewers, + low latency, and bounded host media fanout - Status: ADR-0005 accepts STUN-only direct/peer UDP, bounded SFU/UDP roots, then optional authenticated TURN/UDP for one controller-selected exceptional edge. Production enables the controller for all rooms and has a configured @@ -16,9 +16,10 @@ The smallest current plan is: 1. Use direct host P2P for one or two viewers. -2. For later viewers, use the fixed two-chain browser relay in ADR-0004. It is - controlled by `PEER_ASSISTED_MEDIA`, is limited to at most - eight viewers, and decodes and re-encodes at every relay. +2. For later viewers, use the bounded browser relay DAG in ADR-0004. It is + controlled by `PEER_ASSISTED_MEDIA`, shares the one-through-sixteen room + admission limit, and decodes and re-encodes at every relay. The retained + representative resource/quality gate remains eight viewers. 3. Plan a separate native shared-encode sender regardless of the browser relay result. It reduces duplicate host encoding while retaining at most two standard WebRTC edges, so it does not remove their upload cost. The bounded @@ -527,7 +528,7 @@ lost. | Route | Where copies are emitted | Endpoint cost | Current disposition | | --- | --- | --- | --- | | Direct host P2P | Host emits one copy per viewer | Host upload and sender pipelines grow with viewers | Keep for one or two viewers | -| Bounded browser relay DAG | Host and each Web relay emit at most two copies | Ordinary browser, but every relay decodes and re-encodes and adds a hop | Current capacity-two source candidate, maximum-eight-viewer gate | +| Bounded browser relay DAG | Host and each Web relay emit at most the configured endpoint cap | Ordinary browser, but every relay decodes and re-encodes and adds a hop | Admission defaults to eight and permits 1-16; representative resource/quality gate remains eight viewers | | Native shared-encode host | Host targets one encode for at most two standard WebRTC edges | libwebrtc public-API proxy risk spike, with Pion as fallback | Planned separate sender phase; still pays per-edge upload | | Native volunteer encoded-RTP relay | Each volunteer forwards one encoded copy | Native install, RTP/RTCP forwarding, packaging, and opt-in relay policy | Conditional experiment only if relay re-encoding is the sole browser-spike failure | | SFU virtual parent | SFU normally emits one or two root copies; roots keep peer descendants | Service pays measured root egress; host sends one publication | Accepted primary central fallback after direct/peer UDP; local SFU/UDP and active Host-ingress relay function pass, while public-room/performance evidence remains open | diff --git a/docs/research/peer-assisted-media.md b/docs/research/peer-assisted-media.md index fe3d005..f4c0134 100644 --- a/docs/research/peer-assisted-media.md +++ b/docs/research/peer-assisted-media.md @@ -1,8 +1,9 @@ # Peer-Assisted Media Research - Research date: 2026-08-21 -- Scope: one game-screen broadcaster, at most eight trusted viewers, host media fanout at most two -- Status: historical evidence plus the current capacity-two source candidate; +- Scope: one game-screen broadcaster, explicit admission up to sixteen trusted + viewers, with the retained resource/quality gate at eight +- Status: historical evidence plus the current bounded source candidate; accepted ADR-0005 owns automatic peer/SFU routing. Production later removed the room-`1` rollout boundary; retained SFU media remains unverified. @@ -260,6 +261,17 @@ configures only its local server and gate. A 2026-08-22 Chrome 151 two-second smoke passed with three Viewers at Host2/relay1 and six at Host3/relay3, with all Viewers decoding. This is not a production-default, SFU-capacity, or resource claim. +A 2026-08-22 Windows Chrome 151.0.7922.138 headless loopback then ran sixteen +Viewers for two seconds at 720p30 with cap2 and cap3. Both runs had all sixteen +Viewers decoding and no fatal/check failure. Cap2 peaked at Host2/relay2 with +depth four and a 1,167 ms maximum first-decode diagnostic; cap3 peaked at +Host3/relay3 with depth three and 977 ms. Final samples were only 320x180 at +9-10 fps. The 8-core/16-thread Ryzen 7 9700X runner had 47.1 GiB RAM and 13.7 +GiB free after the runs, but no process CPU, GPU, NIC, or peak-memory totals were +captured. This proves the explicit admission, bounded topology, and decode paths +only; resource, visual quality, endurance, heterogeneous networks, and the +separate 20-viewer gate remain open. + Peer multicast research such as SplitStream demonstrates why load-balanced, failure-tolerant overlays normally introduce multiple trees and content striping. Those mechanisms are intentionally excluded: needing them is a reason @@ -315,11 +327,12 @@ longer has that room boundary. There is no mobile/iPad, UA, or visibility branch This historical gate assumed default-off configuration through `PEER_ASSISTED_MEDIA=false`; current production enables the controller for all rooms. -It cannot be enabled above eight viewers and uses the existing standard WebRTC -screen stream, current Chrome/Edge as the controlled relay cohort, Android -Chrome and iOS Safari as compatibility observations, at most two children per -viewer, and at most eight viewers. It may carry the -existing screen-audio track when the browser provides one. SVC/simulcast, +It uses the existing standard WebRTC screen stream, current Chrome/Edge as the +controlled relay cohort, Android Chrome and iOS Safari as compatibility +observations, and the configured downstream edge cap. The runner accepts the +shared room admission ceiling of sixteen while the representative +resource/quality gate remains at eight. It may carry the existing screen-audio +track when the browser provides one. SVC/simulcast, custom encoded transport, FEC changes, multi-tree striping, transcoding, background mobile relay, and the later ADR-0005 SFU controller are outside this historical browser-relay gate. diff --git a/docs/status.md b/docs/status.md index 38d393e..ae219c2 100644 --- a/docs/status.md +++ b/docs/status.md @@ -16,7 +16,7 @@ This file is the current execution index. Load [verification status](./verificat - Capture precedes room creation. Balanced, automatic codec, and 128 kbps screen audio are defaults; clarity/fluid, H.264/VP8, and 64/128/256 kbps audio are deployed next-share choices. Codec and audio quality lock during a share. - Local-only connection details expose current Host SFU sender evidence, actual codec/fmtp and encoder fields, media-source/encode FPS evidence, A/V playout/jitter/concealment evidence, and selected candidate endpoints. These fields do not enter quality signaling, server state, logs, persistence, or route selection. - Private fragment grants enter directly; code-only Viewers need site access and private rooms additionally need their password. Credentials remain unstored. Web names and presence are session-only; duplicate names alone show an ID suffix. -- Roots and ordinary Web relays are bounded to two downstream edges, Viewer upstream receive is free, and source permits an explicit deployment cap from one through three. SFU root capacity remains separate. +- Rooms default to eight Viewers and accept an explicit admission limit from one through sixteen. Roots and ordinary Web relays are bounded to two downstream edges, Viewer upstream receive is free, and source permits an explicit endpoint cap from one through three. SFU root capacity remains separate. - Relay quality handling compares fresh child FPS with parent input. Two current corroborating children can pause one Viewer relay for 30 seconds and request peer-only reassignment; no candidate keeps the old edge. - A generation-bound 15-second initial-connect deadline enters ICE restart. One-root healthy-SFU make-before-break is deployed but media-unverified; capacity `0 -> 1`, multi-root, browser, and mobile gates remain open. diff --git a/docs/verification-status.md b/docs/verification-status.md index ed61d76..c38d059 100644 --- a/docs/verification-status.md +++ b/docs/verification-status.md @@ -16,7 +16,7 @@ This is the demand-loaded ledger for cross-cutting validation evidence and open ## Retained Functional Evidence -- Chrome 151 synthetic `1/3/5/8` and 720p30 runs preserved bounded fanout and decoding. In one five-Viewer cap2 run, one relay served two children, each advanced 80 decoded frames and about 1.1 MB, every Viewer decoded, and shutdown was clean. This proves function, not capacity or heterogeneous-network quality. See [peer-assisted media](./research/peer-assisted-media.md). +- Chrome 151 synthetic `1/3/5/8` and 720p30 runs preserved bounded fanout and decoding. A later two-second sixteen-Viewer loopback decoded all Viewers at Host2/relay2 depth four and Host3/relay3 depth three, but ended at only 320x180 and 9-10 fps without process resource totals. These prove function, not sustainable capacity or heterogeneous-network quality. See [peer-assisted media](./research/peer-assisted-media.md). - Chrome 151 with local LiveKit 1.13.5/client 2.22.0 proved one SFU/UDP root after `q,f` -> `q,h`: decoded frames advanced 3 -> 23, rendered frames reached 26, Host used one edge, leaves were clean, and TURN was absent. This is functional evidence only. See [low-server media routes](./research/low-server-media-routes.md). - Host A+B and authenticated P2P Viewer C evidence is sanitized, generation-bound, read-only, and closed for stale, ambiguous, or SFU-fed reports. Production can expose a Host-local current SFU `h` snapshot and clears it on publisher identity changes; raw media metadata is not retained. See [realtime quality adaptation](./research/realtime-quality-adaptation.md). - The built-in peer TURN canary was rejected after allocation passed but the direct Host/Pion Viewer path failed before forced relay. Full rollback restored STUN-only client ICE and zero allocations. See [built-in peer ICE TURN](./research/built-in-peer-ice-turn.md). diff --git a/docs/方案设计.md b/docs/方案设计.md index b013d60..23f36d8 100644 --- a/docs/方案设计.md +++ b/docs/方案设计.md @@ -407,7 +407,7 @@ Peer-assisted 信令只接受一个严格对象:`resolution` 为 `720p | 1080p Viewer 在处理 peer-assisted 鉴权快照时先更新 `ViewerRelay` 的 desired settings,再应用拓扑分配;同一个串行队列协调 remote stream、当前 child 更新和以后重建的 child,因此它们读取同一最新目标。每个 `HostPeer` 也把初始 sender 配置、`replaceStream()` 和 `updateProfile()` 放进同一 sender-mutation queue;desired settings 在入队前同步更新,已排队操作执行时读取最新值,使快速连续修改保持 last-wins,而不增加版本号或确认协议。这里同步的是浏览器 sender 的目标上限,不是实际分辨率、帧率、码率或 shared encoding 的性能保证。 -spike 复用现有 screen stream 和浏览器 codec 协商,当前普通 Web relay 不按桌面、Android 或 iOS UA 拆分 capacity,最多八名 viewer;捕获存在时沿同一标准 WebRTC 路径带上音频。移动真机表现作为非阻断兼容观察;Encoded Transform/DataChannel/WebCodecs 媒体、SVC/simulcast、多树、转码、codec ladder、综合评分、移动端后台 relay 和跨模式 SFU 迁移不在本次 spike 内实现。该范围限制不能解释为拒绝产品所需的自动兜底;验收与失败即放弃条件以 ADR-0004 为准,自动路由由后续独立 ADR 定义。 +spike 复用现有 screen stream 和浏览器 codec 协商,当前普通 Web relay 不按桌面、Android 或 iOS UA 拆分 capacity;房间接入默认八名、显式允许 1 至 16,代表性资源/质量门禁仍按八名执行。捕获存在时沿同一标准 WebRTC 路径带上音频。移动真机表现作为非阻断兼容观察;Encoded Transform/DataChannel/WebCodecs 媒体、SVC/simulcast、多树、转码、codec ladder、综合评分、移动端后台 relay 和跨模式 SFU 迁移不在本次 spike 内实现。该范围限制不能解释为拒绝产品所需的自动兜底;验收与失败即放弃条件以 ADR-0004 为准,自动路由由后续独立 ADR 定义。 浏览器不保证多个 `RTCPeerConnection` 共享一次编码。实验允许 host 对一至两个 seed 各自编码。后续 native sender 必须让每个 active representation 只编码一次并跨 direct、peer 与 SFU transport 复用;每条连接仍拥有独立 packetizer、SSRC、pacing、加密、反馈和上传。健康路径选择共享 `HIGH`,最多另有一份弱路径共用的 `LOW`,是否常驻由硬件、游戏和上传门槛决定。该计划不依赖本次浏览器 relay 实验成败,也不在本 spike 中提前抽象或实现。 diff --git a/scripts/peer-assisted-benchmark.ts b/scripts/peer-assisted-benchmark.ts index fcf43dc..5e35dd6 100644 --- a/scripts/peer-assisted-benchmark.ts +++ b/scripts/peer-assisted-benchmark.ts @@ -9,6 +9,7 @@ import WebSocket from "ws"; import { DEFAULT_PEER_RELAY_DOWNSTREAM_EDGES, MAX_PEER_RELAY_DOWNSTREAM_EDGES, + MAX_VIEWERS_PER_ROOM_LIMIT, type ParticipantRouteAssignment, } from "../src/shared/protocol"; import { @@ -202,7 +203,7 @@ interface ManagedProcess { } const DEFAULT_VIEWER_COUNTS = [1, 3, 5, 8]; -const MAX_VIEWERS = 8; +const MAX_VIEWERS = MAX_VIEWERS_PER_ROOM_LIMIT; const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); class BoundedLog { diff --git a/src/server/config.ts b/src/server/config.ts index 36ea590..732b93e 100644 --- a/src/server/config.ts +++ b/src/server/config.ts @@ -16,7 +16,6 @@ const MIN_SELECTED_EDGE_TURN_SECRET_BYTES = 32; const MAX_SELECTED_EDGE_TURN_SECRET_BYTES = 128; const MIN_SELECTED_EDGE_TURN_TTL_SECONDS = 60; const MAX_SELECTED_EDGE_TURN_TTL_SECONDS = 10 * 60; -const MAX_PEER_ASSISTED_VIEWERS = 8; const DEFAULT_MAX_VIEWERS_PER_ROOM = 8; const DEFAULT_MAX_SFU_ROOTS_PER_ROOM = 2; const MAX_SFU_ROOTS_PER_ROOM = 2; @@ -390,12 +389,6 @@ export function loadConfig( if (nodeEnv === "production" && stunUrls.length === 0) { throw new Error("STUN is required in production"); } - if (peerAssistedMedia && maxViewersPerRoom > MAX_PEER_ASSISTED_VIEWERS) { - throw new Error( - "PEER_ASSISTED_MEDIA currently supports at most 8 viewers per room", - ); - } - return { nodeEnv, port, diff --git a/tests/peer-assisted-benchmark.test.ts b/tests/peer-assisted-benchmark.test.ts index 771f48d..a8d8b98 100644 --- a/tests/peer-assisted-benchmark.test.ts +++ b/tests/peer-assisted-benchmark.test.ts @@ -219,7 +219,11 @@ describe("peer topology loopback configuration", () => { it("deduplicates configured viewer counts without changing their order", () => { expect(parseViewerCounts("8, 3,3,1")).toEqual([8, 3, 1]); - expect(() => parseViewerCounts("9")).toThrow(/1 to 8/); + }); + + it("accepts the room viewer ceiling and rejects values above it", () => { + expect(parseViewerCounts("16")).toEqual([16]); + expect(() => parseViewerCounts("17")).toThrow(/1 to 16/); }); it("requires recovery to target one selected case", () => { diff --git a/tests/server-config.test.ts b/tests/server-config.test.ts index 596ed74..7c56fbe 100644 --- a/tests/server-config.test.ts +++ b/tests/server-config.test.ts @@ -207,14 +207,24 @@ describe("server configuration", () => { expect(() => loadConfig({ PEER_ASSISTED_MEDIA: "1" })).toThrow( "PEER_ASSISTED_MEDIA must be true or false", ); + }); + + it("keeps the peer-assisted room default at 8 and accepts explicit 16", () => { + expect( + loadConfig({ PEER_ASSISTED_MEDIA: "true" }).maxViewersPerRoom, + ).toBe(8); + expect( + loadConfig({ + PEER_ASSISTED_MEDIA: "true", + MAX_VIEWERS_PER_ROOM: "16", + }).maxViewersPerRoom, + ).toBe(16); expect(() => loadConfig({ PEER_ASSISTED_MEDIA: "true", - MAX_VIEWERS_PER_ROOM: "9", + MAX_VIEWERS_PER_ROOM: "17", }), - ).toThrow( - "PEER_ASSISTED_MEDIA currently supports at most 8 viewers per room", - ); + ).toThrow("MAX_VIEWERS_PER_ROOM"); }); it.each(["1", "2", "3"])(