# 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.
