Scale display player by switching from SSE to polling

This commit is contained in:
2026-01-24 19:59:00 +01:00
parent a9a1a6cdbe
commit 4df004c18a
2 changed files with 24 additions and 48 deletions

View File

@@ -29,10 +29,14 @@ def _enforce_and_touch_display_session(display: Display, sid: str | None):
return True, None
cutoff = datetime.utcnow() - timedelta(seconds=SESSION_TTL_SECONDS)
# Cleanup old sessions. Avoid committing if nothing was deleted (saves write locks on SQLite).
deleted = (
DisplaySession.query.filter(
DisplaySession.display_id == display.id,
DisplaySession.last_seen_at < cutoff,
).delete(synchronize_session=False)
)
if deleted:
db.session.commit()
existing = DisplaySession.query.filter_by(display_id=display.id, sid=sid).first()

View File

@@ -38,9 +38,6 @@
let idx = 0;
let timer = null;
let es = null;
let esRetryMs = 1000;
async function fetchPlaylist() {
const qs = sid ? `?sid=${encodeURIComponent(sid)}` : '';
const res = await fetch(`/api/display/${token}/playlist${qs}`, { cache: 'no-store' });
@@ -115,37 +112,17 @@
// keep retrying; if a slot frees up the display will start automatically.
}
// Open live event stream: when server signals a change, reload playlist immediately.
connectEvents();
// Fallback refresh (in case SSE is blocked by a proxy/network): every 5 minutes.
// Poll for updates instead of SSE.
// This scales better for 100s of displays because it avoids long-lived HTTP
// connections (which otherwise tie up gunicorn sync workers).
// Default: check every 20s (can be overridden via ?poll=seconds).
const pollParam = parseInt(new URLSearchParams(window.location.search).get('poll') || '', 10);
const pollSeconds = Number.isFinite(pollParam) && pollParam > 0 ? pollParam : 20;
setInterval(async () => {
try {
playlist = await fetchPlaylist();
if (!stage.firstChild) {
idx = 0;
next();
}
} catch(e) {
clearStage();
setNotice(e && e.message ? e.message : 'Unable to load playlist.');
}
}, 300000);
}
function connectEvents() {
if (isPreview) return; // preview shouldn't consume a slot / keep a long-lived connection
try { if (es) es.close(); } catch(e) { /* ignore */ }
const qs = sid ? `?sid=${encodeURIComponent(sid)}` : '';
es = new EventSource(`/api/display/${token}/events${qs}`);
es.addEventListener('changed', async () => {
try {
const newPlaylist = await fetchPlaylist();
// If content changed, restart from the beginning.
// Restart if something changed.
const oldStr = JSON.stringify(playlist);
const newStr = JSON.stringify(newPlaylist);
playlist = newPlaylist;
@@ -154,21 +131,16 @@
next();
}
esRetryMs = 1000; // reset backoff on success
} catch(e) {
// leave current playback; we'll retry via reconnect handler
// If player is blank (e.g. after a temporary error), kick it.
if (!stage.firstChild) {
idx = 0;
next();
}
});
es.onerror = () => {
try { es.close(); } catch(e) { /* ignore */ }
es = null;
// Exponential backoff up to 30s
const wait = esRetryMs;
esRetryMs = Math.min(30000, Math.floor(esRetryMs * 1.7));
setTimeout(connectEvents, wait);
};
} catch(e) {
clearStage();
setNotice(e && e.message ? e.message : 'Unable to load playlist.');
}
}, pollSeconds * 1000);
}
start();