feat: fix voice join/leave, channel edit defaults, channel-update stream restart
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).
This commit is contained in:
@@ -111,6 +111,14 @@ void ConnSession::on_frame(std::vector<uint8_t> frame) {
|
||||
if (st == State::Authenticated)
|
||||
handle_stream_stop(env.stream_stop());
|
||||
break;
|
||||
case voicecat::v1::Envelope::kSubscribeVoice:
|
||||
if (st == State::Authenticated)
|
||||
handle_subscribe_voice(env.request_id());
|
||||
break;
|
||||
case voicecat::v1::Envelope::kUnsubscribeVoice:
|
||||
if (st == State::Authenticated)
|
||||
handle_unsubscribe_voice(env.request_id());
|
||||
break;
|
||||
|
||||
// ── M5 moderation / admin ─────────────────────────────────────────────
|
||||
case voicecat::v1::Envelope::kKick:
|
||||
@@ -521,6 +529,14 @@ void ConnSession::handle_udp_binding(uint64_t req_id, const voicecat::v1::UdpBin
|
||||
|
||||
void ConnSession::handle_stream_announce(uint64_t req_id,
|
||||
const voicecat::v1::StreamAnnounce& msg) {
|
||||
if (!voice_subscribed_.load(std::memory_order_acquire)) {
|
||||
auto env = make_env(req_id);
|
||||
auto* res = env.mutable_stream_announce_result();
|
||||
res->set_ok(false);
|
||||
res->set_error("not subscribed to voice — call vc_join_voice first");
|
||||
send_envelope(env);
|
||||
return;
|
||||
}
|
||||
uint32_t ssrc = registry_->assign_ssrc(session_id_);
|
||||
uint32_t stream_id = next_stream_id_++;
|
||||
|
||||
@@ -595,6 +611,66 @@ void ConnSession::handle_stream_stop(const voicecat::v1::StreamStop& msg) {
|
||||
}
|
||||
}
|
||||
|
||||
void ConnSession::handle_subscribe_voice(uint64_t req_id) {
|
||||
voice_subscribed_.store(true, std::memory_order_release);
|
||||
registry_->set_user_voice_subscribed(user_id_.load(), true);
|
||||
|
||||
// Reply with the result.
|
||||
auto env = make_env(req_id);
|
||||
auto* res = env.mutable_voice_subscription_result();
|
||||
res->set_ok(true);
|
||||
res->set_subscribed(true);
|
||||
send_envelope(env);
|
||||
|
||||
// Broadcast the updated user proto so peers see voice_subscribed = true.
|
||||
if (auto updated_user = registry_->user_snapshot_user(user_id_.load())) {
|
||||
auto bcast = make_env();
|
||||
auto* ue = bcast.mutable_user_event();
|
||||
ue->set_kind(voicecat::v1::UserEvent::UPDATED);
|
||||
*ue->mutable_user() = *updated_user;
|
||||
registry_->broadcast(bcast, /*exclude*/ 0);
|
||||
}
|
||||
}
|
||||
|
||||
void ConnSession::handle_unsubscribe_voice(uint64_t req_id) {
|
||||
voice_subscribed_.store(false, std::memory_order_release);
|
||||
|
||||
// Clear all the user's streams so peers stop receiving from them. Each clear broadcasts
|
||||
// a UserEvent::UPDATED; we collect the ids first to avoid mutating announced_stream_ids_
|
||||
// while iterating.
|
||||
auto stream_ids = announced_stream_ids_;
|
||||
for (uint32_t sid : stream_ids) {
|
||||
auto it = std::find(announced_stream_ids_.begin(), announced_stream_ids_.end(), sid);
|
||||
if (it != announced_stream_ids_.end()) announced_stream_ids_.erase(it);
|
||||
auto updated = registry_->clear_user_stream(user_id_.load(), sid);
|
||||
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, /*exclude*/ 0);
|
||||
}
|
||||
}
|
||||
|
||||
registry_->set_user_voice_subscribed(user_id_.load(), false);
|
||||
|
||||
// Reply with the result.
|
||||
auto env = make_env(req_id);
|
||||
auto* res = env.mutable_voice_subscription_result();
|
||||
res->set_ok(true);
|
||||
res->set_subscribed(false);
|
||||
send_envelope(env);
|
||||
|
||||
// Broadcast the updated user proto so peers see voice_subscribed = false.
|
||||
if (auto updated_user = registry_->user_snapshot_user(user_id_.load())) {
|
||||
auto bcast = make_env();
|
||||
auto* ue = bcast.mutable_user_event();
|
||||
ue->set_kind(voicecat::v1::UserEvent::UPDATED);
|
||||
*ue->mutable_user() = *updated_user;
|
||||
registry_->broadcast(bcast, /*exclude*/ 0);
|
||||
}
|
||||
}
|
||||
|
||||
// ── M5 handlers ──────────────────────────────────────────────────────────────
|
||||
|
||||
void ConnSession::handle_kick_request(uint64_t req_id, const voicecat::v1::KickRequest& msg) {
|
||||
|
||||
@@ -82,6 +82,7 @@ class ConnSession : public std::enable_shared_from_this<ConnSession> {
|
||||
State state() const { return state_.load(); }
|
||||
uint64_t session_id() const { return session_id_; }
|
||||
uint32_t user_id() const { return user_id_; }
|
||||
bool voice_subscribed() const { return voice_subscribed_.load(std::memory_order_acquire); }
|
||||
|
||||
// Keepalive: bump last_seen to "now" on any inbound activity (TCP frame or UDP voice
|
||||
// frame). The server's reaper sweeps sessions whose last_seen is older than the
|
||||
@@ -105,6 +106,8 @@ class ConnSession : public std::enable_shared_from_this<ConnSession> {
|
||||
void handle_stream_announce(uint64_t req_id, const voicecat::v1::StreamAnnounce& msg);
|
||||
void handle_stream_stop(const voicecat::v1::StreamStop& msg);
|
||||
void handle_leave_channel();
|
||||
void handle_subscribe_voice(uint64_t req_id);
|
||||
void handle_unsubscribe_voice(uint64_t req_id);
|
||||
|
||||
// M5 handlers
|
||||
void handle_kick_request(uint64_t req_id, const voicecat::v1::KickRequest& msg);
|
||||
@@ -172,6 +175,11 @@ class ConnSession : public std::enable_shared_from_this<ConnSession> {
|
||||
|
||||
// M5: permissions granted at auth time (server-side authority).
|
||||
voicecat::v1::Permissions permissions_;
|
||||
|
||||
// Voice-plane subscription. When false, the SFU relay excludes this session from the
|
||||
// recipient set (find_channel_sessions), and handle_stream_announce rejects new streams.
|
||||
// Toggled by SubscribeVoiceRequest / UnsubscribeVoiceRequest.
|
||||
std::atomic<bool> voice_subscribed_{false};
|
||||
};
|
||||
|
||||
} // namespace voicecat::server
|
||||
|
||||
@@ -81,6 +81,12 @@ void MediaRelay::on_udp_frame(const uint8_t* data, size_t len,
|
||||
auto sender_session = registry_->find_by_udp_endpoint(sender);
|
||||
if (!sender_session) { note_drop("unmapped-endpoint"); return; }
|
||||
|
||||
// Defense in depth: don't relay voice from sessions not on the voice plane.
|
||||
// handle_stream_announce already rejects announces from non-subscribers, so a
|
||||
// non-subscribed session shouldn't have any active streams — but this catches any
|
||||
// edge case (e.g. a stream announced just before unsubscribing).
|
||||
if (!sender_session->voice_subscribed()) { note_drop("not-subscribed"); return; }
|
||||
|
||||
auto* recv_crypto = sender_session->recv_crypto();
|
||||
if (!recv_crypto) { note_drop("no-recv-crypto"); return; }
|
||||
|
||||
|
||||
@@ -145,6 +145,13 @@ bool SessionRegistry::set_user_channel(uint32_t user_id, uint32_t channel_id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void SessionRegistry::set_user_voice_subscribed(uint32_t user_id, bool subscribed) {
|
||||
std::unique_lock lk(mu_);
|
||||
auto it = users_.find(user_id);
|
||||
if (it == users_.end()) return;
|
||||
it->second.proto.set_voice_subscribed(subscribed);
|
||||
}
|
||||
|
||||
std::vector<voicecat::v1::Channel> SessionRegistry::channel_snapshot() const {
|
||||
std::shared_lock lk(mu_);
|
||||
std::vector<voicecat::v1::Channel> result;
|
||||
@@ -492,7 +499,11 @@ std::vector<std::shared_ptr<ConnSession>> SessionRegistry::find_channel_sessions
|
||||
if (entry.session_id == exclude_session_id) continue;
|
||||
auto sit = sessions_.find(entry.session_id);
|
||||
if (sit == sessions_.end()) continue;
|
||||
if (auto sess = sit->second.lock()) result.push_back(sess);
|
||||
if (auto sess = sit->second.lock()) {
|
||||
// Only relay voice to sessions that are on the voice plane.
|
||||
if (!sess->voice_subscribed()) continue;
|
||||
result.push_back(sess);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -75,6 +75,9 @@ class SessionRegistry {
|
||||
// Move a user to a channel. Returns false if channel doesn't exist.
|
||||
bool set_user_channel(uint32_t user_id, uint32_t channel_id);
|
||||
|
||||
// Set the user's voice-plane subscription flag on their proto (broadcast-ready).
|
||||
void set_user_voice_subscribed(uint32_t user_id, bool subscribed);
|
||||
|
||||
// Snapshot for ServerStateSnapshot message.
|
||||
std::vector<voicecat::v1::Channel> channel_snapshot() const;
|
||||
std::vector<voicecat::v1::User> user_snapshot() const;
|
||||
|
||||
Reference in New Issue
Block a user