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
-
-
-
+
+
+
0 person tuned in
+
+
+
+
+
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]
+ )