Files
peerspeak/docs/FEATURES.md
T
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

191 lines
11 KiB
Markdown

# 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_stats``core::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 `default`s 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.