CLAUDE.md: chrome decoder anchor + always-SFU receive + telemetry + tickets
- SPEAKER_PLAYOUT_DELAY_SEC bumped to 0.7s; mesh peers no longer carry inbound audio so the worklet cushion absorbs host-wiggle on the SFU path unaided. - New "Chromium decoder anchor" section pins the load-bearing hidden muted <audio> requirement so a future refactor can't silently remove it. - New "Audio receive path — SFU only" section documents the always-SFU contract + ground rules for any future mesh reintroduction. - New "Telemetry — signal-server CLIENT_LOG" section documents how to grep /var/log/zebra-spaces-signal.log via SSH, the per-tick line shape, the one-shot session fingerprint, and the per-service /version endpoints on cors-proxy.uncloseai.com. - New "Defect tracking — docs/tickets/" section documents the resolution loop (telemetry → failing test → fix → stamp → deploy → close).
This commit is contained in:
parent
da7181f6d2
commit
40de2aa9c0
1 changed files with 99 additions and 4 deletions
103
CLAUDE.md
103
CLAUDE.md
|
|
@ -280,10 +280,11 @@ Rules that took blood to find:
|
|||
rearmThresholdBlocks=100`) before re-arming; emit silence in the interim.
|
||||
|
||||
2. **Role-aware buffer depth.** Listener gets 4s (lean-back, latency doesn't
|
||||
matter, ride out wiggle-stalls). Speaker / cohost / host get 0.5s (small
|
||||
enough for conversation, big enough to smooth ordinary jitter). Mesh
|
||||
peers always 0.5s. `playoutDelayForRole(role)` and `SPEAKER_PLAYOUT_DELAY_SEC`
|
||||
in `web/zebra-spaces.html`.
|
||||
matter, ride out wiggle-stalls). Speaker / cohost / host get **0.7s**
|
||||
(`SPEAKER_PLAYOUT_DELAY_SEC` in `web/zebra-spaces.html` — bumped from
|
||||
0.5s when we removed the mesh→worklet swap; the worklet cushion now
|
||||
absorbs the 200ms host-wiggle unaided on the SFU receive path).
|
||||
`playoutDelayForRole(role)` returns the active target.
|
||||
|
||||
3. **Worklet retarget on role change**, don't rebuild. Post
|
||||
`{cmd:'retarget', targetSeconds}` to the worklet's port — recomputes
|
||||
|
|
@ -315,3 +316,97 @@ who don't have AudioWorklet support — natural deep buffer on the
|
|||
`<audio>` preload side. Was deadlocking on `bcastMu` until 2026-06-04
|
||||
(commit fixed `bcastInitCapture.Write` self-recursion). Worklet is the
|
||||
primary path; HTTP-pull is the safety net.
|
||||
|
||||
### Chromium decoder anchor — required for remote audio tracks
|
||||
|
||||
**Every remote `MediaStreamTrack` consumed by a
|
||||
`MediaStreamAudioSourceNode` must ALSO be attached to a hidden muted
|
||||
`<audio>` element in the same page.** Without that anchor, chromium
|
||||
does NOT run its WebRTC audio decoder for the track, and the source
|
||||
node produces silence — even though packets arrive at full rate,
|
||||
`audioCtx.state === 'running'`, and the worklet's `started` event
|
||||
fires (queue filled with zeros).
|
||||
|
||||
Firefox does NOT have this restriction. Defects of this shape present
|
||||
as **chrome-only silence with every JS-side invariant green**.
|
||||
|
||||
The anchor lives on the `listenerAudioNodes` entry as `node.anchor`
|
||||
(see `attachAudioStreamViaWorklet`). It is muted, hidden, and
|
||||
autoplays. `setWorkletStream` swaps `anchor.srcObject` alongside the
|
||||
source node. `detachListenerStream` tears the anchor down.
|
||||
|
||||
Diagnostic signal: pavucontrol Playback shows the chrome stream
|
||||
present, level meter pinned at 0 while RTP telemetry shows
|
||||
`aud.recv pkt=N → N+50/sec bytes=growing level=0.000 jbuf=?` and
|
||||
worklet `started` event has fired. → anchor is the load-bearing
|
||||
fix. Do NOT remove it during a refactor.
|
||||
|
||||
Full root-cause writeup: `docs/tickets/0001-fedora-chrome-cannot-hear-speakers.md`.
|
||||
Memory: `feedback-chromium-decoder-anchor`.
|
||||
|
||||
### Audio receive path — SFU only
|
||||
|
||||
**SFU is the only receive-audio path for every role**, including
|
||||
speakers. Mesh PCs (`peers` map) carry our OUTBOUND mic only. Their
|
||||
inbound audio is ignored (`pc.ontrack` in `connectToPeer` is a
|
||||
breadcrumb log only).
|
||||
|
||||
This is a 2026-06-07 change. The previous mesh→worklet swap caused
|
||||
chromium decoders to silently stall at the source-swap moment. The
|
||||
0.5s → 0.7s `SPEAKER_PLAYOUT_DELAY_SEC` bump compensates for the lost
|
||||
mesh latency benefit by giving the worklet enough cushion to absorb
|
||||
the 200ms host-wiggle on the SFU path.
|
||||
|
||||
If you reintroduce a mesh receive path, the right shape is
|
||||
verify-before-swap (poll mesh receiver stats for
|
||||
`jitterBufferEmittedCount > 0` BEFORE disconnecting the SFU source).
|
||||
|
||||
### Telemetry — signal-server CLIENT_LOG
|
||||
|
||||
Every `logLine` call in `web/zebra-spaces.html` ships to the
|
||||
zebra-spaces-signal server as a `client-log` WebSocket message. The
|
||||
server logs to `/var/log/zebra-spaces-signal.log` on
|
||||
proxy.uncloseai.com with the publisher's pubkey + handle attached.
|
||||
|
||||
Diagnostic SSH (read-only):
|
||||
|
||||
```bash
|
||||
ssh -i ~/.ssh/digitalocean -p 22222 root@proxy.uncloseai.com \
|
||||
'tail -2000 /var/log/zebra-spaces-signal.log | grep "actor_handle=\"HANDLE\""'
|
||||
```
|
||||
|
||||
Per-tick (~5s) telemetry line shape:
|
||||
|
||||
```
|
||||
· role=X sub=X/X pub=X/X mesh=N sListen=N streamMode=N muted=X xcr=X ctx.sink=…
|
||||
aud.recv pkt=N lost=N bytes=N jitter=N level=N jbuf=Xs lp=Xs (per SFU sub PC audio receiver)
|
||||
vid.recv pkt=N lost=N frames=N jbuf=Xs lp=Xs (per SFU sub PC video receiver)
|
||||
mesh.recv u=XXXX pkt=N … level=N jbuf=Xs (per mesh PC audio receiver)
|
||||
mic.send.aud / cam.send.vid / scr.send.vid / etc. (outbound stats per publish PC)
|
||||
```
|
||||
|
||||
One-shot session fingerprint on entry (after `joined as X — uuid …`):
|
||||
|
||||
```
|
||||
session: ua=chrome/desktop ctx={sr=48000 baseLat=0.0107 sinkSupp=1 sink=…}
|
||||
devs={out=N in=N cam=N} picked={mic=… cam=… spk=…}
|
||||
```
|
||||
|
||||
The signal-server endpoint backs `cors-proxy.uncloseai.com`. Per-service
|
||||
`/version` is exposed at `/zebra-spaces-signal/version` /
|
||||
`/zebra-spaces-sfu/version` / `/zebra-signal/version` / `/version` (each
|
||||
returns the upstream Go binary's commit + build_time).
|
||||
|
||||
### Defect tracking — docs/tickets/
|
||||
|
||||
Anything that takes more than 15 minutes or needs cross-browser /
|
||||
cross-session repro lives as a numbered markdown file under
|
||||
`docs/tickets/`. Format spec + index in `docs/tickets/README.md`. New
|
||||
tickets get the next number. The resolution loop is:
|
||||
|
||||
1. Reproduce or grab signal-server telemetry.
|
||||
2. Write a failing test in `test/` that pins the contract.
|
||||
3. Fix until test passes.
|
||||
4. `make stamp`, push both repos.
|
||||
5. Update ticket Status to `fixed`, link commit, note what to
|
||||
re-test in production.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue