fix(M2): wire vc_client's real voice plane through the C ABI, not just raw sockets

test_m2_voice passed against raw BSD sockets, but vc_client::stream_start/stop,
UDP binding, and capture/recv were still VC_ERR_NOT_IMPLEMENTED stubs -- meaning
vccli and any GUI client still couldn't actually talk. Implements the real
client-side UDP-binding handshake, media key derivation, capture->encode->seal->
send and recv->open->decode->playback paths, plus server-side StreamInfo
broadcast so peers learn about each other's streams via sync_remote_streams().

Adds test_voice_client_abi (two real vc_client instances, not raw sockets) and
vccli --voice/--mute/--text flags, manually verified live between two instances.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 02:12:50 +02:00
parent 694494a5be
commit c693cab35c
13 changed files with 1017 additions and 30 deletions

View File

@@ -14,8 +14,36 @@ const User* SessionModel::find_user(uint32_t id) const {
return nullptr;
}
std::pair<const User*, const Stream*> SessionModel::find_user_by_ssrc(uint32_t ssrc) const {
for (auto& u : users_) {
for (auto& s : u.streams) {
if (s.ssrc == ssrc) return {&u, &s};
}
}
return {nullptr, nullptr};
}
#ifdef VOICECAT_HAS_NET
namespace {
std::vector<Stream> copy_streams(
const google::protobuf::RepeatedPtrField<voicecat::v1::StreamInfo>& src) {
std::vector<Stream> out;
out.reserve(src.size());
for (const auto& pb : src) {
Stream s;
s.stream_id = pb.stream_id();
s.ssrc = pb.ssrc();
s.kind = static_cast<int>(pb.kind());
s.label = pb.label();
s.sample_rate = pb.audio().sample_rate() ? pb.audio().sample_rate() : 48000;
s.frame_ms = pb.audio().frame_ms() ? pb.audio().frame_ms() : 20;
out.push_back(std::move(s));
}
return out;
}
} // namespace
void SessionModel::apply_snapshot(const voicecat::v1::ServerStateSnapshot& snap) {
channels_.clear();
for (const auto& pb : snap.channels()) {
@@ -32,6 +60,7 @@ void SessionModel::apply_snapshot(const voicecat::v1::ServerStateSnapshot& snap)
u.nickname = pb.nickname();
u.is_guest = pb.is_guest();
u.channel_id = pb.channel_id();
u.streams = copy_streams(pb.streams());
users_.push_back(std::move(u));
}
}
@@ -46,6 +75,7 @@ void SessionModel::apply_user_event(const voicecat::v1::UserEvent& ev) {
u.nickname = pb.nickname();
u.is_guest = pb.is_guest();
u.channel_id = pb.channel_id();
u.streams = copy_streams(pb.streams());
auto it = std::find_if(users_.begin(), users_.end(),
[&](const User& x) { return x.id == u.id; });

View File

@@ -9,6 +9,7 @@
#include <cstdint>
#include <string>
#include <utility>
#include <vector>
#ifdef VOICECAT_HAS_NET
@@ -30,6 +31,8 @@ struct Stream {
uint32_t ssrc{0};
int kind{0};
std::string label;
uint32_t sample_rate{48000};
uint32_t frame_ms{20};
};
struct User {
@@ -48,6 +51,10 @@ class SessionModel {
const Channel* find_channel(uint32_t id) const;
const User* find_user(uint32_t id) const;
// Find the user that owns a given media ssrc, and the matching Stream entry.
// Returns {nullptr, nullptr} if not found.
std::pair<const User*, const Stream*> find_user_by_ssrc(uint32_t ssrc) const;
#ifdef VOICECAT_HAS_NET
void apply_snapshot(const voicecat::v1::ServerStateSnapshot& snap);
void apply_user_event(const voicecat::v1::UserEvent& ev);