Stability fixes: loop end-of-track guard, DJ disconnect grace period, StreamingWorker race condition fix, various audit cleanups

This commit is contained in:
ComputerTech 2026-03-10 18:53:17 +00:00
parent 8c3c2613b1
commit dfccec2b48
10 changed files with 484 additions and 3411 deletions

View File

@ -1,95 +0,0 @@
#!/usr/bin/env python3
"""
Quick memory usage comparison script
Run this to see the difference between web and native versions
"""
import subprocess
import time
def get_process_memory(process_name):
"""Get memory usage of a process in MB"""
try:
result = subprocess.run(
['ps', 'aux'],
capture_output=True,
text=True
)
total_mem = 0
count = 0
for line in result.stdout.split('\n'):
if process_name.lower() in line.lower():
parts = line.split()
if len(parts) > 5:
# RSS is in KB, convert to MB
mem_kb = float(parts[5])
total_mem += mem_kb / 1024
count += 1
return total_mem, count
except Exception as e:
return 0, 0
def main():
print("=" * 60)
print("TechDJ Memory Usage Comparison")
print("=" * 60)
print()
# Check Chrome
chrome_mem, chrome_procs = get_process_memory('chrome')
if chrome_mem > 0:
print(f"[CHROME] Chrome (Web Panel):")
print(f" Total Memory: {chrome_mem:.1f} MB")
print(f" Processes: {chrome_procs}")
print()
else:
print("[CHROME] Chrome: Not running")
print()
# Check PyQt6
qt_mem, qt_procs = get_process_memory('techdj_qt')
if qt_mem > 0:
print(f"[PYQT6] PyQt6 Native App:")
print(f" Total Memory: {qt_mem:.1f} MB")
print(f" Processes: {qt_procs}")
print()
else:
print("[PYQT6] PyQt6 Native App: Not running")
print()
# Comparison
if chrome_mem > 0 and qt_mem > 0:
savings = chrome_mem - qt_mem
percent = (savings / chrome_mem) * 100
print("=" * 60)
print("[STATS] Comparison:")
print(f" Memory Saved: {savings:.1f} MB ({percent:.1f}%)")
print()
# Visual bar chart
max_mem = max(chrome_mem, qt_mem)
chrome_bar = '#' * int((chrome_mem / max_mem) * 40)
qt_bar = '#' * int((qt_mem / max_mem) * 40)
print(" Chrome: " + chrome_bar + f" {chrome_mem:.0f}MB")
print(" PyQt6: " + qt_bar + f" {qt_mem:.0f}MB")
print()
if percent > 50:
print(f" [OK] PyQt6 uses {percent:.0f}% less memory!")
elif percent > 25:
print(f" [OK] PyQt6 uses {percent:.0f}% less memory")
else:
print(f" PyQt6 uses {percent:.0f}% less memory")
print("=" * 60)
print()
print("Tip: Run both versions and execute this script to compare!")
print()
if __name__ == '__main__':
main()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 705 KiB

View File

@ -1,162 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Edge Glow Test</title>
<style>
body {
margin: 0;
padding: 0;
background: #0a0a14;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
font-family: Arial, sans-serif;
}
/* EDGE GLOW - EXACT COPY FROM style.css lines 28-61 */
body::before {
content: '';
position: fixed;
inset: 0;
pointer-events: none;
z-index: 99999;
border: 3px solid rgba(80, 80, 80, 0.3);
}
body.playing-A::before {
border: 15px solid #00f3ff;
box-shadow:
0 0 80px rgba(0, 243, 255, 1),
inset 0 0 80px rgba(0, 243, 255, 0.8);
animation: pulse-cyan 3s ease-in-out infinite;
}
body.playing-B::before {
border: 15px solid #bc13fe;
box-shadow:
0 0 80px rgba(188, 19, 254, 1),
inset 0 0 80px rgba(188, 19, 254, 0.8);
animation: pulse-magenta 3s ease-in-out infinite;
}
body.playing-A.playing-B::before {
border: 15px solid #00f3ff;
box-shadow:
0 0 80px rgba(0, 243, 255, 1),
0 0 120px rgba(188, 19, 254, 1),
inset 0 0 80px rgba(0, 243, 255, 0.6),
inset 0 0 120px rgba(188, 19, 254, 0.6);
animation: pulse-both 3s ease-in-out infinite;
}
@keyframes pulse-cyan {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
@keyframes pulse-magenta {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
@keyframes pulse-both {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.8;
}
}
.controls {
text-align: center;
z-index: 100000;
}
button {
padding: 20px 40px;
margin: 10px;
font-size: 18px;
cursor: pointer;
border: 2px solid #fff;
background: rgba(255, 255, 255, 0.1);
color: #fff;
border-radius: 8px;
}
button:hover {
background: rgba(255, 255, 255, 0.2);
}
.status {
color: #fff;
margin-top: 20px;
font-size: 14px;
}
</style>
</head>
<body>
<div class="controls">
<h1 style="color: #fff;">Edge Glow Test</h1>
<button onclick="testDeckA()">Test Deck A (Cyan)</button>
<button onclick="testDeckB()">Test Deck B (Magenta)</button>
<button onclick="testBoth()">Test Both</button>
<button onclick="testOff()">Turn Off</button>
<div class="status" id="status">Status: No glow</div>
</div>
<script>
function testDeckA() {
document.body.className = 'playing-A';
document.getElementById('status').textContent = 'Status: Deck A (Cyan) - body class: ' + document.body.className;
console.log('Body classes:', document.body.classList.toString());
}
function testDeckB() {
document.body.className = 'playing-B';
document.getElementById('status').textContent = 'Status: Deck B (Magenta) - body class: ' + document.body.className;
console.log('Body classes:', document.body.classList.toString());
}
function testBoth() {
document.body.className = 'playing-A playing-B';
document.getElementById('status').textContent = 'Status: Both (Cyan + Magenta) - body class: ' + document.body.className;
console.log('Body classes:', document.body.classList.toString());
}
function testOff() {
document.body.className = '';
document.getElementById('status').textContent = 'Status: No glow - body class: (empty)';
console.log('Body classes:', document.body.classList.toString());
}
// Auto-test on load
setTimeout(() => {
console.log('=== EDGE GLOW TEST ===');
console.log('If you see a CYAN border around the screen, the glow is working!');
testDeckA();
}, 500);
</script>
</body>
</html>

View File

@ -268,6 +268,7 @@ body.listener-glow::before {
align-items: center;
justify-content: center;
gap: 10px;
width: 100%;
padding: 30px 40px;
margin: 30px 0;
background: linear-gradient(145deg, #1a1a1a, #0a0a0a);
@ -361,3 +362,19 @@ body.listener-glow::before {
margin: 15px 0;
}
}
/* ========== Listener Count Badge ========== */
.listener-count-badge {
font-family: 'Rajdhani', sans-serif;
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.7);
margin-left: 8px;
padding-left: 10px;
border-left: 1px solid rgba(188, 19, 254, 0.4);
}
.listener-count-badge #listener-count {
color: var(--secondary-magenta, #bc13fe);
font-weight: 700;
}

View File

@ -5,7 +5,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>TechDJ Live</title>
<link rel="stylesheet" href="listener.css?v=1.0">
<link rel="stylesheet" href="listener.css?v=2.0">
</head>
<body class="listener-glow listening-active">
@ -16,6 +16,7 @@
<div class="live-indicator">
<span class="pulse-dot"></span>
<span>LIVE</span>
<span class="listener-count-badge"><span id="listener-count">0</span> listening</span>
</div>
</div>
<div class="listener-content">
@ -25,7 +26,7 @@
<!-- Enable Audio Button (shown when autoplay is blocked) -->
<button class="enable-audio-btn" id="enable-audio-btn" onclick="enableListenerAudio()">
<span class="audio-icon"></span>
<span class="audio-icon">🎧</span>
<span class="audio-text">ENABLE AUDIO</span>
<span class="audio-subtitle">Click to start listening</span>
</button>
@ -40,7 +41,7 @@
</div>
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
<script src="listener.js?v=1.0"></script>
<script src="listener.js?v=2.0"></script>
</body>
</html>

View File

@ -1,9 +1,17 @@
// ========== TechDJ Listener ==========
// Standalone listener script — no DJ panel code loaded.
//
// State machine:
// Page load → OFFLINE (waiting for Socket.IO stream_status)
// broadcast_started / stream_status(active) → LIVE
// User clicks Enable Audio + LIVE → CONNECTING → PLAYING
// User clicks Enable Audio + OFFLINE → WAITING (auto-connects when LIVE)
// Stream error / stall while LIVE → RECONNECTING (exponential backoff)
// broadcast_stopped → OFFLINE (pause audio, stop watchdog, stop reconnects)
'use strict';
// --- State ---
// --- Core State ---
let socket = null;
let listenerAudioContext = null;
let listenerGainNode = null;
@ -11,10 +19,21 @@ let listenerAnalyserNode = null;
let listenerMediaElementSourceNode = null;
let listenerVuMeterRunning = false;
// Broadcast & reconnection state
let broadcastActive = false;
let reconnectAttempts = 0;
let reconnectTimer = null;
const MAX_RECONNECT_DELAY = 30000; // 30 s cap
const BASE_RECONNECT_DELAY = 2000; // Start at 2 s
// Stall watchdog state
let stallWatchdogInterval = null;
let lastWatchdogTime = 0;
let stallCount = 0;
// --- Helpers ---
function getMp3FallbackUrl() {
// Use same-origin so this works behind reverse proxies (e.g. Cloudflare)
function getMp3StreamUrl() {
return `${window.location.origin}/stream.mp3`;
}
@ -25,6 +44,56 @@ function updateGlowIntensity(val) {
document.documentElement.style.setProperty('--glow-spread', `${spread}px`);
}
function updateStatus(text, isActive) {
const el = document.getElementById('connection-status');
if (!el) return;
el.textContent = text;
el.classList.toggle('glow-text', !!isActive);
}
function updateNowPlaying(text) {
const el = document.getElementById('listener-now-playing');
if (el) el.textContent = text;
}
function updateListenerCount(count) {
const el = document.getElementById('listener-count');
if (el) el.textContent = count;
}
// --- Reconnection with Exponential Backoff ---
function scheduleReconnect() {
if (reconnectTimer) return; // Already scheduled
if (!broadcastActive) return;
if (!window.listenerAudioEnabled) return;
reconnectAttempts++;
const delay = Math.min(
BASE_RECONNECT_DELAY * Math.pow(1.5, reconnectAttempts - 1),
MAX_RECONNECT_DELAY
);
console.log(`[RECONNECT] Attempt ${reconnectAttempts} in ${(delay / 1000).toFixed(1)}s`);
updateStatus(`Reconnecting in ${Math.ceil(delay / 1000)}s...`, false);
reconnectTimer = setTimeout(() => {
reconnectTimer = null;
connectStream();
}, delay);
}
function cancelScheduledReconnect() {
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
}
function resetReconnectBackoff() {
reconnectAttempts = 0;
cancelScheduledReconnect();
}
// --- VU Meter ---
function startListenerVUMeter() {
@ -41,7 +110,7 @@ function startListenerVUMeter() {
const ctx = canvas.getContext('2d');
if (!ctx) return;
// Keep canvas sized correctly for DPI
// DPI-aware canvas sizing
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
const targetW = Math.max(1, Math.floor(rect.width * dpr));
@ -51,10 +120,9 @@ function startListenerVUMeter() {
canvas.height = targetH;
}
const analyser = listenerAnalyserNode;
const bufferLength = analyser.frequencyBinCount;
const bufferLength = listenerAnalyserNode.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
analyser.getByteFrequencyData(dataArray);
listenerAnalyserNode.getByteFrequencyData(dataArray);
const width = canvas.width;
const height = canvas.height;
@ -64,8 +132,7 @@ function startListenerVUMeter() {
ctx.fillStyle = '#0a0a12';
ctx.fillRect(0, 0, width, height);
// Magenta hue (matches Deck B styling)
const hue = 280;
const hue = 280; // Magenta
for (let i = 0; i < barCount; i++) {
const freqIndex = Math.floor(Math.pow(i / barCount, 1.5) * bufferLength);
const value = (dataArray[freqIndex] || 0) / 255;
@ -84,13 +151,101 @@ function startListenerVUMeter() {
draw();
}
function stopListenerVUMeter() {
listenerVuMeterRunning = false;
const canvas = document.getElementById('viz-listener');
if (canvas) {
const ctx = canvas.getContext('2d');
if (ctx) ctx.clearRect(0, 0, canvas.width, canvas.height);
}
}
// --- Stall Watchdog ---
function startStallWatchdog() {
stopStallWatchdog();
if (!window.listenerAudio) return;
lastWatchdogTime = window.listenerAudio.currentTime;
stallCount = 0;
stallWatchdogInterval = setInterval(() => {
if (!window.listenerAudioEnabled || !window.listenerAudio) return;
if (window.listenerAudio.paused) return;
if (!broadcastActive) {
stopStallWatchdog();
return;
}
const currentTime = window.listenerAudio.currentTime;
if (currentTime === lastWatchdogTime && currentTime > 0) {
stallCount++;
console.warn(`[STALL] Stream stall detected (${stallCount}/5)`);
if (stallCount >= 5) { // 10 seconds of stall
console.error('[STALL] Stream unrecoverable — reconnecting');
stopStallWatchdog();
scheduleReconnect();
}
} else {
stallCount = 0;
}
lastWatchdogTime = currentTime;
}, 2000);
}
function stopStallWatchdog() {
if (stallWatchdogInterval) {
clearInterval(stallWatchdogInterval);
stallWatchdogInterval = null;
}
stallCount = 0;
}
// --- Stream Connection ---
function connectStream() {
if (!window.listenerAudioEnabled || !window.listenerAudio) return;
cancelScheduledReconnect();
if (!broadcastActive) {
updateStatus('Waiting for stream to start...', false);
return;
}
console.log('[STREAM] Connecting to MP3 stream...');
updateStatus('Connecting...', false);
// Cache-bust to avoid stale/cached 503 responses
window.listenerAudio.src = getMp3StreamUrl() + '?t=' + Date.now();
window.listenerAudio.load();
window.listenerAudio.play()
.then(() => {
console.log('[OK] Stream playback started');
resetReconnectBackoff();
updateStatus('Audio Active — Enjoy the stream!', true);
startStallWatchdog();
startListenerVUMeter();
})
.catch(e => {
console.warn('[WARN] play() rejected:', e.name, e.message);
// If broadcast went offline while we were connecting, don't retry
if (!broadcastActive) {
updateStatus('Stream ended — waiting for DJ...', false);
return;
}
scheduleReconnect();
});
}
// --- Socket.IO ---
function initSocket() {
if (socket) return socket;
const serverUrl = window.location.origin;
console.log(`[LISTENER] Initializing Socket.IO connection to: ${serverUrl}`);
console.log(`[SOCKET] Connecting to ${serverUrl}`);
socket = io(serverUrl, {
transports: ['websocket', 'polling'],
@ -101,364 +256,275 @@ function initSocket() {
});
socket.on('connect', () => {
console.log('[OK] Connected to streaming server');
console.log('[SOCKET] Connected');
socket.emit('join_listener');
socket.emit('get_listener_count');
if (window.listenerAudioEnabled) {
updateStatus('Connected', true);
}
});
socket.on('connect_error', (error) => {
console.error('[ERROR] Connection error:', error.message);
console.error('[SOCKET] Connection error:', error.message);
});
socket.on('disconnect', (reason) => {
console.log(`[ERROR] Disconnected: ${reason}`);
console.warn(`[SOCKET] Disconnected: ${reason}`);
if (window.listenerAudioEnabled) {
updateStatus('Server disconnected — reconnecting...', false);
}
});
socket.on('listener_count', (data) => {
const el = document.getElementById('listener-count');
if (el) el.textContent = data.count;
updateListenerCount(data.count);
});
// --- Broadcast lifecycle events ---
socket.on('stream_status', (data) => {
const wasActive = broadcastActive;
broadcastActive = data.active;
if (data.active) {
updateNowPlaying('DJ stream is live!');
// If audio enabled and broadcast just came online, connect
if (!wasActive && window.listenerAudioEnabled) {
resetReconnectBackoff();
connectStream();
}
} else {
updateNowPlaying('Stream offline — waiting for DJ...');
handleBroadcastOffline();
}
});
socket.on('broadcast_started', () => {
console.log('[EVENT] broadcast_started');
broadcastActive = true;
updateNowPlaying('Stream is live!');
if (window.listenerAudioEnabled) {
// Small delay to let the transcoder produce initial data
resetReconnectBackoff();
setTimeout(() => connectStream(), 800);
}
});
socket.on('broadcast_stopped', () => {
console.log('[EVENT] broadcast_stopped');
broadcastActive = false;
updateNowPlaying('Stream ended');
handleBroadcastOffline();
});
return socket;
}
/** Clean up when broadcast goes offline. */
function handleBroadcastOffline() {
cancelScheduledReconnect();
stopStallWatchdog();
if (window.listenerAudioEnabled) {
updateStatus('Stream ended — waiting for DJ...', false);
}
// Pause audio (keep the element alive for when broadcast returns)
if (window.listenerAudio && !window.listenerAudio.paused) {
window.listenerAudio.pause();
}
}
// --- Audio Graph Cleanup ---
function cleanupAudioGraph() {
if (listenerMediaElementSourceNode) {
try { listenerMediaElementSourceNode.disconnect(); } catch (_) { }
listenerMediaElementSourceNode = null;
}
if (listenerAnalyserNode) {
try { listenerAnalyserNode.disconnect(); } catch (_) { }
listenerAnalyserNode = null;
}
if (listenerGainNode) {
try { listenerGainNode.disconnect(); } catch (_) { }
listenerGainNode = null;
}
}
// --- Listener Mode Init ---
function initListenerMode() {
console.log('[LISTENER] Initializing listener mode (MP3 stream)...');
// Apply glow
console.log('[INIT] TechDJ Listener starting...');
updateGlowIntensity(30);
// Clean up old audio element if it exists (e.g. page refresh)
// Clean up any leftover audio element (e.g. page refresh)
if (window.listenerAudio) {
console.log('[CLEAN] Cleaning up old audio element');
try {
window.listenerAudio.pause();
if (window.listenerAudio.src) {
URL.revokeObjectURL(window.listenerAudio.src);
}
window.listenerAudio.removeAttribute('src');
window.listenerAudio.load();
window.listenerAudio.remove();
} catch (e) {
console.warn('Error cleaning up old audio:', e);
}
if (listenerMediaElementSourceNode) {
try { listenerMediaElementSourceNode.disconnect(); } catch (_) { }
listenerMediaElementSourceNode = null;
}
if (listenerAnalyserNode) {
try { listenerAnalyserNode.disconnect(); } catch (_) { }
listenerAnalyserNode = null;
}
if (listenerGainNode) {
try { listenerGainNode.disconnect(); } catch (_) { }
listenerGainNode = null;
}
} catch (_) { }
cleanupAudioGraph();
window.listenerAudio = null;
window.listenerMediaSource = null;
window.listenerAudioEnabled = false;
}
// Create fresh audio element
stopStallWatchdog();
cancelScheduledReconnect();
stopListenerVUMeter();
// Create audio element — but do NOT set src yet.
// Setting src before the user clicks Enable Audio would fire a wasted
// request to /stream.mp3 that returns 503 if no broadcast is active.
const audio = document.createElement('audio');
audio.autoplay = false;
audio.muted = false;
audio.controls = false;
audio.playsInline = true;
audio.setAttribute('playsinline', '');
audio.style.display = 'none';
audio.crossOrigin = 'anonymous';
audio.preload = 'none';
audio.style.display = 'none';
document.body.appendChild(audio);
console.log('[NEW] Created fresh audio element for listener');
// --- Stall Watchdog ---
let lastCheckedTime = 0;
let stallCount = 0;
let watchdogInterval = null;
// --- Audio element event handlers ---
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) {
console.error('[ALERT] Stream stalled. Force reconnecting...');
reconnectStream();
stallCount = 0;
}
} else {
stallCount = 0;
}
lastCheckedTime = audio.currentTime;
}, 2000);
};
const reconnectStream = () => {
if (!window.listenerAudioEnabled || !window.listenerAudio) return;
console.log('[RECONNECT] 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;
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');
}
startListenerVUMeter();
})
.catch(e => console.warn('Reconnect play failed:', e));
}
};
// Set MP3 stream source
audio.src = getMp3FallbackUrl();
audio.load();
console.log(`[STREAM] Listener source set to MP3 stream: ${audio.src}`);
// Auto-reconnect on stream error
audio.onerror = () => {
if (!window.listenerAudioEnabled) return;
console.error('[ERROR] Audio stream error!');
reconnectStream();
const err = audio.error;
console.error(`[AUDIO] Error: code=${err?.code}, msg=${err?.message}`);
if (broadcastActive) {
scheduleReconnect();
} else {
updateStatus('Stream offline', false);
}
};
audio.onplay = () => {
console.log('[PLAY] Stream playing');
startWatchdog();
const statusEl = document.getElementById('connection-status');
if (statusEl) statusEl.classList.add('glow-text');
startStallWatchdog();
if (window.listenerAudioEnabled && broadcastActive) {
updateStatus('Audio Active — Enjoy the stream!', true);
}
};
audio.onpause = () => {
console.log('[PAUSE] Stream paused');
stopWatchdog();
stopStallWatchdog();
};
// Show enable audio button
const enableAudioBtn = document.getElementById('enable-audio-btn');
const statusEl = document.getElementById('connection-status');
audio.onwaiting = () => {
if (window.listenerAudioEnabled) {
updateStatus('Buffering...', false);
}
};
if (enableAudioBtn) {
enableAudioBtn.style.display = 'flex';
}
if (statusEl) {
statusEl.textContent = '[INFO] Click "Enable Audio" to start listening (MP3)';
}
audio.onplaying = () => {
if (window.listenerAudioEnabled && broadcastActive) {
updateStatus('Audio Active — Enjoy the stream!', true);
}
};
// Store for later activation
// Store globally
window.listenerAudio = audio;
window.listenerMediaSource = null;
window.listenerAudioEnabled = false;
// Initialise socket and join
// Initial UI
const enableAudioBtn = document.getElementById('enable-audio-btn');
if (enableAudioBtn) enableAudioBtn.style.display = 'flex';
updateStatus('Click "Enable Audio" to start listening', false);
// Connect socket (stream_status event will set broadcastActive)
initSocket();
socket.emit('join_listener');
// --- Socket event handlers ---
socket.on('broadcast_started', () => {
const nowPlayingEl = document.getElementById('listener-now-playing');
if (nowPlayingEl) nowPlayingEl.textContent = 'Stream is live!';
if (window.listenerAudio) {
console.log('[BROADCAST] 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');
if (statusEl && window.listenerAudioEnabled) {
statusEl.textContent = '[ACTIVE] Connected';
}
socket.emit('join_listener');
});
socket.on('disconnect', () => {
const statusEl = document.getElementById('connection-status');
if (statusEl) statusEl.textContent = '[OFFLINE] Disconnected';
});
}
// --- Enable Audio (user gesture required) ---
// --- Enable Audio (requires user gesture) ---
async function enableListenerAudio() {
console.log('[LISTENER] Enabling audio via user gesture...');
console.log('[ENABLE] User clicked Enable Audio');
const enableAudioBtn = document.getElementById('enable-audio-btn');
const statusEl = document.getElementById('connection-status');
const audioText = enableAudioBtn ? enableAudioBtn.querySelector('.audio-text') : null;
const btn = document.getElementById('enable-audio-btn');
const audioText = btn?.querySelector('.audio-text');
const audioSubtitle = btn?.querySelector('.audio-subtitle');
if (audioText) audioText.textContent = 'INITIALIZING...';
if (audioSubtitle) audioSubtitle.textContent = '';
try {
// 1. Create AudioContext if needed
// 1. Create/resume AudioContext (must happen inside a user gesture)
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');
}
console.log('[OK] AudioContext active');
// 3. Bridge Audio Element to AudioContext
// 2. Build audio graph: source → analyser → gain → speakers
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);
listenerMediaElementSourceNode =
listenerAudioContext.createMediaElementSource(window.listenerAudio);
}
// Clean single connection chain: media -> analyser -> gain -> destination
// Wire up (disconnect first to avoid double-connections)
try { listenerMediaElementSourceNode.disconnect(); } catch (_) { }
try { listenerAnalyserNode.disconnect(); } catch (_) { }
listenerMediaElementSourceNode.connect(listenerAnalyserNode);
listenerAnalyserNode.connect(listenerGainNode);
window.listenerAudio._connectedToContext = true;
console.log('[OK] Connected audio element to AudioContext (with analyser)');
startListenerVUMeter();
console.log('[OK] Audio graph connected');
} catch (e) {
console.warn('Could not connect to AudioContext:', e.message);
// Non-fatal: audio still plays, just no VU meter
console.warn('[WARN] Audio graph setup failed:', e.message);
}
}
// 4. Prepare and start audio playback
if (window.listenerAudio) {
window.listenerAudio.muted = false;
window.listenerAudio.volume = 1.0;
// 3. Apply volume
window.listenerAudio.muted = false;
window.listenerAudio.volume = 1.0;
const volEl = document.getElementById('listener-volume');
setListenerVolume(volEl ? parseInt(volEl.value, 10) || 80 : 80);
const volEl = document.getElementById('listener-volume');
const volValue = volEl ? parseInt(volEl.value, 10) : 80;
setListenerVolume(Number.isFinite(volValue) ? volValue : 80);
// 4. Mark enabled
window.listenerAudioEnabled = true;
const hasBufferedData = () => {
return window.listenerAudio.buffered && window.listenerAudio.buffered.length > 0;
};
window.listenerAudioEnabled = true;
if (audioText) audioText.textContent = 'STARTING...';
console.log('[PLAY] Attempting to play audio...');
const playTimeout = setTimeout(() => {
if (!hasBufferedData()) {
console.warn('[WARN] Audio play is taking a long time (buffering)...');
if (audioText) audioText.textContent = 'STILL BUFFERING...';
window.listenerAudio.load();
}
}, 8000);
const playPromise = window.listenerAudio.play();
if (!hasBufferedData() && audioText) {
audioText.textContent = 'BUFFERING...';
}
try {
await playPromise;
clearTimeout(playTimeout);
console.log('[OK] Audio playback started successfully');
} catch (e) {
clearTimeout(playTimeout);
throw e;
}
// 5. Hide button with a smooth transition
if (btn) {
btn.style.opacity = '0';
btn.style.pointerEvents = 'none';
setTimeout(() => { btn.style.display = 'none'; }, 300);
}
// 5. 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');
// 6. Start streaming if broadcast is active, otherwise wait
if (broadcastActive) {
if (audioText) audioText.textContent = 'CONNECTING...';
connectStream();
} else {
updateStatus('Audio enabled — waiting for stream to start...', false);
updateNowPlaying('Waiting for DJ to start streaming...');
}
} catch (error) {
console.error('[ERROR] Failed to enable audio:', error);
const stashedBtn = document.getElementById('enable-audio-btn');
const stashedStatus = document.getElementById('connection-status');
const aText = stashedBtn ? stashedBtn.querySelector('.audio-text') : null;
console.error('[ERROR] Enable audio failed:', error);
window.listenerAudioEnabled = false;
if (aText) aText.textContent = 'RETRY ENABLE';
if (stashedStatus) {
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 (audioText) audioText.textContent = 'TAP TO RETRY';
if (audioSubtitle) audioSubtitle.textContent = '';
if (btn) btn.style.pointerEvents = 'auto';
if (error.name === 'NotSupportedError') {
stashedStatus.textContent = 'MP3 stream failed. Is ffmpeg installed on the server?';
}
if (error.name === 'NotAllowedError') {
updateStatus('Browser blocked audio — tap the button again.', false);
} else {
updateStatus(`Error: ${error.message}`, false);
}
}
}
@ -467,7 +533,7 @@ async function enableListenerAudio() {
function setListenerVolume(value) {
if (listenerGainNode) {
listenerGainNode.gain.value = value / 100;
listenerGainNode.gain.value = Math.max(0, Math.min(1, value / 100));
}
}

View File

@ -470,13 +470,6 @@ function switchQueueTab() {
function toggleMobileLibrary() {
// This is now handled by tabs, but keep for compatibility if needed
const lib = document.querySelector('.library-section');
lib.classList.toggle('active');
vibrate(20);
}
// Mobile Haptic Helper
function vibrate(ms) {
if (navigator.vibrate) {
@ -608,9 +601,13 @@ function drawWaveform(id) {
const data = decks[id].waveformData;
if (!data) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
const width = canvas.width;
const height = canvas.height;
// Use logical (CSS) dimensions — the canvas context has ctx.scale(dpr)
// applied in initSystem, so coordinates must be in logical pixels.
const dpr = window.devicePixelRatio || 1;
const width = canvas.width / dpr;
const height = canvas.height / dpr;
ctx.clearRect(0, 0, width, height);
const barWidth = width / data.length;
data.forEach((val, i) => {
@ -804,8 +801,6 @@ function playDeck(id) {
if (deckEl) deckEl.classList.add('playing');
document.body.classList.add('playing-' + id);
if (audioCtx.state === 'suspended') {
console.log(`[Deck ${id}] Resuming suspended AudioContext`);
audioCtx.resume();
@ -824,8 +819,6 @@ function playDeck(id) {
const deckEl = document.getElementById('deck-' + id);
if (deckEl) deckEl.classList.remove('playing');
document.body.classList.remove('playing-' + id);
alert(`Playback error: ${error.message}`);
}
} else {
@ -1394,13 +1387,6 @@ function updateLibraryHighlighting() {
});
}
// Utility function no longer needed but kept for future use
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function filterLibrary() {
const query = document.getElementById('lib-search').value.toLowerCase();
const filtered = allSongs.filter(s => s.title.toLowerCase().includes(query));
@ -1411,7 +1397,6 @@ function refreshLibrary() {
fetchLibrary();
}
async function loadFromServer(id, url, title) {
const d = document.getElementById('display-' + id);
d.innerText = '[WAIT] LOADING...';
@ -1839,9 +1824,7 @@ function initSocket() {
if (socket) return socket;
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}`);
console.log(`[SOCKET] Connecting to ${serverUrl}`);
socket = io(serverUrl, {
transports: ['websocket', 'polling'],
@ -1853,21 +1836,15 @@ function initSocket() {
socket.on('connect', () => {
console.log('[OK] Connected to streaming server');
console.log(` Socket ID: ${socket.id}`);
console.log(` Transport: ${socket.io.engine.transport.name}`);
// Get initial listener count soon as we connect
socket.emit('get_listener_count');
});
socket.on('connect_error', (error) => {
console.error('[ERROR] Connection error:', error.message);
console.error(' Make sure server is running on', serverUrl);
});
socket.on('disconnect', (reason) => {
console.log('[ERROR] Disconnected from streaming server');
console.log(` Reason: ${reason}`);
console.warn(`[WARN] Disconnected: ${reason}`);
});
socket.on('listener_count', (data) => {
@ -1876,7 +1853,7 @@ function initSocket() {
});
socket.on('broadcast_started', () => {
console.log('Broadcast started notification received');
console.log('[EVENT] broadcast_started');
// Update relay UI if it's a relay
const relayStatus = document.getElementById('relay-status');
if (relayStatus && relayStatus.textContent.includes('Connecting')) {
@ -1886,7 +1863,7 @@ function initSocket() {
});
socket.on('broadcast_stopped', () => {
console.log('STOP Broadcast stopped notification received');
console.log('[EVENT] broadcast_stopped');
// Reset relay UI if it was active
const startRelayBtn = document.getElementById('start-relay-btn');
const stopRelayBtn = document.getElementById('stop-relay-btn');
@ -2025,7 +2002,7 @@ function startBroadcast() {
isBroadcasting = true;
document.getElementById('broadcast-btn').classList.add('active');
document.getElementById('broadcast-text').textContent = 'STOP BROADCAST';
document.getElementById('broadcast-status').textContent = '[OFFLINE] LIVE';
document.getElementById('broadcast-status').textContent = 'LIVE';
document.getElementById('broadcast-status').classList.add('live');
if (!socket) initSocket();
@ -2228,7 +2205,7 @@ function startBroadcast() {
isBroadcasting = true;
document.getElementById('broadcast-btn').classList.add('active');
document.getElementById('broadcast-text').textContent = 'STOP BROADCAST';
document.getElementById('broadcast-status').textContent = '[OFFLINE] LIVE';
document.getElementById('broadcast-status').textContent = 'LIVE';
document.getElementById('broadcast-status').classList.add('live');
// Notify server that broadcast is active (listeners use MP3 stream)
@ -2259,7 +2236,7 @@ function startBroadcast() {
// Stop broadcasting
function stopBroadcast() {
console.log('STOP Stopping broadcast...');
console.log('[BROADCAST] Stopping...');
if (SERVER_SIDE_AUDIO) {
isBroadcasting = false;
@ -2313,7 +2290,6 @@ function stopBroadcast() {
console.log('[OK] Broadcast stopped');
}
// Restart broadcasting (for auto-recovery)
function restartBroadcast() {
console.log('Restarting broadcast...');
@ -2367,19 +2343,31 @@ function restartBroadcast() {
// Copy stream URL to clipboard
function copyStreamUrl(evt) {
const urlInput = document.getElementById('stream-url');
urlInput.select();
urlInput.setSelectionRange(0, 99999); // For mobile
const text = urlInput.value;
const btn = evt?.target;
try {
document.execCommand('copy');
const btn = evt?.target;
const showFeedback = (success) => {
if (!btn) return;
const originalText = btn.textContent;
btn.textContent = 'OK';
setTimeout(() => {
btn.textContent = originalText;
}, 2000);
} catch (err) {
console.error('Failed to copy:', err);
btn.textContent = success ? 'OK' : 'FAIL';
setTimeout(() => { btn.textContent = originalText; }, 2000);
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text)
.then(() => showFeedback(true))
.catch(() => showFeedback(false));
} else {
// Fallback for older browsers
urlInput.select();
urlInput.setSelectionRange(0, 99999);
try {
document.execCommand('copy');
showFeedback(true);
} catch (err) {
console.error('Failed to copy:', err);
showFeedback(false);
}
}
}
@ -2416,7 +2404,10 @@ function monitorTrackEnd() {
const remaining = decks[id].duration - current;
// If end reached (with 0.5s buffer for safety)
if (remaining <= 0.5) {
// Skip if a loop is active — the Web Audio API handles looping natively
// and lastAnchorPosition is not updated on each native loop repeat, so
// the monotonically-growing `current` would incorrectly trigger end-of-track.
if (remaining <= 0.5 && !decks[id].loopActive) {
// During broadcast, still handle auto-play/queue to avoid dead air
if (isBroadcasting) {
console.log(`Track ending during broadcast on Deck ${id}`);
@ -2482,6 +2473,7 @@ function monitorTrackEnd() {
}, 500); // Check every 0.5s
}
monitorTrackEnd();
// Reset Deck to Default Settings
function resetDeck(id) {
vibrate(20);
@ -2555,7 +2547,6 @@ function resetDeck(id) {
console.log(`[OK] Deck ${id} reset complete!`);
// Visual feedback
const resetBtn = document.querySelector(`#deck-${id} .reset-btn`);
if (resetBtn) {
@ -2966,8 +2957,12 @@ function renderKeyboardMappings() {
<span class="key-display">${formatKeyName(key)}</span>
<span class="key-arrow">RIGHT</span>
<span class="action-label">${mapping.label}</span>
<button class="key-reassign-btn" onclick="reassignKey('${key}', event)">Change</button>
`;
const changeBtn = document.createElement('button');
changeBtn.className = 'key-reassign-btn';
changeBtn.textContent = 'Change';
changeBtn.addEventListener('click', (e) => reassignKey(key, e));
item.appendChild(changeBtn);
list.appendChild(item);
});
}

View File

@ -13,7 +13,6 @@ import collections
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
load_dotenv()
@ -47,23 +46,27 @@ CONFIG_DEBUG = bool(CONFIG.get('debug', False))
DJ_PANEL_PASSWORD = (CONFIG.get('dj_panel_password') or '').strip()
DJ_AUTH_ENABLED = bool(DJ_PANEL_PASSWORD)
# Relay State
# Broadcast State
broadcast_state = {
'active': False,
}
listener_sids = set()
dj_sids = set()
# Grace-period greenlet: auto-stop broadcast if DJ doesn't reconnect in time
_dj_grace_greenlet = None
DJ_GRACE_PERIOD_SECS = 20 # seconds to wait before auto-stopping
# === Optional MP3 fallback stream (server-side transcoding) ===
_ffmpeg_proc = None
_ffmpeg_in_q = queue.Queue(maxsize=20) # Optimized for low-latency live streaming
_ffmpeg_in_q = queue.Queue(maxsize=20)
_current_bitrate = (CONFIG.get('stream_bitrate') or '192k').strip()
_mp3_clients = set() # set[queue.Queue]
_mp3_lock = threading.Lock()
_transcoder_bytes_out = 0
_transcoder_last_error = None
_last_audio_chunk_ts = 0.0
_mp3_preroll = collections.deque(maxlen=512) # Larger pre-roll (~512KB)
_mp3_preroll = collections.deque(maxlen=512)
def _start_transcoder_if_needed(is_mp3_input=False):
@ -254,7 +257,6 @@ def _load_settings():
return {}
SETTINGS = _load_settings()
# Config.json music_folder overrides settings.json if set
_config_music = (CONFIG.get('music_folder') or '').strip()
MUSIC_FOLDER = _config_music or SETTINGS.get('library', {}).get('music_folder', 'music')
@ -275,8 +277,6 @@ def setup_shared_routes(app, index_file='index.html'):
library = []
global MUSIC_FOLDER
if os.path.exists(MUSIC_FOLDER):
# Recursively find music files if desired, or stay top-level.
# The prompt says "choose which folder", so maybe top-level of that folder is fine.
for root, dirs, files in os.walk(MUSIC_FOLDER):
for filename in sorted(files):
if filename.lower().endswith(('.mp3', '.m4a', '.wav', '.flac', '.ogg')):
@ -285,7 +285,7 @@ def setup_shared_routes(app, index_file='index.html'):
"title": os.path.splitext(filename)[0],
"file": f"music_proxy/{rel_path}"
})
break # Only top level for now to keep it simple, or remove break for recursive
break # Top-level only
return jsonify(library)
@app.route('/music_proxy/<path:filename>')
@ -421,15 +421,33 @@ def setup_shared_routes(app, index_file='index.html'):
@app.route('/stream.mp3')
def stream_mp3():
# Streaming response from the ffmpeg transcoder output.
# If ffmpeg isn't available, return 503.
if _ffmpeg_proc is None or _ffmpeg_proc.poll() is not None:
return jsonify({"success": False, "error": "MP3 stream not available"}), 503
"""Live MP3 audio stream from the ffmpeg transcoder."""
# If broadcast is not active, return 503 with audio/mpeg content type.
# Returning JSON here would confuse the browser's <audio> element.
if not broadcast_state.get('active'):
return Response(b'', status=503, content_type='audio/mpeg', headers={
'Retry-After': '5',
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'no-cache, no-store',
})
print(f"LISTENER: New listener joined stream (Bursting {_mp3_preroll.maxlen} frames)")
client_q: queue.Queue = queue.Queue(maxsize=500)
# If the transcoder isn't ready yet, wait briefly (it may still be starting)
waited = 0.0
while (_ffmpeg_proc is None or _ffmpeg_proc.poll() is not None) and waited < 5.0:
eventlet.sleep(0.5)
waited += 0.5
if _ffmpeg_proc is None or _ffmpeg_proc.poll() is not None:
return Response(b'', status=503, content_type='audio/mpeg', headers={
'Retry-After': '3',
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'no-cache, no-store',
})
preroll_count = len(_mp3_preroll)
print(f"LISTENER: New listener joined stream (Pre-roll: {preroll_count} chunks)")
client_q = queue.Queue(maxsize=500)
with _mp3_lock:
# Burst pre-roll to new client so they start playing instantly
for chunk in _mp3_preroll:
try:
client_q.put_nowait(chunk)
@ -438,14 +456,18 @@ def setup_shared_routes(app, index_file='index.html'):
_mp3_clients.add(client_q)
def gen():
idle_checks = 0
try:
while True:
try:
chunk = client_q.get(timeout=30)
chunk = client_q.get(timeout=5)
idle_checks = 0
except queue.Empty:
# No data for 30s - check if broadcast is still active
if not broadcast_state.get('active'):
break
idle_checks += 1
if idle_checks >= 12: # 60s total with no audio data
break
continue
if chunk is None:
break
@ -453,14 +475,16 @@ def setup_shared_routes(app, index_file='index.html'):
finally:
with _mp3_lock:
_mp3_clients.discard(client_q)
print(f"LISTENER: Listener disconnected from stream")
print("LISTENER: Listener disconnected from stream")
return Response(stream_with_context(gen()), content_type='audio/mpeg', headers={
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Connection': 'keep-alive',
'Pragma': 'no-cache',
'Expires': '0',
'X-Content-Type-Options': 'nosniff'
'X-Content-Type-Options': 'nosniff',
'Access-Control-Allow-Origin': '*',
'Icy-Name': 'TechDJ Live',
})
@app.route('/stream_debug')
@ -600,17 +624,46 @@ def dj_connect():
print(f"STREAMPANEL: DJ connected: {request.sid}")
dj_sids.add(request.sid)
def _dj_disconnect_grace():
"""Auto-stop broadcast if no DJ reconnects within the grace period."""
global _dj_grace_greenlet
print(f"INFO: Grace period started — {DJ_GRACE_PERIOD_SECS}s for DJ to reconnect")
eventlet.sleep(DJ_GRACE_PERIOD_SECS)
if broadcast_state.get('active') and not dj_sids:
print("WARNING: Grace period expired, no DJ reconnected — auto-stopping broadcast")
broadcast_state['active'] = False
_stop_transcoder()
listener_socketio.emit('broadcast_stopped', namespace='/')
listener_socketio.emit('stream_status', {'active': False}, namespace='/')
_dj_grace_greenlet = None
@dj_socketio.on('disconnect')
def dj_disconnect():
global _dj_grace_greenlet
dj_sids.discard(request.sid)
print("WARNING: DJ disconnected - broadcast will continue until manually stopped")
print(f"WARNING: DJ disconnected ({request.sid}). Remaining DJs: {len(dj_sids)}")
def stop_broadcast_after_timeout():
"""No longer used - broadcasts don't auto-stop"""
pass
# If broadcast is active and no other DJs remain, start the grace period
if broadcast_state.get('active') and not dj_sids:
if _dj_grace_greenlet is None:
_dj_grace_greenlet = eventlet.spawn(_dj_disconnect_grace)
elif not broadcast_state.get('active'):
# Nothing to do — no active broadcast
pass
else:
print("INFO: Other DJ(s) still connected — broadcast continues uninterrupted")
@dj_socketio.on('start_broadcast')
def dj_start(data=None):
global _dj_grace_greenlet
# Cancel any pending auto-stop grace period (DJ reconnected in time)
if _dj_grace_greenlet is not None:
_dj_grace_greenlet.kill()
_dj_grace_greenlet = None
print("INFO: DJ reconnected within grace period — broadcast continues")
was_already_active = broadcast_state.get('active', False)
broadcast_state['active'] = True
session['is_dj'] = True

View File

@ -869,8 +869,9 @@ class StreamingWorker(QThread):
chunk = self.ffmpeg_proc.stdout.read(8192)
if not chunk:
break
if self.sio.connected:
self.sio.emit('audio_chunk', chunk)
sio = self.sio # Local ref to avoid race with stop_streaming()
if sio and sio.connected:
sio.emit('audio_chunk', chunk)
except Exception as e:
self.streaming_error.emit(f"Streaming thread error: {e}")
@ -1405,11 +1406,9 @@ class DeckWidget(QGroupBox):
def check_queue(self, status):
if status == QMediaPlayer.MediaStatus.EndOfMedia:
# Check if this is a premature EndOfMedia (common in GStreamer with certain VBR MP3s)
# Premature EndOfMedia is common with GStreamer + VBR MP3s
if self.real_duration > 0 and self.player.position() < self.real_duration - 1000:
print(f"[DEBUG] {self.deck_id} Premature EndOfMedia detected. Position: {self.player.position()}, Expected: {self.real_duration}")
# Don't skip yet, maybe the user wants to seek back?
# Or we could try to play again, but usually GStreamer won't go further.
print(f"[DEBUG] {self.deck_id} Premature EndOfMedia at {self.player.position()}ms (expected {self.real_duration}ms)")
if self.playback_mode == 1:
# Loop 1 mode

File diff suppressed because one or more lines are too long