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:
@@ -3,32 +3,139 @@
|
||||
*
|
||||
* This is the primary way the protocol is exercised and verified from M1 onward (see
|
||||
* AGENTS.md). Each milestone's exit criterion is demonstrated by driving two vccli
|
||||
* instances against a real voicecat-server. Today it just shows the C ABI is linkable.
|
||||
* instances against a real voicecat-server.
|
||||
*/
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <csignal>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include "voicecat.h"
|
||||
|
||||
namespace {
|
||||
|
||||
void on_event(void* /*user*/, const vc_event* ev) {
|
||||
std::printf("[event] type=%d state=%d result=%d text=%s\n", ev->type, ev->connection_state,
|
||||
ev->result, ev->text ? ev->text : "");
|
||||
std::atomic<bool> g_stop{false};
|
||||
void on_sigint(int) { g_stop.store(true); }
|
||||
|
||||
struct Stats {
|
||||
std::atomic<bool> auth_done{false};
|
||||
std::atomic<bool> auth_ok{false};
|
||||
};
|
||||
|
||||
void on_event(void* user, const vc_event* ev) {
|
||||
auto* st = static_cast<Stats*>(user);
|
||||
switch (ev->type) {
|
||||
case VC_EVENT_CONNECTION_STATE:
|
||||
std::printf("[state] -> %d\n", static_cast<int>(ev->connection_state));
|
||||
break;
|
||||
case VC_EVENT_AUTH_RESULT:
|
||||
st->auth_ok = (ev->result == VC_OK);
|
||||
st->auth_done = true;
|
||||
std::printf("[auth] ok=%d user_id=%u %s\n", st->auth_ok.load(), ev->user_id,
|
||||
ev->text ? ev->text : "");
|
||||
break;
|
||||
case VC_EVENT_USER_JOINED:
|
||||
std::printf("[user] joined: %s (id=%u, channel=%u)\n", ev->text ? ev->text : "?",
|
||||
ev->user_id, ev->channel_id);
|
||||
break;
|
||||
case VC_EVENT_USER_LEFT:
|
||||
std::printf("[user] left: id=%u\n", ev->user_id);
|
||||
break;
|
||||
case VC_EVENT_USER_UPDATED:
|
||||
std::printf("[user] updated: id=%u\n", ev->user_id);
|
||||
break;
|
||||
case VC_EVENT_STREAM_STARTED:
|
||||
std::printf("[voice] stream started: user_id=%u stream_id=%u\n", ev->user_id,
|
||||
ev->stream_id);
|
||||
break;
|
||||
case VC_EVENT_STREAM_STOPPED:
|
||||
std::printf("[voice] stream stopped: user_id=%u stream_id=%u\n", ev->user_id,
|
||||
ev->stream_id);
|
||||
break;
|
||||
case VC_EVENT_TEXT_MESSAGE:
|
||||
std::printf("[text] from=%u: %s\n", ev->user_id, ev->text ? ev->text : "");
|
||||
break;
|
||||
case VC_EVENT_ERROR:
|
||||
std::fprintf(stderr, "[error] rc=%d: %s\n", ev->result, ev->text ? ev->text : "");
|
||||
break;
|
||||
case VC_EVENT_DISCONNECTED:
|
||||
std::printf("[disconnected] rc=%d: %s\n", ev->result, ev->text ? ev->text : "");
|
||||
g_stop.store(true);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool wait_until(std::atomic<bool>& flag, int timeout_ms) {
|
||||
auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout_ms);
|
||||
while (!flag.load()) {
|
||||
if (std::chrono::steady_clock::now() >= deadline) return false;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void print_usage() {
|
||||
std::printf(
|
||||
"usage: vccli [--host H] [--port P] [--nick NAME] [--channel ID]\n"
|
||||
" [--voice] [--mute] [--text MSG]\n"
|
||||
" --host H server host (default 127.0.0.1)\n"
|
||||
" --port P server TCP port (default 8384)\n"
|
||||
" --nick NAME guest nickname (default vccli-test)\n"
|
||||
" --channel ID channel to join after auth (default 1, Lobby)\n"
|
||||
" --voice start a MIC stream and stay connected until Ctrl+C\n"
|
||||
" --mute start with the mic muted (only meaningful with --voice)\n"
|
||||
" --text MSG send MSG to the channel, then exit\n");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
// MSVCRT/MinGW treat _IOLBF as full buffering for non-console streams, so go unbuffered
|
||||
// to keep output visible immediately when piped to a file or another process.
|
||||
std::setvbuf(stdout, nullptr, _IONBF, 0);
|
||||
|
||||
std::string host = "127.0.0.1";
|
||||
uint16_t port = 8384;
|
||||
std::string nick = "vccli-test";
|
||||
uint32_t channel_id = 1;
|
||||
bool voice_mode = false;
|
||||
bool start_muted = false;
|
||||
std::string text_msg;
|
||||
bool have_text = false;
|
||||
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
std::string a = argv[i];
|
||||
auto next = [&]() -> std::string { return (i + 1 < argc) ? argv[++i] : std::string(); };
|
||||
if (a == "--host") host = next();
|
||||
else if (a == "--port") port = static_cast<uint16_t>(std::stoi(next()));
|
||||
else if (a == "--nick") nick = next();
|
||||
else if (a == "--channel") channel_id = static_cast<uint32_t>(std::stoul(next()));
|
||||
else if (a == "--voice") voice_mode = true;
|
||||
else if (a == "--mute") start_muted = true;
|
||||
else if (a == "--text") { text_msg = next(); have_text = true; }
|
||||
else if (a == "--help" || a == "-h") { print_usage(); return 0; }
|
||||
else { std::fprintf(stderr, "unknown flag: %s\n", a.c_str()); print_usage(); return 1; }
|
||||
}
|
||||
|
||||
std::printf("vccli — VoiceCat test client (core %s, protocol v%d)\n", vc_version_string(),
|
||||
VOICECAT_PROTOCOL_VERSION);
|
||||
|
||||
vc_config cfg{};
|
||||
cfg.client_name = "vccli";
|
||||
cfg.client_version = vc_version_string();
|
||||
cfg.log_level = VC_LOG_INFO;
|
||||
std::signal(SIGINT, on_sigint);
|
||||
|
||||
vc_config cfg{};
|
||||
cfg.client_name = "vccli";
|
||||
cfg.client_version = vc_version_string();
|
||||
cfg.log_level = VC_LOG_INFO;
|
||||
|
||||
Stats st;
|
||||
vc_callbacks cb{};
|
||||
cb.on_event = on_event;
|
||||
cb.user = &st;
|
||||
|
||||
vc_client* c = vc_client_create(&cfg, cb);
|
||||
if (c == nullptr) {
|
||||
@@ -36,11 +143,60 @@ int main(int argc, char** argv) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// M0: demonstrate the call surface. These return VC_ERR_NOT_IMPLEMENTED for now.
|
||||
const char* host = (argc > 1) ? argv[1] : "127.0.0.1";
|
||||
vc_result r = vc_connect(c, host, 8384);
|
||||
std::printf("vc_connect(%s:8384) -> %d (%s)\n", host, r, vc_result_string(r));
|
||||
vc_result r = vc_connect(c, host.c_str(), port);
|
||||
std::printf("vc_connect(%s:%u) -> %d (%s)\n", host.c_str(), port, r, vc_result_string(r));
|
||||
if (r != VC_OK) {
|
||||
vc_client_destroy(c);
|
||||
return 1;
|
||||
}
|
||||
|
||||
r = vc_authenticate_guest(c, nick.c_str());
|
||||
std::printf("vc_authenticate_guest(%s) -> %d (%s)\n", nick.c_str(), r, vc_result_string(r));
|
||||
|
||||
if (!wait_until(st.auth_done, 8000) || !st.auth_ok.load()) {
|
||||
std::fprintf(stderr, "authentication failed or timed out\n");
|
||||
vc_disconnect(c);
|
||||
vc_client_destroy(c);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (channel_id != 1) {
|
||||
r = vc_join_channel(c, channel_id, nullptr);
|
||||
std::printf("vc_join_channel(%u) -> %d (%s)\n", channel_id, r, vc_result_string(r));
|
||||
}
|
||||
|
||||
if (have_text) {
|
||||
r = vc_send_text(c, VC_TEXT_CHANNEL, channel_id, text_msg.c_str());
|
||||
std::printf("vc_send_text -> %d (%s)\n", r, vc_result_string(r));
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(300)); // let the relay land
|
||||
}
|
||||
|
||||
if (voice_mode) {
|
||||
// Give the async UDP binding handshake (TCP UdpBinding -> ack -> plaintext
|
||||
// bootstrap packet) a moment to land before announcing a stream.
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
|
||||
if (start_muted) vc_set_self_mute(c, 1, 0);
|
||||
|
||||
vc_stream_desc desc{};
|
||||
desc.kind = VC_STREAM_MIC;
|
||||
desc.device_id = nullptr;
|
||||
desc.label = "Microphone";
|
||||
|
||||
uint32_t stream_id = 0;
|
||||
r = vc_stream_start(c, &desc, &stream_id);
|
||||
std::printf("vc_stream_start -> %d (%s), stream_id=%u\n", r, vc_result_string(r),
|
||||
stream_id);
|
||||
|
||||
std::printf("voice mode: streaming mic, listening for remote streams. Ctrl+C to stop.\n");
|
||||
while (!g_stop.load()) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
}
|
||||
|
||||
vc_stream_stop(c, stream_id);
|
||||
}
|
||||
|
||||
vc_disconnect(c);
|
||||
vc_client_destroy(c);
|
||||
std::printf("ok\n");
|
||||
return 0;
|
||||
|
||||
Reference in New Issue
Block a user