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).
366 lines
13 KiB
C++
366 lines
13 KiB
C++
/*
|
||
* voicecat.cpp — C ABI implementation.
|
||
*
|
||
* Lifecycle (create/destroy) and trivial accessors are always real. Everything else below
|
||
* just delegates to vc_client (core/src/core/client.cpp): under VOICECAT_HAS_NET
|
||
* (`dev`/`release`/`server-release` — see docs/building.md) that's the real M1–M3 implementation;
|
||
* under the no-deps `skeleton` preset, client.cpp's `#else` branch returns VC_ERR_NOT_IMPLEMENTED
|
||
* for all of it, to keep that skeleton build green.
|
||
*/
|
||
#include "voicecat.h"
|
||
|
||
#include <new>
|
||
|
||
#include "core/client.h"
|
||
|
||
#define VC_STR2(x) #x
|
||
#define VC_STR(x) VC_STR2(x)
|
||
|
||
extern "C" {
|
||
|
||
const char* vc_version_string(void) {
|
||
static const char* kVersion = VC_STR(VOICECAT_VERSION_MAJOR) "." VC_STR(
|
||
VOICECAT_VERSION_MINOR) "." VC_STR(VOICECAT_VERSION_PATCH);
|
||
return kVersion;
|
||
}
|
||
|
||
const char* vc_result_string(vc_result code) {
|
||
switch (code) {
|
||
case VC_OK: return "ok";
|
||
case VC_ERR_NOT_IMPLEMENTED: return "not implemented";
|
||
case VC_ERR_INVALID_ARG: return "invalid argument";
|
||
case VC_ERR_NOT_CONNECTED: return "not connected";
|
||
case VC_ERR_ALREADY: return "already in requested state";
|
||
case VC_ERR_AUTH_FAILED: return "authentication failed";
|
||
case VC_ERR_PERMISSION_DENIED: return "permission denied";
|
||
case VC_ERR_TIMEOUT: return "timeout";
|
||
case VC_ERR_IO: return "i/o error";
|
||
case VC_ERR_PROTOCOL: return "protocol error";
|
||
case VC_ERR_CRYPTO: return "crypto error";
|
||
case VC_ERR_AUDIO: return "audio error";
|
||
case VC_ERR_INTERNAL: return "internal error";
|
||
}
|
||
return "unknown";
|
||
}
|
||
|
||
vc_client* vc_client_create(const vc_config* cfg, vc_callbacks cb) {
|
||
if (cfg == nullptr) return nullptr;
|
||
return new (std::nothrow) vc_client(*cfg, cb);
|
||
}
|
||
|
||
void vc_client_destroy(vc_client* c) { delete c; }
|
||
|
||
/* ── Everything below delegates to vc_client (real or stub, per preset above). ───────── */
|
||
|
||
vc_result vc_connect(vc_client* c, const char* host, uint16_t port) {
|
||
if (c == nullptr || host == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->connect(host, port);
|
||
}
|
||
|
||
vc_result vc_disconnect(vc_client* c) {
|
||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->disconnect();
|
||
}
|
||
|
||
vc_result vc_authenticate_guest(vc_client* c, const char* nickname) {
|
||
if (c == nullptr || nickname == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->authenticate_guest(nickname);
|
||
}
|
||
|
||
vc_result vc_authenticate_user(vc_client* c, const char* username, const char* password) {
|
||
if (c == nullptr || username == nullptr || password == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->authenticate_user(username, password);
|
||
}
|
||
|
||
vc_result vc_join_channel(vc_client* c, uint32_t channel_id, const char* password) {
|
||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->join_channel(channel_id, password);
|
||
}
|
||
|
||
vc_result vc_leave_channel(vc_client* c) {
|
||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->leave_channel();
|
||
}
|
||
|
||
vc_result vc_join_voice(vc_client* c) {
|
||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->join_voice();
|
||
}
|
||
|
||
vc_result vc_leave_voice(vc_client* c) {
|
||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->leave_voice();
|
||
}
|
||
|
||
vc_result vc_stream_start(vc_client* c, const vc_stream_desc* desc, uint32_t* out_stream_id) {
|
||
if (c == nullptr || desc == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->stream_start(*desc, out_stream_id);
|
||
}
|
||
|
||
vc_result vc_stream_stop(vc_client* c, uint32_t stream_id) {
|
||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->stream_stop(stream_id);
|
||
}
|
||
|
||
vc_result vc_set_input_device(vc_client* c, uint32_t stream_id, const char* device_id) {
|
||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->set_input_device(stream_id, device_id);
|
||
}
|
||
|
||
vc_result vc_set_input_mode(vc_client* c, vc_input_mode mode) {
|
||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->set_input_mode(mode);
|
||
}
|
||
|
||
vc_result vc_set_vad_threshold(vc_client* c, float threshold) {
|
||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->set_vad_threshold(threshold);
|
||
}
|
||
|
||
vc_result vc_set_push_to_talk(vc_client* c, int active) {
|
||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->set_push_to_talk(active != 0);
|
||
}
|
||
|
||
vc_result vc_set_self_mute(vc_client* c, int mic_muted, int deafened) {
|
||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->set_self_mute(mic_muted != 0, deafened != 0);
|
||
}
|
||
|
||
vc_result vc_set_output_volume(vc_client* c, float gain) {
|
||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->set_output_volume(gain);
|
||
}
|
||
|
||
vc_result vc_set_input_gain(vc_client* c, float gain) {
|
||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->set_input_gain(gain);
|
||
}
|
||
|
||
vc_result vc_set_input_noise_reduction(vc_client* c, int enable) {
|
||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->set_input_noise_reduction(enable != 0);
|
||
}
|
||
|
||
vc_result vc_set_remote_stream(vc_client* c, uint32_t user_id, uint32_t stream_id, float gain,
|
||
int muted, int noise_reduction) {
|
||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->set_remote_stream(user_id, stream_id, gain, muted != 0, noise_reduction != 0);
|
||
}
|
||
|
||
vc_result vc_get_remote_stream(vc_client* c, uint32_t user_id, uint32_t stream_id,
|
||
vc_remote_stream_state* out) {
|
||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->get_remote_stream(user_id, stream_id, out);
|
||
}
|
||
|
||
vc_result vc_get_stream_audio_config(vc_client* c, uint32_t user_id, uint32_t stream_id,
|
||
vc_audio_config* out) {
|
||
if (c == nullptr || out == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->get_stream_audio_config(user_id, stream_id, out);
|
||
}
|
||
|
||
vc_result vc_test_inject_capture(vc_client* c, uint32_t stream_id, const int16_t* pcm,
|
||
size_t samples) {
|
||
if (c == nullptr || pcm == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->stream_feed_pcm(stream_id, pcm, samples, 1);
|
||
}
|
||
|
||
vc_result vc_stream_feed_pcm(vc_client* c, uint32_t stream_id, const int16_t* pcm,
|
||
size_t samples_per_channel, uint32_t channels) {
|
||
if (c == nullptr || pcm == nullptr) return VC_ERR_INVALID_ARG;
|
||
if (channels != 1 && channels != 2) return VC_ERR_INVALID_ARG;
|
||
return c->stream_feed_pcm(stream_id, pcm, samples_per_channel, channels);
|
||
}
|
||
|
||
vc_result vc_set_pcm_sink(vc_client* c, vc_pcm_sink_cb cb, void* user) {
|
||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->set_pcm_sink(cb, user);
|
||
}
|
||
|
||
vc_result vc_set_mixed_output_sink(vc_client* c, vc_mixed_output_cb cb, void* user) {
|
||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->set_mixed_output_sink(cb, user);
|
||
}
|
||
|
||
vc_result vc_set_external_playback(vc_client* c, int enable) {
|
||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->set_external_playback(enable != 0);
|
||
}
|
||
|
||
vc_result vc_set_capture_channels(vc_client* c, uint32_t stream_id, uint32_t channels) {
|
||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->set_capture_channels(stream_id, channels);
|
||
}
|
||
|
||
vc_result vc_send_text(vc_client* c, vc_text_scope scope, uint32_t target_id,
|
||
const char* utf8) {
|
||
if (c == nullptr || utf8 == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->send_text(scope, target_id, utf8);
|
||
}
|
||
|
||
vc_result vc_list_devices(vc_client* c, vc_device_kind kind, vc_device_list* out) {
|
||
if (c == nullptr || out == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->list_devices(kind, out);
|
||
}
|
||
|
||
void vc_free_device_list(vc_device_list* list) {
|
||
if (list == nullptr || list->items == nullptr) return;
|
||
for (size_t i = 0; i < list->count; ++i) {
|
||
delete[] list->items[i].id;
|
||
delete[] list->items[i].name;
|
||
}
|
||
delete[] list->items;
|
||
list->items = nullptr;
|
||
list->count = 0;
|
||
}
|
||
|
||
vc_result vc_list_channels(vc_client* c, vc_channel_list* out) {
|
||
if (c == nullptr || out == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->list_channels(out);
|
||
}
|
||
|
||
void vc_free_channel_list(vc_channel_list* list) {
|
||
if (list == nullptr || list->items == nullptr) return;
|
||
for (size_t i = 0; i < list->count; ++i) {
|
||
delete[] list->items[i].name;
|
||
delete[] list->items[i].topic;
|
||
}
|
||
delete[] list->items;
|
||
list->items = nullptr;
|
||
list->count = 0;
|
||
}
|
||
|
||
vc_result vc_list_users(vc_client* c, vc_user_list* out) {
|
||
if (c == nullptr || out == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->list_users(out);
|
||
}
|
||
|
||
void vc_free_user_list(vc_user_list* list) {
|
||
if (list == nullptr || list->items == nullptr) return;
|
||
for (size_t i = 0; i < list->count; ++i) delete[] list->items[i].nickname;
|
||
delete[] list->items;
|
||
list->items = nullptr;
|
||
list->count = 0;
|
||
}
|
||
|
||
vc_result vc_list_user_streams(vc_client* c, uint32_t user_id, vc_stream_summary_list* out) {
|
||
if (c == nullptr || out == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->list_user_streams(user_id, out);
|
||
}
|
||
|
||
void vc_free_stream_summary_list(vc_stream_summary_list* list) {
|
||
if (list == nullptr || list->items == nullptr) return;
|
||
for (size_t i = 0; i < list->count; ++i) delete[] list->items[i].label;
|
||
delete[] list->items;
|
||
list->items = nullptr;
|
||
list->count = 0;
|
||
}
|
||
|
||
vc_result vc_confirm_server_identity(vc_client* c, int accept) {
|
||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->confirm_server_identity(accept != 0);
|
||
}
|
||
|
||
vc_result vc_get_server_identity_display(vc_client* c, char* out_buf, size_t buf_cap,
|
||
size_t* out_len) {
|
||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->get_server_identity_display(out_buf, buf_cap, out_len);
|
||
}
|
||
|
||
vc_result vc_kick_user(vc_client* c, uint32_t user_id, const char* reason) {
|
||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->kick_user(user_id, reason);
|
||
}
|
||
|
||
vc_result vc_ban_user(vc_client* c, uint32_t user_id, const char* reason,
|
||
uint64_t expires_unix_ms) {
|
||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->ban_user(user_id, reason, expires_unix_ms);
|
||
}
|
||
|
||
vc_result vc_set_permission(vc_client* c, uint32_t user_id, const vc_permissions* perms) {
|
||
if (c == nullptr || perms == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->set_permission(user_id, perms);
|
||
}
|
||
|
||
vc_result vc_set_server_mute(vc_client* c, uint32_t user_id, int muted, int deafened) {
|
||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->set_server_mute(user_id, muted != 0, deafened != 0);
|
||
}
|
||
|
||
vc_result vc_move_user(vc_client* c, uint32_t user_id, uint32_t channel_id) {
|
||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->move_user(user_id, channel_id);
|
||
}
|
||
|
||
vc_result vc_create_channel(vc_client* c, const vc_channel_info* info) {
|
||
if (c == nullptr || info == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->create_channel(info);
|
||
}
|
||
|
||
vc_result vc_edit_channel(vc_client* c, const vc_channel_info* info) {
|
||
if (c == nullptr || info == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->edit_channel(info);
|
||
}
|
||
|
||
vc_result vc_delete_channel(vc_client* c, uint32_t channel_id) {
|
||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->delete_channel(channel_id);
|
||
}
|
||
|
||
vc_result vc_create_account(vc_client* c, const char* username, const char* password) {
|
||
if (c == nullptr || username == nullptr || password == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->create_account(username, password);
|
||
}
|
||
|
||
vc_result vc_reset_password(vc_client* c, const char* username, const char* new_password) {
|
||
if (c == nullptr || username == nullptr || new_password == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->reset_password(username, new_password);
|
||
}
|
||
|
||
vc_result vc_delete_account(vc_client* c, const char* username) {
|
||
if (c == nullptr || username == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->delete_account(username);
|
||
}
|
||
|
||
vc_result vc_list_accounts(vc_client* c) {
|
||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->list_accounts();
|
||
}
|
||
|
||
vc_result vc_get_account_list(vc_client* c, vc_account_list* out) {
|
||
if (c == nullptr || out == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->get_account_list(out);
|
||
}
|
||
|
||
void vc_free_account_list(vc_account_list* list) {
|
||
if (list == nullptr || list->items == nullptr) return;
|
||
for (size_t i = 0; i < list->count; ++i) delete[] list->items[i].username;
|
||
delete[] list->items;
|
||
list->items = nullptr;
|
||
list->count = 0;
|
||
}
|
||
|
||
vc_result vc_get_permissions(vc_client* c, vc_permissions* out) {
|
||
if (c == nullptr || out == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->get_permissions(out);
|
||
}
|
||
|
||
vc_result vc_audio_suspend(vc_client* c) {
|
||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->audio_suspend();
|
||
}
|
||
|
||
vc_result vc_audio_resume(vc_client* c) {
|
||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->audio_resume();
|
||
}
|
||
|
||
vc_result vc_audio_restart(vc_client* c) {
|
||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||
return c->audio_restart();
|
||
}
|
||
|
||
} // extern "C"
|