[fix](trx-frontend-http): add PTT safety timeout and audio compatibility fallback
Add a 120-second TX safety timeout that auto-releases PTT if no mic data flows (browser crash, disconnect). Timer resets on each audio callback. Shows countdown in status when < 10s remaining. Add beforeunload handler that releases PTT via navigator.sendBeacon when the browser tab is closed during TX. Detect WebCodecs support on page load and show "Audio requires Chrome/Edge" on non-Chromium browsers instead of silently failing. Remove dead playBuffer/playNode variables that were declared but never used. Fix TX AudioData to always use mono (channel 0) with numberOfChannels: 1 matching the f32-planar format. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
@@ -471,13 +471,51 @@ let txActive = false;
|
|||||||
let txStream = null;
|
let txStream = null;
|
||||||
let txProcessor = null;
|
let txProcessor = null;
|
||||||
let streamInfo = null;
|
let streamInfo = null;
|
||||||
|
const TX_TIMEOUT_SECS = 120;
|
||||||
|
let txTimeoutTimer = null;
|
||||||
|
let txTimeoutRemaining = 0;
|
||||||
|
let txTimeoutInterval = null;
|
||||||
|
const hasWebCodecs = typeof AudioDecoder !== "undefined" && typeof AudioEncoder !== "undefined";
|
||||||
|
|
||||||
// Simple ring-buffer based audio player
|
// Show compatibility warning for non-Chromium browsers
|
||||||
let playBuffer = [];
|
if (!hasWebCodecs) {
|
||||||
let playNode = null;
|
rxAudioBtn.disabled = true;
|
||||||
|
txAudioBtn.disabled = true;
|
||||||
|
audioStatus.textContent = "Audio requires Chrome/Edge";
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetTxTimeout() {
|
||||||
|
txTimeoutRemaining = TX_TIMEOUT_SECS;
|
||||||
|
if (txTimeoutTimer) clearTimeout(txTimeoutTimer);
|
||||||
|
txTimeoutTimer = setTimeout(() => {
|
||||||
|
console.warn("PTT safety timeout — stopping TX");
|
||||||
|
stopTxAudio();
|
||||||
|
}, TX_TIMEOUT_SECS * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startTxTimeoutCountdown() {
|
||||||
|
txTimeoutRemaining = TX_TIMEOUT_SECS;
|
||||||
|
if (txTimeoutInterval) clearInterval(txTimeoutInterval);
|
||||||
|
txTimeoutInterval = setInterval(() => {
|
||||||
|
txTimeoutRemaining--;
|
||||||
|
if (txTimeoutRemaining <= 10 && txTimeoutRemaining > 0 && txActive) {
|
||||||
|
audioStatus.textContent = `TX timeout ${txTimeoutRemaining}s`;
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearTxTimeout() {
|
||||||
|
if (txTimeoutTimer) { clearTimeout(txTimeoutTimer); txTimeoutTimer = null; }
|
||||||
|
if (txTimeoutInterval) { clearInterval(txTimeoutInterval); txTimeoutInterval = null; }
|
||||||
|
txTimeoutRemaining = 0;
|
||||||
|
}
|
||||||
|
|
||||||
function startRxAudio() {
|
function startRxAudio() {
|
||||||
if (rxActive) { stopRxAudio(); return; }
|
if (rxActive) { stopRxAudio(); return; }
|
||||||
|
if (!hasWebCodecs) {
|
||||||
|
audioStatus.textContent = "Audio requires Chrome/Edge";
|
||||||
|
return;
|
||||||
|
}
|
||||||
const proto = location.protocol === "https:" ? "wss:" : "ws:";
|
const proto = location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
audioWs = new WebSocket(`${proto}//${location.host}/audio`);
|
audioWs = new WebSocket(`${proto}//${location.host}/audio`);
|
||||||
audioWs.binaryType = "arraybuffer";
|
audioWs.binaryType = "arraybuffer";
|
||||||
@@ -599,6 +637,10 @@ function stopRxAudio() {
|
|||||||
|
|
||||||
function startTxAudio() {
|
function startTxAudio() {
|
||||||
if (txActive) { stopTxAudio(); return; }
|
if (txActive) { stopTxAudio(); return; }
|
||||||
|
if (!hasWebCodecs) {
|
||||||
|
audioStatus.textContent = "Audio requires Chrome/Edge";
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!audioWs || audioWs.readyState !== WebSocket.OPEN) {
|
if (!audioWs || audioWs.readyState !== WebSocket.OPEN) {
|
||||||
audioStatus.textContent = "RX first";
|
audioStatus.textContent = "RX first";
|
||||||
return;
|
return;
|
||||||
@@ -614,11 +656,13 @@ function startTxAudio() {
|
|||||||
txAudioBtn.style.color = "#e55353";
|
txAudioBtn.style.color = "#e55353";
|
||||||
audioStatus.textContent = "RX+TX";
|
audioStatus.textContent = "RX+TX";
|
||||||
|
|
||||||
|
// Start PTT safety timeout
|
||||||
|
resetTxTimeout();
|
||||||
|
startTxTimeoutCountdown();
|
||||||
|
|
||||||
// Engage PTT automatically
|
// Engage PTT automatically
|
||||||
try { await postPath("/set_ptt?ptt=true"); } catch (e) { console.error("PTT on failed", e); }
|
try { await postPath("/set_ptt?ptt=true"); } catch (e) { console.error("PTT on failed", e); }
|
||||||
|
|
||||||
// If WebCodecs AudioEncoder is available, use it for Opus encoding
|
|
||||||
if (typeof AudioEncoder !== "undefined") {
|
|
||||||
const sampleRate = streamInfo.sample_rate || 48000;
|
const sampleRate = streamInfo.sample_rate || 48000;
|
||||||
const channels = streamInfo.channels || 1;
|
const channels = streamInfo.channels || 1;
|
||||||
const encoder = new AudioEncoder({
|
const encoder = new AudioEncoder({
|
||||||
@@ -650,21 +694,18 @@ function startTxAudio() {
|
|||||||
processor.onaudioprocess = (e) => {
|
processor.onaudioprocess = (e) => {
|
||||||
if (!txActive || !window._txEncoder) return;
|
if (!txActive || !window._txEncoder) return;
|
||||||
const input = e.inputBuffer;
|
const input = e.inputBuffer;
|
||||||
const data = new Float32Array(input.length * input.numberOfChannels);
|
// Reset PTT safety timeout on each audio callback
|
||||||
for (let ch = 0; ch < input.numberOfChannels; ch++) {
|
resetTxTimeout();
|
||||||
const chData = input.getChannelData(ch);
|
// Use mono (channel 0) for f32-planar format
|
||||||
for (let i = 0; i < input.length; i++) {
|
const monoData = input.getChannelData(0);
|
||||||
data[i * input.numberOfChannels + ch] = chData[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const frame = new AudioData({
|
const frame = new AudioData({
|
||||||
format: "f32-planar",
|
format: "f32-planar",
|
||||||
sampleRate: input.sampleRate,
|
sampleRate: input.sampleRate,
|
||||||
numberOfFrames: input.length,
|
numberOfFrames: input.length,
|
||||||
numberOfChannels: input.numberOfChannels,
|
numberOfChannels: 1,
|
||||||
timestamp: tsCounter,
|
timestamp: tsCounter,
|
||||||
data: input.getChannelData(0),
|
data: monoData,
|
||||||
});
|
});
|
||||||
tsCounter += (input.length / input.sampleRate) * 1_000_000;
|
tsCounter += (input.length / input.sampleRate) * 1_000_000;
|
||||||
window._txEncoder.encode(frame);
|
window._txEncoder.encode(frame);
|
||||||
@@ -676,7 +717,6 @@ function startTxAudio() {
|
|||||||
source.connect(processor);
|
source.connect(processor);
|
||||||
processor.connect(audioCtx.destination);
|
processor.connect(audioCtx.destination);
|
||||||
txProcessor = { source, processor };
|
txProcessor = { source, processor };
|
||||||
}
|
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.error("getUserMedia failed:", err);
|
console.error("getUserMedia failed:", err);
|
||||||
audioStatus.textContent = "Mic denied";
|
audioStatus.textContent = "Mic denied";
|
||||||
@@ -686,6 +726,7 @@ function startTxAudio() {
|
|||||||
async function stopTxAudio() {
|
async function stopTxAudio() {
|
||||||
if (!txActive) return;
|
if (!txActive) return;
|
||||||
txActive = false;
|
txActive = false;
|
||||||
|
clearTxTimeout();
|
||||||
|
|
||||||
// Release PTT automatically
|
// Release PTT automatically
|
||||||
try { await postPath("/set_ptt?ptt=false"); } catch (e) { console.error("PTT off failed", e); }
|
try { await postPath("/set_ptt?ptt=false"); } catch (e) { console.error("PTT off failed", e); }
|
||||||
@@ -710,3 +751,10 @@ async function stopTxAudio() {
|
|||||||
|
|
||||||
rxAudioBtn.addEventListener("click", startRxAudio);
|
rxAudioBtn.addEventListener("click", startRxAudio);
|
||||||
txAudioBtn.addEventListener("click", startTxAudio);
|
txAudioBtn.addEventListener("click", startTxAudio);
|
||||||
|
|
||||||
|
// Release PTT on page unload to prevent stuck transmit
|
||||||
|
window.addEventListener("beforeunload", () => {
|
||||||
|
if (txActive) {
|
||||||
|
navigator.sendBeacon("/set_ptt?ptt=false", "");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user