feat: add live listener count

This commit is contained in:
2024-07-26 22:34:44 +01:00
parent eae55aeb94
commit 45853a4d55
6 changed files with 138 additions and 17 deletions

13
.gitignore vendored
View File

@@ -4,8 +4,7 @@
### Linux ### ### Linux ###
*~ *~
# temporary files which can be created if a process still has a handle open of a deleted file # temporary files which can be created if a process still has a handle open of a deleted file .fuse_hidden*
.fuse_hidden*
# KDE directory preferences # KDE directory preferences
.directory .directory
@@ -249,3 +248,13 @@ $RECYCLE.BIN/
# End of https://www.toptal.com/developers/gitignore/api/python,windows,macos,linux # 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

View File

@@ -3,10 +3,11 @@ import os
import websocket import websocket
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from logger import log_info, log_warn from logger import log_info, log_warn
from websocket_connection_manager import WebSocketConnectionManager
# the index of the current audio track from 0 to 9 # the index of the current audio track from 0 to 9
current_index = -1 current_index = -1
@@ -15,6 +16,8 @@ t = None
# websocket connection to the inference server # websocket connection to the inference server
ws = None ws = None
ws_url = "" ws_url = ""
ws_connection_manager = WebSocketConnectionManager()
active_listeners = set()
@asynccontextmanager @asynccontextmanager
@@ -83,7 +86,6 @@ def advance():
current_index = 0 current_index = 0
else: else:
current_index = current_index + 1 current_index = current_index + 1
threading.Thread(target=generate_new_audio).start() threading.Thread(target=generate_new_audio).start()
t = threading.Timer(60, advance) t = threading.Timer(60, advance)
@@ -98,4 +100,35 @@ def get_current_audio():
return FileResponse(f"{current_index}.mp3") 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") app.mount("/", StaticFiles(directory="web", html=True), name="web")

View File

@@ -27,11 +27,16 @@
<div class="button-container"> <div class="button-container">
<button id="play-btn" class="button">play</button> <button id="play-btn" class="button">play</button>
</div> </div>
<div class="status-bar">
<p id="listener-count">0 person tuned in</p>
<div class="volume-slider-container"> <div class="volume-slider-container">
<output id="current-volume-label" for="volume-slider">100%</output> <output id="current-volume-label" for="volume-slider">100%</output>
<input id="volume-slider" type="range" min="0" max="100" step="1"> <input id="volume-slider" type="range" min="0" max="100" step="1" />
</div>
</div> </div>
</main> </main>
<img class="cat" src="./images/cat-0.png"> <img class="cat" src="./images/cat-0.png">
<img class="eeping-cat" src="./images/eeping-cat.png"> <img class="eeping-cat" src="./images/eeping-cat.png">
</div> </div>

View File

@@ -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 playBtn = document.getElementById("play-btn");
const catImg = document.getElementsByClassName("cat")[0]; const catImg = document.getElementsByClassName("cat")[0];
const volumeSlider = document.getElementById("volume-slider"); const volumeSlider = document.getElementById("volume-slider");
@@ -5,16 +9,14 @@ const currentVolumeLabel = document.getElementById("current-volume-label");
const clickAudio = document.getElementById("click-audio"); const clickAudio = document.getElementById("click-audio");
const clickReleaseAudio = document.getElementById("click-release-audio"); const clickReleaseAudio = document.getElementById("click-release-audio");
const meowAudio = document.getElementById("meow-audio"); const meowAudio = document.getElementById("meow-audio");
const listenerCountLabel = document.getElementById("listener-count");
const CROSSFADE_DURATION_MS = 5000;
const CROSSFADE_INTERVAL_MS = 20;
const AUDIO_DURATION_MS = 60000;
let isPlaying = false; let isPlaying = false;
let isFading = false; let isFading = false;
let currentAudio; let currentAudio;
let maxVolume = 100; let maxVolume = 100;
let currentVolume = 0; let currentVolume = 0;
let ws = connectToWebSocket();
function playAudio() { function playAudio() {
// add a random query parameter at the end to prevent browser caching // add a random query parameter at the end to prevent browser caching
@@ -22,11 +24,17 @@ function playAudio() {
currentAudio.onplay = () => { currentAudio.onplay = () => {
isPlaying = true; isPlaying = true;
playBtn.innerText = "pause"; playBtn.innerText = "pause";
if (ws) {
ws.send("playing");
}
}; };
currentAudio.onpause = () => { currentAudio.onpause = () => {
isPlaying = false; isPlaying = false;
currentVolume = 0; currentVolume = 0;
playBtn.innerText = "play"; playBtn.innerText = "play";
if (ws) {
ws.send("paused");
}
}; };
currentAudio.onended = () => { currentAudio.onended = () => {
currentVolume = 0; 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 = () => { playBtn.onmousedown = () => {
clickAudio.play(); clickAudio.play();
document.addEventListener( document.addEventListener(
@@ -157,7 +190,15 @@ volumeSlider.oninput = () => {
clickReleaseAudio.volume = volumeSlider.value / 100; clickReleaseAudio.volume = volumeSlider.value / 100;
meowAudio.volume = volumeSlider.value / 100; meowAudio.volume = volumeSlider.value / 100;
}; };
volumeSlider.value = 100; volumeSlider.value = 100;
window.addEventListener("offline", () => {
ws = null;
});
window.addEventListener("online", () => {
ws = connectToWebSocket();
});
animateCat(); animateCat();
enableSpaceBarControl(); enableSpaceBarControl();

View File

@@ -108,6 +108,25 @@ a {
border-radius: 2px; 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 { .header {
font-weight: 800; font-weight: 800;
margin-bottom: 1rem; margin-bottom: 1rem;
@@ -152,11 +171,6 @@ a {
} }
.volume-slider-container { .volume-slider-container {
position: absolute;
top: 0;
right: 0;
padding: 0.5rem 1rem;
margin: 1rem;
display: flex; display: flex;
justify-content: start; justify-content: start;
align-items: center; align-items: center;

View File

@@ -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]
)