blastoise-archive/public/streamSync.js

155 lines
4.6 KiB
JavaScript

// MusicRoom - Stream Sync module
// WebSocket connection and server synchronization
(function() {
const M = window.MusicRoom;
// Load available streams and connect to first one
M.loadStreams = async function() {
try {
const res = await fetch("/api/streams");
const streams = await res.json();
if (streams.length === 0) {
M.$("#track-title").textContent = "No streams available";
return;
}
if (streams.length > 1) {
const sel = document.createElement("select");
for (const s of streams) {
const opt = document.createElement("option");
opt.value = s.id;
opt.textContent = s.name;
sel.appendChild(opt);
}
sel.onchange = () => M.connectStream(sel.value);
M.$("#stream-select").appendChild(sel);
}
M.connectStream(streams[0].id);
} catch (e) {
M.$("#track-title").textContent = "Server unavailable";
M.$("#status").textContent = "Local (offline)";
M.synced = false;
M.updateUI();
}
};
// Connect to a stream via WebSocket
M.connectStream = function(id) {
if (M.ws) {
const oldWs = M.ws;
M.ws = null;
oldWs.onclose = null;
oldWs.close();
}
M.currentStreamId = id;
const proto = location.protocol === "https:" ? "wss:" : "ws:";
M.ws = new WebSocket(proto + "//" + location.host + "/api/streams/" + id + "/ws");
M.ws.onmessage = (e) => {
const data = JSON.parse(e.data);
// Handle library updates
if (data.type === "track_added") {
M.showToast(`"${data.track.title}" is now available`);
if (data.library) {
M.library = data.library;
M.renderLibrary();
if (M.selectedPlaylistId === "all") {
M.playlist = [...M.library];
M.renderPlaylist();
}
}
return;
}
if (data.type === "track_removed") {
M.showToast(`"${data.track.title}" was removed`);
if (data.library) {
M.library = data.library;
M.renderLibrary();
if (M.selectedPlaylistId === "all") {
M.playlist = [...M.library];
M.renderPlaylist();
}
}
return;
}
// Normal stream update
M.handleUpdate(data);
};
M.ws.onclose = () => {
M.synced = false;
M.ws = null;
M.$("#sync-indicator").classList.add("disconnected");
M.updateUI();
// Auto-reconnect if user wants to be synced
if (M.wantSync) {
setTimeout(() => M.connectStream(id), 3000);
}
};
M.ws.onopen = () => {
M.synced = true;
M.$("#sync-indicator").classList.remove("disconnected");
M.updateUI();
};
};
// Handle stream state update from server
M.handleUpdate = async function(data) {
if (!data.track) {
M.$("#track-title").textContent = "No tracks";
return;
}
M.$("#stream-name").textContent = data.streamName || "";
M.serverTimestamp = data.currentTimestamp;
M.serverTrackDuration = data.track.duration;
M.lastServerUpdate = Date.now();
const wasServerPaused = M.serverPaused;
M.serverPaused = data.paused ?? true;
// Update playlist if provided
if (data.playlist) {
M.playlist = data.playlist;
M.currentIndex = data.currentIndex ?? 0;
M.renderPlaylist();
} else if (data.currentIndex !== undefined && data.currentIndex !== M.currentIndex) {
M.currentIndex = data.currentIndex;
M.renderPlaylist();
}
// Cache track info for local mode
const isNewTrack = data.track.filename !== M.currentFilename;
if (isNewTrack) {
M.currentFilename = data.track.filename;
M.currentTitle = data.track.title;
M.$("#track-title").textContent = data.track.title;
M.loadingSegments.clear();
}
if (M.synced) {
if (!M.serverPaused) {
// Server is playing - ensure we're playing and synced
if (isNewTrack || !M.audio.src) {
// Try cache first
const cachedUrl = await M.loadTrackBlob(M.currentFilename);
M.audio.src = cachedUrl || M.getTrackUrl(M.currentFilename);
}
if (M.audio.paused) {
M.audio.currentTime = data.currentTimestamp;
M.audio.play().catch(() => {});
} else {
// Check drift
const drift = Math.abs(M.audio.currentTime - data.currentTimestamp);
if (drift >= 2) {
M.audio.currentTime = data.currentTimestamp;
}
}
} else if (!wasServerPaused && M.serverPaused) {
// Server just paused
M.audio.pause();
}
}
M.updateUI();
};
})();