document.addEventListener("DOMContentLoaded", () => { // --- DOM Elements --- const joinScreen = document.getElementById("join-screen"); const roomScreen = document.getElementById("room-screen"); const roomNameInput = document.getElementById("room-name-input"); const userNameInput = document.getElementById("user-name-input"); const joinBtn = document.getElementById("join-btn"); const errorMessage = document.getElementById("error-message"); const roomHeader = document.getElementById("room-header"); const userList = document.getElementById("user-list"); const nowPlaying = document.getElementById("now-playing"); const audioPlayer = document.getElementById("audio-player"); const playPauseBtn = document.getElementById("play-pause-btn"); const seekBar = document.getElementById("seek-bar"); const timeDisplay = document.getElementById("time-display"); const playlistElement = document.getElementById("playlist"); const adminControls = document.getElementById("admin-controls"); const fileInput = document.getElementById("file-input"); const uploadBtn = document.getElementById("upload-btn"); const uploadStatus = document.getElementById("upload-status"); // --- State --- let ws = null; let roomName = ""; let userName = ""; let userId = ""; let isAdmin = false; let isSeeking = false; // --- Event Listeners --- joinBtn.addEventListener("click", joinRoom); playPauseBtn.addEventListener("click", togglePlayPause); uploadBtn.addEventListener("click", uploadFile); seekBar.addEventListener("input", () => { isSeeking = true; }); seekBar.addEventListener("change", () => { isSeeking = false; if (isAdmin) { const newPosition = parseFloat(seekBar.value); audioPlayer.currentTime = newPosition; sendMessage({ action: "seek", position: newPosition }); } }); audioPlayer.addEventListener("timeupdate", updateSeekBar); audioPlayer.addEventListener("loadedmetadata", updateSeekBar); // --- Functions --- function joinRoom() { roomName = roomNameInput.value.trim(); userName = userNameInput.value.trim(); if (!roomName || !userName) { errorMessage.textContent = "Room and User name are required."; return; } errorMessage.textContent = ""; const wsProtocol = window.location.protocol === "https:" ? "wss://" : "ws://"; const wsURL = `${wsProtocol}${window.location.host}/ws/${encodeURIComponent(roomName)}/${encodeURIComponent(userName)}`; ws = new WebSocket(wsURL); ws.onopen = () => { console.log("WebSocket connection established."); joinScreen.classList.remove("active"); roomScreen.classList.add("active"); roomHeader.textContent = `Room: ${roomName}`; }; ws.onmessage = (event) => { const message = JSON.parse(event.data); handleWebSocketMessage(message); }; ws.onerror = (error) => { console.error("WebSocket error:", error); errorMessage.textContent = "Failed to connect to the room."; }; ws.onclose = () => { console.log("WebSocket connection closed."); alert("Connection lost. Please refresh the page to reconnect."); roomScreen.classList.remove("active"); joinScreen.classList.add("active"); }; } function handleWebSocketMessage(message) { console.log("Received message:", message); switch (message.type) { case "initial_state": userId = message.user_id; updateState(message.payload); break; case "state_update": updateState(message.payload); break; case "user_list_update": // Handles only user changes without interrupting playback updateUI(message.payload); break; } } function updateState(state) { // Determine admin status before updating the UI isAdmin = state.admin_user_id === userId; updateUI(state); // Sync audio player const currentTrackUrl = state.current_track ? `/tmp/${encodeURIComponent(state.current_track)}` : ""; if (state.current_track && audioPlayer.src.endsWith(currentTrackUrl) === false) { audioPlayer.src = currentTrackUrl; } if (!state.current_track) { audioPlayer.src = ""; nowPlaying.textContent = "Now Playing: Nothing"; return; } // Avoid race conditions by only applying server state if not currently seeking if (!isSeeking) { // Correct for latency by gently nudging the time const timeDifference = Math.abs(audioPlayer.currentTime - state.position); if (timeDifference > 1.5) { // Resync if more than 1.5s out of sync audioPlayer.currentTime = state.position; } } if (state.is_playing && audioPlayer.paused) { audioPlayer.play().catch(e => console.error("Autoplay failed:", e)); } else if (!state.is_playing && !audioPlayer.paused) { audioPlayer.pause(); } } function updateUI(state) { // Update User List userList.innerHTML = "Users: " + Object.values(state.users) .map(u => `${u.name}${state.admin_user_id === Object.keys(state.users).find(key => state.users[key] === u) ? ' (Admin)' : ''}`) .join(", "); // Update Now Playing nowPlaying.textContent = state.current_track ? `Now Playing: ${state.current_track}` : "Now Playing: Nothing"; // Update Playlist playlistElement.innerHTML = ""; state.playlist.forEach(track => { const li = document.createElement("li"); li.textContent = track; if (track === state.current_track) { li.classList.add("active"); } if (isAdmin) { const playTrackBtn = document.createElement("button"); playTrackBtn.textContent = "Play"; playTrackBtn.className = "play-track-btn"; playTrackBtn.onclick = () => { sendMessage({ action: "change_track", track: track }); }; li.appendChild(playTrackBtn); } playlistElement.appendChild(li); }); // Toggle Admin Controls const isCurrentlyAdmin = state.admin_user_id === userId; if (isCurrentlyAdmin) { adminControls.style.display = "block"; playPauseBtn.disabled = false; seekBar.disabled = false; } else { adminControls.style.display = "none"; playPauseBtn.disabled = true; seekBar.disabled = true; } // Update Play/Pause button text playPauseBtn.textContent = state.is_playing ? "Pause" : "Play"; } function togglePlayPause() { if (!isAdmin) return; const isPlaying = !audioPlayer.paused; const action = isPlaying ? "pause" : "play"; sendMessage({ action: action, position: audioPlayer.currentTime }); } function updateSeekBar() { if (!isSeeking) { seekBar.max = audioPlayer.duration || 0; seekBar.value = audioPlayer.currentTime; } timeDisplay.textContent = `${formatTime(audioPlayer.currentTime)} / ${formatTime(audioPlayer.duration || 0)}`; } function formatTime(seconds) { const minutes = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${minutes}:${secs.toString().padStart(2, '0')}`; } async function uploadFile() { if (!fileInput.files.length) { uploadStatus.textContent = "Please select a file."; return; } const file = fileInput.files[0]; const formData = new FormData(); formData.append("file", file); uploadStatus.textContent = "Uploading..."; try { const response = await fetch(`/upload/${encodeURIComponent(roomName)}`, { method: "POST", body: formData, }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.detail || "Upload failed"); } const result = await response.json(); uploadStatus.textContent = `Upload successful: ${result.filename}`; fileInput.value = ""; // Reset file input } catch (error) { uploadStatus.textContent = `Error: ${error.message}`; console.error("Upload error:", error); } } function sendMessage(message) { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify(message)); } } });