forked from computertech/techdj
Update TechDJ: fix viewer controls, boost listener volume, improve deck sync
This commit is contained in:
353
script.js
353
script.js
@@ -3,7 +3,21 @@
|
||||
// ==========================================
|
||||
|
||||
// Server-side audio mode (true = server processes audio, false = browser processes)
|
||||
const SERVER_SIDE_AUDIO = false;
|
||||
const SERVER_SIDE_AUDIO = true;
|
||||
|
||||
function getDjIdentity() {
|
||||
try {
|
||||
const existing = localStorage.getItem('techdj_identity');
|
||||
if (existing) return existing;
|
||||
const id = (crypto && typeof crypto.randomUUID === 'function')
|
||||
? crypto.randomUUID()
|
||||
: String(Date.now()) + '-' + String(Math.random()).slice(2);
|
||||
localStorage.setItem('techdj_identity', id);
|
||||
return id;
|
||||
} catch {
|
||||
return 'anon';
|
||||
}
|
||||
}
|
||||
|
||||
let audioCtx;
|
||||
const decks = {
|
||||
@@ -12,6 +26,8 @@ const decks = {
|
||||
playing: false,
|
||||
pausedAt: 0,
|
||||
duration: 0,
|
||||
serverPitch: 1.0,
|
||||
lastAnchorWallTime: 0,
|
||||
localBuffer: null,
|
||||
localSource: null,
|
||||
gainNode: null,
|
||||
@@ -35,6 +51,8 @@ const decks = {
|
||||
playing: false,
|
||||
pausedAt: 0,
|
||||
duration: 0,
|
||||
serverPitch: 1.0,
|
||||
lastAnchorWallTime: 0,
|
||||
localBuffer: null,
|
||||
localSource: null,
|
||||
gainNode: null,
|
||||
@@ -499,41 +517,43 @@ function updateTimeDisplays() {
|
||||
if (!anyPlaying) return;
|
||||
|
||||
['A', 'B'].forEach(id => {
|
||||
if (decks[id].playing && decks[id].localBuffer) {
|
||||
const playbackRate = decks[id].localSource ? decks[id].localSource.playbackRate.value : 1.0;
|
||||
const realElapsed = audioCtx.currentTime - decks[id].lastAnchorTime;
|
||||
let current = decks[id].lastAnchorPosition + (realElapsed * playbackRate);
|
||||
if (!decks[id].playing) return;
|
||||
|
||||
// Handle Looping wrapping for UI
|
||||
if (decks[id].loopActive && decks[id].loopStart !== null && decks[id].loopEnd !== null) {
|
||||
const loopLen = decks[id].loopEnd - decks[id].loopStart;
|
||||
if (current >= decks[id].loopEnd && loopLen > 0) {
|
||||
current = ((current - decks[id].loopStart) % loopLen) + decks[id].loopStart;
|
||||
}
|
||||
} else if (settings[`repeat${id}`]) {
|
||||
// Full song repeat wrapping for UI
|
||||
current = current % decks[id].duration;
|
||||
} else {
|
||||
current = Math.min(current, decks[id].duration);
|
||||
}
|
||||
// Server-side mode: viewers may not have AudioContext or localBuffer.
|
||||
if (!decks[id].localBuffer && !SERVER_SIDE_AUDIO) return;
|
||||
|
||||
document.getElementById('time-current-' + id).textContent = formatTime(current);
|
||||
const current = getCurrentPosition(id);
|
||||
const timerEl = document.getElementById('time-current-' + id);
|
||||
if (timerEl) timerEl.textContent = formatTime(current);
|
||||
|
||||
// Update playhead
|
||||
const progress = (current / decks[id].duration) * 100;
|
||||
const playhead = document.getElementById('playhead-' + id);
|
||||
if (playhead) playhead.style.left = progress + '%';
|
||||
// Update playhead (guard against zero duration)
|
||||
const dur = Number(decks[id].duration) || 0;
|
||||
const playhead = document.getElementById('playhead-' + id);
|
||||
if (playhead && dur > 0) {
|
||||
const progress = (current / dur) * 100;
|
||||
playhead.style.left = progress + '%';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getCurrentPosition(id) {
|
||||
if (!decks[id].playing) return decks[id].pausedAt;
|
||||
if (!audioCtx) return decks[id].pausedAt;
|
||||
|
||||
const playbackRate = decks[id].localSource ? decks[id].localSource.playbackRate.value : 1.0;
|
||||
const realElapsed = audioCtx.currentTime - decks[id].lastAnchorTime;
|
||||
let pos = decks[id].lastAnchorPosition + (realElapsed * playbackRate);
|
||||
// If AudioContext isn't initialized (common in server-side mode / viewers),
|
||||
// still advance position using wall-clock time + server pitch.
|
||||
let pos;
|
||||
if (!audioCtx) {
|
||||
const now = (performance && typeof performance.now === 'function')
|
||||
? performance.now() / 1000
|
||||
: Date.now() / 1000;
|
||||
const realElapsed = now - (decks[id].lastAnchorWallTime || 0);
|
||||
const playbackRate = Number.isFinite(decks[id].serverPitch) ? decks[id].serverPitch : 1.0;
|
||||
pos = (decks[id].lastAnchorPosition || 0) + (realElapsed * playbackRate);
|
||||
} else {
|
||||
const playbackRate = decks[id].localSource ? decks[id].localSource.playbackRate.value : 1.0;
|
||||
const realElapsed = audioCtx.currentTime - decks[id].lastAnchorTime;
|
||||
pos = decks[id].lastAnchorPosition + (realElapsed * playbackRate);
|
||||
}
|
||||
|
||||
// Handle wrapping for correct position return
|
||||
if (decks[id].loopActive && decks[id].loopStart !== null && decks[id].loopEnd !== null) {
|
||||
@@ -565,6 +585,42 @@ function playDeck(id) {
|
||||
decks[id].playing = true;
|
||||
const deckEl = document.getElementById('deck-' + id);
|
||||
if (deckEl) deckEl.classList.add('playing');
|
||||
|
||||
// Local monitoring (controller only): keep the DJ able to hear while the server streams.
|
||||
if (djController?.youAreController && audioCtx && decks[id].localBuffer) {
|
||||
try {
|
||||
if (decks[id].localSource) {
|
||||
try {
|
||||
decks[id].localSource.stop();
|
||||
} catch (e) { }
|
||||
decks[id].localSource.onended = null;
|
||||
}
|
||||
|
||||
const src = audioCtx.createBufferSource();
|
||||
src.buffer = decks[id].localBuffer;
|
||||
|
||||
if (decks[id].loopActive) {
|
||||
src.loop = true;
|
||||
src.loopStart = decks[id].loopStart || 0;
|
||||
src.loopEnd = decks[id].loopEnd || decks[id].duration;
|
||||
}
|
||||
|
||||
src.connect(decks[id].filters.low);
|
||||
const speedSlider = document.querySelector(`#deck-${id} .speed-slider`);
|
||||
const speed = speedSlider ? parseFloat(speedSlider.value) : 1.0;
|
||||
src.playbackRate.value = speed;
|
||||
|
||||
decks[id].localSource = src;
|
||||
decks[id].lastAnchorTime = audioCtx.currentTime;
|
||||
decks[id].lastAnchorPosition = decks[id].pausedAt || 0;
|
||||
|
||||
src.start(0, decks[id].pausedAt || 0);
|
||||
if (audioCtx.state === 'suspended') audioCtx.resume();
|
||||
} catch (e) {
|
||||
console.warn(`[Deck ${id}] Local monitor play failed:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[Deck ${id}] Play command sent to server`);
|
||||
return;
|
||||
}
|
||||
@@ -612,6 +668,16 @@ function pauseDeck(id) {
|
||||
socket.emit('audio_pause', { deck: id });
|
||||
decks[id].playing = false;
|
||||
document.getElementById('deck-' + id).classList.remove('playing');
|
||||
|
||||
// Local monitoring (controller only)
|
||||
if (djController?.youAreController && decks[id].localSource) {
|
||||
try {
|
||||
decks[id].localSource.stop();
|
||||
decks[id].localSource.onended = null;
|
||||
} catch (e) { }
|
||||
decks[id].localSource = null;
|
||||
}
|
||||
|
||||
console.log(`[Deck ${id}] Pause command sent to server`);
|
||||
return;
|
||||
}
|
||||
@@ -1274,6 +1340,21 @@ async function loadFromServer(id, url, title) {
|
||||
socket.emit('audio_load_track', { deck: id, filename: filename });
|
||||
console.log(`[Deck ${id}] 📡 Load command sent to server: ${filename}`);
|
||||
|
||||
// In server-side mode, if the user is controller and AudioContext isn't initialized,
|
||||
// init it so they can have local monitoring + waveform.
|
||||
if (SERVER_SIDE_AUDIO && djController.youAreController && !audioCtx) {
|
||||
initSystem();
|
||||
}
|
||||
|
||||
// If AudioContext still isn't initialized (e.g., viewer or failed init), skip local loading.
|
||||
if (!audioCtx) {
|
||||
decks[id].currentFile = url;
|
||||
decks[id].localBuffer = null;
|
||||
d.innerText = title.toUpperCase();
|
||||
d.classList.remove('blink');
|
||||
return;
|
||||
}
|
||||
|
||||
// We DON'T return here anymore. We continue below to load for the UI.
|
||||
}
|
||||
|
||||
@@ -1527,12 +1608,21 @@ window.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
if (!isListenerPort && !isListenerHostname) {
|
||||
// In server-side audio mode, allow viewing the UI without initializing AudioContext.
|
||||
if (SERVER_SIDE_AUDIO) {
|
||||
const overlay = document.getElementById('start-overlay');
|
||||
if (overlay) overlay.style.display = 'none';
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
// Connect early so controller/viewer status is visible and UI can be synced.
|
||||
initSocket();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1543,6 +1633,7 @@ let streamDestination = null;
|
||||
let streamProcessor = null;
|
||||
let isBroadcasting = false;
|
||||
let autoStartStream = false;
|
||||
let djController = { controllerActive: false, youAreController: false, controllerSid: null };
|
||||
let listenerAudioContext = null;
|
||||
let listenerGainNode = null;
|
||||
let listenerAnalyserNode = null;
|
||||
@@ -1645,6 +1736,30 @@ function initSocket() {
|
||||
console.log('✅ Connected to streaming server');
|
||||
console.log(` Socket ID: ${socket.id}`);
|
||||
console.log(` Transport: ${socket.io.engine.transport.name}`);
|
||||
|
||||
// DJ page reconnect hydration: explicitly request the latest mixer state.
|
||||
// (Server also pushes on connect, but this ensures we never miss it.)
|
||||
const isDjUi = !!document.getElementById('deck-A') || !!document.getElementById('deck-B');
|
||||
if (isDjUi) {
|
||||
socket.emit('dj_identity', {
|
||||
id: getDjIdentity(),
|
||||
auto_reclaim: (localStorage.getItem('techdj_wants_autoreclaim') ?? 'true') === 'true'
|
||||
});
|
||||
socket.emit('get_mixer_status');
|
||||
socket.emit('get_listener_count');
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('controller_status', (data) => {
|
||||
djController.controllerActive = !!data.controller_active;
|
||||
djController.youAreController = !!data.you_are_controller;
|
||||
djController.controllerSid = data.controller_sid || null;
|
||||
|
||||
// If we ever become controller, remember that we want auto-reclaim on reconnect.
|
||||
if (djController.youAreController) {
|
||||
try { localStorage.setItem('techdj_wants_autoreclaim', 'true'); } catch { }
|
||||
}
|
||||
updateDjControllerUI();
|
||||
});
|
||||
|
||||
socket.on('connect_error', (error) => {
|
||||
@@ -1662,6 +1777,30 @@ function initSocket() {
|
||||
if (el) el.textContent = data.count;
|
||||
});
|
||||
|
||||
// DJ-side: keep broadcast UI in sync for multi-DJ viewing.
|
||||
socket.on('stream_status', (data) => {
|
||||
const broadcastBtn = document.getElementById('broadcast-btn');
|
||||
const broadcastText = document.getElementById('broadcast-text');
|
||||
const broadcastStatus = document.getElementById('broadcast-status');
|
||||
|
||||
// If we're not on the DJ page, these elements won't exist.
|
||||
if (!broadcastBtn || !broadcastText || !broadcastStatus) return;
|
||||
|
||||
isBroadcasting = !!data.active;
|
||||
|
||||
if (isBroadcasting) {
|
||||
broadcastBtn.classList.add('active');
|
||||
broadcastText.textContent = 'STOP BROADCAST';
|
||||
broadcastStatus.textContent = '🔴 LIVE';
|
||||
broadcastStatus.classList.add('live');
|
||||
} else {
|
||||
broadcastBtn.classList.remove('active');
|
||||
broadcastText.textContent = 'START BROADCAST';
|
||||
broadcastStatus.textContent = 'Offline';
|
||||
broadcastStatus.classList.remove('live');
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('broadcast_started', () => {
|
||||
console.log('🎙️ Broadcast started notification received');
|
||||
// Update relay UI if it's a relay
|
||||
@@ -1691,6 +1830,12 @@ function initSocket() {
|
||||
|
||||
socket.on('error', (data) => {
|
||||
console.error('📡 Server error:', data.message);
|
||||
// Avoid spamming alerts for expected controller lock denials.
|
||||
if (data?.message === 'Control is currently held by another DJ' ||
|
||||
data?.message === 'No active DJ controller. Click Take Control.') {
|
||||
setDjControlHint(data.message, true);
|
||||
return;
|
||||
}
|
||||
alert(`SERVER ERROR: ${data.message}`);
|
||||
// Reset relay UI on error
|
||||
document.getElementById('start-relay-btn').style.display = 'inline-block';
|
||||
@@ -1701,6 +1846,67 @@ function initSocket() {
|
||||
return socket;
|
||||
}
|
||||
|
||||
function setDjControlHint(text, isError = false) {
|
||||
const hint = document.getElementById('dj-control-hint');
|
||||
if (!hint) return;
|
||||
hint.textContent = text || '';
|
||||
hint.style.color = isError ? '#ff4444' : '';
|
||||
}
|
||||
|
||||
function updateDjControllerUI() {
|
||||
const statusEl = document.getElementById('dj-control-status');
|
||||
const btn = document.getElementById('take-control-btn');
|
||||
|
||||
if (statusEl) {
|
||||
if (!djController.controllerActive) {
|
||||
statusEl.textContent = 'Controller: None';
|
||||
} else if (djController.youAreController) {
|
||||
statusEl.textContent = 'Controller: YOU';
|
||||
} else {
|
||||
statusEl.textContent = 'Controller: Another DJ';
|
||||
}
|
||||
}
|
||||
|
||||
if (btn) {
|
||||
btn.disabled = djController.controllerActive;
|
||||
btn.textContent = djController.youAreController ? 'YOU HAVE CONTROL' : 'TAKE CONTROL';
|
||||
}
|
||||
|
||||
// Update hint text based on latest state.
|
||||
if (!djController.controllerActive) {
|
||||
setDjControlHint('No controller. Click TAKE CONTROL.', false);
|
||||
} else if (djController.youAreController) {
|
||||
setDjControlHint('You have control.', false);
|
||||
} else {
|
||||
setDjControlHint('Another DJ has control. You are in view-only mode.', false);
|
||||
}
|
||||
|
||||
const isViewer = djController.controllerActive && !djController.youAreController;
|
||||
document.body.classList.toggle('viewer-mode', isViewer);
|
||||
|
||||
// Disable broadcast + relay controls for viewers
|
||||
const broadcastBtn = document.getElementById('broadcast-btn');
|
||||
if (broadcastBtn) broadcastBtn.disabled = isViewer;
|
||||
const relayStart = document.getElementById('start-relay-btn');
|
||||
const relayStop = document.getElementById('stop-relay-btn');
|
||||
const relayUrl = document.getElementById('remote-stream-url');
|
||||
if (relayStart) relayStart.disabled = isViewer;
|
||||
if (relayStop) relayStop.disabled = isViewer;
|
||||
if (relayUrl) relayUrl.disabled = isViewer;
|
||||
|
||||
// Disable volume/EQ sliders and crossfader for viewers
|
||||
const controlSliders = document.querySelectorAll('input[data-role="volume"], input[data-role="eq"], #crossfader, .speed-slider');
|
||||
controlSliders.forEach(slider => {
|
||||
slider.disabled = isViewer;
|
||||
});
|
||||
}
|
||||
|
||||
function takeDjControl() {
|
||||
if (!socket) initSocket();
|
||||
setDjControlHint('Requesting control...', false);
|
||||
socket.emit('take_control');
|
||||
}
|
||||
|
||||
// Update DJ UI from server status
|
||||
function updateUIFromMixerStatus(status) {
|
||||
if (!status) return;
|
||||
@@ -1709,6 +1915,29 @@ function updateUIFromMixerStatus(status) {
|
||||
const deckStatus = id === 'A' ? status.deck_a : status.deck_b;
|
||||
if (!deckStatus) return;
|
||||
|
||||
// Reflect playing state in the UI (important for reconnects/viewers).
|
||||
const deckEl = document.getElementById('deck-' + id);
|
||||
if (deckEl) {
|
||||
deckEl.classList.toggle('playing', !!deckStatus.playing);
|
||||
}
|
||||
|
||||
// Mirror volume + EQ sliders for viewers (and keep controllers visually in sync too).
|
||||
const volSlider = document.querySelector(`input[data-role="volume"][data-deck="${id}"]`);
|
||||
if (volSlider && typeof deckStatus.volume === 'number') {
|
||||
const target = Math.max(0, Math.min(100, Math.round(deckStatus.volume * 100)));
|
||||
if (volSlider.value !== String(target)) volSlider.value = String(target);
|
||||
}
|
||||
|
||||
const eq = deckStatus.eq || {};
|
||||
['high', 'mid', 'low'].forEach((band) => {
|
||||
const eqSlider = document.querySelector(`input[data-role="eq"][data-deck="${id}"][data-band="${band}"]`);
|
||||
if (!eqSlider) return;
|
||||
const val = Number(eq[band]);
|
||||
if (!Number.isFinite(val)) return;
|
||||
const clamped = Math.max(-20, Math.min(20, val));
|
||||
if (eqSlider.value !== String(clamped)) eqSlider.value = String(clamped);
|
||||
});
|
||||
|
||||
// Update position (only if not currently dragging the waveform)
|
||||
const timeSinceSeek = Date.now() - (decks[id].lastSeekTime || 0);
|
||||
if (timeSinceSeek < 1500) {
|
||||
@@ -1719,8 +1948,8 @@ function updateUIFromMixerStatus(status) {
|
||||
// Update playing state
|
||||
decks[id].playing = deckStatus.playing;
|
||||
|
||||
// Update loaded track if changed
|
||||
if (deckStatus.filename && (!decks[id].currentFile || decks[id].currentFile !== deckStatus.filename)) {
|
||||
// Update loaded track (always sync from server to ensure UI matches authoritative state)
|
||||
if (deckStatus.filename) {
|
||||
console.log(`📡 Server synced: Deck ${id} is playing ${deckStatus.filename}`);
|
||||
decks[id].currentFile = deckStatus.filename;
|
||||
decks[id].duration = deckStatus.duration;
|
||||
@@ -1737,9 +1966,18 @@ function updateUIFromMixerStatus(status) {
|
||||
const speedSlider = document.querySelector(`#deck-${id} .speed-slider`);
|
||||
if (speedSlider) speedSlider.value = deckStatus.pitch;
|
||||
|
||||
// Store pitch so viewers (no AudioContext) can still animate time/playhead.
|
||||
if (typeof deckStatus.pitch === 'number' && Number.isFinite(deckStatus.pitch)) {
|
||||
decks[id].serverPitch = deckStatus.pitch;
|
||||
}
|
||||
|
||||
// Update anchor for local interpolation
|
||||
decks[id].lastAnchorPosition = deckStatus.position;
|
||||
decks[id].lastAnchorTime = audioCtx ? audioCtx.currentTime : 0;
|
||||
const now = (performance && typeof performance.now === 'function')
|
||||
? performance.now() / 1000
|
||||
: Date.now() / 1000;
|
||||
decks[id].lastAnchorWallTime = now;
|
||||
|
||||
if (!decks[id].playing) {
|
||||
decks[id].pausedAt = deckStatus.position;
|
||||
@@ -1754,9 +1992,10 @@ function updateUIFromMixerStatus(status) {
|
||||
if (timer) timer.textContent = formatTime(currentPos);
|
||||
});
|
||||
|
||||
// Update crossfader if changed significantly
|
||||
if (Math.abs(decks.crossfader - status.crossfader) > 1) {
|
||||
// We'd update the UI slider here
|
||||
// Update crossfader UI (do NOT call updateCrossfader here; viewers must not emit).
|
||||
const xf = document.getElementById('crossfader');
|
||||
if (xf && typeof status.crossfader === 'number') {
|
||||
xf.value = String(status.crossfader);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1773,7 +2012,8 @@ function toggleStreamingPanel() {
|
||||
|
||||
// Toggle broadcast
|
||||
function toggleBroadcast() {
|
||||
if (!audioCtx) {
|
||||
// In server-side audio mode, broadcast does not require the browser AudioContext.
|
||||
if (!SERVER_SIDE_AUDIO && !audioCtx) {
|
||||
alert('Please initialize the system first (click INITIALIZE SYSTEM)');
|
||||
return;
|
||||
}
|
||||
@@ -1793,18 +2033,12 @@ function startBroadcast() {
|
||||
try {
|
||||
console.log('🎙️ Starting broadcast...');
|
||||
|
||||
if (!audioCtx) {
|
||||
alert('Please initialize the system first!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Server-side audio mode
|
||||
if (SERVER_SIDE_AUDIO) {
|
||||
isBroadcasting = true;
|
||||
document.getElementById('broadcast-btn').classList.add('active');
|
||||
document.getElementById('broadcast-text').textContent = 'STOP BROADCAST';
|
||||
document.getElementById('broadcast-status').textContent = '🔴 LIVE';
|
||||
document.getElementById('broadcast-status').classList.add('live');
|
||||
if (!djController?.youAreController) {
|
||||
setDjControlHint('You must TAKE CONTROL before starting the broadcast.', true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!socket) initSocket();
|
||||
socket.emit('start_broadcast');
|
||||
@@ -1814,6 +2048,11 @@ function startBroadcast() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!audioCtx) {
|
||||
alert('Please initialize the system first!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Browser-side audio mode (original code)
|
||||
// Check if any audio is playing
|
||||
const anyPlaying = decks.A.playing || decks.B.playing;
|
||||
@@ -2038,10 +2277,11 @@ function stopBroadcast() {
|
||||
console.log('🛑 Stopping broadcast...');
|
||||
|
||||
if (SERVER_SIDE_AUDIO) {
|
||||
isBroadcasting = false;
|
||||
if (socket) {
|
||||
socket.emit('stop_broadcast');
|
||||
if (!djController?.youAreController) {
|
||||
setDjControlHint('Only the controller can stop the broadcast.', true);
|
||||
return;
|
||||
}
|
||||
if (socket) socket.emit('stop_broadcast');
|
||||
} else {
|
||||
if (streamProcessor) {
|
||||
streamProcessor.stop();
|
||||
@@ -2080,11 +2320,13 @@ function stopBroadcast() {
|
||||
}
|
||||
}
|
||||
|
||||
// Update UI
|
||||
document.getElementById('broadcast-btn').classList.remove('active');
|
||||
document.getElementById('broadcast-text').textContent = 'START BROADCAST';
|
||||
document.getElementById('broadcast-status').textContent = 'Offline';
|
||||
document.getElementById('broadcast-status').classList.remove('live');
|
||||
// Update UI only in browser-side mode. In server-side mode, rely on stream_status.
|
||||
if (!SERVER_SIDE_AUDIO) {
|
||||
document.getElementById('broadcast-btn').classList.remove('active');
|
||||
document.getElementById('broadcast-text').textContent = 'START BROADCAST';
|
||||
document.getElementById('broadcast-status').textContent = 'Offline';
|
||||
document.getElementById('broadcast-status').classList.remove('live');
|
||||
}
|
||||
|
||||
console.log('✅ Broadcast stopped');
|
||||
}
|
||||
@@ -2384,7 +2626,7 @@ async function enableListenerAudio() {
|
||||
try {
|
||||
if (!listenerGainNode) {
|
||||
listenerGainNode = listenerAudioContext.createGain();
|
||||
listenerGainNode.gain.value = 0.8;
|
||||
listenerGainNode.gain.value = 1.0;
|
||||
listenerGainNode.connect(listenerAudioContext.destination);
|
||||
}
|
||||
|
||||
@@ -2431,8 +2673,11 @@ async function enableListenerAudio() {
|
||||
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 volValue = volEl ? parseInt(volEl.value, 10) : 100;
|
||||
const savedVol = localStorage.getItem('listenerVolume');
|
||||
const finalVol = savedVol ? parseInt(savedVol, 10) : volValue;
|
||||
if (volEl) volEl.value = finalVol;
|
||||
setListenerVolume(Number.isFinite(finalVol) ? finalVol : 100);
|
||||
|
||||
const hasBufferedData = () => {
|
||||
return window.listenerAudio.buffered && window.listenerAudio.buffered.length > 0;
|
||||
@@ -2497,6 +2742,8 @@ function setListenerVolume(value) {
|
||||
if (listenerGainNode) {
|
||||
listenerGainNode.gain.value = value / 100;
|
||||
}
|
||||
// Save to localStorage
|
||||
localStorage.setItem('listenerVolume', value);
|
||||
}
|
||||
|
||||
// Load auto-start preference
|
||||
|
||||
Reference in New Issue
Block a user