Files
molluskandClaude Opus 5 63c246d976
CI / check (push) Canceled after 0s
docs: record the jitter buffer's unreachable shrink path as a known bug
`target_delay` grows +1 per disruption to MAX_DELAY_FRAMES (240 ms) but only
shrinks after 250 consecutive clean frames — 5 s of unbroken audio. Two of the
five `clean_run` resets fire on every natural pause in speech (jitter.rs:201
benign underrun, jitter.rs:181 re-prime), and the sender stops transmitting
outright while the gate is closed (core/mod.rs:2041). The AIMD decrease half is
therefore unreachable under conversational voice: one early jitter burst pins
the extra latency for the rest of the session.

Found by code review; not yet reproduced live. Pairs with field-test debt #5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 22:38:26 -04:00

11 KiB

PeerSpeak Feature Inventory

A capability list of what PeerSpeak already does, so we don't keep re-deriving it. This is the user-facing companion to ARCHITECTURE.md (which covers internals). When you ship a feature, add it here.

Field-test status legend:

  • Verified — confirmed working on a real call (usually desktop ↔ dopedart).
  • 🧪 Tests-green — unit-tested and/or smoke-tested locally, but not yet exercised on a real 2-machine call. Treat as unproven.
  • ⚙️ Plumbing — wiring/persistence that has no separate field test of its own.

Rooms & sessions

Feature Status Notes
Create a room → shareable join ticket Ticket carries host addr + topic id.
Join a room via ticket Paste ticket on the launch screen.
Leave a room
Multi-peer rooms (mesh) Presence over the iroh gossip plane.
Copy ticket to clipboard Button on the room screen.
Remembered nickname config.username pre-fills the launch field, saved on join/create.
In-call duration timer call_started drives the header clock.

Audio I/O & codec

Feature Status Notes
PipeWire capture + playback src/audio/pipewire_impl.rs.
Input device selection
Output device selection
App-internal input (mic) gain slider Unity default; applied pre-gate/encode.
App-internal output gain slider Unity default; applied on the mixed output.
Opus codec 48 kHz mono, Application::Voip, 20 ms / 960-sample frames.
Adaptive jitter buffer + Opus PLC 🧪 src/core/jitter.rs; reorders by seq, adaptive playout delay, conceals drops. Not field-tested.

Voice controls

Feature Status Notes
Self mute
Deafen
Push-to-talk + rebindable hotkey TogglePtt / StartSettingHotkey; gate ahead of the encoder.
Noise gate (hysteresis + attack/release/hangover) src/audio/gate.rs.
Draggable gate threshold on the mic meter Discord/OBS-style GateMeter canvas widget.
Mic level meter — in-call
Mic level meter — off-call "Test mic" SetMicMonitor / standalone capture stream.
Mix-bus soft limiter 🧪 src/audio/limiter.rs; A/B never confirmed on a real call.
Echo cancellation (opt-in) 🧪 src/audio/echo_cancel.rs; PipeWire module-echo-cancel (WebRTC AEC+NS+AGC) via RAII pactl guard. Needs a 2-machine call through speakers, not headphones.

Per-peer controls

Feature Status Notes
Per-peer volume SetPeerVolume.
Per-peer local mute Still decoded (levels show) but not mixed.
Per-peer speaking indicator audio_levels per EndpointId.

Text chat

Feature Status Notes
In-room text chat 🧪 Over the gossip plane; full-width bottom-dock UI. 2-machine delivery not confirmed.
Outgoing + incoming sanitization sanitize_chat: strips control chars, collapses whitespace, caps 2000.
History cap ⚙️ CHAT_HISTORY_MAX = 300.
Local echo of own messages

Screen share (via pixelpass)

Feature Status Notes
Share your screen Spawns pixelpass --host --output json, announces the ticket on presence. src/screenshare/mod.rs. Field-verified desktop↔dopedart 2026-06-06. Capture portal appears on the host when the first viewer connects.
Watch a peer's share 👁 Watch spawns a pixelpass viewer → opens the stream in mpv (vlc fallback). Field-verified: laptop watched the desktop's screen.
Live badge on sharing peers 🔴 Live on the peer card; 🔴 Sharing badge on the self card.
Multiple simultaneous sharers 🧪 Discord-style: each sharer is an independent pixelpass host; the room shows one offer per sharer. Single-sharer path verified; concurrent not yet.
Late-joiner sees in-progress share 🧪 Sharing rides presence, so the existing gossip re-announce delivers the offer to late joiners. Not yet explicitly tested.
Graceful "needs pixelpass" state Share button disabled + labelled when the binary isn't on $PATH. Config override: pixelpass_path.
Viewer limit ⚙️ Not overridden — pixelpass bandwidth-measures its own safe cap (protects the sharer's uplink); refusals surface as an error.
Audio of the share ⚙️ Video only on the room path (so it can't echo/double with the voice mix).

Recording

Feature Status Notes
Local call recording (mic + incoming mix → WAV) 🧪 src/audio/recorder.rs; writes to ~/peerspeak-recordings/. Solo + 2-machine recording not confirmed. Header REC timer present.

Notifications & sounds

Feature Status Notes
Desktop notifications toggle notifications_enabled.
Event chimes 8 events: self-join, peer-join, peer-leave, reconnect-attempt, reconnected, self-leave, mic-toggle, reconnect-failed.
Per-event custom sound override custom_sound_* paths in config.

Networking & privacy

Feature Status Notes
iroh QUIC transport
Network mode picker RelayNoDiscovery (default), N0Full, DirectOnly. Takes effect next join.
Retained-address reconnect Dials last-known full addr before falling back to bare id.
Per-peer connection badge (direct/relay + RTT, hover for addr/loss/bitrate) Peer-card badge fed by a 1 Hz poll of the live audio link's selected QUIC path (connection_statscore::connstats::derive). Field-verified on a real 2-machine call 2026-07-08.
Reconnect + eviction model Incl. two-outage reconnect-eviction fix + regression test.
Self-hosted relay Decided against — rely on n0 relays, RelayNoDiscovery default.

UI & layout

Feature Status Notes
Selectable room layouts 3-Column, Bottom Dock (default), Drawer.
Layout picker popup w/ canvas thumbnails LayoutThumb, no image dependency.
Draggable + persisted panel dividers Per-layout, clamped to window; saved on drag-release.
Chat drawer toggle (drawer layout) 🧪 Toggle + restart-persistence not explicitly confirmed.
Responsive full-window Settings screen
Canvas-drawn icon set 18 icons (Icon/IconKind in src/app/mod.rs), no image/font dep, theme-recolored. Replaces all emoji; SVG design source in ~/Documents/peerspeak-mockups/icons/.
Selectable UI themes 10 palettes (src/theme.rs): Catppuccin Mocha (default)/Macchiato/Frappé/Latte, Dracula, Nord, Tokyo Night, Gruvbox Dark, Solarized Light, Gruvbox Light. Picked via canvas palette swatches in Settings (ThemeSwatch), applied live + persisted (config.theme). All palettes unit-tested for WCAG-AA text legibility.

Persistence

Feature Status Notes
Config file ~/.config/peerspeak/config.json.
Backward-compatible loading serde defaults fill missing fields; unknown fields tolerated.
Window size restored window_width/window_height, saved on close.
Window position restored window_x/window_y, saved on close. X11 only — see Platform support.

Platform support (Linux desktop)

Concern Status Notes
Wayland Default on this dev box; winit's Wayland backend. App/taskbar icon comes from the .desktop file matched by application_id = "peerspeak".
X11 (incl. XWayland) winit's X11 backend (both backends compile in by default; winit auto-selects — Wayland if WAYLAND_DISPLAY is set, else X11 via DISPLAY). Launch verified rendering on X11. The embedded RGBA window icon (from_rgba) is honored on X11 even without the .desktop installed.
Window position restore X11 only Wayland's xdg-shell gives clients no way to place their own window, so we center there. On X11 the saved window_x/window_y is restored via Position::Specific (outer position; no drift across save/restore — iced's Moved and Position::Specific both use the outer position). Verified live on X11 (saved an off-center position; the window reopened there, not centered). Gated by is_wayland() (src/app/mod.rs); decision logic unit-tested (initial_window_position).

Known bugs

Defects found by code review, not yet fixed.

  1. Adaptive playout delay never shrinks back in real conversation (src/core/jitter.rs) — target_delay grows +1 per disruption up to MAX_DELAY_FRAMES (12 frames = 240 ms) but only shrinks after CLEAN_RUN_TO_SHRINK = 250 consecutive cleanly-played frames, i.e. 5 s of unbroken audio. clean_run is reset in five places; two of them fire on every natural pause in speech: the benign-underrun branch (jitter.rs:201, talker went quiet) and the subsequent re-prime (jitter.rs:181). Because the sender skips transmitting entirely while the noise gate is closed (src/core/mod.rs:2041), a pause between sentences always underruns the receiver and zeroes the clean run — twice.

    Net effect: the controller is a one-way ratchet. A single burst of jitter early in a call pins up to 240 ms of extra playout latency for the rest of the session, because no conversational speaker talks for 5 continuous seconds without the gate closing. The AIMD "decrease" half is effectively unreachable under the workload the app is built for.

    Likely fix: let clean_run survive a benign idle→re-prime transition rather than resetting it. Silence is not evidence the link is bad, so it should not count against the clean run. Distinguish "talker stopped" (benign) from "playout broke" (real) at jitter.rs:195-202.

    Found 2026-07-31 by code review. Not yet reproduced in a live call — pairs with field-test debt item 5 below.

Known field-test debt (the 🧪 rows above, collected)

Re-run on a real desktop ↔ dopedart call before calling these done:

  1. Echo cancellation — 2-machine call through speakers (the only path that actually exercises AEC).
  2. Mix-bus soft limiter — A/B a hot signal.
  3. Local call recording — solo + 2-machine, verify the WAV.
  4. In-room text chat — 2-machine delivery.
  5. Adaptive jitter buffer — behaviour under real packet loss/jitter.
  6. Chat drawer — toggle + persistence across restart.
  7. Screen share — single-sharer 2-machine path VERIFIED 2026-06-06 (desktop hosted, laptop watched via mpv). Remaining: multiple concurrent sharers, late-joiner sees an in-progress share, and Stop clears the peer's badge. Requires pixelpass + mpv on both ends.

Not built (candidate features)

Things that came up but do not exist yet:

  • Soundboard (play short clips into the call mix).
  • Invite links beyond the raw ticket / room persistence.