diff --git a/.gitignore b/.gitignore index 2e60826..5502dcb 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,7 @@ ### Linux ### *~ -# temporary files which can be created if a process still has a handle open of a deleted file -.fuse_hidden* +# temporary files which can be created if a process still has a handle open of a deleted file .fuse_hidden* # KDE directory preferences .directory @@ -249,3 +248,13 @@ $RECYCLE.BIN/ # End of https://www.toptal.com/developers/gitignore/api/python,windows,macos,linux +0.mp3 +1.mp3 +2.mp3 +3.mp3 +4.mp3 +5.mp3 +6.mp3 +7.mp3 +8.mp3 +9.mp3 diff --git a/server.py b/server.py index d6d7f70..3830443 100644 --- a/server.py +++ b/server.py @@ -3,10 +3,11 @@ import websocket from contextlib import asynccontextmanager -from fastapi import FastAPI +from fastapi import FastAPI, WebSocket, WebSocketDisconnect from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from logger import log_info, log_warn +from websocket_connection_manager import WebSocketConnectionManager # the index of the current audio track from 0 to 9 current_index = -1 @@ -15,6 +16,8 @@ # websocket connection to the inference server ws = None ws_url = "" +ws_connection_manager = WebSocketConnectionManager() +active_listeners = set() @asynccontextmanager @@ -83,7 +86,6 @@ def advance(): current_index = 0 else: current_index = current_index + 1 - threading.Thread(target=generate_new_audio).start() t = threading.Timer(60, advance) @@ -98,4 +100,35 @@ def get_current_audio(): return FileResponse(f"{current_index}.mp3") +@app.websocket("/ws") +async def ws_endpoint(ws: WebSocket): + await ws_connection_manager.connect(ws) + + addr = "" + if ws.client: + addr, _ = ws.client + else: + await ws.close() + ws_connection_manager.disconnect(ws) + + try: + while True: + msg = await ws.receive_text() + + if msg == "playing": + active_listeners.add(addr) + await ws_connection_manager.broadcast(f"{len(active_listeners)}") + elif msg == "paused": + active_listeners.remove(addr) + await ws_connection_manager.broadcast(f"{len(active_listeners)}") + + except WebSocketDisconnect: + if ws.client: + addr, _ = ws.client + active_listeners.discard(addr) + ws_connection_manager.disconnect(ws) + + await ws_connection_manager.broadcast(f"{len(active_listeners)}") + + app.mount("/", StaticFiles(directory="web", html=True), name="web") diff --git a/web/index.html b/web/index.html index 0691e29..c881cf2 100644 --- a/web/index.html +++ b/web/index.html @@ -27,11 +27,16 @@

infinite lo-fi music in the background

-
- 100% - + +
+

0 person tuned in

+
+ 100% + +
+
diff --git a/web/script.js b/web/script.js index 8e1af08..9971532 100644 --- a/web/script.js +++ b/web/script.js @@ -1,3 +1,7 @@ +const CROSSFADE_DURATION_MS = 5000; +const CROSSFADE_INTERVAL_MS = 20; +const AUDIO_DURATION_MS = 60000; + const playBtn = document.getElementById("play-btn"); const catImg = document.getElementsByClassName("cat")[0]; const volumeSlider = document.getElementById("volume-slider"); @@ -5,16 +9,14 @@ const currentVolumeLabel = document.getElementById("current-volume-label"); const clickAudio = document.getElementById("click-audio"); const clickReleaseAudio = document.getElementById("click-release-audio"); const meowAudio = document.getElementById("meow-audio"); - -const CROSSFADE_DURATION_MS = 5000; -const CROSSFADE_INTERVAL_MS = 20; -const AUDIO_DURATION_MS = 60000; +const listenerCountLabel = document.getElementById("listener-count"); let isPlaying = false; let isFading = false; let currentAudio; let maxVolume = 100; let currentVolume = 0; +let ws = connectToWebSocket(); function playAudio() { // add a random query parameter at the end to prevent browser caching @@ -22,11 +24,17 @@ function playAudio() { currentAudio.onplay = () => { isPlaying = true; playBtn.innerText = "pause"; + if (ws) { + ws.send("playing"); + } }; currentAudio.onpause = () => { isPlaying = false; currentVolume = 0; playBtn.innerText = "play"; + if (ws) { + ws.send("paused"); + } }; currentAudio.onended = () => { currentVolume = 0; @@ -122,6 +130,31 @@ function enableSpaceBarControl() { }); } +function connectToWebSocket() { + const ws = new WebSocket(`ws://${location.host}/ws`); + ws.onmessage = (event) => { + console.log(event.data); + + if (typeof event.data !== "string") { + return; + } + + const listenerCountStr = event.data; + const listenerCount = Number.parseInt(listenerCountStr); + if (Number.isNaN(listenerCount)) { + return; + } + + if (listenerCount <= 1) { + listenerCountLabel.innerText = `${listenerCount} person tuned in`; + } else { + listenerCountLabel.innerText = `${listenerCount} ppl tuned in`; + } + }; + + return ws; +} + playBtn.onmousedown = () => { clickAudio.play(); document.addEventListener( @@ -157,7 +190,15 @@ volumeSlider.oninput = () => { clickReleaseAudio.volume = volumeSlider.value / 100; meowAudio.volume = volumeSlider.value / 100; }; - volumeSlider.value = 100; + +window.addEventListener("offline", () => { + ws = null; +}); + +window.addEventListener("online", () => { + ws = connectToWebSocket(); +}); + animateCat(); enableSpaceBarControl(); diff --git a/web/style.css b/web/style.css index 537ec54..5611991 100644 --- a/web/style.css +++ b/web/style.css @@ -108,6 +108,25 @@ a { border-radius: 2px; } +.status-bar { + position: absolute; + top: 0; + left: 0; + right: 0; + z-index: -10; + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 2rem; + z-index: 0; + color: var(--text); +} + +.status-bar > #listener-count { + margin: 0; + opacity: 0.8; +} + .header { font-weight: 800; margin-bottom: 1rem; @@ -152,11 +171,6 @@ a { } .volume-slider-container { - position: absolute; - top: 0; - right: 0; - padding: 0.5rem 1rem; - margin: 1rem; display: flex; justify-content: start; align-items: center; diff --git a/websocket_connection_manager.py b/websocket_connection_manager.py new file mode 100644 index 0000000..779fdac --- /dev/null +++ b/websocket_connection_manager.py @@ -0,0 +1,19 @@ +import asyncio +from fastapi import WebSocket + + +class WebSocketConnectionManager: + def __init__(self) -> None: + self.__active_connections: list[WebSocket] = [] + + async def connect(self, ws: WebSocket): + await ws.accept() + self.__active_connections.append(ws) + + def disconnect(self, ws: WebSocket): + self.__active_connections.remove(ws) + + async def broadcast(self, msg: str): + await asyncio.gather( + *[conn.send_text(msg) for conn in self.__active_connections] + )