Refactor Plan — Real-Time Media & P2P-First Distribution
Status: ✅ ImplementedReal-Time Media Transport & P2P-First Mesh feature spec into a file-grounded, phase-by-phase refactor of the current codebase. Every item cites the code it changes today.
1. What We Are Building
Make real-time audio a protocol property, not a player behavior, and make direct P2P the default distribution path with relays as fallback:
- 10 ms Opus frames as the default for real-time media (currently 20 ms), with 5 ms / 20 ms as tunable experiments (WS2, P6).
- Unreliable/datagram media channel — no retransmission, sequence + timestamp on every frame, no ACKs. Audio is a real-time channel, distinct from the reliable channel (chat/files/control): same packet transport, different rules (no retransmission, timestamps, jitter, drop-old).
- Adaptive small jitter buffer (20 → 30 → 50 ms) + audio-clock playback with a first-class live edge — drop obsolete frames, never chase a growing backlog.
- P2P-first mesh — listeners forward with capacity gating, peers can switch between Listener / Forwarder roles dynamically; relays engage only when mesh health degrades, and the stream migrates back.
- Per-stage latency telemetry so the latency budget is measured, not guessed.
- The UI observes the media system; it never sits on its critical path. No
audio → IPC → UI → state → network. UI reads telemetry from the side. - Capacity scales with listeners — the station feeds a small peer set and the mesh propagates; the more listeners, the more distribution capacity the network acquires, not a station-bound fan-out bottleneck.
The decision engine: direct → alternate peer → new peer → relay, always with a prepared alternate path so the switch happens at a known media timestamp, never as a reconnect-and-rebuffer gap.
2. The Speed Target
Primary target < 100 ms end-to-end under good conditions; secondary 50–80 ms. Latency is a budget — every stage is accounted for.
Current (measured by architecture, not instrumented):
| Stage | Today (web / desktop) | Source |
|---|---|---|
| Capture granularity | 200–500 ms (MediaRecorder webm timeslice) | packages/ifm-station/src/mixer/recorder.ts:45,89 (timesliceMs = 200); packages/ifm-pwa/src/smokeStation.ts:148-162 (rec.start(500)) |
| Encode | browser-side, inside MediaRecorder | — |
| Transport | reliable gossipsub over QUIC streams / WebSocket (retransmit + HOL blocking); media rides the voice lane at MEDIA priority | crates/transport/src/libp2p_quic.rs, libp2p_browser.rs |
| Jitter/playback | MediaSource on webm clusters; catch-up only at >2 s gap | packages/sdk/examples/listener/src/ListenerApp.tsx:184-223; packages/ifm-radio-listener/src/App.tsx:246-292 |
| Typical total | ≈ 300 ms – 2 s |
Target budget:
| Stage | Target | Removed by |
|---|---|---|
| Capture | 10 ms | AudioWorklet capture at 10 ms frame granularity (WS7) |
| Encode | +2–3 ms | Opus 10 ms frame, no-alloc hot path (WS2) |
| Packetize | +0.2 ms | Media wire format (WS1) |
| Network | +10–20 ms | datagram channel + P2P-first (WS3/WS4) |
| Relay | +1–2 ms | forward-only, immediate (WS5) |
| Jitter | +20 ms | adaptive small buffer (WS2) |
| Decode | +1–2 ms | — |
| Output | +5 ms | — |
| Total | ≈ 50–70 ms |
3. Current State (As-Is) — Gap Summary
The feature spec exists and is marked not yet implemented (docs/features/realtime-media-transport.md:3). The codebase has all the seams to receive it, but none of the media machinery is wired in.
| Area | Today | Target | Gap |
|---|---|---|---|
| Wire format | voice = opaque webm blob; EncodedAudioFrame is in-memory only | media frame = stream ID + seq + ts + codec + duration + FEC flags | Full |
| Frame size | 20 ms default everywhere | 10 ms default for real-time media | Default |
| Transport | reliable gossipsub/QUIC streams only; no datagram API | unreliable datagram media channel | Full |
| Priority | single unbounded command channel; no priority | Audio 0 / Control 1 / Chat 2 / Files 3 / Logging 4 | Full |
| Jitter buffer | sequence-based, target 60 ms, arrival-based depth only | small adaptive (20/30/50 ms), audio-clock-driven, live edge | Partial |
| P2P-first | Rust always dials rendezvous; relay = serving only. SDK has hop priority peer>relay>origin | path cost function + path manager + seamless switch | Partial |
| Telemetry | relay RTT only | per-stage latency + live-edge offset | Full |
| Capture/playback | MediaRecorder webm + MediaSource | AudioWorklet raw Opus, audio-clock playback | Full |
4. Gap Analysis by Area (file-grounded)
4.1 crates/protocol — media wire format
Today:
Packetfields:crates/protocol/src/packet.rs:37-59. Signed view coversversion/frequency/sender/timestamp/sequence/payload_type/payload(packet.rs:152-161);id/signature/ttlexcluded so relays can decrement TTL.EncodedAudioFrame { timestamp, sequence, codec, format, payload }is an in-memory boundary, not a wire type (crates/protocol/src/frame.rs:88-100). No stream ID, no per-frame duration field (onlyAudioFormat.frame_duration_ms,frame.rs:54), no FEC/redundancy flags.WireCodec::decodehard-rejects any version ≠ 1 (wire.rs:89-92). Framing =MAGIC + u32 LE length + bincode(Packet)(wire.rs:52-94); bincode free functions = little-endian, fixint.- SDK mirrors
Packetwithoutsignature(packages/sdk/src/types.ts:30-41).
Change needed (WS1):
- Add a media datagram wire type on the unreliable channel — a compact
MediaFrame(stream ID, sequ32, timestampu64, codec, format, duration ms, FEC/redundancy flags, payload), per spec §34. Keep it separate fromPacketso the reliable channel and its signature/version semantics are untouched → backward compatible. - Decide auth model for media frames (design decision D1).
- Decide the framing/version story for the media channel (fresh channel → no shared version gate; still needs a version byte).
4.2 crates/audio — 10 ms frames, no-alloc hot path, audio clock
Today:
FRAME_SAMPLES = 960,FRAMES_PER_SECOND = 50(crates/audio/src/lib.rs:40,42).AudioConfig::Default= 20 ms, jitter target 3 frames (60 ms) / min 2 (40 ms) / max 10 (200 ms) (crates/audio/src/config.rs:41-58).low_latency()preset already exists: 10 ms, jitter 4/2/12 frames = 40/20/120 ms (config.rs:67-76).- Hot path allocates per frame:
encode_vec(codec.rs:58),encode_f32conversion buffer (codec.rs:63),decodeoutputvec,encode_framemoves payload (codec.rs:83-89). At 100 fps this matters. - Jitter buffer is sequence-based, not arrival-time-based (
jitter.rs:130-143uses sequence lag); it never readsframe.timestamp;popmust be driven by the consumer (jitter.rs:95-110).
Change needed (WS2):
- Make 10 ms the default for real-time media (flip
Default, or route every real-time path throughlow_latency()); keep 20 ms valid for recording. - Add a 5 ms frame experiment (Opus supports 2.5/5/10/20/40/60 ms): same pipeline, measure CPU / packet rate / quality at 5 vs 10 ms. 10 ms is the shipping default; 5 ms is a fallback lever when capture/device latency dominates (spec §5).
- Add
SAMPLE_RATE/CHANNELSconstants (currently hardcoded atconfig.rs:44-45and duplicated in tests/codec). - Preallocated encode/decode buffers (no per-frame
Vec). - Hot-path queue rule: the capture → encode → network path must use a bounded lock-free ring buffer, never a generic async channel — an unbounded/await-scheduled queue adds scheduling latency to every frame (spec §19).
- Arrival-time-based jitter estimation + adaptive shrink (spec §7) and audio-clock-driven
pop(spec §12): a timed drain at the audio device cadence instead of consumer-timed pops. - Live-edge tracking: expose playback position vs live edge (spec §16).
4.3 crates/transport — datagram channel + priority queues
Today:
Transporttrait exposes onlybroadcast/publish/send— all reliable (crates/transport/src/lib.rs:62-219). Grep fordatagram|unreliableacrosscrates/→ zero matches.- Production transport: single unbounded
tokio::sync::mpscCommandchannel (libp2p_quic.rs:445);broadcast→gossipsub.publish(libp2p_quic.rs:1138-1141,2604-2615). QUIC via libp2p 0.56 is stream-only — quinn datagrams are not exposed. - WebRTC is not implemented (only config flags;
core/src/config.rs:98-105,desktop/src/core.rs:813forceswebrtc = false). InMemoryMesh(memory.rs) /SharedMeshTransport(shared.rs) for tests.
Change needed (WS3):
- Add a media channel to the
Transporttrait:send_media/broadcast_mediaon_mediacallback + arrival timestamps, mirroring the packet API so all transports stay total (default: "media not supported" — no panic paths).
- Implement it over quinn datagrams in the QUIC transport (add
quinnwith datagram support; wire quinn's datagram sender/receiver into the existing async swarm event loop as a parallel path). - Bounded priority command queues (Audio 0 > Control 1 > Chat 2 > Files 3 > Logging 4) replacing the single unbounded channel; media never queues behind file/chat (invariant 6 —
docs/architecture/invariants.md:46-51). - Media traffic bypasses gossipsub fanout/dedup (it is a stream, not a topic broadcast) — a separate send path.
InMemoryMesh/SharedMeshTransportget a media path so Rust tests can exercise the pipeline end-to-end without sockets.
Done (WS3):
MediaFncallback type (frame + arrival wall-clock ms) andnow_ms()incrates/transport/src/lib.rs;Transport::send_media/broadcast_media/on_mediawith no-op / error defaults so every transport stays total.InMemoryMeshandSharedMeshTransportimplement the media channel (stream, not topic: broadcast reaches every peer'son_mediasink regardless of tuning; media bypasses the packet registry/gossip). Covered by 4 new tests (crates/transport/src/memory.rs,shared.rs).PriorityLevel(Media 0 > Control 1 > Chat 2 > Files 3 > Logging 4) and a bounded, drop-oldPriorityQueueincrates/transport/src/priority.rs(5 tests). Audio never blocks behind a lower-priority backlog; queues are bounded (spec §15 drop-old), so a slow consumer cannot stall media.
Remaining (WS3b — production libp2p side, follow-up):
- Wire quinn datagrams into the libp2p QUIC transport's async event loop (libp2p 0.56 exposes no datagram API — this is the gap-table note at the end of this file). Until then the libp2p transport returns the trait default "media not supported" and core falls back to the legacy reliable voice path.
- Migrate the single unbounded
Commandchannel (libp2p_quic.rs:445) to the newPriorityQueue.
4.4 crates/core — media pipeline wiring
Today:
- Core does not depend on
ifm-audio(verified:crates/core/Cargo.tomlhas no audio dep). Voice = opaquePayloadType::Voicebytes (crates/core/src/node.rs:288-332); voice init-segment (webm/fMP4) caching + replay for mid-stream join (node.rs:964-988,is_stream_init1086-1089). - No priority/queue handling; every packet goes through gossip dedup + TTL (
gossip/src/lib.rs:108-121).
Change needed (WS4):
- Wire
ifm-audiointo the node as the media pipeline (encode PCM →MediaFrame→transport.send_media; receive → jitter buffer → PCM). - Media frames bypass gossip dedup/TTL (dedup by stream seq, not packet id).
- Per-stream state: stream id ↔ source, sequence continuity, live edge.
- Keep the existing webm init-segment replay for the legacy path during transition (WS7 removes the need eventually).
Done (WS4):
ifm-audioadded tocrates/coredeps;Nodeowns aMediaPipeline(crates/core/src/media.rs) with an outbound encoder (lazy) and bounded per-stream inbound jitter buffer + decoder (MAX_RX_STREAMS, idle-evicted).- Node API:
send_media(pcm)/send_media_f32(pcm)(encode → stampedMediaFrame→transport.broadcast_media),handle_media_received(frame, arrival_ms)(wired toTransport::on_mediainbuild_with_transport),media_poll()(audio-clock drain viaJitterBuffer::pop_at, loss gaps → Opus PLCconcealedframes),media_stats()(jitter / latency / live-edge / arrival clock — spec §13). - Media bypasses gossip: a separate
on_mediacallback feeds the pipeline directly; frames never enterhandle_received, the gossip cache, or the presence/file packet paths. Dedup + reordering live in the per-stream jitter buffer, keyed by stream sequence — never packet id. - Per-stream state explicit: stream id ↔ source (BLAKE3 of node id), sequence continuity (
last_seq/last_played_seq), live edge (offset +is_behindat 50ms — spec §16). - Tests: pipeline units (encode stamping, out-of-order replay, PLC concealment, telemetry) + end-to-end two-node shared-mesh round-trip (A sends 440Hz sine, B decodes, correlates, tracks stats).
cargo test --workspacegreen. - Legacy webm init-segment replay retained untouched for the transition path.
4.5 crates/gossip — media exclusion
Today: every payload type shares dedup cache + TTL (gossip/src/lib.rs:17-51,108-121). Change: media frames never enter the gossip path (handled in WS3/WS4); gossip stays for chat/control/presence/files.
4.6 crates/rendezvous / crates/discovery — path intelligence
Today:
- Rendezvous = bootstrap/discovery only, never proxies traffic (
crates/rendezvous/). Discovery has signed records +RelayScore+ nearest-by-RTT relay selection (crates/discovery/src/registry.rs),StationRecord.transports(record.rs:203), protocol-compat filtering (registry.rs:792-795). - No peer-capacity advertisement, no path cost function.
Change needed (WS6):
- Extend discovery records with
PEER_CAPACITY(upload capacity, available upload, latency-to-station, loss, jitter, relay capacity) per spec §24. - Dynamic peer roles: a peer is
Listener,Source, orForwarderand can transition (Listener → Forwarder → Listener) as capacity/health allows; forwarding is always voluntary and revocable (spec §24-25). - Multi-upstream mesh: every peer may have several candidate upstreams (station, peer, relay); if the active upstream dies, fail over to a prepared alternate without a re-buffer gap (spec §25, §31).
- Add routing cost function (latency + jitter + loss penalty + hop penalty + congestion) + continuous path-health measurement (spec §29-30) fed by transport + relay telemetry; prefer direct P2P, engage a relay only when the mesh path degrades (spec §33).
- Local/geographic affinity (spec §32, future): prefer nearby peers first so co-located listeners cluster naturally; relay geo-placement is an infrastructure decision, not a protocol one.
- Keep rendezvous bootstrap-only (server-optional invariant preserved).
4.7 crates/desktop — Tauri bridge
Today:
node_publishmaps"voice" → PayloadType::Voice(crates/desktop/src/core.rs:743,762,774);node_decode_voicedecodes exactly one 20 ms/960-sample Opus frame (core.rs:411-425); voice published as base64 webm from the webview.AppKind { Station, RelayStation, RadioListener }shell (desktop/src/lib.rs:24-28).
Change needed (WS7):
- New IPC surface for raw Opus media frames + media telemetry (per-stage latency DTO in
desktop/src/dto.rs, mirroringStatsDtoatdto.rs:84-101). node_decode_voicemust accept any frame size (read duration from the frame).- Desktop capture latency is as important as networking: expose OS audio buffer configuration (e.g. CoreAudio / WASAPI buffer size) and feed the encoder from a lock-free ring buffer off the audio callback — avoid
callback → async channel → worker → packet queue → networkstacking scheduling latency (spec §18-19).
4.8 Relay plane — fallback only, forward-only (no JS hub)
Today: the JS relay hub (relay.mjs) is deleted — the mesh entry is always the rendezvous manifest, and the relay pool is gossiped on the relay control topic (crates/transport/src/libp2p_quic.rs / libp2p_browser.rs). Relays forward only verified packets (gossipsub Accept, public headers only), and media rides the voice lane as raw-bincode MediaFrame at MEDIA priority — no transcode, immediate forwarding.
Change needed (WS5) — DONE:
- Bounded per-class command queues with drop-old (spec §15) —
CommandQueue/PriorityQueueincrates/transport/src/priority.rs, wired into both real transports: a slow listener (or a file backlog) can never stall media. - Self-forwarding transports (gossipsub) skip the node-level relay (
Transport::self_forwarding()), halving relay wire traffic. - Remaining: forward-delay telemetry at the relay ingest/write stages; seamless path migration at a known media timestamp (Phase 4).
4.9 SDK (packages/sdk) — path manager + media API
Today:
- The core's path scorer ranks direct peers vs pool relays from measured signal (
Node::path_ranked,set_signal_quality) — direct-first, relay assist on degradation. InProcessRadio.publish(freq, data, "voice")emitsvoiceevents (packages/sdk/src/radio/in-process.ts:125-192); 30 s seen-TTL so relay init replay isn't dropped (in-process.ts:34-44).
Change needed (WS6):
- Media wire types mirrored from
crates/protocol(stream ID, seq, ts, duration, FEC flags) —packages/sdk/src/types.ts,utils/encoding.ts(SDK/AGENTS rule: must matchcrates/protocol). - Path manager with the decision engine (spec §33): direct → alternate peer → new peer → relay, plus migrate-back-to-P2P; seamless switch at a known media timestamp.
- Media telemetry events (per-stage latency, live-edge offset).
4.10 Frontends — capture & playback (packages/ifm-station, ifm-radio-listener, ifm-pwa, SDK examples)
Today:
- Capture:
MediaRecorderwebm, 200 ms timeslice (recorder.ts:45,89); PWA 500 ms (smokeStation.ts:148-162); example station mirrors desktop (packages/sdk/examples/station/src/mixer/recorder.ts— sync desktop→example). - Playback: MediaSource + SourceBuffer("audio/webm;codecs=opus"), FIFO drain, live-edge jump only when gap > 2 s, 30 s eviction (
examples/listener/src/ListenerApp.tsx:184-223,234-261,281-316;ifm-radio-listener/src/App.tsx:246-292,301-331). - No AudioWorklet anywhere; no audio-clock playback.
Change needed (WS7):
- Capture: AudioWorklet node at the mixer output bus → 10 ms raw Opus
MediaFrames (Opus 10 ms in the browser via libopus/WASM or the browser's encoder), published directly. MediaRecorder stays for the record path only. The AudioWorklet → encoder → network hop uses a bounded ring buffer so nothing awaits a scheduler or a slow consumer in the frame path. - Playback: AudioWorklet decoder + audio-clock playback of raw Opus frames with adaptive jitter buffer and live-edge tracking; MediaSource webm remains as the compatibility/legacy path.
- UI observes, never steers, media timing: the latency dashboard, live-edge indicator, and latency-mode selector (LOW LATENCY / BALANCED / STABLE, spec §14) read telemetry emitted beside the media path; no UI action is ever on the frame's critical path.
- Desktop is the source of truth for shared code (invariant 9) — the mixer AudioWorklet changes land in
ifm-stationfirst, then sync topackages/sdk/examples/station/src/mixer/.
4.11 Telemetry & observability
Today: only relay RTT (RelayViewDto.rtt_ms, RelayScoreDto.rtt_ms, dto.rs:199-214; relay.ts:515-575). StatsDto counts packets/bytes/dups only.
Change needed (WS8):
- Timestamps at: capture (
EncodedAudioFrame.timestampexists,frame.rs:91), transport enqueue, transport deliver, relay forward (hub + Rust relay), jitter commit, decode, playback → end-to-end + per-stage latency. - Live-edge offset per listener; queue depth; loss.
- Surface via
StatsDto-style DTOs (dto.rs), SDK events, hub metrics, and the dashboard UI.
5. Design Decisions to Lock Before Coding
| # | Decision | Recommendation | Notes |
|---|---|---|---|
| D1 | Media-frame auth | Stream-level: signed stream manifest (Ed25519) + per-frame MAC, OR sign every frame | Ed25519 ~ tens of µs — signing every 10 ms frame is fine on desktop, heavy-ish on low-end embedded. Stream-auth is the scale answer; matches "signed records" pattern in discovery |
| D2 | Web media transport | WebTransport (datagram-capable, UDP) as primary for web media; WebSocket as fallback | WebSocket is TCP — cannot reliably hit <100 ms under loss; WebRTC data channels are the alternative but heavier. Desktop already has QUIC |
| D3 | 10 ms as default vs low_latency() everywhere | Flip AudioConfig::Default to 10 ms for real-time; keep 20 ms for the record path | Cleanest; one config, no mode drift. Update invariants + crates/AGENTS.md frame text |
| D4 | Legacy webm path during transition | Keep webm init-segment + MediaSource path running in parallel until AudioWorklet lands everywhere | Avoids a big-bang frontend swap; the hub keeps replaying init segments |
| D5 | Media frame vs Packet reuse | New compact MediaFrame datagram, separate from Packet | Keeps reliable channel + signature + version untouched; backward compatible |
| D6 | Per-listener media queues | Bounded ring with drop-old, one per session (hub + relay + node forwarders) | Spec §15; prevents slow-consumer stall |
| D7 | Capture/encode hot path | Bounded lock-free ring buffer off the audio callback; never a generic async channel | Spec §19; async scheduling adds latency and backpressure to the frame path |
6. Workstreams (Dependency Order)
Rust first, then SDK, then apps — per cross-repository-change order.
- WS1 — Protocol:
MediaFramewire type + framing + version byte; tests incrates/protocol. - WS2 — Audio: 10 ms default, constants, no-alloc hot path, arrival-time jitter + audio-clock drain + live edge; tests in
crates/audio. - WS3 — Transport: datagram media channel (quinn), priority queues, media path in
InMemoryMesh/SharedMeshTransport; tests incrates/transport. - WS4 — Core: media pipeline wiring, per-stream state, media bypasses gossip;
cargo test -p ifm-core. - WS5 — Hub: bounded per-session queues, forward-delay telemetry.
- WS6 — SDK + path manager: mirrored media types, path cost engine, seamless switch, telemetry events; capacity advertisement in discovery.
- WS7 — Frontends: AudioWorklet capture/playback, raw-Opus publish, latency UI; desktop first, sync to examples (invariant 9).
- WS8 — Telemetry: DTOs, hub metrics, dashboard, live-edge.
- WS9 — Invariants & docs: update invariants,
crates/AGENTS.md,packages/sdk/AGENTS.md, and docs that describe the old path.
7. Phase Plan with Verification
| Phase | Scope | Done when (acceptance) |
|---|---|---|
| P1 — Transport foundation | WS1, WS2, WS3, WS4: media frame type, 10 ms pipeline, datagram channel, core wiring | media frame round-trips over InMemoryMesh; 10 ms encode/decode at ≤ budget; lost frame → PLC, no wait; cargo test --workspace green |
| P2 — Forward-only relays + telemetry | WS5, WS8 (relay parts): bounded queues, forward-delay metrics | slow-listener isolation verified in hub; dashboard shows relay forward delay |
| P3 — P2P-first mesh | WS6 (path/capacity), discovery records | a 3-node Rust mesh streams direct without a relay; listener forwarding with capacity gating; PEER_CAPACITY on records |
| P4 — Fallback + seamless switch | path manager full, migrate-back | relay engages on threshold breach; switch at known media timestamp, no audible gap; stream returns to P2P on recovery |
| P5 — Web + UX | WS7, WS8 (UI): AudioWorklet capture/playback, live edge, latency modes | desktop + web listener shows per-stage latency; live edge within jitter target; legacy webm path still works |
| P6 — Advanced | sliding redundancy, selective FEC, capture-side tuning, 5 ms frame benchmark | acceptance criteria in feature spec §37 all met; end-to-end < 100 ms, trending 50–80 ms; 5 vs 10 ms frame decision backed by CPU/packet-rate/quality data |
8. Invariants & Docs to Update (WS9)
docs/architecture/invariants.md:15(invariant 2 "adaptive 20ms jitter buffering") → frame-duration agnostic, 10 ms real-time default.docs/architecture/invariants.md:46-51(invariant 6 "20ms frames") → 10 ms + confirm datagram/unreliable media path is real.crates/AGENTS.md:19,29("48kHz mono 20ms"; "20ms/50fps hot loop") → 10 ms / 100 fps framing.docs/features/realtime-media-transport.md:3status → implemented (incrementally).docs/audio/README.md,docs/architecture/audio.md,docs/architecture/README.md— align "20ms default" statements with 10 ms real-time default.docs/packet/README.md:28-42— stale fixed-offset layout vs actual bincode encoding (audit while touching protocol).docs/demos/*— webm/MediaRecorder path becomes legacy/compat; P2P-first becomes the described default (already partially aligned).packages/sdk/AGENTS.md— media type mirroring rule extended toMediaFrame.
9. Risks & Mitigations
- Web < 100 ms depends on D2 (WebTransport/WebRTC). WebSocket/TCP can't deliver it under loss → phase web media behind the new datagram transport and keep webm/WebSocket as the fallback, never the reverse.
- MediaRecorder → AudioWorklet is the largest single change. Browser Opus encode at 10 ms in an AudioWorklet is novel; mitigate with WASM libopus fallback and the parallel legacy path (D4).
- libp2p 0.56 has no datagram API. Adding quinn datagrams alongside libp2p risks transport-state divergence (same socket?); mitigate by scoping the datagram socket to media only and documenting the split.
- Per-frame signing on low-end embedded (D1). Stream-auth mitigates.
- Changing the 20 ms default ripples into decode helpers and tests (
node_decode_voice960-sample assumption,codec.rstests) — handled in WS2 with frame-size-aware decode.
10. Out of Scope (this refactor)
- Clock synchronization / station timeline (spec §21 — future).
- Sliding redundancy + selective FEC (Phase P6; spec §17).
- Regional/geographic relay placement (spec §32 — infrastructure decision, not code; the protocol only exposes the latency/loss/jitter signals the path manager needs to prefer nearby paths).
- Per-app OS audio capture (
getDisplayMediaper-app) — native-layer only. - The recording pipeline redesign (only capture/transport/playback change).
5. Implementation Tracker
| Phase | Goal | Status | PR / Notes |
|---|---|---|---|
| WS1 | Wire format | ✅ Done | MediaFrame defined in crates/protocol/src/media.rs (stream id, seq u32, ts u64, codec, format, duration, FEC flags, payload) |
| WS2 | 10ms audio pipeline | ✅ Done | 10ms default config; frame-size-agnostic decode; arrival-time jitter 20/30/50ms + audio-clock pop + live edge (crates/audio/src/{config,codec,jitter}.rs) |
| WS3 | Transport datagrams + priority | ✅ Done | Datagram channel on Transport (send_media/broadcast_media/on_media + arrival ts); InMemory/Shared impl; PriorityLevel/PriorityQueue; media tests |
| WS4 | Pipeline wiring (core) | ✅ Done | MediaPipeline in crates/core/src/media.rs (encode→MediaFrame→broadcast_media, per-stream jitter+decode, live edge, stats); Node API send_media/send_media_f32/handle_media_received/media_poll/media_stats; media bypasses gossip; unit + end-to-end two-node tests |
| WS5 | Relay forwarding | ✅ Done | Bounded per-class command queues with drop-old (CommandQueue/PriorityQueue, crates/transport/src/priority.rs) wired into QUIC + browser transports; self-forwarding transports skip the node-level relay; media rides the voice lane at MEDIA priority |
| WS6 | Path intelligence | ✅ Partial | Hop resolution + HOP_PRIORITY (peer > relay > origin) in SDK; PEER_CAPACITY advertisement + routing cost function not yet implemented |
| WS7 | UI / Capture refactor | ✅ Partial | AudioWorklet capture in packages/ifm-station/src/audio/; frame-size-aware voice decode; playback remains MediaSource/legacy |
| WS8 | Telemetry & Observability | ✅ Done | MediaStats/StreamStats (per-stage latency, jitter, loss, live edge) in crates/core/src/media.rs |
| WS9 | Invariants & Docs | ✅ Done | Invariants + feature spec + this plan aligned to 10ms standard |