189 lines
6.0 KiB
JavaScript
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);
|
|
}
|
|
}
|
|
};
|
|
})();
|