Files
Piik/docs/research/browser-local-encoding-pool.md
T
AAA 383f3656c7 fix(media): stabilize sharing startup and recovery
## Problem and outcome / 问题与结果

Sharing could fail when an optional output or encoder comparison failed, while retired Browser work could block media cleanup. Browser pooling also spent its startup protection before publication and classified its synthetic transport clock as realtime video, delaying recovery after a bandwidth drop. These repairs stay within the existing capture, producer, edge and connection owners.

Self-hosting samples now follow the configured public Origin and permit the site's requested camera/microphone capture. Piik's own Origin rejection has a specific bilingual explanation, distinct from arbitrary proxy 403 responses.

## Release notes

### 中文

这是一次分享稳定性维护更新,重点修复开播、换源与画质恢复,并改善自部署站点的配置和错误说明。

- Windows 自动选择编码器时,如果已验证 H.264 可用,后续软件编码对比失败或超时会保留可用的 H.264,避免因此无法开播。
- 较低画质输出或单条音频发送连接失败时,保留仍然健康的分享和其他连接。
- 修复停止分享、换源和媒体连接退役时,旧异步任务阻塞清理与恢复的问题。
- 改善浏览器共享编码的首播画质与网络波动后的恢复,避免因短时码率变化反复重建编码器。
- 自部署配置示例默认跟随站点网址;创建房间因网址未获允许而返回 403 时,页面会指出需要检查的配置。nginx 示例也修正了对本站摄像头、麦克风的权限限制。

已有自部署站点若遇到 403,请检查 `PUBLIC_BASE_URL` 与 `ALLOWED_ORIGINS`。使用旧 nginx 示例的站点需更新 `Permissions-Policy`;步骤见[自部署教程](https://piik.tv/docs/zh/self-hosting)。本次更新不改变房间、信令或 App 控制协议。

### English

This maintenance update improves sharing startup, source changes and video-quality recovery, with clearer configuration and error guidance for self-hosted sites.

- Windows automatic codec selection retains a verified H.264 encoder if the subsequent software comparison fails or times out, instead of failing to start sharing.
- A failed lower-quality output or individual audio sender no longer interrupts healthy sharing and other connections.
- Fixes retired asynchronous work blocking cleanup and recovery when stopping sharing, changing sources or retiring media connections.
- Improves initial quality and recovery after network fluctuations in Browser pooled encoding, while avoiding unnecessary encoder recreation after short bitrate changes.
- The self-hosting sample follows the configured site address by default. Piik's Origin-related room-creation 403 now identifies the settings to check. The nginx sample also permits camera and microphone requests from the site itself.

If an existing self-hosted site returns 403, check `PUBLIC_BASE_URL` and `ALLOWED_ORIGINS`. Sites using the old nginx sample must update `Permissions-Policy`; see the [self-hosting guide](https://piik.tv/docs/self-hosting). Room, signaling and App control protocols are unchanged.

## Verification / 验证

- `npm run check`: 75 files / 1,106 tests, TypeScript and production Web build passed.
- `npm run check:go`: Go formatting, tests, vet, supported core builds, Windows native compile and regressions passed. Darwin cgo packaging remains owned by its macOS CI runner.
- Repository hygiene, documentation checks and website/documentation build passed.
- Isolated VP8/H.264 Browser checks: constrained-child isolation and recovery, short congestion pulse, live and quiet settings, pause/resume, source replacement and ordinary fallback passed. Controlled bandwidth can still cause temporary low resolution, stalls and A/V jitter.
- Windows native Auto/H.264/VP8 startup, window resize/fullscreen/minimize/restore, Browser-to-App fanout and embedded-SFU checks passed in the bounded local fixtures.
- Old-code negative controls reproduce the repaired failure paths. A real Chromium policy check rejects camera/microphone with the previous nginx header and captures both fake devices with the corrected same-origin policy; geolocation remains denied.
- Independent read-only review covered root ownership, cancellation and the producer/carrier adaptation boundary.

## Impact / 影响

Compatible patch release, planned as v1.6.4. No wire, storage, routing-authority or interaction-model change. Matching Server restart interrupts current process-owned media; package cutover retains the existing bounded rollback procedure. Unknown field reports still require device/network evidence and are not claimed resolved by local fixtures. Raw diagnostics and experiment artifacts remain ignored; accepted constraints and concise evidence live in their existing documentation owners.
2026-09-25 14:42:59 +08:00

15 KiB

Browser Node-Local Encoding Pool

Reviewed and executed 2026-09-09 on Windows, Chrome 152.0.7977.82. ADR-0014 owns the selected design. The observations below belong to that measured baseline; status owns current adoption and release state, and verification status owns remaining physical limits.

Result And Scope

An independent local WebRTC producer can supply compatible direct children without making one real child the encoding authority. Each child's existing connection retains RTP, ICE, congestion control and recovery. Different weak demands can require separate local producers; healthy siblings keep their original producer. There is no ancestor cache or application quality ladder.

The selected Chromium encoded-stream path copies the producer's payload, type and exposed codec metadata, assigning the outgoing carrier's RTP timestamp. Native scaling on a source clone bootstraps each carrier. After the first real producer frame, its existing sender switches to a CPU-resident 16x16 Canvas track driven by producer events. It does not first warm another full-size encoder, and there is no independent carrier timer. Unsupported APIs and failed pooling retain ordinary senders. The original capture/received track is never replaced by a carrier.

This is one full encode plus small carrier encodes for compatible children, not literally one encoder or one Encode call. Each producer also has a local connection and receiver decoder. Browser relay reuses its local encoded output across direct children; it still decodes/re-encodes its received source.

Why The Earlier Implementations Were Removed

  • Borrowing a real child's encoder couples siblings to that child's adaptation. It does not test an independent producer.
  • Quiet placeholders omit native sender accounting and do not provide a usable clock. They are not a zero-cost product implementation.
  • Standard own-frame payload replacement can transmit real pictures, but retains carrier metadata. At 1080p, VP8 intermittently stalled. Extra carrier key-type rendezvous and a canvas clock did not establish reliable parity.
  • The selected complete-frame API removes that Worker and key-type rendezvous. It preserves exposed metadata, not every internal native capture timestamp; played-audio and source-age measurements remain necessary.
  • Starting two full ordinary encoders before pooling caused a transient extra encoding load and, in one run, 500-700 ms early played-audio lag. Starting the tiny carrier immediately removed that unnecessary warmup; the next VP8 1080p check measured offsets between about -64 and +42 ms.
  • Hand-subtracting audio/repair from available BWE introduced another inaccurate allocator. The pool now uses the existing sender's native video payload target, bounded by Host settings. This is not the same unit conversion as Native's measured RTP-to-codec budget, which remains necessary.
  • Scaling the full received H264 track for each tiny carrier retained substantial processing cost. Replacing that input with a CPU-resident Canvas, driven only by producer frames, reduced the six-second relay fixture from 7.27 to 5.36 CPU-seconds; ordinary relay used 5.39. Summed relay encoding time fell from about 4,840 to 1,177 ms (ordinary: 2,268 ms). This removes the extra cost rather than claiming a total CPU win. Picture-age p95 was 124/161 ms versus 43/49 ms for ordinary relay; no additional timing controller was added to chase it.
  • A waiting member must replan when a shared producer outgrows its allocation. An already-available better healthy output must not require its old encoder to recover first. Both conditions produced failed startup checks; the fixes reuse membership and the existing output comparison, without a new timer.

The replaced experiments and selected original measurements remain recoverable at Git checkpoint 4e3de79; runtime code and the runner keep only the selected implementation. Failed cases are evidence, not acceptance passes.

Current Executed Evidence

The selected summaries retain exact fixture hashes, raw artifact names, delivered frames, source ages and limitations. The actual HostPeer and pool are bundled into the fixture; it does not copy their implementation. All workloads ran serially.

Actual product path Observed result
VP8, 1080p30, 400 kbps constrained A A reached 480x270 and returned to 1080p; B kept 1080p. A decoded 195 frames in 14 constrained seconds and 981 in 40 recovery seconds.
H264, same constraint A reached 960x540 and returned to 1080p; B kept 1080p. A decoded 366 and 1,165 frames in the corresponding windows.
Both codecs, live settings 720p60, quiet-source 480p30, pause, resume and new source delivered the requested ceilings without replacing the outgoing connections.
Producer failure Both codecs returned to ordinary sending on the existing connections; VP8 delivered 181 frames per child in six seconds, with no retained groups.
VP8, late child Both children shared the existing producer and decoded fresh 1080p, without backward source IDs.
VP8, received-track relay One local producer supplied two children at 1080p; source-age p95 was about 71/122 ms, with played audio present.
Actual hidden page Native fake source continued at its available 20 fps; both receivers decoded 200 frames in ten seconds. The first run had a temporary profile-cleanup failure, recorded separately from media behavior.

The source generates moving bars and a frame-ID barcode. It is not a game perceptual-quality benchmark. Played audio is recaptured from the receiving video element; this is element-level synchronization, not physical speakers or display scanout. Hidden-page checks use a native fake source because a background canvas timer is itself throttled. They establish neither real-game 60 fps endurance nor background audio timing.

The constrained runs use a test-only forward UDP bridge with about 250 ms maximum queued service; reverse feedback is unshaped. They are not a model of public NAT, geographic RTT or every congestion/loss pattern. Reduced quality and temporary stalls under real constraints remain possible.

Balanced Startup And Recovery

Serial Chrome 152 checks on 2026-09-25 reproduced two composition defects in VP8, 1080p30, motion + balanced, with a 5 Mbps ceiling. A forward-only UDP shaper limited A to 400 kbps; B remained unshaped. These synthetic-source runs establish delivered dimensions and recovery, not game perceptual quality or the cause of reports without diagnostics.

  • A group's local warmup spent its five-frame startup protection before any child published its frames. Restoring balanced then allowed native initial downscaling against an untrained allocation. Protection now begins at the first committed outgoing frame; paused/empty frames cannot spend it and later children cannot reset it. The frame threshold and polling cadence are unchanged.
  • The synthetic carrier used motion content intent. Chromium classifies that as realtime video, without the default screen-content ALR probing. After a producer adapted downward, low real output could leave the outgoing native bandwidth estimate slow to recover. The carrier now uses detail once at construction, selecting the framework's screen-content behavior. The real producer retains motion and the Host's degradation preference.

Pinned WebRTC explains both boundaries: starting the quality scaler restarts initial frame dropping; content hints select the screencast option, which selects ALR probing configuration. The screen probing default is enabled. Detail also changes the tiny encoder's content type; it is not an independent public probing switch or a request to change the real picture's quality.

VP8, 14-second constraint / 40-second recovery Initial A / B Constrained A / B Recovered A / B
Ordinary 1080p / 1080p 540p / 1080p 1080p / 1080p
Previous pool 270p / 270p 270p / 540p 720p / 1080p
Publication-start correction alone 1080p / 1080p 270p / 1080p 540p / 1080p
Publication start + screen-content carrier 1080p / 1080p 270p / 1080p 1080p / 1080p

The combined VP8 run returned to 1080p about 17 seconds after shaping ended. H264 with played audio also returned to 1080p; its unconstrained child retained 1080p throughout. A separate one-second pulse after 45 healthy seconds still caused native downscaling: the repaired VP8/audio path briefly reached 180p, then regained 1080p about 13 seconds after pulse start. The previous pool's no-audio pulse ended the 40-second recovery at 360p. Audio changes the bandwidth composition, so these pulse runs do not establish an exact speedup. Normal WebRTC can also temporarily reduce resolution after a pulse; this repair does not promise blur-free delivery under changing network or device load.

A single-consumer control, retaining its mature producer, recovered without the deeper dual-consumer drop. Together with the publication-start control, this distinguishes cold producer adaptation from repeated producer churn. The rate-owner repair separately retains existing encoders through output-rate spikes and shared budget changes; a genuinely weaker child can still need its own producer. No delayed-budget policy, bitrate floor or manual recovery probe was added. Probe padding remains framework-owned traffic under native limits.

Budget attribution remains unchanged: targetBitrate is the encoder's allocated target, not raw link bandwidth (upstream stats correction). Replacing it with availableOutgoingBitrate would bypass native allocation and protection. Keep producer, carrier, egress and decoded observations separate.

Cost And Accounting

Before the CPU-carrier refinement, a matched actual-product VP8 1080p30 comparison's ordinary pair made 350 full-size encodes in six seconds, using about 1,201 ms summed encode time. The pool made 170 full-size and 342 tiny encodes, using about 516 ms summed encode time. Whole isolated Chrome CPU was 9.23 versus 8.95 CPU-seconds. The fixture includes source drawing, barcode reads, all receiving pages and the extra producer receiver; this is not Host-only CPU, GPU or thermal measurement.

Earlier independent-producer comparisons showed a larger whole-fixture VP8 saving, but H264 showed no total CPU win despite fewer full encodes. Do not generalize one percentage or count dropped frames as an optimization. The current H264 pair likewise reduced summed encode time from about 1,649 to 927 ms, while whole-fixture CPU increased from 4.17 to 4.56 CPU-seconds over six seconds (about 0.065 of one CPU core). Less full-resolution encoding does not guarantee lower overall CPU on hardware codecs. The owner accepts a uniform path from the first eligible consumer, without an audience-count threshold. Singleton and hardware costs remain explicit.

Receiver details stay real inbound observations. Sender transport bytes/loss belong to each connection; dimensions/cadence describe the frames actually written; native limitation evidence comes from the assigned real producer. Shared encode counts/total encode time are not duplicated per child. Retired producer samples keep their latest per-instance report rather than reverting to an older polling snapshot.

Reproduction

Use the existing isolated-browser harness and stable executable paths:

npx tsx scripts/browser-local-pool-probe.ts ordinary --1080 --av --preference=balanced
npx tsx scripts/browser-local-pool-probe.ts carrier --1080 --av --preference=balanced
npx tsx scripts/browser-local-pool-probe.ts carrier --1080 --av --network --auto --rate=400000 --preference=balanced
npx tsx scripts/browser-local-pool-probe.ts carrier --1080 --av --lifecycle
npx tsx scripts/browser-local-pool-probe.ts carrier --1080 --av --relay
npx tsx scripts/browser-local-pool-probe.ts carrier --1080 --background

Add --h264, --single or --late for the corresponding case. Set --network --auto --short-pulse for a 45-second warmup, one-second constraint and 40-second recovery instead of the ordinary 14-second constraint. Set CHROME_PATH for another installed Chromium binary. --auto extends the recovery observation; product code owns all adaptation. Results go to ignored build/browser-local-pool; summarize with node scripts/browser-local-pool-summary.mjs <result.json>. A successful process exit does not establish visual/performance parity.

Primary References And Remaining Boundaries

  • W3C Encoded Transform enforces standard frame owner/order. The selected legacy API is a different available API boundary, not a feature flag relaxing those checks.
  • Chromium M152 encoded frame supports copy construction with validated RTP timestamp metadata.
  • Pinned WebRTC transform delegate reconstructs copied native frames; internal capture/presentation timing is not wholly preserved. Current negotiated VP8/H264 connections lack generic dependency-descriptor extensions, so no frame-ID mapping was added.
  • LiveKit capability detection recognizes createEncodedStreams(); it is not itself a reusable encoding pool.
  • WebRTC stats distinguishes the video encoder payload target from link bandwidth.
  • Chrome timer throttling explains why a JS canvas timer is not a reliable hidden-page media clock.

The future RTCEncodedSource proposal is not exposed by the installed M152 Browser. No future API, feature flag, SFU policy change, Browser cross-level encoded passthrough or public-network performance guarantee is assumed. Matching Debug/artifact acceptance and representative game/hardware feedback remain separate from these bounded runs.