Fix 6 bugs: remove dead listener code from DJ panel, fix StreamingWorker socket reuse, fix GUI-blocking time.sleep, fix abort import

- script.js: Remove ~500 lines of dead listener mode code (initListenerMode, enableListenerAudio, setListenerVolume, startListenerVUMeter, getMp3FallbackUrl, listener variables). Listener page now uses listener.js exclusively.
- script.js: Remove ?listen=true detection from DOMContentLoaded that could activate broken listener UI on DJ panel.
- script.js: Clean up initSocket() to remove dead listener mode detection logic.
- index.html: Remove dead #listener-mode div (now served by listener.html).
- server.py: Move 'abort' import to top-level Flask import instead of per-request import.
- techdj_qt.py: Fix StreamingWorker to create fresh socketio.Client on each streaming session, preventing stale socket state on reconnect.
- techdj_qt.py: Fix time.sleep(0.2) blocking GUI thread in stop_streaming() by removing it and using try/except for clean disconnect.
This commit is contained in:
ComputerTech 2026-03-09 19:16:42 +00:00
parent 7c33c678aa
commit abf907ddfb
4 changed files with 23 additions and 552 deletions

View File

@ -420,37 +420,6 @@
</div>
</div>
<!-- Listener Mode (Hidden by default) -->
<div class="listener-mode" id="listener-mode" style="display: none;">
<div class="listener-header">
<h1>TECHDJ LIVE</h1>
<div class="live-indicator">
<span class="pulse-dot"></span>
<span>LIVE</span>
</div>
</div>
<div class="listener-content">
<div class="now-playing" id="listener-now-playing">Waiting for stream...</div>
<canvas id="viz-listener" width="400" height="100"></canvas>
<!-- Enable Audio Button (shown when autoplay is blocked) -->
<button class="enable-audio-btn" id="enable-audio-btn" style="display: none;"
onclick="enableListenerAudio()">
<span class="audio-icon"></span>
<span class="audio-text">ENABLE AUDIO</span>
<span class="audio-subtitle">Click to start listening</span>
</button>
<div class="volume-control">
<label>Volume</label>
<input type="range" id="listener-volume" min="0" max="100" value="80"
oninput="setListenerVolume(this.value)">
</div>
<div class="connection-status" id="connection-status">Connecting...</div>
</div>
</div>
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
<script src="script.js"></script>

512
script.js
View File

@ -1816,23 +1816,12 @@ window.addEventListener('DOMContentLoaded', () => {
updateManualGlow('A', settings.glowA);
updateManualGlow('B', settings.glowB);
// Check if this is the listener page based on hostname or port
const isListenerPort = window.location.port === '5001';
const isListenerHostname = window.location.hostname.startsWith('music.') || window.location.hostname.startsWith('listen.');
const urlParams = new URLSearchParams(window.location.search);
if (isListenerPort || isListenerHostname || urlParams.get('listen') === 'true') {
initListenerMode();
}
if (!isListenerPort && !isListenerHostname) {
// Set stream URL to the listener domain
const streamUrl = window.location.hostname.startsWith('dj.')
? `${window.location.protocol}//music.${window.location.hostname.split('.').slice(1).join('.')}`
: `${window.location.protocol}//${window.location.hostname}:5001`;
const streamInput = document.getElementById('stream-url');
if (streamInput) streamInput.value = streamUrl;
}
// Set stream URL in the streaming panel
const streamUrl = window.location.hostname.startsWith('dj.')
? `${window.location.protocol}//music.${window.location.hostname.split('.').slice(1).join('.')}`
: `${window.location.protocol}//${window.location.hostname}:5001`;
const streamInput = document.getElementById('stream-url');
if (streamInput) streamInput.value = streamUrl;
});
// ========== LIVE STREAMING FUNCTIONALITY ==========
@ -1843,94 +1832,13 @@ let streamProcessor = null;
let mediaRecorder = null;
let isBroadcasting = false;
let autoStartStream = false;
let listenerAudioContext = null;
let listenerGainNode = null;
let listenerAnalyserNode = null;
let listenerMediaElementSourceNode = null;
let listenerVuMeterRunning = false;
let listenerChunksReceived = 0;
function startListenerVUMeter() {
if (listenerVuMeterRunning) return;
listenerVuMeterRunning = true;
const draw = () => {
if (!listenerVuMeterRunning) return;
requestAnimationFrame(draw);
const canvas = document.getElementById('viz-listener');
if (!canvas || !listenerAnalyserNode) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
// Keep canvas sized correctly for DPI
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
const targetW = Math.max(1, Math.floor(rect.width * dpr));
const targetH = Math.max(1, Math.floor(rect.height * dpr));
if (canvas.width !== targetW || canvas.height !== targetH) {
canvas.width = targetW;
canvas.height = targetH;
}
const analyser = listenerAnalyserNode;
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
analyser.getByteFrequencyData(dataArray);
const width = canvas.width;
const height = canvas.height;
const barCount = 32;
const barWidth = width / barCount;
ctx.fillStyle = '#0a0a12';
ctx.fillRect(0, 0, width, height);
// Listener uses the magenta hue (matches Deck B styling)
const hue = 280;
for (let i = 0; i < barCount; i++) {
const freqIndex = Math.floor(Math.pow(i / barCount, 1.5) * bufferLength);
const value = (dataArray[freqIndex] || 0) / 255;
const barHeight = value * height;
const lightness = 30 + (value * 50);
const gradient = ctx.createLinearGradient(0, height, 0, height - barHeight);
gradient.addColorStop(0, `hsl(${hue}, 100%, ${lightness}%)`);
gradient.addColorStop(1, `hsl(${hue}, 100%, ${Math.min(lightness + 20, 80)}%)`);
ctx.fillStyle = gradient;
ctx.fillRect(i * barWidth, height - barHeight, barWidth - 2, barHeight);
}
};
draw();
}
let currentStreamMimeType = null;
function getMp3FallbackUrl() {
// Use same-origin so this works behind reverse proxies (e.g., Cloudflare) where :5001 may not be reachable.
return `${window.location.origin}/stream.mp3`;
}
// Initialise SocketIO connection
function initSocket() {
if (socket) return socket;
// Log connection details
const urlParams = new URLSearchParams(window.location.search);
const isListenerMode =
window.location.port === '5001' ||
window.location.hostname.startsWith('music.') ||
window.location.hostname.startsWith('listen.') ||
urlParams.get('listen') === 'true';
// If someone opens listener mode on the DJ dev port (:5000?listen=true),
// use the listener backend (:5001). For proxied deployments (Cloudflare),
// do NOT force a port (it may be blocked); stick to same-origin.
const serverUrl = (isListenerMode && window.location.port === '5000')
? `${window.location.protocol}//${window.location.hostname}:5001`
: window.location.origin;
const serverUrl = window.location.origin;
console.log(`CONNECT Initializing Socket.IO connection to: ${serverUrl}`);
console.log(` Protocol: ${window.location.protocol}`);
console.log(` Host: ${window.location.host}`);
@ -2479,412 +2387,6 @@ function toggleAutoStream(enabled) {
localStorage.setItem('autoStartStream', enabled);
}
// ========== LISTENER MODE ==========
function initListenerMode() {
console.log('STREAMPANEL Initializing listener mode (MP3 stream)...');
// UI Feedback for listener
const appContainer = document.querySelector('.app-container');
const settingsBtn = document.querySelector('.settings-btn');
const streamingBtn = document.querySelector('.streaming-btn');
if (appContainer) appContainer.style.display = 'none';
if (settingsBtn) settingsBtn.style.display = 'none';
if (streamingBtn) streamingBtn.style.display = 'none';
const startOverlay = document.getElementById('start-overlay');
if (startOverlay) startOverlay.style.display = 'none';
// Hide landscape prompt for listeners (not needed)
const landscapePrompt = document.getElementById('landscape-prompt');
if (landscapePrompt) landscapePrompt.style.display = 'none';
const listenerMode = document.getElementById('listener-mode');
if (listenerMode) listenerMode.style.display = 'flex';
// Add atmospheric glow for listeners
document.body.classList.add('listener-glow');
document.body.classList.add('listening-active'); // For global CSS targeting
updateGlowIntensity(settings.glowIntensity || 30);
// AudioContext will be created when user enables audio to avoid suspension
// ALWAYS create a fresh audio element to avoid MediaSource/MediaElementSource conflicts
// This is critical for page refreshes - you can only create MediaElementSource once per element
let audio;
// Clean up old audio element if it exists
if (window.listenerAudio) {
console.log('CLEAN Cleaning up old audio element and AudioContext nodes');
try {
window.listenerAudio.pause();
if (window.listenerAudio.src) {
URL.revokeObjectURL(window.listenerAudio.src);
}
window.listenerAudio.removeAttribute('src');
window.listenerAudio.remove(); // Remove from DOM
} catch (e) {
console.warn('Error cleaning up old audio:', e);
}
// Reset all AudioContext-related nodes
if (listenerMediaElementSourceNode) {
try {
listenerMediaElementSourceNode.disconnect();
} catch (e) { }
listenerMediaElementSourceNode = null;
}
if (listenerAnalyserNode) {
try {
listenerAnalyserNode.disconnect();
} catch (e) { }
listenerAnalyserNode = null;
}
if (listenerGainNode) {
try {
listenerGainNode.disconnect();
} catch (e) { }
listenerGainNode = null;
}
window.listenerAudio = null;
window.listenerMediaSource = null;
window.listenerAudioEnabled = false;
}
// Create a new hidden media element.
// For MP3 we can use a plain <audio> element.
audio = document.createElement('audio');
audio.autoplay = false; // Don't autoplay - we use the Enable Audio button
audio.muted = false;
audio.controls = false;
audio.playsInline = true;
audio.setAttribute('playsinline', '');
audio.style.display = 'none';
audio.crossOrigin = 'anonymous'; // Helps with certain browser stream policies
document.body.appendChild(audio);
console.log('NEW Created fresh media element (audio) for listener');
// Stall Watchdog Variables
let lastCheckedTime = 0;
let stallCount = 0;
let watchdogInterval = null;
const stopWatchdog = () => {
if (watchdogInterval) {
clearInterval(watchdogInterval);
watchdogInterval = null;
}
};
const startWatchdog = () => {
stopWatchdog();
lastCheckedTime = audio.currentTime;
stallCount = 0;
watchdogInterval = setInterval(() => {
if (!window.listenerAudioEnabled || audio.paused) return;
if (audio.currentTime === lastCheckedTime && audio.currentTime > 0) {
stallCount++;
console.warn(`[WARN] Stream stall detected (${stallCount}/3)...`);
if (stallCount >= 3) { // 3 cycles * 2s = 6s of stalling
console.error('ALERT Stream is completely stalled. Force reconnecting...');
reconnectStream();
stallCount = 0;
}
} else {
stallCount = 0;
}
lastCheckedTime = audio.currentTime;
}, 2000);
};
const reconnectStream = () => {
if (!window.listenerAudioEnabled || !window.listenerAudio) return;
console.log('RECONNECTING Reconnecting stream...');
const statusEl = document.getElementById('connection-status');
if (statusEl) {
statusEl.textContent = '[WAIT] Connection weak - Reconnecting...';
statusEl.classList.remove('glow-text');
}
const wasPaused = window.listenerAudio.paused;
// Bust cache with timestamp
window.listenerAudio.src = getMp3FallbackUrl() + '?t=' + Date.now();
window.listenerAudio.load();
if (!wasPaused) {
window.listenerAudio.play()
.then(() => {
if (statusEl) {
statusEl.textContent = '[ACTIVE] Reconnected';
statusEl.classList.add('glow-text');
}
// Re-sync visualiser
startListenerVUMeter();
})
.catch(e => console.warn('Reconnect play failed:', e));
}
};
// MP3 stream (server-side) - requires ffmpeg on the server.
audio.src = getMp3FallbackUrl();
audio.load();
console.log(`STREAMPANEL Listener source set to MP3 stream: ${audio.src}`);
// Auto-reconnect logic if stream errors out
audio.onerror = () => {
if (!window.listenerAudioEnabled) return;
console.error('[ERROR] Audio stream error!');
reconnectStream();
};
audio.onplay = () => {
console.log('PLAY Stream playing');
startWatchdog();
const statusEl = document.getElementById('connection-status');
if (statusEl) statusEl.classList.add('glow-text');
};
audio.onpause = () => {
console.log('PAUSE Stream paused');
stopWatchdog();
};
// Show enable audio button instead of attempting autoplay
const enableAudioBtn = document.getElementById('enable-audio-btn');
const statusEl = document.getElementById('connection-status');
if (enableAudioBtn) {
enableAudioBtn.style.display = 'flex';
}
if (statusEl) {
statusEl.textContent = '[INFO] Click "Enable Audio" to start listening (MP3)';
}
// Store audio element and context for later activation
window.listenerAudio = audio;
window.listenerMediaSource = null;
window.listenerAudioEnabled = false; // Track if user has enabled audio
// Initialise socket and join
initSocket();
socket.emit('join_listener');
// No socket audio chunks needed in MP3-only mode.
socket.on('broadcast_started', () => {
const nowPlayingEl = document.getElementById('listener-now-playing');
if (nowPlayingEl) nowPlayingEl.textContent = 'Stream is live!';
// Force a reload of the audio element to capture the fresh stream
if (window.listenerAudio) {
console.log('Broadcast started: Refreshing audio stream...');
const wasPlaying = !window.listenerAudio.paused;
window.listenerAudio.src = getMp3FallbackUrl();
window.listenerAudio.load();
if (wasPlaying || window.listenerAudioEnabled) {
window.listenerAudio.play().catch(e => console.warn('Auto-play after refresh blocked:', e));
}
}
});
socket.on('stream_status', (data) => {
const nowPlayingEl = document.getElementById('listener-now-playing');
if (nowPlayingEl) {
if (data.active) {
const status = data.remote_relay ? 'Remote stream is live!' : 'DJ stream is live!';
nowPlayingEl.textContent = status;
} else {
nowPlayingEl.textContent = 'Stream offline - waiting for DJ...';
}
}
});
socket.on('broadcast_stopped', () => {
const nowPlayingEl = document.getElementById('listener-now-playing');
if (nowPlayingEl) nowPlayingEl.textContent = 'Stream ended';
});
socket.on('connect', () => {
const statusEl = document.getElementById('connection-status');
// Only update if audio is enabled, otherwise keep the "Click Enable Audio" message
if (statusEl && window.listenerAudioEnabled) {
statusEl.textContent = '[ACTIVE] Connected';
}
// Re-join as listener on reconnect so server tracks us
socket.emit('join_listener');
});
socket.on('disconnect', () => {
const statusEl = document.getElementById('connection-status');
// Always show disconnect status as it's critical
if (statusEl) statusEl.textContent = '[OFFLINE] Disconnected';
});
}
// Enable audio for listener mode (called when user clicks the button)
async function enableListenerAudio() {
console.log('STREAMPANEL Enabling audio via user gesture...');
const enableAudioBtn = document.getElementById('enable-audio-btn');
const statusEl = document.getElementById('connection-status');
const audioText = enableAudioBtn ? enableAudioBtn.querySelector('.audio-text') : null;
if (audioText) audioText.textContent = 'INITIALIZING...';
try {
// 1. Create AudioContext if it somehow doesn't exist
if (!listenerAudioContext) {
listenerAudioContext = new (window.AudioContext || window.webkitAudioContext)();
}
// 2. Resume audio context (CRITICAL for Chrome/Safari)
if (listenerAudioContext.state === 'suspended') {
await listenerAudioContext.resume();
console.log('[OK] Audio context resumed');
}
// 3. Bridge Audio Element to AudioContext if not already connected
if (window.listenerAudio) {
try {
if (!listenerGainNode) {
listenerGainNode = listenerAudioContext.createGain();
listenerGainNode.gain.value = 0.8;
listenerGainNode.connect(listenerAudioContext.destination);
}
if (!listenerAnalyserNode) {
listenerAnalyserNode = listenerAudioContext.createAnalyser();
listenerAnalyserNode.fftSize = 256;
}
if (!listenerMediaElementSourceNode) {
listenerMediaElementSourceNode = listenerAudioContext.createMediaElementSource(window.listenerAudio);
}
// Ensure a clean, single connection chain:
// media element -> analyser -> gain -> destination
try { listenerMediaElementSourceNode.disconnect(); } catch (_) { }
try { listenerAnalyserNode.disconnect(); } catch (_) { }
listenerMediaElementSourceNode.connect(listenerAnalyserNode);
listenerAnalyserNode.connect(listenerGainNode);
window.listenerAudio._connectedToContext = true;
console.log('Connected audio element to AudioContext (with analyser)');
// Start visualiser after the graph exists
startListenerVUMeter();
} catch (e) {
console.warn('Could not connect to AudioContext:', e.message);
}
}
// 4. Prepare and start audio playback
if (window.listenerAudio) {
console.log('[DATA] Audio element state:', {
readyState: window.listenerAudio.readyState,
networkState: window.listenerAudio.networkState,
src: window.listenerAudio.src ? 'set' : 'not set',
buffered: window.listenerAudio.buffered.length,
paused: window.listenerAudio.paused
});
// Unmute just in case
window.listenerAudio.muted = false;
// Volume is controlled via listenerGainNode; keep element volume sane.
window.listenerAudio.volume = 1.0;
const volEl = document.getElementById('listener-volume');
const volValue = volEl ? parseInt(volEl.value, 10) : 80;
setListenerVolume(Number.isFinite(volValue) ? volValue : 80);
const hasBufferedData = () => {
return window.listenerAudio.buffered && window.listenerAudio.buffered.length > 0;
};
// Mark audio as enabled immediately so reconnection logic is active
window.listenerAudioEnabled = true;
// MP3 stream: call play() immediately to capture the user gesture.
if (audioText) audioText.textContent = 'STARTING...';
console.log('Attempting to play audio...');
// Create a timeout for the play promise to prevent indefinite 'buffering' state
const playTimeout = setTimeout(() => {
if (!hasBufferedData()) {
console.warn('[WARN] Audio play is taking a long time (buffering)...');
if (audioText) audioText.textContent = 'STILL BUFFERING...';
// Trigger a load() to nudge the browser if it's stuck
window.listenerAudio.load();
}
}, 8000);
const playPromise = window.listenerAudio.play();
// If not buffered yet, show buffering but don't block.
if (!hasBufferedData() && audioText) {
audioText.textContent = 'BUFFERING...';
}
try {
await playPromise;
clearTimeout(playTimeout);
console.log('[OK] Audio playback started successfully');
} catch (e) {
clearTimeout(playTimeout);
throw e; // Re-throw to be caught by the outer catch block
}
}
// 4. Hide the button and update status
if (enableAudioBtn) {
enableAudioBtn.style.opacity = '0';
setTimeout(() => {
enableAudioBtn.style.display = 'none';
}, 300);
}
if (statusEl) {
statusEl.textContent = '[ACTIVE] Audio Active - Enjoy the stream';
statusEl.classList.add('glow-text');
}
} catch (error) {
console.error('[ERROR] Failed to enable audio:', error);
const stashedStatus = document.getElementById('connection-status');
const stashedBtn = document.getElementById('enable-audio-btn');
const audioText = stashedBtn ? stashedBtn.querySelector('.audio-text') : null;
if (audioText) audioText.textContent = 'RETRY ENABLE';
if (stashedStatus) {
// Show the actual error message to help debugging
let errorMsg = error.name + ': ' + error.message;
if (error.name === 'NotAllowedError') {
errorMsg = 'Browser blocked audio (NotAllowedError). Check permissions.';
} else if (error.name === 'NotSupportedError') {
errorMsg = 'MP3 stream not supported or unavailable (NotSupportedError).';
}
stashedStatus.textContent = '' + errorMsg;
if (error.name === 'NotSupportedError') {
stashedStatus.textContent = 'MP3 stream failed. Is ffmpeg installed on the server?';
}
}
}
}
function setListenerVolume(value) {
if (listenerGainNode) {
listenerGainNode.gain.value = value / 100;
}
}
// Load auto-start preference
window.addEventListener('load', () => {
const autoStart = localStorage.getItem('autoStartStream');

View File

@ -10,7 +10,7 @@ import threading
import queue
import time
import collections
from flask import Flask, send_from_directory, jsonify, request, session, Response, stream_with_context
from flask import Flask, send_from_directory, jsonify, request, session, Response, stream_with_context, abort
from flask_socketio import SocketIO, emit
from dotenv import load_dotenv
# Load environment variables from .env file
@ -344,13 +344,11 @@ def setup_shared_routes(app, index_file='index.html'):
def serve_static(filename):
# Prevent path traversal
if '..' in filename or filename.startswith('/'):
from flask import abort
abort(403)
# Allow only known safe static file extensions
allowed_extensions = ('.css', '.js', '.html', '.htm', '.png', '.jpg', '.jpeg',
'.gif', '.svg', '.ico', '.woff', '.woff2', '.ttf', '.eot', '.map')
if not filename.endswith(allowed_extensions):
from flask import abort
abort(403)
response = send_from_directory('.', filename)
if filename.endswith(('.css', '.js', '.html')):
@ -679,12 +677,10 @@ def _restrict_listener_routes():
"""Prevent listeners from accessing DJ-only endpoints and files."""
blocked_paths = ('/update_settings', '/upload', '/save_keymaps', '/browse_directories')
if request.path in blocked_paths:
from flask import abort
abort(403)
# Block DJ-only files — prevents serving the DJ panel even via direct URL
dj_only_files = ('/index.html', '/script.js', '/style.css')
if request.path in dj_only_files:
from flask import abort
abort(403)
listener_socketio = SocketIO(
listener_app,

View File

@ -668,16 +668,10 @@ class StreamingWorker(QThread):
def __init__(self, parent=None):
super().__init__(parent)
self.sio = socketio.Client()
self.sio = None
self.stream_url = ""
self.is_running = False
self.ffmpeg_proc = None
# Socket.IO event handlers
self.sio.on('connect', self.on_connect)
self.sio.on('disconnect', self.on_disconnect)
self.sio.on('listener_count', self.on_listener_count)
self.sio.on('connect_error', self.on_connect_error)
def on_connect(self):
print("[SOCKET] Connected to DJ server")
@ -695,6 +689,13 @@ class StreamingWorker(QThread):
def run(self):
try:
# Create a fresh Socket.IO client for each session to avoid stale state
self.sio = socketio.Client()
self.sio.on('connect', self.on_connect)
self.sio.on('disconnect', self.on_disconnect)
self.sio.on('listener_count', self.on_listener_count)
self.sio.on('connect_error', self.on_connect_error)
# Connect to socket
self.sio.connect(self.stream_url)
@ -737,10 +738,13 @@ class StreamingWorker(QThread):
try: self.ffmpeg_proc.terminate()
except: pass
self.ffmpeg_proc = None
if self.sio.connected:
self.sio.emit('stop_broadcast')
time.sleep(0.2)
self.sio.disconnect()
if self.sio and self.sio.connected:
try:
self.sio.emit('stop_broadcast')
self.sio.disconnect()
except Exception:
pass
self.sio = None
# --- WIDGETS ---