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

@@ -82,6 +82,10 @@ void ConnSession::on_frame(std::vector<uint8_t> frame) {
if (st == State::Authenticated)
handle_stream_announce(env.request_id(), env.stream_announce());
break;
case voicecat::v1::Envelope::kStreamStop:
if (st == State::Authenticated)
handle_stream_stop(env.stream_stop());
break;
default:
break;
}
@@ -346,7 +350,37 @@ void ConnSession::handle_stream_announce(uint64_t req_id,
if (eff->bitrate_bps() == 0) eff->set_bitrate_bps(24000);
if (eff->frame_ms() == 0) eff->set_frame_ms(20);
announced_stream_id_ = res->stream_id();
voicecat::v1::StreamInfo info;
info.set_stream_id(res->stream_id());
info.set_ssrc(ssrc);
info.set_kind(msg.kind());
*info.mutable_audio() = *eff;
info.set_label(msg.label());
send_envelope(env);
auto updated = registry_->set_user_stream(user_id_.load(), info);
if (updated) {
auto bcast = make_env();
auto* ue = bcast.mutable_user_event();
ue->set_kind(voicecat::v1::UserEvent::UPDATED);
*ue->mutable_user() = *updated;
registry_->broadcast(bcast, session_id_);
}
}
void ConnSession::handle_stream_stop(const voicecat::v1::StreamStop& msg) {
auto updated = registry_->clear_user_stream(user_id_.load(), msg.stream_id());
if (msg.stream_id() == announced_stream_id_) announced_stream_id_ = 0;
if (updated) {
auto bcast = make_env();
auto* ue = bcast.mutable_user_event();
ue->set_kind(voicecat::v1::UserEvent::UPDATED);
*ue->mutable_user() = *updated;
registry_->broadcast(bcast, session_id_);
}
}
void ConnSession::send_disconnect_and_close(uint32_t code, const std::string& reason) {

View File

@@ -86,6 +86,7 @@ class ConnSession : public std::enable_shared_from_this<ConnSession> {
void handle_ping(const voicecat::v1::Ping& msg);
void handle_udp_binding(uint64_t req_id, const voicecat::v1::UdpBinding& msg);
void handle_stream_announce(uint64_t req_id, const voicecat::v1::StreamAnnounce& msg);
void handle_stream_stop(const voicecat::v1::StreamStop& msg);
void finish_guest_auth(const voicecat::v1::GuestAuth& guest, uint64_t req_id);
void finish_password_auth(const std::string& username, const std::string& password,
@@ -119,6 +120,9 @@ class ConnSession : public std::enable_shared_from_this<ConnSession> {
mutable std::mutex crypto_mu_;
std::unique_ptr<voicecat::crypto::SodiumMediaCrypto> send_crypto_;
std::unique_ptr<voicecat::crypto::SodiumMediaCrypto> recv_crypto_;
// M2: locally-announced stream (single MIC stream per session for now)
uint32_t announced_stream_id_{0};
};
} // namespace voicecat::server

View File

@@ -166,6 +166,37 @@ std::vector<std::shared_ptr<ConnSession>> SessionRegistry::find_channel_sessions
return result;
}
std::optional<voicecat::v1::User> SessionRegistry::set_user_stream(
uint32_t user_id, const voicecat::v1::StreamInfo& info) {
std::unique_lock lk(mu_);
auto it = users_.find(user_id);
if (it == users_.end()) return std::nullopt;
auto* streams = it->second.proto.mutable_streams();
for (int i = 0; i < streams->size(); ++i) {
if (streams->Get(i).stream_id() == info.stream_id()) {
*streams->Mutable(i) = info;
return it->second.proto;
}
}
*streams->Add() = info;
return it->second.proto;
}
std::optional<voicecat::v1::User> SessionRegistry::clear_user_stream(uint32_t user_id,
uint32_t stream_id) {
std::unique_lock lk(mu_);
auto it = users_.find(user_id);
if (it == users_.end()) return std::nullopt;
auto* streams = it->second.proto.mutable_streams();
for (int i = 0; i < streams->size(); ++i) {
if (streams->Get(i).stream_id() == stream_id) {
streams->erase(streams->begin() + i);
break;
}
}
return it->second.proto;
}
uint32_t SessionRegistry::user_channel(uint32_t user_id) const {
std::shared_lock lk(mu_);
auto it = users_.find(user_id);

View File

@@ -14,6 +14,7 @@
#include <atomic>
#include <cstdint>
#include <memory>
#include <optional>
#include <shared_mutex>
#include <string>
#include <unordered_map>
@@ -95,6 +96,15 @@ class SessionRegistry {
// Assign an SSRC for a new stream. Returns the assigned SSRC.
uint32_t assign_ssrc(uint64_t session_id);
// Add/replace a stream entry on a user (called when StreamAnnounce succeeds).
// Returns the updated User proto for broadcasting, or nullopt if user not found.
std::optional<voicecat::v1::User> set_user_stream(uint32_t user_id,
const voicecat::v1::StreamInfo& info);
// Remove a stream entry from a user (called on StreamStop). Returns the updated
// User proto for broadcasting, or nullopt if user not found.
std::optional<voicecat::v1::User> clear_user_stream(uint32_t user_id, uint32_t stream_id);
// Get all sessions in a channel except the one excluded (for SFU relay).
std::vector<std::shared_ptr<ConnSession>> find_channel_sessions(
uint32_t channel_id, uint64_t exclude_session_id = 0) const;