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:
@@ -349,10 +349,21 @@ public partial class MainForm : Form
|
||||
HandleStreamStarted(ev);
|
||||
break;
|
||||
case VcEventType.StreamStopped:
|
||||
if (_users.TryGetValue(ev.UserId, out var stUser) &&
|
||||
if (ev.UserId == _selfUserId)
|
||||
{
|
||||
if (ev.StreamId == _micStreamId)
|
||||
{
|
||||
_micStreamId = 0;
|
||||
pbLevel.Value = 0;
|
||||
}
|
||||
}
|
||||
else if (_users.TryGetValue(ev.UserId, out var stUser) &&
|
||||
stUser.ChannelId == _currentChannelId)
|
||||
AddActivity($"{stUser.Nickname} stopped a stream");
|
||||
break;
|
||||
case VcEventType.VoiceState:
|
||||
HandleVoiceState(ev);
|
||||
break;
|
||||
case VcEventType.Disconnected:
|
||||
HandleDisconnected(ev);
|
||||
break;
|
||||
@@ -378,7 +389,7 @@ public partial class MainForm : Form
|
||||
private void HandleUserJoined(VoiceCatEvent ev)
|
||||
{
|
||||
var user = new UserInfo(ev.UserId, ev.Text ?? $"User#{ev.UserId}", false, ev.ChannelId,
|
||||
false, false, false, false);
|
||||
false, false, false, false, false);
|
||||
_users[ev.UserId] = user;
|
||||
RefreshChannelTree();
|
||||
RefreshUserList();
|
||||
@@ -663,6 +674,32 @@ public partial class MainForm : Form
|
||||
private void BtnMicToggle_Click(object? sender, EventArgs e)
|
||||
{
|
||||
if (_micStreamId == 0)
|
||||
{
|
||||
var result = _client.JoinVoice();
|
||||
if (result != VcResult.Ok)
|
||||
AddActivity($"Failed to join voice: {result}");
|
||||
}
|
||||
else
|
||||
{
|
||||
StopAuxStream();
|
||||
if (_screenStreamId != 0) StopScreenAudio();
|
||||
_client.SetPushToTalk(false);
|
||||
_client.LeaveVoice();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetVoiceJoinedState(bool joined)
|
||||
{
|
||||
tsbJoinVoice.Text = joined ? "Leave Voice" : "Join Voice";
|
||||
_miJoinVoice.Text = joined ? "Leave &Voice" : "&Join Voice";
|
||||
chkMute.Enabled = joined;
|
||||
chkDeafen.Enabled = joined;
|
||||
}
|
||||
|
||||
private void HandleVoiceState(VoiceCatEvent ev)
|
||||
{
|
||||
bool subscribed = ev.U32a != 0;
|
||||
if (subscribed)
|
||||
{
|
||||
var (result, streamId) = _client.StartStream(VcStreamKind.Mic, "Microphone");
|
||||
if (result == VcResult.Ok)
|
||||
@@ -680,7 +717,7 @@ public partial class MainForm : Form
|
||||
SetVoiceJoinedState(true);
|
||||
AddActivity("Joined voice — microphone active");
|
||||
_feedback.PlaySound(SoundEvent.VoiceOn);
|
||||
StartAuxStream(); // no-op unless the aux stream is enabled in settings
|
||||
StartAuxStream();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -689,25 +726,12 @@ public partial class MainForm : Form
|
||||
}
|
||||
else
|
||||
{
|
||||
StopAuxStream();
|
||||
_client.SetPushToTalk(false);
|
||||
_client.StopStream(_micStreamId);
|
||||
_micStreamId = 0;
|
||||
pbLevel.Value = 0;
|
||||
SetVoiceJoinedState(false);
|
||||
AddActivity("Left voice");
|
||||
_feedback.PlaySound(SoundEvent.VoiceOff);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetVoiceJoinedState(bool joined)
|
||||
{
|
||||
tsbJoinVoice.Text = joined ? "Leave Voice" : "Join Voice";
|
||||
_miJoinVoice.Text = joined ? "Leave &Voice" : "&Join Voice";
|
||||
chkMute.Enabled = joined;
|
||||
chkDeafen.Enabled = joined;
|
||||
}
|
||||
|
||||
private void ApplySelfMute() =>
|
||||
_client.SetSelfMute(chkMute.Checked, chkDeafen.Checked);
|
||||
|
||||
@@ -1122,8 +1146,8 @@ public partial class MainForm : Form
|
||||
|
||||
var editInfo = new ChannelEditInfo(
|
||||
channel.Id, channel.ParentId, channel.Name, channel.Topic,
|
||||
channel.PasswordProtected, null, channel.MaxUsers, 0,
|
||||
new AudioConfigInfo(0, false, 48000, 0, 20, 0, true, 0, false, 10, false));
|
||||
channel.PasswordProtected, null, channel.MaxUsers, channel.SortOrder,
|
||||
channel.Audio);
|
||||
|
||||
using var dlg = new ChannelEditDialog(_channels, editInfo);
|
||||
if (dlg.ShowDialog(this) != DialogResult.OK || dlg.Result is null) return;
|
||||
|
||||
@@ -92,6 +92,8 @@ public enum VcEventType
|
||||
GenericResult = 14,
|
||||
/// <summary>M5: reply to VoiceCatClient.RequestAccountList — call ListAccounts() to read.</summary>
|
||||
AccountList = 15,
|
||||
/// <summary>Voice-plane subscription state. u32a = 1 (subscribed) or 0 (unsubscribed).</summary>
|
||||
VoiceState = 16,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -38,7 +38,9 @@ internal static class Marshaling
|
||||
Marshal.PtrToStringUTF8(raw.Name) ?? string.Empty,
|
||||
Marshal.PtrToStringUTF8(raw.Topic) ?? string.Empty,
|
||||
raw.PasswordProtected != 0,
|
||||
raw.MaxUsers));
|
||||
raw.MaxUsers,
|
||||
raw.SortOrder,
|
||||
ToManaged(in raw.Audio)));
|
||||
}
|
||||
NativeMethods.vc_free_channel_list(ref native);
|
||||
return result;
|
||||
@@ -59,7 +61,8 @@ internal static class Marshaling
|
||||
raw.SelfMicMuted != 0,
|
||||
raw.SelfDeafened != 0,
|
||||
raw.ServerMuted != 0,
|
||||
raw.ServerDeafened != 0));
|
||||
raw.ServerDeafened != 0,
|
||||
raw.VoiceSubscribed != 0));
|
||||
}
|
||||
NativeMethods.vc_free_user_list(ref native);
|
||||
return result;
|
||||
|
||||
@@ -9,7 +9,9 @@ public sealed record ChannelInfo(
|
||||
string Name,
|
||||
string Topic,
|
||||
bool PasswordProtected,
|
||||
uint MaxUsers);
|
||||
uint MaxUsers,
|
||||
uint SortOrder,
|
||||
AudioConfigInfo Audio);
|
||||
|
||||
public sealed record ChannelEditInfo(
|
||||
uint Id,
|
||||
@@ -30,7 +32,8 @@ public sealed record UserInfo(
|
||||
bool SelfMicMuted,
|
||||
bool SelfDeafened,
|
||||
bool ServerMuted,
|
||||
bool ServerDeafened);
|
||||
bool ServerDeafened,
|
||||
bool VoiceSubscribed);
|
||||
|
||||
public sealed record PermissionsInfo(
|
||||
bool CanCreateTempChannel,
|
||||
|
||||
@@ -54,6 +54,12 @@ internal static partial class NativeMethods
|
||||
[LibraryImport(LibName)]
|
||||
internal static partial VcResult vc_leave_channel(nint c);
|
||||
|
||||
[LibraryImport(LibName)]
|
||||
internal static partial VcResult vc_join_voice(nint c);
|
||||
|
||||
[LibraryImport(LibName)]
|
||||
internal static partial VcResult vc_leave_voice(nint c);
|
||||
|
||||
// ── Local media streams ─────────────────────────────────────────────────────────────────
|
||||
[LibraryImport(LibName)]
|
||||
internal static partial VcResult vc_stream_start(nint c, in VcStreamDescNative desc,
|
||||
|
||||
@@ -116,6 +116,8 @@ internal struct VcChannelNative
|
||||
public IntPtr Topic;
|
||||
public int PasswordProtected;
|
||||
public uint MaxUsers;
|
||||
public uint SortOrder;
|
||||
public VcAudioConfigNative Audio;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
@@ -136,6 +138,7 @@ internal struct VcUserNative
|
||||
public int SelfDeafened;
|
||||
public int ServerMuted;
|
||||
public int ServerDeafened;
|
||||
public int VoiceSubscribed;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
|
||||
@@ -152,6 +152,12 @@ public sealed class VoiceCatClient : IDisposable
|
||||
public VcResult LeaveChannel() =>
|
||||
NativeMethods.vc_leave_channel(_handle.DangerousGetHandle());
|
||||
|
||||
public VcResult JoinVoice() =>
|
||||
NativeMethods.vc_join_voice(_handle.DangerousGetHandle());
|
||||
|
||||
public VcResult LeaveVoice() =>
|
||||
NativeMethods.vc_leave_voice(_handle.DangerousGetHandle());
|
||||
|
||||
public List<ChannelInfo> ListChannels()
|
||||
{
|
||||
NativeMethods.vc_list_channels(_handle.DangerousGetHandle(), out var native);
|
||||
|
||||
Reference in New Issue
Block a user