fix(core): complete voice joins on server accept

This commit is contained in:
Edison Jwa
2026-05-20 14:52:33 +09:00
parent 2861f5b010
commit 7254f81c65
+13 -79
View File
@@ -642,13 +642,6 @@ impl ChanoraSession {
if let Some(projection) = projection {
self.emit_voice_state(projection).await;
}
// Bring the audio engine up so the user can immediately
// hear other speakers + transmit on PTT. Tolerates
// failure the same way voice_join does: server-side
// we're in the channel regardless; if audio fails
// (missing mic permission, no device), the UI will
// still expose the controls and the user can resolve
// the underlying issue.
if let Err(audio_err) = self.ensure_audio_running().await {
warn!(
target: "chanora_core",
@@ -1241,6 +1234,18 @@ impl ChanoraSession {
if let Some(pw) = requested_password {
state.channel_passwords.insert(channel_id, pw);
}
let epoch = state.join_state.authoritative.confirmed_epoch;
let _ = channel_join::reduce(
&mut state.join_state,
ChannelJoinEvent::AuthoritativeSelfMove {
channel: Some(JoinChannelId(channel_id)),
epoch,
source: AuthoritativeSource::LiveDelta,
},
);
let projection = channel_join::project(&state.join_state);
self.voice_selector.set_in_channel(true);
self.emit_voice_state(projection).await;
drop(guard);
// 2. Bring the audio engine up. Tolerate failure: the
// server-side channel move has ALREADY succeeded (step
@@ -1272,78 +1277,7 @@ impl ChanoraSession {
// UI gains the channel + mute + PTT controls.
let _ = self.events_tx.send(SessionEvent::AudioStopped);
}
// 3. Belt-and-braces: if the server replied Ok but never
// actually moved us (legacy server, command processed
// but rolled back later, etc.) the snapshot poll catches
// it. Keeps the move/confirm contract honest even when
// upstream tsclientlib changes shape.
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(1500);
let confirmed = loop {
if let Ok(snap) = self.snapshot().await {
if let Some((my_id, my_channel)) = self.find_own_in(&snap).await {
if my_channel == channel_id {
info!(
target: "chanora_core",
client_id = my_id,
channel_id,
"voice_join confirmed by snapshot"
);
break true;
}
}
}
if std::time::Instant::now() >= deadline {
break false;
}
tokio::time::sleep(std::time::Duration::from_millis(80)).await;
};
if !confirmed {
let mut guard = self.inner.lock().await;
if let Some(state) = guard.as_mut() {
let _ = channel_join::reduce(
&mut state.join_state,
ChannelJoinEvent::JoinTimeout {
key: pending_key,
now: std::time::Instant::now(),
},
);
let projection = channel_join::project(&state.join_state);
let still_in_channel = projection.current_channel.is_some();
self.voice_selector.set_in_channel(still_in_channel);
self.emit_voice_state(projection).await;
}
return Err(CoreError::Protocol(
chanora_protocol::ProtocolError::ServerRejected {
// Use a sentinel "unknown" code (the canonical
// TS3 error catalogue uses 0x0001 for `undefined`).
code: 0x0001,
message: "channel move did not take effect server-side within timeout"
.to_string(),
},
));
}
let mut guard = self.inner.lock().await;
if let Some(state) = guard.as_mut() {
let epoch = state.join_state.authoritative.confirmed_epoch;
let _ = channel_join::reduce(
&mut state.join_state,
ChannelJoinEvent::AuthoritativeSelfMove {
channel: Some(JoinChannelId(channel_id)),
epoch,
source: AuthoritativeSource::Snapshot,
},
);
}
self.voice_selector.set_in_channel(true);
let projection = {
let guard = self.inner.lock().await;
guard
.as_ref()
.map(|state| channel_join::project(&state.join_state))
};
if let Some(projection) = projection {
self.emit_voice_state(projection).await;
}
info!(target: "chanora_core", channel_id, "voice_join accepted by server");
Ok(())
}