feat: add live listener count
This commit is contained in:
13
.gitignore
vendored
13
.gitignore
vendored
@@ -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
|
||||
|
37
server.py
37
server.py
@@ -3,10 +3,11 @@ import os
|
||||
|
||||
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 @@ t = None
|
||||
# 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")
|
||||
|
@@ -27,11 +27,16 @@
|
||||
<div class="button-container">
|
||||
<button id="play-btn" class="button">play</button>
|
||||
</div>
|
||||
|
||||
<div class="status-bar">
|
||||
<p id="listener-count">0 person tuned in</p>
|
||||
<div class="volume-slider-container">
|
||||
<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>
|
||||
</main>
|
||||
|
||||
<img class="cat" src="./images/cat-0.png">
|
||||
<img class="eeping-cat" src="./images/eeping-cat.png">
|
||||
</div>
|
||||
|
@@ -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();
|
||||
|
@@ -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;
|
||||
|
19
websocket_connection_manager.py
Normal file
19
websocket_connection_manager.py
Normal 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]
|
||||
)
|
Reference in New Issue
Block a user