[fix](trx-frontend-http): start browser audio on user gesture

Create and resume the RX AudioContext from the audio button click so Chromium does not leave playback suspended until a later interaction.

Reuse that context when stream metadata arrives instead of recreating it from the WebSocket message path.

Co-authored-by: OpenAI Codex <noreply@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-23 22:52:28 +01:00
parent 32e1618384
commit db5fa26bd9
@@ -7352,6 +7352,25 @@ function setAudioLevel(levelPct) {
audioLevelFill.style.width = `${clamped}%`; audioLevelFill.style.width = `${clamped}%`;
} }
// Create/resume the output context from a direct user gesture so Chromium
// does not leave playback suspended until a later click.
function ensureRxAudioContext(preferredSampleRate) {
if (!audioCtx) {
try {
audioCtx = Number.isFinite(preferredSampleRate) && preferredSampleRate > 0
? new AudioContext({ sampleRate: preferredSampleRate })
: new AudioContext();
} catch (e) {
audioCtx = new AudioContext();
}
}
audioCtx.resume().catch(() => {});
if (!rxGainNode) {
rxGainNode = audioCtx.createGain();
rxGainNode.connect(audioCtx.destination);
}
}
function levelFromChannels(channels, frameCount) { function levelFromChannels(channels, frameCount) {
if (!Array.isArray(channels) || channels.length === 0 || !Number.isFinite(frameCount) || frameCount <= 0) { if (!Array.isArray(channels) || channels.length === 0 || !Number.isFinite(frameCount) || frameCount <= 0) {
return 0; return 0;
@@ -7596,23 +7615,10 @@ function resetRxDecoder() {
function configureRxStream(nextInfo) { function configureRxStream(nextInfo) {
const nextSampleRate = (nextInfo && nextInfo.sample_rate) || 48000; const nextSampleRate = (nextInfo && nextInfo.sample_rate) || 48000;
const sampleRateChanged = !audioCtx || audioCtx.sampleRate !== nextSampleRate;
streamInfo = nextInfo; streamInfo = nextInfo;
updateWfmControls(); updateWfmControls();
resetRxDecoder(); resetRxDecoder();
if (sampleRateChanged && audioCtx) { ensureRxAudioContext(nextSampleRate);
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; rxGainNode.gain.value = rxVolSlider.value / 100;
rxActive = true; rxActive = true;
setAudioLevel(0); setAudioLevel(0);
@@ -7662,6 +7668,7 @@ function startRxAudio() {
audioStatus.textContent = "Audio requires Chrome/Edge"; audioStatus.textContent = "Audio requires Chrome/Edge";
return; return;
} }
ensureRxAudioContext((streamInfo && streamInfo.sample_rate) || 48000);
const proto = location.protocol === "https:" ? "wss:" : "ws:"; const proto = location.protocol === "https:" ? "wss:" : "ws:";
let audioPath; let audioPath;
if (_audioChannelOverride) { if (_audioChannelOverride) {