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

@@ -15,9 +15,14 @@
#include <optional>
#include <string>
#include <thread>
#include <unordered_map>
#include <utility>
#include <vector>
#include "audio/audio_engine.h"
#include "codec/opus_codec.h"
#include "crypto/crypto.h"
#include "net/voice_frame.h"
#include "protocol/envelope.h"
#include "protocol/protocol.h"
#include "session/session.h"
@@ -103,6 +108,41 @@ struct vc_client {
// Client-side session model
voicecat::session::SessionModel session_model_;
// ── M2: UDP / media plane ────────────────────────────────────────────────────
std::array<uint8_t, 16> udp_token_{};
uint16_t server_udp_port_{0};
std::string udp_host_;
std::atomic<int> udp_fd_{-1};
std::thread udp_thread_;
std::atomic<bool> udp_stop_{false};
std::atomic<bool> udp_ready_{false};
std::unique_ptr<voicecat::crypto::SodiumMediaCrypto> media_send_crypto_;
std::unique_ptr<voicecat::crypto::SodiumMediaCrypto> media_recv_crypto_;
voicecat::audio::AudioEngine audio_engine_;
voicecat::codec::OpusEncoder local_encoder_;
std::atomic<bool> local_stream_active_{false};
bool local_stream_pending_{false}; // announced, waiting for result
uint32_t local_stream_id_{0};
uint32_t local_ssrc_{0};
uint32_t next_local_stream_id_{1};
uint32_t local_timestamp_{0};
uint16_t local_frame_samples_{960};
// ssrc → (user_id, stream_id) for remote streams already wired into audio_engine_.
mutable std::mutex remote_streams_mu_;
std::unordered_map<uint32_t, std::pair<uint32_t, uint32_t>> remote_streams_;
// Local UDP destination (server media endpoint), resolved once during binding.
uint32_t udp_dest_addr_{0}; // network byte order
uint16_t udp_dest_port_{0}; // network byte order
std::atomic<bool> self_mic_muted_{false};
std::atomic<bool> self_deafened_{false};
// ── io_thread_ entry point ──────────────────────────────────────────────────
void run_io(std::string host, uint16_t port);
@@ -114,6 +154,26 @@ struct vc_client {
void handle_user_event(const voicecat::v1::UserEvent& ue);
void handle_text_message(const voicecat::v1::TextMessage& msg);
void handle_disconnect(const voicecat::v1::Disconnect& msg);
void handle_udp_binding_ack(const voicecat::v1::UdpBinding& msg);
void handle_stream_announce_result(const voicecat::v1::StreamAnnounceResult& msg);
// ── M2: UDP / media helpers ──────────────────────────────────────────────────
// Kicks off TCP UdpBinding request; called once after a successful AuthResult.
void start_udp_binding();
// Opens the UDP socket, sends the plaintext bootstrap packet, starts udp_thread_.
void finish_udp_binding();
// udp_thread_ entry point: recv loop, AEAD-open, decode, push to audio_engine_.
void run_udp_recv();
// capture_cb passed to audio_engine_.start(): encode + seal + send one frame.
void on_capture_frame(const int16_t* pcm, int samples);
// Inspect a User proto's streams and wire up any new remote ssrc into audio_engine_,
// emitting VC_EVENT_STREAM_STARTED/STOPPED as streams appear/disappear.
void sync_remote_streams(const voicecat::v1::User& user);
// Starts audio_engine_ (capture+playback) if not already running.
void ensure_audio_running();
// Joins udp_thread_, stops audio_engine_, clears media crypto/remote-stream state.
// Safe to call multiple times. Called both from run_io()'s cleanup and disconnect().
void teardown_voice();
// ── Helpers (io_thread_ and caller threads) ─────────────────────────────────
// Queue an encoded envelope to be sent on io_thread_.