361dbbe201
The legacy-container timestamp on each audio frame was previously
captured at send-time via `frameStartMark.elapsedNow()`. That mark
advances on wall clock, but `current.send(...)` is a suspending call
that backs off on transport backpressure — so a frame captured at T
gets stamped at T+δ once the previous send drains, and the watcher's
WebCodecs AudioDecoder schedules playback at the wrong instant. Audible
as a slowly-growing offset between speaker and listener over the life
of a session that's seen any meaningful send-side queueing.
Replace with a frame-index counter pinned to the codec grid:
`timestampUs = nextFrameIndex * AudioFormat.FRAME_DURATION_US`. The
counter advances once per captured PCM frame BEFORE the encode/mute/
send path, so:
- Encoder failures, empty-encoded frames, and muted frames each
consume one slot — preserving the wall-clock gap on the wire so
a 5 s mute shows up as a 5 s timestamp jump (matches the prior
wall-clock behaviour for mute and silence).
- Send-time stalls don't drift the timestamp: it's computed at the
top of the iteration, not after the suspending send.
- Survives publisher hot-swap (single-broadcast, single-encoder;
the new publisher inherits the running counter) — same guarantee
the prior `TimeMark` had.
Also adds [AudioFormat.FRAME_DURATION_US] = 20_000 (960 / 48 kHz × 1e6)
so the constant lives next to FRAME_SIZE_SAMPLES rather than being
implicit in every call site.
https://claude.ai/code/session_014JfZJHSTvyYYWJbC9VbB47