[feat](trx-rs): per-virtual-channel audio streaming
Add end-to-end audio routing for virtual DSP channels: Server (trx-server): - New wire protocol: AUDIO_MSG_RX_FRAME_CH (0x0b), VCHAN_ALLOCATED (0x0c), VCHAN_SUB (0x0d), VCHAN_UNSUB (0x0e), VCHAN_FREQ (0x0f), VCHAN_MODE (0x10), VCHAN_REMOVE (0x11) frame types in trx-core audio.rs - Add frame helpers: write_vchan_uuid_msg, write_vchan_audio_frame, parse_vchan_audio_frame, parse_vchan_uuid_msg - Add ensure_channel_pcm() to VirtualChannelManager trait; implement in SdrVirtualChannelManager with create-or-subscribe semantics using client UUID - Extend audio.rs handle_audio_client: VChanCmd dispatcher, per-channel Opus encoder tasks, VCHAN_SUB/UNSUB/FREQ/MODE/REMOVE reader loop handlers - Thread vchan_manager through run_audio_listener / spawn_rig_audio_stack Client (trx-client): - Add VChanAudioCmd enum to trx-frontend; add vchan_audio and vchan_audio_cmd fields to FrontendRuntimeContext - Extend audio_client: demux AUDIO_MSG_RX_FRAME_CH to per-channel broadcasters, handle VCHAN_ALLOCATED; forward VChanAudioCmd over TCP write loop - Wire vchan_cmd_tx/rx channel in main.rs; pass vchan_audio map to audio_client - ClientChannelManager.set_audio_cmd() / send_audio_cmd(): dispatch Subscribe/Remove/SetFreq/SetMode on allocate/delete/freq/mode operations - Wire audio_cmd sender in server.rs serve() after creating vchan_mgr HTTP frontend: - /audio?channel_id=<uuid>: route WebSocket to per-channel Opus broadcaster - vchan.js: vchanReconnectAudio() stops/restarts RX audio on channel switch; _audioChannelOverride in app.js selects primary vs virtual WS endpoint - app.js: _audioChannelOverride variable; startRxAudio appends channel param Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -5879,6 +5879,9 @@ function extractAudioFrameChannels(frame) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Optional channel_id injected by vchan.js when connecting to a virtual channel.
|
||||
let _audioChannelOverride = null;
|
||||
|
||||
function startRxAudio() {
|
||||
if (rxActive) { stopRxAudio(); return; }
|
||||
if (!hasWebCodecs) {
|
||||
@@ -5886,7 +5889,10 @@ function startRxAudio() {
|
||||
return;
|
||||
}
|
||||
const proto = location.protocol === "https:" ? "wss:" : "ws:";
|
||||
audioWs = new WebSocket(`${proto}//${location.host}/audio`);
|
||||
const audioPath = _audioChannelOverride
|
||||
? `/audio?channel_id=${encodeURIComponent(_audioChannelOverride)}`
|
||||
: "/audio";
|
||||
audioWs = new WebSocket(`${proto}//${location.host}${audioPath}`);
|
||||
audioWs.binaryType = "arraybuffer";
|
||||
audioStatus.textContent = "Connecting…";
|
||||
|
||||
|
||||
@@ -36,10 +36,11 @@ function vchanHandleChannels(data) {
|
||||
const d = JSON.parse(data);
|
||||
vchanRigId = d.rig_id || null;
|
||||
vchanChannels = d.channels || [];
|
||||
// If the active channel was evicted, fall back to channel 0.
|
||||
// If the active channel was evicted, fall back to channel 0 and reconnect audio.
|
||||
const ids = new Set(vchanChannels.map(c => c.id));
|
||||
if (vchanActiveId && !ids.has(vchanActiveId)) {
|
||||
vchanActiveId = vchanChannels.length > 0 ? vchanChannels[0].id : null;
|
||||
vchanReconnectAudio();
|
||||
}
|
||||
vchanRender();
|
||||
} catch (e) {
|
||||
@@ -120,6 +121,7 @@ async function vchanAllocate() {
|
||||
// The SSE `channels` event will trigger vchanRender(); optimistically
|
||||
// mark active so the picker feels responsive even before the event arrives.
|
||||
vchanRender();
|
||||
vchanReconnectAudio();
|
||||
} catch (e) {
|
||||
console.error("vchan: allocate error", e);
|
||||
}
|
||||
@@ -159,11 +161,30 @@ async function vchanSubscribe(channelId) {
|
||||
vchanActiveId = channelId;
|
||||
vchanRender();
|
||||
vchanSyncModeDisplay();
|
||||
vchanReconnectAudio();
|
||||
} catch (e) {
|
||||
console.error("vchan: subscribe error", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Reconnect the audio WebSocket to the appropriate endpoint:
|
||||
// - virtual channel: /audio?channel_id=<uuid>
|
||||
// - primary channel: /audio (no param)
|
||||
// Only reconnects if RX audio is currently active.
|
||||
function vchanReconnectAudio() {
|
||||
if (typeof rxActive === "undefined" || !rxActive) return;
|
||||
// Set the channel override so startRxAudio picks up the right URL.
|
||||
const ch = vchanIsOnVirtual() ? vchanActiveChannel() : null;
|
||||
if (typeof _audioChannelOverride !== "undefined") {
|
||||
_audioChannelOverride = ch ? ch.id : null;
|
||||
}
|
||||
if (typeof stopRxAudio === "function") stopRxAudio();
|
||||
// Small delay so the server has time to set up the per-channel encoder.
|
||||
setTimeout(() => {
|
||||
if (typeof startRxAudio === "function") startRxAudio();
|
||||
}, 200);
|
||||
}
|
||||
|
||||
// Called by app.js from applyCapabilities().
|
||||
// Shows the channel picker only for SDR rigs.
|
||||
function vchanApplyCapabilities(caps) {
|
||||
|
||||
Reference in New Issue
Block a user