diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/vchan.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/vchan.js index 9bea36f..ac91822 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/vchan.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/vchan.js @@ -170,19 +170,23 @@ async function vchanSubscribe(channelId) { // Reconnect the audio WebSocket to the appropriate endpoint: // - virtual channel: /audio?channel_id= // - primary channel: /audio (no param) -// Only reconnects if RX audio is currently active. +// Always updates _audioChannelOverride so that starting audio later +// connects to the correct channel. Only reconnects if RX audio is active. function vchanReconnectAudio() { - if (typeof rxActive === "undefined" || !rxActive) return; - // Set the channel override so startRxAudio picks up the right URL. + // Always update the override so startRxAudio picks up the right URL, + // even when audio isn't currently running. const ch = vchanIsOnVirtual() ? vchanActiveChannel() : null; if (typeof _audioChannelOverride !== "undefined") { _audioChannelOverride = ch ? ch.id : null; } + if (typeof rxActive === "undefined" || !rxActive) return; if (typeof stopRxAudio === "function") stopRxAudio(); - // Small delay so the server has time to set up the per-channel encoder. + // Delay so the server has time to set up the per-channel encoder. + // The server-side audio_ws handler also polls for up to 2 s, so this + // just needs to be long enough for the WS upgrade to reach the server. setTimeout(() => { if (typeof startRxAudio === "function") startRxAudio(); - }, 200); + }, 300); } // Called by app.js from applyCapabilities(). diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/audio.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/audio.rs index 4e97d1c..15dd9f6 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/audio.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/audio.rs @@ -380,16 +380,24 @@ pub async fn audio_ws( } // If a channel_id is specified, subscribe to the per-channel broadcaster. - // Otherwise fall back to the primary RX broadcast. + // The entry is created asynchronously when AUDIO_MSG_VCHAN_ALLOCATED arrives + // from the server, which may lag the HTTP allocation by up to ~100 ms. + // Poll for up to 2 s so a tight JS timer doesn't race and get a 404. let rx_sub: broadcast::Receiver = if let Some(ch_id) = query.channel_id { - match context.vchan_audio.read() { - Ok(map) => match map.get(&ch_id) { - Some(tx) => tx.subscribe(), - None => { - return Ok(HttpResponse::NotFound().body("channel not found")); + let deadline = Instant::now() + Duration::from_secs(2); + loop { + match context.vchan_audio.read() { + Ok(map) => { + if let Some(tx) = map.get(&ch_id) { + break tx.subscribe(); + } } - }, - Err(_) => return Ok(HttpResponse::InternalServerError().finish()), + Err(_) => return Ok(HttpResponse::InternalServerError().finish()), + } + if Instant::now() >= deadline { + return Ok(HttpResponse::NotFound().body("channel not found")); + } + tokio::time::sleep(Duration::from_millis(50)).await; } } else { let Some(rx) = context.audio_rx.as_ref() else { diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/vchan.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/vchan.rs index f12563e..09bcbfa 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/vchan.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/vchan.rs @@ -318,6 +318,12 @@ impl ClientChannelManager { changed = true; } } + // Collect IDs of non-permanent channels about to be evicted (0 subscribers). + let to_remove: Vec = channels + .iter() + .filter(|c| !c.permanent && c.session_ids.is_empty()) + .map(|c| c.id) + .collect(); // Remove non-permanent channels with no subscribers. let before = channels.len(); channels.retain(|c| c.permanent || !c.session_ids.is_empty()); @@ -327,6 +333,12 @@ impl ClientChannelManager { if changed { self.broadcast_change(rig_id, channels); } + drop(rigs); + // Notify the audio-client task so it can tear down the server-side + // DSP pipeline and Opus encoder for each evicted channel. + for id in to_remove { + self.send_audio_cmd(VChanAudioCmd::Remove(id)); + } } /// Explicitly delete a channel by UUID (any session may do this).