blastoise-archive/public/controls.js

189 lines
6.0 KiB
JavaScript

// MusicRoom - Controls module
// Play, pause, seek, volume, prev/next track
(function() {
const M = window.MusicRoom;
// Load saved volume
const savedVolume = localStorage.getItem(M.STORAGE_KEY);
if (savedVolume !== null) {
M.audio.volume = parseFloat(savedVolume);
M.$("#volume").value = savedVolume;
} else {
// No saved volume - sync audio to slider's default value
M.audio.volume = parseFloat(M.$("#volume").value);
}
// Toggle play/pause
function togglePlayback() {
if (!M.currentFilename) return;
if (M.synced) {
if (M.ws && M.ws.readyState === WebSocket.OPEN) {
M.ws.send(JSON.stringify({ action: M.serverPaused ? "unpause" : "pause" }));
}
} else {
if (M.audio.paused) {
if (!M.audio.src) {
M.audio.src = M.getTrackUrl(M.currentFilename);
M.audio.currentTime = M.localTimestamp;
}
M.audio.play();
} else {
M.localTimestamp = M.audio.currentTime;
M.audio.pause();
}
M.updateUI();
}
}
// Jump to a specific track index
async function jumpToTrack(index) {
if (M.playlist.length === 0) return;
const newIndex = (index + M.playlist.length) % M.playlist.length;
if (M.synced && M.currentStreamId) {
const res = await fetch("/api/streams/" + M.currentStreamId + "/jump", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ index: newIndex })
});
if (res.status === 403) M.flashPermissionDenied();
} else {
const track = M.playlist[newIndex];
M.currentIndex = newIndex;
M.currentFilename = track.filename;
M.serverTrackDuration = track.duration;
M.$("#track-title").textContent = track.title?.trim() || track.filename.replace(/\.[^.]+$/, "");
M.loadingSegments.clear();
const cachedUrl = await M.loadTrackBlob(track.filename);
M.audio.src = cachedUrl || M.getTrackUrl(track.filename);
M.audio.currentTime = 0;
M.localTimestamp = 0;
M.audio.play();
M.renderPlaylist();
}
}
// Sync toggle
M.$("#btn-sync").onclick = () => {
M.wantSync = !M.wantSync;
if (M.wantSync) {
// User wants to sync - try to connect
if (M.currentStreamId) {
M.connectStream(M.currentStreamId);
}
} else {
// User wants local mode - disconnect
M.synced = false;
M.localTimestamp = M.audio.currentTime || M.getServerTime();
if (M.ws) {
const oldWs = M.ws;
M.ws = null;
oldWs.onclose = null;
oldWs.close();
}
}
M.updateUI();
};
// Play/pause button
M.$("#status-icon").onclick = togglePlayback;
// Prev/next buttons
M.$("#btn-prev").onclick = () => jumpToTrack(M.currentIndex - 1);
M.$("#btn-next").onclick = () => jumpToTrack(M.currentIndex + 1);
// Progress bar seek tooltip
M.$("#progress-container").onmousemove = (e) => {
if (M.serverTrackDuration <= 0) return;
const rect = M.$("#progress-container").getBoundingClientRect();
const pct = (e.clientX - rect.left) / rect.width;
const hoverTime = pct * M.serverTrackDuration;
const tooltip = M.$("#seek-tooltip");
tooltip.textContent = M.fmt(hoverTime);
tooltip.style.left = (pct * 100) + "%";
tooltip.style.display = "block";
};
M.$("#progress-container").onmouseleave = () => {
M.$("#seek-tooltip").style.display = "none";
};
// Progress bar seek
M.$("#progress-container").onclick = (e) => {
const dur = M.synced ? M.serverTrackDuration : (M.audio.duration || M.serverTrackDuration);
if (!M.currentFilename || dur <= 0) return;
const rect = M.$("#progress-container").getBoundingClientRect();
const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
const seekTime = pct * dur;
if (M.synced && M.currentStreamId) {
fetch("/api/streams/" + M.currentStreamId + "/seek", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ timestamp: seekTime })
}).then(res => { if (res.status === 403) M.flashPermissionDenied(); });
} else {
if (!M.audio.src) {
M.audio.src = M.getTrackUrl(M.currentFilename);
}
M.audio.currentTime = seekTime;
M.localTimestamp = seekTime;
}
};
// Mute toggle
M.$("#btn-mute").onclick = () => {
if (M.audio.volume > 0) {
M.preMuteVolume = M.audio.volume;
M.audio.volume = 0;
M.$("#volume").value = 0;
} else {
M.audio.volume = M.preMuteVolume;
M.$("#volume").value = M.preMuteVolume;
}
localStorage.setItem(M.STORAGE_KEY, M.audio.volume);
M.updateUI();
};
// Volume slider
M.$("#volume").oninput = (e) => {
M.audio.volume = e.target.value;
localStorage.setItem(M.STORAGE_KEY, e.target.value);
M.updateUI();
};
// Audio element events
M.audio.onplay = () => { M.$("#progress-bar").classList.add("playing"); M.updateUI(); };
M.audio.onpause = () => { M.$("#progress-bar").classList.remove("playing"); M.updateUI(); };
// Track loading state from audio element's progress
M.audio.onprogress = () => {
if (M.serverTrackDuration <= 0) return;
const segmentDur = M.serverTrackDuration / M.SEGMENTS;
M.loadingSegments.clear();
for (let i = 0; i < M.SEGMENTS; i++) {
const segStart = i * segmentDur;
const segEnd = (i + 1) * segmentDur;
let fullyBuffered = false;
let partiallyBuffered = false;
for (let j = 0; j < M.audio.buffered.length; j++) {
const bufStart = M.audio.buffered.start(j);
const bufEnd = M.audio.buffered.end(j);
if (bufStart <= segStart && bufEnd >= segEnd) {
fullyBuffered = true;
break;
}
// Check if buffer is actively loading into this segment
if (bufEnd > segStart && bufEnd < segEnd && bufStart <= segStart) {
partiallyBuffered = true;
}
}
if (partiallyBuffered && !fullyBuffered) {
M.loadingSegments.add(i);
}
}
};
})();