docs: initial design baseline for VoiceCat voice/text chat
Establish the design spec in docs/ before implementation:
- README: overview, locked decisions, principles, glossary
- architecture: shared C++ core + C ABI, native UIs (Swift/C#),
threading model, server design (SFU relay)
- protocol: TCP/TLS control plane, protobuf Envelope + message
catalog, connection lifecycle, extensibility rules
- voice: UDP media frame format, per-channel Opus config,
multi-stream model, two-sided noise reduction, VAD/PTT,
jitter buffer, iOS ReplayKit screen-audio
- security: mandatory encryption (TLS 1.3 + exported-key AEAD),
TOFU server identity, admin-provisioned accounts, anti-replay
- tech-stack: permissive-only deps (mbedTLS, libsodium, opus,
miniaudio, webrtc-apm, ...), build tooling, no GPL/LGPL
- deployment: zero-config self-host (Docker / binary / source)
- roadmap: M0-M5 milestones, resolved decisions
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:47:09 +02:00
|
|
|
|
# Control Protocol
|
|
|
|
|
|
|
|
|
|
|
|
The control plane runs over **TCP, wrapped in TLS 1.3**. It carries everything that is not
|
|
|
|
|
|
real-time media: handshake, authentication, channel/user/presence state, text chat, and
|
|
|
|
|
|
voice *signaling* (announcing that a media stream is starting/stopping). Real-time voice
|
|
|
|
|
|
travels separately over UDP — see [voice.md](voice.md).
|
|
|
|
|
|
|
|
|
|
|
|
## 1. Framing
|
|
|
|
|
|
|
|
|
|
|
|
Inside the TLS stream, messages are length-prefixed:
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
┌──────────────┬───────────────────────────────────────────────┐
|
|
|
|
|
|
│ u32 length │ protobuf-encoded Envelope (length bytes) │
|
|
|
|
|
|
│ (big-endian)│ │
|
|
|
|
|
|
└──────────────┴───────────────────────────────────────────────┘
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
- `length` is the byte count of the payload that follows (not including the 4 length
|
|
|
|
|
|
bytes). Hard cap (e.g. 16 MiB) to bound memory; oversized frame → protocol error +
|
|
|
|
|
|
disconnect.
|
|
|
|
|
|
- The payload is a single **`Envelope`** protobuf message. We do **not** add our own type
|
|
|
|
|
|
byte; the type is the `oneof` discriminator inside the Envelope, which keeps the framing
|
|
|
|
|
|
trivial and lets protobuf own all forward/backward compatibility.
|
|
|
|
|
|
|
|
|
|
|
|
TLS already provides record framing, integrity, and ordering; we only add message
|
|
|
|
|
|
boundaries on top of the TLS byte stream.
|
|
|
|
|
|
|
|
|
|
|
|
## 2. Why Protocol Buffers for control
|
|
|
|
|
|
|
|
|
|
|
|
- Schema-driven codegen for **C++, C#, and Swift** (all first-class) → no hand-rolled
|
|
|
|
|
|
parsers, no drift between client and server.
|
|
|
|
|
|
- **Forward/backward compatible by construction**: unknown fields are preserved/ignored,
|
|
|
|
|
|
new fields and new `oneof` arms are additive. This is exactly the "extensible protocol"
|
|
|
|
|
|
requirement.
|
|
|
|
|
|
- Compact enough for a control plane (text/state, not media). We use **`oneof`** envelopes
|
|
|
|
|
|
rather than `Any` so the wire stays tight and the switch is exhaustive.
|
|
|
|
|
|
|
|
|
|
|
|
> Media frames do **not** use protobuf — they use a fixed binary header (see voice.md),
|
|
|
|
|
|
> because per-packet protobuf overhead and allocation are unacceptable on the RT path.
|
|
|
|
|
|
|
|
|
|
|
|
## 3. The Envelope
|
|
|
|
|
|
|
|
|
|
|
|
```proto
|
|
|
|
|
|
syntax = "proto3";
|
|
|
|
|
|
package voicecat.v1;
|
|
|
|
|
|
|
|
|
|
|
|
message Envelope {
|
|
|
|
|
|
// Monotonic per-connection id set by the sender of a request; echoed in the
|
|
|
|
|
|
// matching response so async callers can correlate. 0 for unsolicited events.
|
|
|
|
|
|
uint64 request_id = 1;
|
|
|
|
|
|
|
|
|
|
|
|
oneof body {
|
|
|
|
|
|
// ── Session / handshake ───────────────────────────────
|
|
|
|
|
|
ClientHello client_hello = 10;
|
|
|
|
|
|
ServerHello server_hello = 11;
|
|
|
|
|
|
AuthRequest auth_request = 12;
|
|
|
|
|
|
AuthResult auth_result = 13;
|
|
|
|
|
|
Disconnect disconnect = 14;
|
|
|
|
|
|
Ping ping = 15;
|
|
|
|
|
|
Pong pong = 16;
|
|
|
|
|
|
|
|
|
|
|
|
// ── State sync ────────────────────────────────────────
|
|
|
|
|
|
ServerStateSnapshot server_state = 20;
|
|
|
|
|
|
ChannelEvent channel_event = 21; // created/updated/deleted
|
|
|
|
|
|
UserEvent user_event = 22; // joined/left/updated
|
|
|
|
|
|
SubscribeRequest subscribe = 23;
|
|
|
|
|
|
|
|
|
|
|
|
// ── Channel operations ────────────────────────────────
|
|
|
|
|
|
JoinChannelRequest join_channel = 30;
|
|
|
|
|
|
JoinChannelResult join_channel_result= 31;
|
|
|
|
|
|
LeaveChannelRequest leave_channel = 32;
|
|
|
|
|
|
CreateChannelRequest create_channel = 33;
|
|
|
|
|
|
EditChannelRequest edit_channel = 34;
|
|
|
|
|
|
DeleteChannelRequest delete_channel = 35;
|
|
|
|
|
|
MoveUserRequest move_user = 36;
|
|
|
|
|
|
GenericResult generic_result = 37; // ack/err for the above
|
|
|
|
|
|
|
|
|
|
|
|
// ── Voice signaling (media is on UDP) ─────────────────
|
|
|
|
|
|
StreamAnnounce stream_announce = 40;
|
|
|
|
|
|
StreamAnnounceResult stream_announce_result = 41;
|
|
|
|
|
|
StreamStop stream_stop = 42;
|
|
|
|
|
|
StreamStateUpdate stream_state = 43; // talking/muted indicator
|
|
|
|
|
|
UdpBinding udp_binding = 44; // token to bind the UDP 5-tuple
|
feat: fix voice join/leave, channel edit defaults, channel-update stream restart
Three bugs fixed across the full stack (proto/server/core/ABI/Win/macOS/iOS):
1. Join/Leave Voice now truly subscribes/unsubscribes from the voice plane.
Previously the button only toggled the local mic — receiving was always on
(gated by channel membership alone). Added a protocol-level voice subscription
concept: new SubscribeVoiceRequest/UnsubscribeVoiceRequest/VoiceSubscriptionResult
proto messages, User.voice_subscribed field, vc_join_voice/vc_leave_voice C ABI
functions, VC_EVENT_VOICE_STATE event, server-side voice_subscribed flag checked
by the SFU relay recipient filter, and core-client gating of remote-stream
decoder setup. All three clients rewired to subscribe+mic on Join / unsubscribe
on Leave. Text chat works regardless of voice subscription.
2. Channel edit dialog now shows the channel's actual current settings. The read
struct vc_channel was missing sort_order and audio fields — only the write
struct vc_channel_info had them. Extended vc_channel with both (additive, no
ABI break), updated the session model and list_channels marshaling to populate
them, and updated all three clients' edit callers to use actual channel info
instead of hardcoded defaults.
3. Channel parameter updates now automatically restart everyone's streams.
Previously editing a channel's audio config persisted and broadcast a
ChannelEvent::UPDATED, but no layer restarted streams — encoders/decoders are
frozen at announce time. handle_channel_event now detects audio-config changes
on the user's current channel and stop->starts each active local stream. The
server reads the updated config on re-announce; peers wire up fresh decoders
at the new ssrc.
All 29 CTest tests pass; Windows DLL + C# client build clean. Apple clients not
yet compile-verified (Windows environment).
2026-06-24 14:29:39 +02:00
|
|
|
|
SubscribeVoiceRequest subscribe_voice = 45; // join the voice plane
|
|
|
|
|
|
UnsubscribeVoiceRequest unsubscribe_voice = 46; // leave the voice plane
|
|
|
|
|
|
VoiceSubscriptionResult voice_subscription_result = 47; // ack with subscribed flag
|
docs: initial design baseline for VoiceCat voice/text chat
Establish the design spec in docs/ before implementation:
- README: overview, locked decisions, principles, glossary
- architecture: shared C++ core + C ABI, native UIs (Swift/C#),
threading model, server design (SFU relay)
- protocol: TCP/TLS control plane, protobuf Envelope + message
catalog, connection lifecycle, extensibility rules
- voice: UDP media frame format, per-channel Opus config,
multi-stream model, two-sided noise reduction, VAD/PTT,
jitter buffer, iOS ReplayKit screen-audio
- security: mandatory encryption (TLS 1.3 + exported-key AEAD),
TOFU server identity, admin-provisioned accounts, anti-replay
- tech-stack: permissive-only deps (mbedTLS, libsodium, opus,
miniaudio, webrtc-apm, ...), build tooling, no GPL/LGPL
- deployment: zero-config self-host (Docker / binary / source)
- roadmap: M0-M5 milestones, resolved decisions
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:47:09 +02:00
|
|
|
|
|
|
|
|
|
|
// ── Text ──────────────────────────────────────────────
|
|
|
|
|
|
TextMessage text_message = 50;
|
|
|
|
|
|
TextMessageAck text_message_ack = 51;
|
|
|
|
|
|
TypingIndicator typing = 52;
|
|
|
|
|
|
|
|
|
|
|
|
// ── Moderation / permissions ──────────────────────────
|
|
|
|
|
|
KickRequest kick = 60;
|
|
|
|
|
|
BanRequest ban = 61;
|
|
|
|
|
|
SetPermissionRequest set_permission = 62;
|
M5: moderation, permissions, channel CRUD, in-app account management
- Server-side moderation & permissions (kick/ban/move/server-mute, channel CRUD).
- Database schema v2: channels, bans; BLAKE2b channel passwords, Argon2id accounts.
- C ABI additions and client-side handling (vc_kick_user, vc_ban_user, vc_set_permission, vc_set_server_mute, vc_move_user, vc_create/edit/delete_channel, vc_create/reset/delete/list_account).
- vccli flags for all M5 operations plus --username/--password auth.
- Four new tests covering permissions, kick/ban/move/mute, admin accounts, channel CRUD.
- Docs: protocol.md envelope updates, security.md channel-password hashing, PROGRESS.md.
2026-06-17 15:08:05 +02:00
|
|
|
|
ServerMuteRequest server_mute = 63;
|
docs: initial design baseline for VoiceCat voice/text chat
Establish the design spec in docs/ before implementation:
- README: overview, locked decisions, principles, glossary
- architecture: shared C++ core + C ABI, native UIs (Swift/C#),
threading model, server design (SFU relay)
- protocol: TCP/TLS control plane, protobuf Envelope + message
catalog, connection lifecycle, extensibility rules
- voice: UDP media frame format, per-channel Opus config,
multi-stream model, two-sided noise reduction, VAD/PTT,
jitter buffer, iOS ReplayKit screen-audio
- security: mandatory encryption (TLS 1.3 + exported-key AEAD),
TOFU server identity, admin-provisioned accounts, anti-replay
- tech-stack: permissive-only deps (mbedTLS, libsodium, opus,
miniaudio, webrtc-apm, ...), build tooling, no GPL/LGPL
- deployment: zero-config self-host (Docker / binary / source)
- roadmap: M0-M5 milestones, resolved decisions
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:47:09 +02:00
|
|
|
|
|
|
|
|
|
|
// ── Admin account management (privileged) ─────────────
|
|
|
|
|
|
// Accounts are admin-provisioned (no self-serve registration in v1).
|
|
|
|
|
|
// These ride the same TLS control channel and require an admin permission.
|
|
|
|
|
|
CreateAccountRequest create_account = 70;
|
|
|
|
|
|
ResetPasswordRequest reset_password = 71;
|
|
|
|
|
|
DeleteAccountRequest delete_account = 72;
|
|
|
|
|
|
ListAccountsRequest list_accounts = 73;
|
M5: moderation, permissions, channel CRUD, in-app account management
- Server-side moderation & permissions (kick/ban/move/server-mute, channel CRUD).
- Database schema v2: channels, bans; BLAKE2b channel passwords, Argon2id accounts.
- C ABI additions and client-side handling (vc_kick_user, vc_ban_user, vc_set_permission, vc_set_server_mute, vc_move_user, vc_create/edit/delete_channel, vc_create/reset/delete/list_account).
- vccli flags for all M5 operations plus --username/--password auth.
- Four new tests covering permissions, kick/ban/move/mute, admin accounts, channel CRUD.
- Docs: protocol.md envelope updates, security.md channel-password hashing, PROGRESS.md.
2026-06-17 15:08:05 +02:00
|
|
|
|
ListAccountsResult list_accounts_result = 74;
|
docs: initial design baseline for VoiceCat voice/text chat
Establish the design spec in docs/ before implementation:
- README: overview, locked decisions, principles, glossary
- architecture: shared C++ core + C ABI, native UIs (Swift/C#),
threading model, server design (SFU relay)
- protocol: TCP/TLS control plane, protobuf Envelope + message
catalog, connection lifecycle, extensibility rules
- voice: UDP media frame format, per-channel Opus config,
multi-stream model, two-sided noise reduction, VAD/PTT,
jitter buffer, iOS ReplayKit screen-audio
- security: mandatory encryption (TLS 1.3 + exported-key AEAD),
TOFU server identity, admin-provisioned accounts, anti-replay
- tech-stack: permissive-only deps (mbedTLS, libsodium, opus,
miniaudio, webrtc-apm, ...), build tooling, no GPL/LGPL
- deployment: zero-config self-host (Docker / binary / source)
- roadmap: M0-M5 milestones, resolved decisions
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:47:09 +02:00
|
|
|
|
|
|
|
|
|
|
// ── Extension escape hatch ────────────────────────────
|
|
|
|
|
|
Extension extension = 200; // {string ns; bytes payload;}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Reserved tag ranges keep future families from colliding: **10–19** session, **20–29** state,
|
|
|
|
|
|
**30–39** channels, **40–49** voice signaling, **50–59** text, **60–99** moderation,
|
|
|
|
|
|
**100–199** future (e.g. file transfer = 100–109), **200+** extensions.
|
|
|
|
|
|
|
|
|
|
|
|
## 4. Connection lifecycle
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
Client Server
|
|
|
|
|
|
│ TCP connect ───────────────────────────────────▶│
|
|
|
|
|
|
│ ◀──────────────── TLS 1.3 handshake ────────────▶│ server cert (TOFU/PKI, see security.md)
|
|
|
|
|
|
│ │
|
|
|
|
|
|
│ ClientHello (proto_version, features[], info) ──▶│
|
|
|
|
|
|
│ ◀── ServerHello (proto_version, features[], │ feature intersection negotiated here
|
|
|
|
|
|
│ server_info, auth_methods, udp_port) │
|
|
|
|
|
|
│ │
|
|
|
|
|
|
│ AuthRequest (guest{nick} | user{name,pass}) ────▶│ password verified w/ Argon2id
|
|
|
|
|
|
│ ◀── AuthResult (ok, session_id, self, perms, │
|
|
|
|
|
|
│ udp_token) │
|
|
|
|
|
|
│ │
|
|
|
|
|
|
│ ◀── ServerStateSnapshot (channel tree, users) ───│ initial sync
|
|
|
|
|
|
│ │
|
feat(M2): UDP voice/media plane -- SFU relay, Opus, AEAD, jitter buffer
Adds the full voice pipeline: 14-byte binary frame header, ChaCha20-Poly1305
AEAD keyed from the TLS exporter, libopus encode/decode with FEC/PLC/DTX,
an adaptive per-ssrc jitter buffer, a miniaudio capture/playback engine, an
APM passthrough stub, and the UdpBinding/StreamAnnounce signaling chain
wired through ConnSession/SessionRegistry into a new server-side SFU
(MediaRelay) that decrypts and re-encrypts frames per channel member.
Exit criterion verified: test_m2_voice — two headless clients relay 50
encrypted Opus frames through the server; ctest --preset m1-dev is 9/9
green. Also corrects protocol.md's UdpBinding diagram, which described the
UDP-side binding packet as AEAD-sealed when it is in fact a plaintext
bootstrap frame (separate from the TCP/TLS UdpBinding ack).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 01:31:14 +02:00
|
|
|
|
│ UdpBinding(udp_token) [TCP/TLS] ─────────────────▶│ confirms token, no-ops if mismatched
|
|
|
|
|
|
│ ◀── UdpBinding(ack=true) [TCP/TLS] ───────────────│
|
|
|
|
|
|
│ │
|
docs: initial design baseline for VoiceCat voice/text chat
Establish the design spec in docs/ before implementation:
- README: overview, locked decisions, principles, glossary
- architecture: shared C++ core + C ABI, native UIs (Swift/C#),
threading model, server design (SFU relay)
- protocol: TCP/TLS control plane, protobuf Envelope + message
catalog, connection lifecycle, extensibility rules
- voice: UDP media frame format, per-channel Opus config,
multi-stream model, two-sided noise reduction, VAD/PTT,
jitter buffer, iOS ReplayKit screen-audio
- security: mandatory encryption (TLS 1.3 + exported-key AEAD),
TOFU server identity, admin-provisioned accounts, anti-replay
- tech-stack: permissive-only deps (mbedTLS, libsodium, opus,
miniaudio, webrtc-apm, ...), build tooling, no GPL/LGPL
- deployment: zero-config self-host (Docker / binary / source)
- roadmap: M0-M5 milestones, resolved decisions
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:47:09 +02:00
|
|
|
|
│ ===== UDP side (parallel) ===================== │
|
|
|
|
|
|
│ (media keys derived from TLS exporter — no 2nd │
|
|
|
|
|
|
│ handshake; see security.md §2) │
|
feat(M2): UDP voice/media plane -- SFU relay, Opus, AEAD, jitter buffer
Adds the full voice pipeline: 14-byte binary frame header, ChaCha20-Poly1305
AEAD keyed from the TLS exporter, libopus encode/decode with FEC/PLC/DTX,
an adaptive per-ssrc jitter buffer, a miniaudio capture/playback engine, an
APM passthrough stub, and the UdpBinding/StreamAnnounce signaling chain
wired through ConnSession/SessionRegistry into a new server-side SFU
(MediaRelay) that decrypts and re-encrypts frames per channel member.
Exit criterion verified: test_m2_voice — two headless clients relay 50
encrypted Opus frames through the server; ctest --preset m1-dev is 9/9
green. Also corrects protocol.md's UdpBinding diagram, which described the
UDP-side binding packet as AEAD-sealed when it is in fact a plaintext
bootstrap frame (separate from the TCP/TLS UdpBinding ack).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 01:31:14 +02:00
|
|
|
|
│ UDP_BINDING frame(udp_token) [plaintext] ───────▶│ binds 5-tuple → session_id
|
|
|
|
|
|
│ ── voice frames (AEAD, exported keys) ──────────▶│
|
docs: initial design baseline for VoiceCat voice/text chat
Establish the design spec in docs/ before implementation:
- README: overview, locked decisions, principles, glossary
- architecture: shared C++ core + C ABI, native UIs (Swift/C#),
threading model, server design (SFU relay)
- protocol: TCP/TLS control plane, protobuf Envelope + message
catalog, connection lifecycle, extensibility rules
- voice: UDP media frame format, per-channel Opus config,
multi-stream model, two-sided noise reduction, VAD/PTT,
jitter buffer, iOS ReplayKit screen-audio
- security: mandatory encryption (TLS 1.3 + exported-key AEAD),
TOFU server identity, admin-provisioned accounts, anti-replay
- tech-stack: permissive-only deps (mbedTLS, libsodium, opus,
miniaudio, webrtc-apm, ...), build tooling, no GPL/LGPL
- deployment: zero-config self-host (Docker / binary / source)
- roadmap: M0-M5 milestones, resolved decisions
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:47:09 +02:00
|
|
|
|
│ │
|
|
|
|
|
|
│ JoinChannelRequest(id, password?) ──────────────▶│
|
|
|
|
|
|
│ ◀── JoinChannelResult(ok, members, audio_cfg) ───│
|
|
|
|
|
|
│ StreamAnnounce(kind=mic, opus_params) ──────────▶│
|
|
|
|
|
|
│ ◀── StreamAnnounceResult(ok, ssrc) │
|
|
|
|
|
|
│ ── voice frames flow over UDP ──────────────────▶│
|
|
|
|
|
|
│ │
|
|
|
|
|
|
│ Ping / Pong (TCP keepalive) ◀──────────────────▶│
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Notes:
|
|
|
|
|
|
|
|
|
|
|
|
- **Version negotiation.** Each side sends `proto_version` (integer) and a `features`
|
|
|
|
|
|
string list. The effective version is `min(client, server)`; the effective feature set
|
|
|
|
|
|
is the intersection. A client that doesn't understand a feature simply never uses it.
|
2026-06-21 17:45:28 +02:00
|
|
|
|
The **current `proto_version` is 2**. v2 widened the UDP voice frame `seq` field from
|
|
|
|
|
|
u16 to u64 (voice.md §2) — a wire-format change with no backward compatibility on the
|
|
|
|
|
|
media path, so the server rejects any peer not on v2 rather than min-negotiating down.
|
docs: initial design baseline for VoiceCat voice/text chat
Establish the design spec in docs/ before implementation:
- README: overview, locked decisions, principles, glossary
- architecture: shared C++ core + C ABI, native UIs (Swift/C#),
threading model, server design (SFU relay)
- protocol: TCP/TLS control plane, protobuf Envelope + message
catalog, connection lifecycle, extensibility rules
- voice: UDP media frame format, per-channel Opus config,
multi-stream model, two-sided noise reduction, VAD/PTT,
jitter buffer, iOS ReplayKit screen-audio
- security: mandatory encryption (TLS 1.3 + exported-key AEAD),
TOFU server identity, admin-provisioned accounts, anti-replay
- tech-stack: permissive-only deps (mbedTLS, libsodium, opus,
miniaudio, webrtc-apm, ...), build tooling, no GPL/LGPL
- deployment: zero-config self-host (Docker / binary / source)
- roadmap: M0-M5 milestones, resolved decisions
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:47:09 +02:00
|
|
|
|
- **Auth over TLS.** Passwords cross the wire only inside TLS 1.3 and are verified against
|
|
|
|
|
|
an Argon2id hash at rest (see security.md). `auth_methods` in `ServerHello` advertises
|
|
|
|
|
|
whether `guest` is enabled.
|
feat(M2): UDP voice/media plane -- SFU relay, Opus, AEAD, jitter buffer
Adds the full voice pipeline: 14-byte binary frame header, ChaCha20-Poly1305
AEAD keyed from the TLS exporter, libopus encode/decode with FEC/PLC/DTX,
an adaptive per-ssrc jitter buffer, a miniaudio capture/playback engine, an
APM passthrough stub, and the UdpBinding/StreamAnnounce signaling chain
wired through ConnSession/SessionRegistry into a new server-side SFU
(MediaRelay) that decrypts and re-encrypts frames per channel member.
Exit criterion verified: test_m2_voice — two headless clients relay 50
encrypted Opus frames through the server; ctest --preset m1-dev is 9/9
green. Also corrects protocol.md's UdpBinding diagram, which described the
UDP-side binding packet as AEAD-sealed when it is in fact a plaintext
bootstrap frame (separate from the TCP/TLS UdpBinding ack).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 01:31:14 +02:00
|
|
|
|
- **UDP token.** `AuthResult.udp_token` is a short-lived opaque token. The client confirms it
|
|
|
|
|
|
over TCP/TLS (`UdpBinding` request/ack) and also sends it as the payload of a plaintext
|
|
|
|
|
|
`UDP_BINDING`-type media frame so the server can bind the UDP 5-tuple to the authenticated
|
|
|
|
|
|
session without trusting the source address. This bootstrap frame is the only UDP message
|
|
|
|
|
|
that carries identity material in the clear; everything after (voice frames) is AEAD-sealed
|
|
|
|
|
|
and routed purely by the bound tuple + media-AEAD session.
|
docs: initial design baseline for VoiceCat voice/text chat
Establish the design spec in docs/ before implementation:
- README: overview, locked decisions, principles, glossary
- architecture: shared C++ core + C ABI, native UIs (Swift/C#),
threading model, server design (SFU relay)
- protocol: TCP/TLS control plane, protobuf Envelope + message
catalog, connection lifecycle, extensibility rules
- voice: UDP media frame format, per-channel Opus config,
multi-stream model, two-sided noise reduction, VAD/PTT,
jitter buffer, iOS ReplayKit screen-audio
- security: mandatory encryption (TLS 1.3 + exported-key AEAD),
TOFU server identity, admin-provisioned accounts, anti-replay
- tech-stack: permissive-only deps (mbedTLS, libsodium, opus,
miniaudio, webrtc-apm, ...), build tooling, no GPL/LGPL
- deployment: zero-config self-host (Docker / binary / source)
- roadmap: M0-M5 milestones, resolved decisions
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:47:09 +02:00
|
|
|
|
- **Snapshot then deltas.** After auth the server pushes a `ServerStateSnapshot` (full
|
|
|
|
|
|
channel tree + visible users), then streams incremental `ChannelEvent`/`UserEvent`
|
|
|
|
|
|
deltas. Clients reconcile by id.
|
|
|
|
|
|
|
|
|
|
|
|
## 5. Message catalog (selected definitions)
|
|
|
|
|
|
|
|
|
|
|
|
Representative messages; the full `.proto` is the source of truth in `core/proto/`.
|
|
|
|
|
|
|
|
|
|
|
|
```proto
|
|
|
|
|
|
message ClientHello {
|
|
|
|
|
|
uint32 proto_version = 1;
|
|
|
|
|
|
repeated string features = 2; // e.g. "opus", "fec", "screen-audio"
|
|
|
|
|
|
string client_name = 3; // "VoiceCat-macOS"
|
|
|
|
|
|
string client_version = 4;
|
|
|
|
|
|
string preferred_locale = 5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
message ServerHello {
|
|
|
|
|
|
uint32 proto_version = 1;
|
|
|
|
|
|
repeated string features = 2;
|
|
|
|
|
|
string server_name = 3;
|
|
|
|
|
|
string server_version = 4;
|
|
|
|
|
|
repeated string auth_methods = 5; // "guest", "password"
|
|
|
|
|
|
uint32 udp_port = 6;
|
|
|
|
|
|
bytes server_identity_fingerprint = 7; // Ed25519 key fp for TOFU display
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
message AuthRequest {
|
|
|
|
|
|
oneof method {
|
|
|
|
|
|
GuestAuth guest = 1; // { string nickname; }
|
|
|
|
|
|
PasswordAuth password = 2; // { string username; string password; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
message AuthResult {
|
|
|
|
|
|
bool ok = 1;
|
|
|
|
|
|
string error = 2;
|
|
|
|
|
|
uint64 session_id = 3;
|
|
|
|
|
|
User self = 4;
|
|
|
|
|
|
Permissions permissions = 5;
|
|
|
|
|
|
bytes udp_token = 6; // bind UDP 5-tuple with this
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
message Channel {
|
|
|
|
|
|
uint32 id = 1;
|
|
|
|
|
|
uint32 parent_id = 2; // 0 = root
|
|
|
|
|
|
string name = 3;
|
|
|
|
|
|
string topic = 4;
|
|
|
|
|
|
bool password_protected = 5;
|
|
|
|
|
|
uint32 max_users = 6;
|
|
|
|
|
|
ChannelType type = 7; // PERMANENT / TEMPORARY
|
|
|
|
|
|
AudioConfig audio = 8; // per-channel Opus settings (see voice.md)
|
|
|
|
|
|
int32 order = 9;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
message User {
|
|
|
|
|
|
uint32 id = 1;
|
|
|
|
|
|
string nickname = 2;
|
|
|
|
|
|
bool is_guest = 3;
|
|
|
|
|
|
uint32 channel_id = 4;
|
|
|
|
|
|
bool self_mic_muted = 5;
|
|
|
|
|
|
bool self_deafened = 6;
|
|
|
|
|
|
bool server_muted = 7;
|
|
|
|
|
|
repeated StreamInfo streams = 8; // active media streams this user publishes
|
M5: moderation, permissions, channel CRUD, in-app account management
- Server-side moderation & permissions (kick/ban/move/server-mute, channel CRUD).
- Database schema v2: channels, bans; BLAKE2b channel passwords, Argon2id accounts.
- C ABI additions and client-side handling (vc_kick_user, vc_ban_user, vc_set_permission, vc_set_server_mute, vc_move_user, vc_create/edit/delete_channel, vc_create/reset/delete/list_account).
- vccli flags for all M5 operations plus --username/--password auth.
- Four new tests covering permissions, kick/ban/move/mute, admin accounts, channel CRUD.
- Docs: protocol.md envelope updates, security.md channel-password hashing, PROGRESS.md.
2026-06-17 15:08:05 +02:00
|
|
|
|
bool server_deafened = 9; // M5: server-imposed deafen
|
docs: initial design baseline for VoiceCat voice/text chat
Establish the design spec in docs/ before implementation:
- README: overview, locked decisions, principles, glossary
- architecture: shared C++ core + C ABI, native UIs (Swift/C#),
threading model, server design (SFU relay)
- protocol: TCP/TLS control plane, protobuf Envelope + message
catalog, connection lifecycle, extensibility rules
- voice: UDP media frame format, per-channel Opus config,
multi-stream model, two-sided noise reduction, VAD/PTT,
jitter buffer, iOS ReplayKit screen-audio
- security: mandatory encryption (TLS 1.3 + exported-key AEAD),
TOFU server identity, admin-provisioned accounts, anti-replay
- tech-stack: permissive-only deps (mbedTLS, libsodium, opus,
miniaudio, webrtc-apm, ...), build tooling, no GPL/LGPL
- deployment: zero-config self-host (Docker / binary / source)
- roadmap: M0-M5 milestones, resolved decisions
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:47:09 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
message StreamInfo {
|
|
|
|
|
|
uint32 stream_id = 1; // unique within the user
|
|
|
|
|
|
uint32 ssrc = 2; // media-plane id assigned by server
|
|
|
|
|
|
StreamKind kind = 3; // MIC / SCREEN_AUDIO / AUX_DEVICE
|
|
|
|
|
|
AudioConfig audio = 4;
|
|
|
|
|
|
string label = 5; // "Microphone", "Desktop audio"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
message StreamAnnounce { // client → server: "I'm about to publish media"
|
|
|
|
|
|
StreamKind kind = 1;
|
|
|
|
|
|
AudioConfig requested_audio = 2; // server may clamp to channel policy
|
|
|
|
|
|
string label = 3;
|
|
|
|
|
|
}
|
|
|
|
|
|
message StreamAnnounceResult {
|
|
|
|
|
|
bool ok = 1; string error = 2;
|
|
|
|
|
|
uint32 stream_id = 3; uint32 ssrc = 4;
|
|
|
|
|
|
AudioConfig effective_audio = 5; // authoritative params to encode with
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
message TextMessage {
|
|
|
|
|
|
TextScope scope = 1; // CHANNEL / PRIVATE / SERVER
|
|
|
|
|
|
uint32 target_id = 2; // channel_id or user_id depending on scope
|
|
|
|
|
|
uint32 sender_id = 3; // set by server on relay
|
|
|
|
|
|
string body = 4; // UTF-8, server-bounded length
|
|
|
|
|
|
uint64 sent_at_unix_ms = 5; // server timestamp on relay
|
|
|
|
|
|
string client_msg_id = 6; // client-chosen, echoed in ack (dedup)
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
> **Text is ephemeral (v1).** The server relays messages live to currently-connected,
|
|
|
|
|
|
> subscribed recipients and **does not persist history** — there is no store and no backfill
|
|
|
|
|
|
> on join. Clients may keep their own local scrollback for the session. Server-side history
|
|
|
|
|
|
> is a deliberate non-feature for now (it can be added later behind a capability flag without
|
|
|
|
|
|
> changing `TextMessage`).
|
|
|
|
|
|
|
|
|
|
|
|
## 6. Request / response & errors
|
|
|
|
|
|
|
|
|
|
|
|
- Any message a client expects a direct answer to sets a nonzero **`request_id`**; the
|
|
|
|
|
|
server echoes it in the response (`*Result` or `GenericResult`). Unsolicited
|
|
|
|
|
|
server→client events use `request_id = 0`.
|
|
|
|
|
|
- **`GenericResult { bool ok; uint32 code; string message; }`** is the default
|
|
|
|
|
|
acknowledgement for operations without a richer reply (create/edit/delete channel, move
|
M5: moderation, permissions, channel CRUD, in-app account management
- Server-side moderation & permissions (kick/ban/move/server-mute, channel CRUD).
- Database schema v2: channels, bans; BLAKE2b channel passwords, Argon2id accounts.
- C ABI additions and client-side handling (vc_kick_user, vc_ban_user, vc_set_permission, vc_set_server_mute, vc_move_user, vc_create/edit/delete_channel, vc_create/reset/delete/list_account).
- vccli flags for all M5 operations plus --username/--password auth.
- Four new tests covering permissions, kick/ban/move/mute, admin accounts, channel CRUD.
- Docs: protocol.md envelope updates, security.md channel-password hashing, PROGRESS.md.
2026-06-17 15:08:05 +02:00
|
|
|
|
user, kick, ban, server-mute, set-permission, create/reset/delete account). Error `code`s
|
|
|
|
|
|
are an enumerated, stable list.
|
docs: initial design baseline for VoiceCat voice/text chat
Establish the design spec in docs/ before implementation:
- README: overview, locked decisions, principles, glossary
- architecture: shared C++ core + C ABI, native UIs (Swift/C#),
threading model, server design (SFU relay)
- protocol: TCP/TLS control plane, protobuf Envelope + message
catalog, connection lifecycle, extensibility rules
- voice: UDP media frame format, per-channel Opus config,
multi-stream model, two-sided noise reduction, VAD/PTT,
jitter buffer, iOS ReplayKit screen-audio
- security: mandatory encryption (TLS 1.3 + exported-key AEAD),
TOFU server identity, admin-provisioned accounts, anti-replay
- tech-stack: permissive-only deps (mbedTLS, libsodium, opus,
miniaudio, webrtc-apm, ...), build tooling, no GPL/LGPL
- deployment: zero-config self-host (Docker / binary / source)
- roadmap: M0-M5 milestones, resolved decisions
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:47:09 +02:00
|
|
|
|
- Fatal conditions send **`Disconnect { code; reason }`** then close the TLS connection.
|
fix(net): broadcast LEFT on disconnect, add keepalive/reaper, cap PLC hiss
Three reported bugs traced to one root cause plus two missing designed features:
1. Stale users + eternal PLC hiss (root cause): ConnSession::close() silently
erased dropped users without broadcasting UserEvent::LEFT, so peers never
learned the user left and their audio engines never called remove_stream —
Opus PLC synthesized comfort noise forever. Fix: broadcast_left() helper
+ close() broadcasts LEFT before erasing.
2. PLC cap (defense-in-depth): on_playback now caps pure PLC at ~2s, then
emits digital silence so a stale stream can never hiss forever even if
remove_stream is skipped. Resets automatically on fresh packets.
3. No timeout / no ping: client never sent Ping, server had no last_seen /
reaper, so half-open connections (NAT timeout, wifi loss, sleep) left
ghost users forever. Fix: client Ping every 15s with RTT measurement,
ConnSession::last_seen bumped on every inbound TCP/UDP frame, steady_timer
reaper sweeps every 15s and drops sessions older than 45s (configurable
via server::Config).
4. UDP KEEPALIVE: client sends plaintext kFrameKeepalive every 5s; server
bumps last_seen + echoes back. Keeps NAT bindings alive and lets media
activity defer the reaper independently of TCP.
5. Graceful client disconnect: vc_disconnect() sends Disconnect{code=0} via
a flag-based io-thread exit (no double-close race); server handles
client-sent Disconnect with immediate close() + LEFT broadcast.
3 new tests: disconnect_left, plc_cap, reaper_timeout. 21/21 ctest green.
Docs: protocol.md §6/§7, voice.md §6, architecture.md §5, PROGRESS.md.
2026-06-18 01:18:33 +02:00
|
|
|
|
`code ≥ 1` is server-sent (1 = protocol error, 2 = kicked). `code = 0` is client-sent
|
|
|
|
|
|
graceful disconnect (§7): the server broadcasts `UserEvent::LEFT` and closes immediately.
|
2026-06-17 20:48:50 +02:00
|
|
|
|
- **The response is for the request; the broadcast is for the state.** A `*Result` only
|
|
|
|
|
|
acknowledges the actor's request (correlation via `request_id`, error text, and any
|
|
|
|
|
|
actor-private payload — e.g. the channel `AudioConfig` in `JoinChannelResult`). The
|
|
|
|
|
|
resulting *state change* is delivered to **every** connected client **including the actor**
|
|
|
|
|
|
via the normal `UserEvent` / `ChannelEvent` / relayed `TextMessage` path. Clients apply
|
|
|
|
|
|
those events to their local model and never re-derive their own state from a `*Result`
|
|
|
|
|
|
(doing so drifts: the actor would miss its own change and a later event for another user
|
|
|
|
|
|
would surface the stale value).
|
docs: initial design baseline for VoiceCat voice/text chat
Establish the design spec in docs/ before implementation:
- README: overview, locked decisions, principles, glossary
- architecture: shared C++ core + C ABI, native UIs (Swift/C#),
threading model, server design (SFU relay)
- protocol: TCP/TLS control plane, protobuf Envelope + message
catalog, connection lifecycle, extensibility rules
- voice: UDP media frame format, per-channel Opus config,
multi-stream model, two-sided noise reduction, VAD/PTT,
jitter buffer, iOS ReplayKit screen-audio
- security: mandatory encryption (TLS 1.3 + exported-key AEAD),
TOFU server identity, admin-provisioned accounts, anti-replay
- tech-stack: permissive-only deps (mbedTLS, libsodium, opus,
miniaudio, webrtc-apm, ...), build tooling, no GPL/LGPL
- deployment: zero-config self-host (Docker / binary / source)
- roadmap: M0-M5 milestones, resolved decisions
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:47:09 +02:00
|
|
|
|
|
|
|
|
|
|
## 7. Keepalive & timeouts
|
|
|
|
|
|
|
fix(net): broadcast LEFT on disconnect, add keepalive/reaper, cap PLC hiss
Three reported bugs traced to one root cause plus two missing designed features:
1. Stale users + eternal PLC hiss (root cause): ConnSession::close() silently
erased dropped users without broadcasting UserEvent::LEFT, so peers never
learned the user left and their audio engines never called remove_stream —
Opus PLC synthesized comfort noise forever. Fix: broadcast_left() helper
+ close() broadcasts LEFT before erasing.
2. PLC cap (defense-in-depth): on_playback now caps pure PLC at ~2s, then
emits digital silence so a stale stream can never hiss forever even if
remove_stream is skipped. Resets automatically on fresh packets.
3. No timeout / no ping: client never sent Ping, server had no last_seen /
reaper, so half-open connections (NAT timeout, wifi loss, sleep) left
ghost users forever. Fix: client Ping every 15s with RTT measurement,
ConnSession::last_seen bumped on every inbound TCP/UDP frame, steady_timer
reaper sweeps every 15s and drops sessions older than 45s (configurable
via server::Config).
4. UDP KEEPALIVE: client sends plaintext kFrameKeepalive every 5s; server
bumps last_seen + echoes back. Keeps NAT bindings alive and lets media
activity defer the reaper independently of TCP.
5. Graceful client disconnect: vc_disconnect() sends Disconnect{code=0} via
a flag-based io-thread exit (no double-close race); server handles
client-sent Disconnect with immediate close() + LEFT broadcast.
3 new tests: disconnect_left, plc_cap, reaper_timeout. 21/21 ctest green.
Docs: protocol.md §6/§7, voice.md §6, architecture.md §5, PROGRESS.md.
2026-06-18 01:18:33 +02:00
|
|
|
|
- **TCP:** `Ping`/`Pong` every ~15 s; missing 3 consecutive pongs (45 s) → the server's
|
|
|
|
|
|
reaper drops the session. `Pong` echoes the `Ping` nonce so RTT is measurable. The
|
|
|
|
|
|
client sends `Ping` automatically from its io thread; the server answers with `Pong`
|
|
|
|
|
|
in any state.
|
|
|
|
|
|
- **`last_seen` reaper.** Every `ConnSession` tracks `last_seen` — bumped on *any*
|
|
|
|
|
|
inbound TCP frame (not just `Ping`) and on any inbound UDP voice/keepalive frame. A
|
|
|
|
|
|
periodic sweep (`asio::steady_timer`, every 15 s) drops sessions whose `last_seen` is
|
|
|
|
|
|
older than 45 s. Each drop calls `close()`, which broadcasts `UserEvent::LEFT` to
|
|
|
|
|
|
remaining clients — so half-open connections (NAT timeout, wifi loss without RST,
|
|
|
|
|
|
laptop sleep) that never produce a TCP EOF are cleaned up, and peers' audio engines
|
|
|
|
|
|
`remove_stream` and stop PLC. The timeout and sweep interval are configurable via
|
|
|
|
|
|
`server::Config::reaper_timeout_ms` / `reaper_sweep_ms` (set to 0 to disable).
|
docs: initial design baseline for VoiceCat voice/text chat
Establish the design spec in docs/ before implementation:
- README: overview, locked decisions, principles, glossary
- architecture: shared C++ core + C ABI, native UIs (Swift/C#),
threading model, server design (SFU relay)
- protocol: TCP/TLS control plane, protobuf Envelope + message
catalog, connection lifecycle, extensibility rules
- voice: UDP media frame format, per-channel Opus config,
multi-stream model, two-sided noise reduction, VAD/PTT,
jitter buffer, iOS ReplayKit screen-audio
- security: mandatory encryption (TLS 1.3 + exported-key AEAD),
TOFU server identity, admin-provisioned accounts, anti-replay
- tech-stack: permissive-only deps (mbedTLS, libsodium, opus,
miniaudio, webrtc-apm, ...), build tooling, no GPL/LGPL
- deployment: zero-config self-host (Docker / binary / source)
- roadmap: M0-M5 milestones, resolved decisions
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:47:09 +02:00
|
|
|
|
- **UDP:** a separate lightweight keepalive on the media channel (voice.md §6) keeps NAT
|
|
|
|
|
|
bindings alive and detects media-path failure independently of the control channel.
|
fix(net): broadcast LEFT on disconnect, add keepalive/reaper, cap PLC hiss
Three reported bugs traced to one root cause plus two missing designed features:
1. Stale users + eternal PLC hiss (root cause): ConnSession::close() silently
erased dropped users without broadcasting UserEvent::LEFT, so peers never
learned the user left and their audio engines never called remove_stream —
Opus PLC synthesized comfort noise forever. Fix: broadcast_left() helper
+ close() broadcasts LEFT before erasing.
2. PLC cap (defense-in-depth): on_playback now caps pure PLC at ~2s, then
emits digital silence so a stale stream can never hiss forever even if
remove_stream is skipped. Resets automatically on fresh packets.
3. No timeout / no ping: client never sent Ping, server had no last_seen /
reaper, so half-open connections (NAT timeout, wifi loss, sleep) left
ghost users forever. Fix: client Ping every 15s with RTT measurement,
ConnSession::last_seen bumped on every inbound TCP/UDP frame, steady_timer
reaper sweeps every 15s and drops sessions older than 45s (configurable
via server::Config).
4. UDP KEEPALIVE: client sends plaintext kFrameKeepalive every 5s; server
bumps last_seen + echoes back. Keeps NAT bindings alive and lets media
activity defer the reaper independently of TCP.
5. Graceful client disconnect: vc_disconnect() sends Disconnect{code=0} via
a flag-based io-thread exit (no double-close race); server handles
client-sent Disconnect with immediate close() + LEFT broadcast.
3 new tests: disconnect_left, plc_cap, reaper_timeout. 21/21 ctest green.
Docs: protocol.md §6/§7, voice.md §6, architecture.md §5, PROGRESS.md.
2026-06-18 01:18:33 +02:00
|
|
|
|
- **Graceful disconnect.** A client ending its session sends `Disconnect { code = 0;
|
|
|
|
|
|
reason }` before closing the socket. The server calls `close()` on receipt —
|
|
|
|
|
|
broadcasting `UserEvent::LEFT` immediately, without waiting for TCP EOF or the reaper.
|
|
|
|
|
|
The client's `vc_disconnect()` queues this message and waits for the io thread to flush
|
|
|
|
|
|
it before closing the socket. `code = 0` is reserved for client-initiated graceful
|
|
|
|
|
|
disconnect; server-sent fatal `Disconnect` uses `code ≥ 1` (1 = protocol error,
|
|
|
|
|
|
2 = kicked).
|
docs: initial design baseline for VoiceCat voice/text chat
Establish the design spec in docs/ before implementation:
- README: overview, locked decisions, principles, glossary
- architecture: shared C++ core + C ABI, native UIs (Swift/C#),
threading model, server design (SFU relay)
- protocol: TCP/TLS control plane, protobuf Envelope + message
catalog, connection lifecycle, extensibility rules
- voice: UDP media frame format, per-channel Opus config,
multi-stream model, two-sided noise reduction, VAD/PTT,
jitter buffer, iOS ReplayKit screen-audio
- security: mandatory encryption (TLS 1.3 + exported-key AEAD),
TOFU server identity, admin-provisioned accounts, anti-replay
- tech-stack: permissive-only deps (mbedTLS, libsodium, opus,
miniaudio, webrtc-apm, ...), build tooling, no GPL/LGPL
- deployment: zero-config self-host (Docker / binary / source)
- roadmap: M0-M5 milestones, resolved decisions
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:47:09 +02:00
|
|
|
|
|
feat: external PCM feed/tap API (vc_stream_feed_pcm + vc_set_pcm_sink)
Promotes vc_test_inject_capture (mono-only, TEST-ONLY) to a public,
stereo-capable production API and adds a symmetric PCM tap on the
receive side. Enables ReplayKit (iOS), ScreenCaptureKit (macOS), bots,
soundboards, and custom clients — all without a hardware audio device.
Core C++:
- voicecat.h: new vc_stream_feed_pcm, vc_pcm_sink_cb typedef,
vc_set_pcm_sink; vc_test_inject_capture kept as deprecated alias
- audio_engine: stereo-aware inject_capture (channels param + ring
reset on channel-count change); atomic pcm_sink_ fired per decoded
frame in on_playback; RemoteStream carries user_id/stream_id for
RT-safe sink metadata; init_recv_stream takes user_id+stream_id
- client.cpp: stream_feed_pcm / set_pcm_sink implementations;
sync_remote_streams passes user_id/stream_id to init_recv_stream
- voicecat.cpp: trampolines + channels=1/2 validation
Tests: test_external_pcm (headless, 3 sub-tests: mono round-trip,
stereo feed L≠R, sink metadata+disable). ctest 23/23.
Swift: feedPcm / setPcmSink in VoiceCatClient.swift + 4 XCTest
smoke tests (ExternalPcmTests.swift).
C#: StreamFeedPcm / SetPcmSink in VoiceCatClient.cs + NativeMethods.cs
(vc_stream_feed_pcm unsafe P/Invoke, VcPcmSinkCallback delegate,
vc_set_pcm_sink via nint) + 4 xUnit smoke tests (ExternalPcmTests.cs).
Docs: architecture.md §4 new subsection, voice.md §9 updated
(macOS/iOS now reference vc_stream_feed_pcm), protocol.md §8 explicit
no-protocol-change note, roadmap.md M5 entry.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 17:52:09 +02:00
|
|
|
|
## 8. Client-local features (no protocol changes)
|
|
|
|
|
|
|
|
|
|
|
|
Some features are entirely client-side and involve no changes to the wire format:
|
|
|
|
|
|
|
|
|
|
|
|
- **External PCM feed (`vc_stream_feed_pcm`)** — the caller supplies interleaved int16 PCM
|
|
|
|
|
|
that the core frames, encodes, and sends over the existing UDP media path. From the server
|
|
|
|
|
|
and peers' perspective the stream is indistinguishable from a hardware-captured stream. No
|
|
|
|
|
|
new messages, fields, or tags are needed.
|
|
|
|
|
|
- **PCM tap (`vc_set_pcm_sink`)** — receives decoded per-stream audio before hardware mixing.
|
|
|
|
|
|
Entirely local to the listener; no protocol traffic of any kind.
|
|
|
|
|
|
|
|
|
|
|
|
These are noted here to prevent future contributors from looking for corresponding protocol
|
|
|
|
|
|
changes: there are none.
|
|
|
|
|
|
|
|
|
|
|
|
## 9. Extensibility checklist
|
docs: initial design baseline for VoiceCat voice/text chat
Establish the design spec in docs/ before implementation:
- README: overview, locked decisions, principles, glossary
- architecture: shared C++ core + C ABI, native UIs (Swift/C#),
threading model, server design (SFU relay)
- protocol: TCP/TLS control plane, protobuf Envelope + message
catalog, connection lifecycle, extensibility rules
- voice: UDP media frame format, per-channel Opus config,
multi-stream model, two-sided noise reduction, VAD/PTT,
jitter buffer, iOS ReplayKit screen-audio
- security: mandatory encryption (TLS 1.3 + exported-key AEAD),
TOFU server identity, admin-provisioned accounts, anti-replay
- tech-stack: permissive-only deps (mbedTLS, libsodium, opus,
miniaudio, webrtc-apm, ...), build tooling, no GPL/LGPL
- deployment: zero-config self-host (Docker / binary / source)
- roadmap: M0-M5 milestones, resolved decisions
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:47:09 +02:00
|
|
|
|
|
|
|
|
|
|
When adding a feature later (e.g. **file transfer**), the rules are:
|
|
|
|
|
|
|
|
|
|
|
|
1. Add new `oneof` arms in the reserved tag range (file transfer = 100–109) — never reuse
|
|
|
|
|
|
or renumber existing tags.
|
|
|
|
|
|
2. Advertise a feature string in `ClientHello`/`ServerHello`; only use the feature if both
|
|
|
|
|
|
peers list it.
|
|
|
|
|
|
3. Prefer extending an existing message with new fields (additive) over inventing a new
|
|
|
|
|
|
message where it fits.
|
|
|
|
|
|
4. For experimental/out-of-tree features, ride inside `Extension { ns; payload }` until it
|
|
|
|
|
|
is promoted to a first-class `oneof` arm.
|
|
|
|
|
|
|
|
|
|
|
|
This guarantees a v1 client and a v3 server interoperate at the negotiated lowest common
|
|
|
|
|
|
denominator.
|