fix: waterfall monitor audio delay and unresponsive stop button

- _waitForPlayback now only succeeds on playing/timeupdate events, not
  loadeddata/canplay which fire from just the WAV header before real
  audio arrives
- stopMonitor() pauses audio and updates UI immediately instead of
  blocking on the backend stop request (1+ second delay)
- Reduced backend audio stop sleep from 1.0s to 0.15s; the start
  retry loop already handles USB contention

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-02-23 20:59:40 +00:00
parent b0af1d16d2
commit f889c53d92
5 changed files with 514 additions and 495 deletions
+2
View File
@@ -7,6 +7,8 @@ All notable changes to iNTERCEPT will be documented in this file.
### Fixed ### Fixed
- Waterfall control panel rendered as unstyled text for up to 20 seconds on first visit — CSS is now loaded eagerly with the rest of the page assets - Waterfall control panel rendered as unstyled text for up to 20 seconds on first visit — CSS is now loaded eagerly with the rest of the page assets
- WebSDR globe failed to render on first page load — initialization now waits for a layout frame before mounting the WebGL renderer, ensuring the container has non-zero dimensions - WebSDR globe failed to render on first page load — initialization now waits for a layout frame before mounting the WebGL renderer, ensuring the container has non-zero dimensions
- Waterfall monitor audio took minutes to start — `_waitForPlayback` now only reports success on actual audio playback (`playing`/`timeupdate`), not from the WAV header alone (`loadeddata`/`canplay`)
- Waterfall monitor could not be stopped — `stopMonitor()` now pauses audio and updates the UI immediately instead of waiting for the backend stop request (which blocked for 1+ seconds during SDR process cleanup)
--- ---
+2
View File
@@ -17,6 +17,8 @@ CHANGELOG = [
"highlights": [ "highlights": [
"Waterfall control panel no longer shows as unstyled text on first visit", "Waterfall control panel no longer shows as unstyled text on first visit",
"WebSDR globe renders correctly on first page load without requiring a refresh", "WebSDR globe renders correctly on first page load without requiring a refresh",
"Waterfall monitor audio no longer takes minutes to start — playback detection now waits for real audio data instead of just the WAV header",
"Waterfall monitor stop is now instant — audio pauses and UI updates immediately instead of waiting for backend cleanup",
] ]
}, },
{ {
+486 -484
View File
File diff suppressed because it is too large Load Diff
+23 -10
View File
@@ -900,19 +900,27 @@ const Waterfall = (function () {
resolve(ok); resolve(ok);
}; };
const onReady = () => finish(true); // Only treat actual playback as success. `loadeddata` and
// `canplay` fire when just the WAV header arrives — before any
// real audio samples have been decoded — which caused the
// monitor to report "started" while the stream was still silent.
const onReady = () => {
if (player.currentTime > 0 || (!player.paused && player.readyState >= 4)) {
finish(true);
}
};
const onFail = () => finish(false); const onFail = () => finish(false);
const events = ['playing', 'timeupdate', 'canplay', 'loadeddata']; const events = ['playing', 'timeupdate'];
const failEvents = ['error', 'abort', 'stalled', 'ended']; const failEvents = ['error', 'abort', 'stalled', 'ended'];
events.forEach((evt) => player.addEventListener(evt, onReady)); events.forEach((evt) => player.addEventListener(evt, onReady));
failEvents.forEach((evt) => player.addEventListener(evt, onFail)); failEvents.forEach((evt) => player.addEventListener(evt, onFail));
timer = setTimeout(() => { timer = setTimeout(() => {
finish(!player.paused && (player.currentTime > 0 || player.readyState >= 2)); finish(!player.paused && player.currentTime > 0);
}, timeoutMs); }, timeoutMs);
if (!player.paused && (player.currentTime > 0 || player.readyState >= 2)) { if (!player.paused && player.currentTime > 0) {
finish(true); finish(true);
} }
}); });
@@ -2571,6 +2579,7 @@ const Waterfall = (function () {
} }
if (attempt < maxAttempts) { if (attempt < maxAttempts) {
_setMonitorState(`Waiting for audio stream (attempt ${attempt}/${maxAttempts})...`);
await _wait(220 * attempt); await _wait(220 * attempt);
continue; continue;
} }
@@ -2813,12 +2822,9 @@ const Waterfall = (function () {
clearTimeout(_monitorRetuneTimer); clearTimeout(_monitorRetuneTimer);
_audioConnectNonce += 1; _audioConnectNonce += 1;
try { // Immediately pause audio and update the UI so the user gets instant
await fetch('/receiver/audio/stop', { method: 'POST' }); // feedback. The backend cleanup (which can block for 1-2 s while the
} catch (_) { // SDR process group is reaped) happens afterwards.
// Ignore backend stop errors
}
_stopSmeter(); _stopSmeter();
_setUnlockVisible(false); _setUnlockVisible(false);
_audioUnlockRequired = false; _audioUnlockRequired = false;
@@ -2836,6 +2842,13 @@ const Waterfall = (function () {
_setVisualStatus('READY'); _setVisualStatus('READY');
} }
// Backend stop is fire-and-forget; UI is already updated above.
try {
await fetch('/receiver/audio/stop', { method: 'POST' });
} catch (_) {
// Ignore backend stop errors
}
if (resumeWaterfall && _active) { if (resumeWaterfall && _active) {
_resumeWaterfallAfterMonitor = false; _resumeWaterfallAfterMonitor = false;
await start(); await start();
+1 -1
View File
@@ -2936,7 +2936,7 @@
<script src="{{ url_for('static', filename='js/core/voice-alerts.js') }}?v={{ version }}&r=voicefix2"></script> <script src="{{ url_for('static', filename='js/core/voice-alerts.js') }}?v={{ version }}&r=voicefix2"></script>
<script src="{{ url_for('static', filename='js/core/keyboard-shortcuts.js') }}"></script> <script src="{{ url_for('static', filename='js/core/keyboard-shortcuts.js') }}"></script>
<script src="{{ url_for('static', filename='js/core/cheat-sheets.js') }}"></script> <script src="{{ url_for('static', filename='js/core/cheat-sheets.js') }}"></script>
<script src="{{ url_for('static', filename='js/modes/waterfall.js') }}?v={{ version }}&r=wfdeck20"></script> <script src="{{ url_for('static', filename='js/modes/waterfall.js') }}?v={{ version }}&r=wfdeck21"></script>
<script> <script>
// ============================================ // ============================================