170 lines
5.3 KiB
JavaScript
170 lines
5.3 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 - use track.id (content hash) as the identifier
|
|
const trackId = data.track.id || data.track.filename; // Fallback for compatibility
|
|
const isNewTrack = trackId !== M.currentTrackId;
|
|
if (isNewTrack) {
|
|
M.currentTrackId = trackId;
|
|
M.currentTitle = data.track.title;
|
|
M.$("#track-title").textContent = data.track.title;
|
|
M.loadingSegments.clear();
|
|
|
|
// Debug: log cache state for this track
|
|
const trackCache = M.trackCaches.get(trackId);
|
|
console.log("[Playback] Starting track:", data.track.title, {
|
|
trackId: trackId,
|
|
segments: trackCache ? [...trackCache] : [],
|
|
segmentCount: trackCache ? trackCache.size : 0,
|
|
inCachedTracks: M.cachedTracks.has(trackId),
|
|
bulkStarted: M.bulkDownloadStarted.get(trackId) || false,
|
|
hasBlobUrl: M.trackBlobs.has(trackId)
|
|
});
|
|
|
|
// Check if this track already has all segments cached
|
|
M.checkAndCacheComplete(trackId);
|
|
}
|
|
|
|
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.currentTrackId);
|
|
M.audio.src = cachedUrl || M.getTrackUrl(M.currentTrackId);
|
|
}
|
|
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();
|
|
};
|
|
})();
|