[fix](trx-frontend-http): reconfigure audio stream on rig change
Co-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -2642,6 +2642,40 @@ function clearTxTimeout() {
|
|||||||
txTimeoutRemaining = 0;
|
txTimeoutRemaining = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetRxDecoder() {
|
||||||
|
if (opusDecoder) {
|
||||||
|
try { opusDecoder.close(); } catch (e) {}
|
||||||
|
opusDecoder = null;
|
||||||
|
}
|
||||||
|
nextPlayTime = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function configureRxStream(nextInfo) {
|
||||||
|
const nextSampleRate = (nextInfo && nextInfo.sample_rate) || 48000;
|
||||||
|
const sampleRateChanged = !audioCtx || audioCtx.sampleRate !== nextSampleRate;
|
||||||
|
streamInfo = nextInfo;
|
||||||
|
updateWfmControls();
|
||||||
|
resetRxDecoder();
|
||||||
|
if (sampleRateChanged && audioCtx) {
|
||||||
|
audioCtx.close().catch(() => {});
|
||||||
|
audioCtx = null;
|
||||||
|
rxGainNode = null;
|
||||||
|
}
|
||||||
|
if (!audioCtx) {
|
||||||
|
audioCtx = new AudioContext({ sampleRate: nextSampleRate });
|
||||||
|
audioCtx.resume().catch(() => {});
|
||||||
|
}
|
||||||
|
if (!rxGainNode) {
|
||||||
|
rxGainNode = audioCtx.createGain();
|
||||||
|
rxGainNode.connect(audioCtx.destination);
|
||||||
|
}
|
||||||
|
rxGainNode.gain.value = rxVolSlider.value / 100;
|
||||||
|
rxActive = true;
|
||||||
|
rxAudioBtn.style.borderColor = "#00d17f";
|
||||||
|
rxAudioBtn.style.color = "#00d17f";
|
||||||
|
audioStatus.textContent = "RX";
|
||||||
|
}
|
||||||
|
|
||||||
function startRxAudio() {
|
function startRxAudio() {
|
||||||
if (rxActive) { stopRxAudio(); return; }
|
if (rxActive) { stopRxAudio(); return; }
|
||||||
if (!hasWebCodecs) {
|
if (!hasWebCodecs) {
|
||||||
@@ -2661,17 +2695,7 @@ function startRxAudio() {
|
|||||||
if (typeof evt.data === "string") {
|
if (typeof evt.data === "string") {
|
||||||
// Stream info JSON
|
// Stream info JSON
|
||||||
try {
|
try {
|
||||||
streamInfo = JSON.parse(evt.data);
|
configureRxStream(JSON.parse(evt.data));
|
||||||
updateWfmControls();
|
|
||||||
audioCtx = new AudioContext({ sampleRate: streamInfo.sample_rate || 48000 });
|
|
||||||
audioCtx.resume().catch(() => {});
|
|
||||||
rxGainNode = audioCtx.createGain();
|
|
||||||
rxGainNode.gain.value = rxVolSlider.value / 100;
|
|
||||||
rxGainNode.connect(audioCtx.destination);
|
|
||||||
rxActive = true;
|
|
||||||
rxAudioBtn.style.borderColor = "#00d17f";
|
|
||||||
rxAudioBtn.style.color = "#00d17f";
|
|
||||||
audioStatus.textContent = "RX";
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Audio stream info parse error", e);
|
console.error("Audio stream info parse error", e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ pub async fn audio_ws(
|
|||||||
let (response, mut session, mut msg_stream) = actix_ws::handle(&req, body)?;
|
let (response, mut session, mut msg_stream) = actix_ws::handle(&req, body)?;
|
||||||
|
|
||||||
actix_web::rt::spawn(async move {
|
actix_web::rt::spawn(async move {
|
||||||
let info = loop {
|
let mut current_info = loop {
|
||||||
if let Some(info) = info_rx.borrow().clone() {
|
if let Some(info) = info_rx.borrow().clone() {
|
||||||
break info;
|
break info;
|
||||||
}
|
}
|
||||||
@@ -236,7 +236,7 @@ pub async fn audio_ws(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let info_json = match serde_json::to_string(&info) {
|
let info_json = match serde_json::to_string(¤t_info) {
|
||||||
Ok(j) => j,
|
Ok(j) => j,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
let _ = session.close(None).await;
|
let _ = session.close(None).await;
|
||||||
@@ -247,34 +247,56 @@ pub async fn audio_ws(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut rx_session = session.clone();
|
loop {
|
||||||
let rx_handle = actix_web::rt::spawn(async move {
|
tokio::select! {
|
||||||
loop {
|
changed = info_rx.changed() => {
|
||||||
match rx_sub.recv().await {
|
match changed {
|
||||||
Ok(packet) => {
|
Ok(()) => {
|
||||||
if rx_session.binary(packet).await.is_err() {
|
let Some(next_info) = info_rx.borrow().clone() else {
|
||||||
break;
|
continue;
|
||||||
|
};
|
||||||
|
let changed = next_info.sample_rate != current_info.sample_rate
|
||||||
|
|| next_info.channels != current_info.channels
|
||||||
|
|| next_info.frame_duration_ms != current_info.frame_duration_ms;
|
||||||
|
if changed {
|
||||||
|
current_info = next_info;
|
||||||
|
let info_json = match serde_json::to_string(¤t_info) {
|
||||||
|
Ok(j) => j,
|
||||||
|
Err(_) => break,
|
||||||
|
};
|
||||||
|
if session.text(info_json).await.is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Err(_) => break,
|
||||||
}
|
}
|
||||||
Err(broadcast::error::RecvError::Lagged(n)) => {
|
}
|
||||||
warn!("Audio WS: dropped {} RX frames", n);
|
packet = rx_sub.recv() => {
|
||||||
|
match packet {
|
||||||
|
Ok(packet) => {
|
||||||
|
if session.binary(packet).await.is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(broadcast::error::RecvError::Lagged(n)) => {
|
||||||
|
warn!("Audio WS: dropped {} RX frames", n);
|
||||||
|
}
|
||||||
|
Err(broadcast::error::RecvError::Closed) => break,
|
||||||
}
|
}
|
||||||
Err(broadcast::error::RecvError::Closed) => break,
|
|
||||||
}
|
}
|
||||||
}
|
msg = msg_stream.recv() => {
|
||||||
});
|
match msg {
|
||||||
|
Some(Ok(Message::Binary(data))) => {
|
||||||
while let Some(Ok(msg)) = msg_stream.recv().await {
|
let _ = tx_sender.send(Bytes::from(data.to_vec())).await;
|
||||||
match msg {
|
}
|
||||||
Message::Binary(data) => {
|
Some(Ok(Message::Close(_))) => break,
|
||||||
let _ = tx_sender.send(Bytes::from(data.to_vec())).await;
|
Some(Ok(_)) => {}
|
||||||
|
Some(Err(_)) | None => break,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Message::Close(_) => break,
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rx_handle.abort();
|
|
||||||
let _ = session.close(None).await;
|
let _ = session.close(None).await;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user