mirror of
https://github.com/fishtank-dashboard/fishtank-dashboard.git
synced 2026-04-30 10:42:02 -04:00
minor fixes
This commit is contained in:
committed by
GitHub
parent
2f50144fe7
commit
1c63a3c95f
@@ -735,6 +735,118 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Camera reconnecting overlay */
|
||||
.cam-reconnecting {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0,0,0,0.7);
|
||||
gap: 10px;
|
||||
pointer-events: none;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.cam-reconnecting-spinner {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 2px solid rgba(255,255,255,0.15);
|
||||
border-top-color: var(--accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
.cam-reconnecting-label {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 9px;
|
||||
color: var(--muted);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.cam-featured-wrap .cam-reconnecting-spinner {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.cam-featured-wrap .cam-reconnecting-label {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* Viewer count badge */
|
||||
.cam-viewers {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
background: rgba(0,0,0,0.65);
|
||||
color: #fff;
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 9px;
|
||||
padding: 2px 5px;
|
||||
border-radius: 3px;
|
||||
pointer-events: none;
|
||||
letter-spacing: 0.5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.cam-viewers::before {
|
||||
content: '';
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cam-featured-wrap .cam-viewers {
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
font-size: 11px;
|
||||
padding: 3px 8px;
|
||||
}
|
||||
|
||||
/* Camera reconnecting overlay */
|
||||
.cam-reconnecting {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0,0,0,0.7);
|
||||
gap: 10px;
|
||||
pointer-events: none;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.cam-reconnecting-spinner {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 2px solid rgba(255,255,255,0.15);
|
||||
border-top-color: var(--accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
.cam-reconnecting-label {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 9px;
|
||||
color: var(--muted);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.cam-featured-wrap .cam-reconnecting-spinner {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.cam-featured-wrap .cam-reconnecting-label {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* Viewer count badge */
|
||||
.cam-viewers {
|
||||
position: absolute;
|
||||
@@ -1103,6 +1215,8 @@
|
||||
<div id="apiControl" style="display:flex;align-items:center;gap:8px;">
|
||||
<div class="dot" id="apiDot" title="API status"></div>
|
||||
<span id="apiStatus" style="font-family:'Share Tech Mono',monospace;font-size:10px;color:var(--muted);">NO TOKEN</span>
|
||||
<div class="dot" id="wsDot" title="WebSocket status" style="margin-left:4px;"></div>
|
||||
<span id="wsLabel" style="font-family:'Share Tech Mono',monospace;font-size:10px;color:var(--muted);">WS —</span>
|
||||
<input id="inlineToken" type="password" placeholder="Paste token or full cookie value..." style="display:none;background:var(--panel);border:1px solid var(--accent);color:var(--text);font-family:'Share Tech Mono',monospace;font-size:10px;padding:4px 10px;border-radius:4px;width:260px;outline:none;" onkeydown="if(event.key==='Enter')inlineConnect();if(event.key==='Escape')closeInlineToken();" />
|
||||
<button id="linkApiBtn" onclick="toggleInlineToken()" style="background:transparent;border:1px solid var(--green);color:var(--green);font-family:'Share Tech Mono',monospace;font-size:10px;padding:4px 12px;border-radius:4px;cursor:pointer;letter-spacing:1px;">LINK API</button>
|
||||
<button id="connectInlineBtn" onclick="inlineConnect()" style="display:none;background:var(--green);color:var(--bg);border:none;font-family:'Share Tech Mono',monospace;font-size:10px;padding:4px 12px;border-radius:4px;cursor:pointer;letter-spacing:1px;">CONNECT</button>
|
||||
@@ -1115,10 +1229,6 @@
|
||||
<option value="120000">2m</option>
|
||||
<option value="300000">5m</option>
|
||||
</select>
|
||||
<div class="status-dot">
|
||||
<div class="dot" id="wsDot"></div>
|
||||
<span id="wsLabel" style="font-family:'Share Tech Mono',monospace;font-size:10px;color:var(--muted);">WS —</span>
|
||||
</div>
|
||||
<span id="lastUpdated" style="font-size:13px;color:#e2e8f0;font-family:'Share Tech Mono',monospace;"></span>
|
||||
</div>
|
||||
</header>
|
||||
@@ -1756,6 +1866,22 @@
|
||||
// directorCell = which grid index currently shows the Director stream (null = director is in featured)
|
||||
let directorCell = null;
|
||||
|
||||
function showReconnecting(video) {
|
||||
const cell = video.closest('.cam-cell') || video.closest('.cam-featured-wrap');
|
||||
if (!cell || cell.querySelector('.cam-reconnecting')) return;
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'cam-reconnecting';
|
||||
overlay.innerHTML = '<div class="cam-reconnecting-spinner"></div><div class="cam-reconnecting-label">RECONNECTING</div>';
|
||||
cell.appendChild(overlay);
|
||||
}
|
||||
|
||||
function hideReconnecting(video) {
|
||||
const cell = video.closest('.cam-cell') || video.closest('.cam-featured-wrap');
|
||||
if (!cell) return;
|
||||
const overlay = cell.querySelector('.cam-reconnecting');
|
||||
if (overlay) overlay.remove();
|
||||
}
|
||||
|
||||
function makeHls(slug, video, muted, retryDelay) {
|
||||
retryDelay = retryDelay || 2000;
|
||||
const hls = new Hls({ lowLatencyMode: true, maxBufferLength: 8 });
|
||||
@@ -1764,11 +1890,12 @@
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||
video.muted = muted;
|
||||
video.play().catch(() => {});
|
||||
hideReconnecting(video);
|
||||
});
|
||||
hls.on(Hls.Events.ERROR, (e, d) => {
|
||||
if (!d.fatal) return;
|
||||
showReconnecting(video);
|
||||
hls.destroy();
|
||||
// Retry indefinitely with backoff — cap at 10s, keep trying forever
|
||||
const nextDelay = Math.min(retryDelay * 1.5, 10000);
|
||||
setTimeout(() => {
|
||||
if (!video.isConnected) return;
|
||||
@@ -1817,6 +1944,9 @@
|
||||
if (typeof updateContestantOverlays === 'function' && window._lastContestantData && Object.keys(window._lastContestantData).length) {
|
||||
updateContestantOverlays(window._lastContestantData);
|
||||
}
|
||||
if (typeof updateViewerCounts === 'function' && window._lastPresenceData) {
|
||||
updateViewerCounts(window._lastPresenceData);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2474,6 +2604,10 @@
|
||||
if (msg.event === 'contestants:cams') {
|
||||
updateContestantOverlays(msg.data.data || msg.data);
|
||||
}
|
||||
if (msg.event === 'presence') {
|
||||
window._lastPresenceData = msg.data;
|
||||
updateViewerCounts(msg.data);
|
||||
}
|
||||
|
||||
if (msg.event === 'poll:vote') {
|
||||
// data is [{value, score}, ...] — update scores in cached poll
|
||||
@@ -2599,6 +2733,45 @@
|
||||
};
|
||||
// ── End stocks panel collapse ─────────────────────────────────
|
||||
|
||||
// ── Viewer counts ────────────────────────────────────────────
|
||||
window.updateViewerCounts = function(data) {
|
||||
const cammanIdx = typeof CAMERAS !== 'undefined' ? CAMERAS.findIndex(([,s]) => s === 'cameraman-5') : -1;
|
||||
if (typeof CAMERAS !== 'undefined') {
|
||||
CAMERAS.forEach(([name, slug], i) => {
|
||||
if (i === (typeof DEFAULT_IDX !== 'undefined' ? DEFAULT_IDX : 1)) return;
|
||||
if (i === cammanIdx) return;
|
||||
const cell = document.getElementById('cam-' + i);
|
||||
if (cell) setViewerBadge(cell, data[slug]);
|
||||
});
|
||||
}
|
||||
const featWrap = document.getElementById('camFeaturedWrap');
|
||||
if (featWrap && typeof CAMERAS !== 'undefined' && typeof featuredIdx !== 'undefined') {
|
||||
const slug = CAMERAS[featuredIdx] ? CAMERAS[featuredIdx][1] : null;
|
||||
const featCell = featWrap.querySelector('.cam-cell') || featWrap;
|
||||
if (slug) {
|
||||
const dirSlug = CAMERAS[typeof DEFAULT_IDX !== 'undefined' ? DEFAULT_IDX : 1][1];
|
||||
setViewerBadge(featCell, data[slug]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function setViewerBadge(cell, count) {
|
||||
let badge = cell.querySelector('.cam-viewers');
|
||||
if (count === undefined || count === null || count === 0) {
|
||||
if (badge) badge.remove();
|
||||
return;
|
||||
}
|
||||
if (!badge) {
|
||||
badge = document.createElement('div');
|
||||
badge.className = 'cam-viewers';
|
||||
cell.appendChild(badge);
|
||||
}
|
||||
badge.textContent = count >= 1000
|
||||
? (count / 1000).toFixed(1) + 'k'
|
||||
: count.toString();
|
||||
}
|
||||
// ── End viewer counts ─────────────────────────────────────────
|
||||
|
||||
// ── Contestant cam overlays ──────────────────────────────────
|
||||
const AVATAR_CDN = 'https://cdn.fishtank.live/images/contestants/s5/';
|
||||
|
||||
|
||||
18
server.js
18
server.js
@@ -89,6 +89,22 @@ function sendSubscriptions() {
|
||||
sendBinary(buildSubscribeFrame('presence'));
|
||||
}
|
||||
|
||||
// Presence is client-driven — must re-request every 30s to get updated counts
|
||||
let presenceTimer = null;
|
||||
|
||||
function startPresencePolling() {
|
||||
if (presenceTimer) clearInterval(presenceTimer);
|
||||
presenceTimer = setInterval(() => {
|
||||
if (ftSocket && ftSocket.readyState === 1) {
|
||||
sendBinary(buildSubscribeFrame('presence'));
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
function stopPresencePolling() {
|
||||
if (presenceTimer) { clearInterval(presenceTimer); presenceTimer = null; }
|
||||
}
|
||||
|
||||
// ── Simple msgpack decoder (enough for fishtank events) ─────
|
||||
function mpDecode(buf, offset = 0) {
|
||||
if (offset >= buf.length) return [null, offset];
|
||||
@@ -205,6 +221,7 @@ function handleBinaryFrame(buf) {
|
||||
namespaceReady = true;
|
||||
broadcast({ _ft: 'ws_status', status: 'connected' });
|
||||
sendSubscriptions();
|
||||
startPresencePolling();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -286,6 +303,7 @@ function connectFishtankWS(token) {
|
||||
console.log(`[WS] Disconnected (${code} ${reason || ''}). Reconnecting in 5s...`);
|
||||
broadcast({ _ft: 'ws_status', status: 'disconnected' });
|
||||
namespaceReady = false;
|
||||
stopPresencePolling();
|
||||
reconnectTimer = setTimeout(() => connectFishtankWS(null), 5000);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user