Scale display player by switching from SSE to polling
This commit is contained in:
@@ -29,10 +29,14 @@ def _enforce_and_touch_display_session(display: Display, sid: str | None):
|
|||||||
return True, None
|
return True, None
|
||||||
|
|
||||||
cutoff = datetime.utcnow() - timedelta(seconds=SESSION_TTL_SECONDS)
|
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.query.filter(
|
||||||
DisplaySession.display_id == display.id,
|
DisplaySession.display_id == display.id,
|
||||||
DisplaySession.last_seen_at < cutoff,
|
DisplaySession.last_seen_at < cutoff,
|
||||||
).delete(synchronize_session=False)
|
).delete(synchronize_session=False)
|
||||||
|
)
|
||||||
|
if deleted:
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
existing = DisplaySession.query.filter_by(display_id=display.id, sid=sid).first()
|
existing = DisplaySession.query.filter_by(display_id=display.id, sid=sid).first()
|
||||||
|
|||||||
@@ -38,9 +38,6 @@
|
|||||||
let idx = 0;
|
let idx = 0;
|
||||||
let timer = null;
|
let timer = null;
|
||||||
|
|
||||||
let es = null;
|
|
||||||
let esRetryMs = 1000;
|
|
||||||
|
|
||||||
async function fetchPlaylist() {
|
async function fetchPlaylist() {
|
||||||
const qs = sid ? `?sid=${encodeURIComponent(sid)}` : '';
|
const qs = sid ? `?sid=${encodeURIComponent(sid)}` : '';
|
||||||
const res = await fetch(`/api/display/${token}/playlist${qs}`, { cache: 'no-store' });
|
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.
|
// keep retrying; if a slot frees up the display will start automatically.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open live event stream: when server signals a change, reload playlist immediately.
|
// Poll for updates instead of SSE.
|
||||||
connectEvents();
|
// This scales better for 100s of displays because it avoids long-lived HTTP
|
||||||
|
// connections (which otherwise tie up gunicorn sync workers).
|
||||||
// Fallback refresh (in case SSE is blocked by a proxy/network): every 5 minutes.
|
// 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 () => {
|
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 {
|
try {
|
||||||
const newPlaylist = await fetchPlaylist();
|
const newPlaylist = await fetchPlaylist();
|
||||||
|
|
||||||
// If content changed, restart from the beginning.
|
// Restart if something changed.
|
||||||
const oldStr = JSON.stringify(playlist);
|
const oldStr = JSON.stringify(playlist);
|
||||||
const newStr = JSON.stringify(newPlaylist);
|
const newStr = JSON.stringify(newPlaylist);
|
||||||
playlist = newPlaylist;
|
playlist = newPlaylist;
|
||||||
@@ -154,21 +131,16 @@
|
|||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
esRetryMs = 1000; // reset backoff on success
|
// If player is blank (e.g. after a temporary error), kick it.
|
||||||
} catch(e) {
|
if (!stage.firstChild) {
|
||||||
// leave current playback; we'll retry via reconnect handler
|
idx = 0;
|
||||||
|
next();
|
||||||
}
|
}
|
||||||
});
|
} catch(e) {
|
||||||
|
clearStage();
|
||||||
es.onerror = () => {
|
setNotice(e && e.message ? e.message : 'Unable to load playlist.');
|
||||||
try { es.close(); } catch(e) { /* ignore */ }
|
}
|
||||||
es = null;
|
}, pollSeconds * 1000);
|
||||||
|
|
||||||
// Exponential backoff up to 30s
|
|
||||||
const wait = esRetryMs;
|
|
||||||
esRetryMs = Math.min(30000, Math.floor(esRetryMs * 1.7));
|
|
||||||
setTimeout(connectEvents, wait);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
start();
|
start();
|
||||||
|
|||||||
Reference in New Issue
Block a user