blastoise-archive/public/index.html

194 lines
6.9 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MusicRoom</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #111; color: #eee; display: flex; justify-content: center; align-items: center; min-height: 100vh; }
#app { width: 100%; max-width: 480px; padding: 2rem; }
h1 { font-size: 1.2rem; color: #888; margin-bottom: 1.5rem; }
#stream-select { margin-bottom: 1.5rem; }
#stream-select select { background: #222; color: #eee; border: 1px solid #333; padding: 0.4rem 0.8rem; border-radius: 4px; font-size: 0.9rem; }
#now-playing { margin-bottom: 1rem; }
#track-name { font-size: 1.4rem; font-weight: 600; margin-bottom: 0.3rem; }
#stream-name { font-size: 0.85rem; color: #888; }
#progress-container { background: #222; border-radius: 4px; height: 6px; margin: 1rem 0; cursor: pointer; }
#progress-bar { background: #555; height: 100%; border-radius: 4px; width: 0%; transition: width 0.3s linear; }
#progress-bar.playing { background: #4e8; }
#time { display: flex; justify-content: space-between; font-size: 0.8rem; color: #888; margin-bottom: 1rem; }
#controls { display: flex; gap: 1rem; align-items: center; }
#volume { width: 100px; accent-color: #4e8; }
button { background: #222; color: #eee; border: 1px solid #333; padding: 0.5rem 1.2rem; border-radius: 4px; cursor: pointer; font-size: 0.9rem; }
button:hover { background: #333; }
#status { margin-top: 1.5rem; font-size: 0.8rem; color: #666; }
.empty { color: #666; font-style: italic; }
</style>
</head>
<body>
<div id="app">
<h1>MusicRoom</h1>
<div id="stream-select"></div>
<div id="now-playing">
<div id="track-name" class="empty">Loading...</div>
<div id="stream-name"></div>
</div>
<div id="progress-container"><div id="progress-bar"></div></div>
<div id="time"><span id="time-current">0:00</span><span id="time-total">0:00</span></div>
<div id="controls">
<button id="btn-listen">Unlisten</button>
<button id="btn-play">Play</button>
<button id="btn-pause" disabled>Pause</button>
<button id="btn-stop">Stop</button>
<button id="btn-server-pause">Server Pause</button>
<input type="range" id="volume" min="0" max="1" step="0.01" value="1">
</div>
<div id="status"></div>
</div>
<script>
(function() {
const audio = new Audio();
let ws = null;
let currentStreamId = null;
let currentFilename = null;
let serverTimestamp = 0;
let serverTrackDuration = 0;
let lastServerUpdate = 0;
let serverPaused = false;
let listening = true;
const $ = (s) => document.querySelector(s);
function fmt(sec) {
if (!sec || !isFinite(sec)) return "0:00";
const m = Math.floor(sec / 60);
const s = Math.floor(sec % 60);
return m + ":" + String(s).padStart(2, "0");
}
function getServerTime() {
if (serverPaused) return serverTimestamp;
return serverTimestamp + (Date.now() - lastServerUpdate) / 1000;
}
// Update progress bar from server time (always), use audio time when playing
setInterval(() => {
if (!listening || serverTrackDuration <= 0) return;
const t = audio.paused ? getServerTime() : audio.currentTime;
const dur = audio.paused ? serverTrackDuration : (audio.duration || serverTrackDuration);
const pct = Math.min((t / dur) * 100, 100);
$("#progress-bar").style.width = pct + "%";
$("#time-current").textContent = fmt(t);
$("#time-total").textContent = fmt(dur);
}, 250);
// Load streams
async function loadStreams() {
const res = await fetch("/api/streams");
const streams = await res.json();
if (streams.length === 0) {
$("#track-name").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 = () => connectStream(sel.value);
$("#stream-select").appendChild(sel);
}
connectStream(streams[0].id);
}
function connectStream(id) {
if (ws) ws.close();
currentStreamId = id;
listening = true;
$("#btn-listen").textContent = "Unlisten";
const proto = location.protocol === "https:" ? "wss:" : "ws:";
ws = new WebSocket(proto + "//" + location.host + "/api/streams/" + id + "/ws");
ws.onmessage = (e) => handleUpdate(JSON.parse(e.data));
ws.onclose = () => { $("#status").textContent = "Disconnected. Reconnecting..."; setTimeout(() => { if (listening) connectStream(id); }, 3000); };
ws.onopen = () => { $("#status").textContent = "Connected"; };
}
function handleUpdate(data) {
if (!data.track) {
$("#track-name").textContent = "No tracks";
return;
}
$("#stream-name").textContent = data.streamName || "";
serverTimestamp = data.currentTimestamp;
serverTrackDuration = data.track.duration;
lastServerUpdate = Date.now();
serverPaused = data.paused || false;
if (data.track.filename !== currentFilename) {
// New track
currentFilename = data.track.filename;
$("#track-name").textContent = data.track.title;
if (!audio.paused) {
audio.src = "/api/tracks/" + encodeURIComponent(data.track.filename);
audio.currentTime = data.currentTimestamp;
audio.play().catch(() => {});
}
} else if (!audio.paused) {
// Same track - check drift
const drift = Math.abs(audio.currentTime - data.currentTimestamp);
if (drift >= 2) {
audio.currentTime = data.currentTimestamp;
}
}
}
$("#btn-listen").onclick = () => {
if (listening) {
listening = false;
if (ws) ws.close();
$("#btn-listen").textContent = "Listen";
$("#status").textContent = "Not listening";
} else {
connectStream(currentStreamId);
}
};
$("#btn-play").onclick = () => {
if (!currentFilename) return;
if (!audio.src) {
audio.src = "/api/tracks/" + encodeURIComponent(currentFilename);
}
audio.currentTime = getServerTime();
audio.play();
};
$("#btn-pause").onclick = () => {
audio.pause();
};
$("#btn-stop").onclick = () => {
audio.pause();
audio.src = "";
};
$("#btn-server-pause").onclick = () => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ action: serverPaused ? "unpause" : "pause" }));
}
};
$("#volume").oninput = (e) => { audio.volume = e.target.value; };
audio.onplay = () => { $("#btn-play").disabled = true; $("#btn-pause").disabled = false; $("#progress-bar").classList.add("playing"); };
audio.onpause = () => { $("#btn-play").disabled = false; $("#btn-pause").disabled = true; $("#progress-bar").classList.remove("playing"); };
loadStreams();
})();
</script>
</body>
</html>