From 33c20bf59d81b386c9e000f949b522a824b2fe86 Mon Sep 17 00:00:00 2001 From: doomtube Date: Sun, 11 Jan 2026 10:57:46 -0500 Subject: [PATCH] fixes lol --- backend/src/controllers/StreamController.cpp | 12 ++++--- backend/src/controllers/StreamController.h | 3 +- backend/src/services/StatsService.cpp | 23 +++++++++----- .../src/lib/components/StreamPlayer.svelte | 26 ++++++++++++++++ .../lib/components/chat/ChatTerminal.svelte | 12 +++---- .../lib/components/watch/YouTubePlayer.svelte | 6 ++-- frontend/src/lib/stores/ebookReader.js | 31 ++++++++++++------- frontend/src/lib/stores/streamTiles.js | 13 ++++++++ frontend/src/routes/[realm]/live/+page.svelte | 13 +++++--- .../src/routes/chat/terminal/+page.svelte | 12 +++---- frontend/src/routes/watch/[id]/+page.svelte | 1 + 11 files changed, 107 insertions(+), 45 deletions(-) diff --git a/backend/src/controllers/StreamController.cpp b/backend/src/controllers/StreamController.cpp index a8ad05b..c0cdbb3 100644 --- a/backend/src/controllers/StreamController.cpp +++ b/backend/src/controllers/StreamController.cpp @@ -211,20 +211,22 @@ void StreamController::issueViewerToken(const HttpRequestPtr &, void StreamController::heartbeat(const HttpRequestPtr &req, std::function &&callback, + const std::string &realmId, const std::string &streamKey) { - auto token = req->getCookie("viewer_token"); + // Use realm-specific cookie to support multi-stream viewing + auto token = req->getCookie("viewer_token_" + realmId); if (token.empty()) { callback(jsonResp({}, k403Forbidden)); return; } - - RedisHelper::getKeyAsync("viewer_token:" + token, + + RedisHelper::getKeyAsync("viewer_token:" + token, [callback, streamKey, token](const std::string& storedStreamKey) { if (storedStreamKey != streamKey) { callback(jsonResp({}, k403Forbidden)); return; } - + // Refresh token TTL to 5 minutes on heartbeat services::RedisHelper::instance().expireAsync("viewer_token:" + token, 300, [callback](bool success) { @@ -232,7 +234,7 @@ void StreamController::heartbeat(const HttpRequestPtr &req, callback(jsonResp({}, k500InternalServerError)); return; } - + callback(jsonOk(json({ {"success", true}, {"renewed", true} diff --git a/backend/src/controllers/StreamController.h b/backend/src/controllers/StreamController.h index 068cd60..6eadbf3 100644 --- a/backend/src/controllers/StreamController.h +++ b/backend/src/controllers/StreamController.h @@ -17,7 +17,7 @@ public: ADD_METHOD_TO(StreamController::getStreamStats, "/api/stream/stats/{1}", Get); ADD_METHOD_TO(StreamController::getActiveStreams, "/api/stream/active", Get); ADD_METHOD_TO(StreamController::issueViewerToken, "/api/stream/token/{1}", Get); - ADD_METHOD_TO(StreamController::heartbeat, "/api/stream/heartbeat/{1}", Post); + ADD_METHOD_TO(StreamController::heartbeat, "/api/stream/heartbeat/{1}/{2}", Post); // OvenMediaEngine webhook endpoints ADD_METHOD_TO(StreamController::handleOmeWebhook, "/api/webhook/ome", Post); ADD_METHOD_TO(StreamController::handleOmeAdmission, "/api/webhook/ome/admission", Post); @@ -47,6 +47,7 @@ public: void heartbeat(const HttpRequestPtr &req, std::function &&callback, + const std::string &realmId, const std::string &streamKey); // OvenMediaEngine webhook handlers diff --git a/backend/src/services/StatsService.cpp b/backend/src/services/StatsService.cpp index 8999ad0..6fb2288 100644 --- a/backend/src/services/StatsService.cpp +++ b/backend/src/services/StatsService.cpp @@ -188,17 +188,24 @@ void StatsService::updateStreamStats(const std::string& streamKey) { // Only broadcast if stream has meaningful data or is live if (updatedStats.isLive || updatedStats.totalBytesIn > 0 || updatedStats.uniqueViewers > 0) { - // Fetch live_started_at for duration display + // Fetch live_started_at and viewer_multiplier for duration display and consistent counts auto dbClient = app().getDbClient(); - *dbClient << "SELECT live_started_at FROM realms WHERE stream_key = $1" + *dbClient << "SELECT live_started_at, viewer_multiplier FROM realms WHERE stream_key = $1" << streamKey >> [streamKey, updatedStats](const orm::Result& r) { Json::Value msg; msg["type"] = "stats_update"; msg["stream_key"] = streamKey; + // Apply viewer multiplier for consistent display across all sources + int multiplier = 1; + if (!r.empty() && !r[0]["viewer_multiplier"].isNull()) { + multiplier = r[0]["viewer_multiplier"].as(); + if (multiplier < 1) multiplier = 1; + } + auto& s = msg["stats"]; - JSON_INT(s, "connections", updatedStats.uniqueViewers); + JSON_INT(s, "connections", updatedStats.uniqueViewers * multiplier); JSON_INT(s, "raw_connections", updatedStats.currentConnections); s["bitrate"] = updatedStats.bitrate; s["resolution"] = updatedStats.resolution; @@ -213,12 +220,12 @@ void StatsService::updateStreamStats(const std::string& streamKey) { s["live_started_at"] = r[0]["live_started_at"].as(); } - // Protocol breakdown + // Protocol breakdown (also apply multiplier for consistency) auto& pc = s["protocol_connections"]; - JSON_INT(pc, "webrtc", updatedStats.protocolConnections.webrtc); - JSON_INT(pc, "hls", updatedStats.protocolConnections.hls); - JSON_INT(pc, "llhls", updatedStats.protocolConnections.llhls); - JSON_INT(pc, "dash", updatedStats.protocolConnections.dash); + JSON_INT(pc, "webrtc", updatedStats.protocolConnections.webrtc * multiplier); + JSON_INT(pc, "hls", updatedStats.protocolConnections.hls * multiplier); + JSON_INT(pc, "llhls", updatedStats.protocolConnections.llhls * multiplier); + JSON_INT(pc, "dash", updatedStats.protocolConnections.dash * multiplier); StreamWebSocketController::broadcastStatsUpdate(msg); } diff --git a/frontend/src/lib/components/StreamPlayer.svelte b/frontend/src/lib/components/StreamPlayer.svelte index f6e469d..473f525 100644 --- a/frontend/src/lib/components/StreamPlayer.svelte +++ b/frontend/src/lib/components/StreamPlayer.svelte @@ -33,6 +33,7 @@ let playerId = `player-${stream.realmId}-${Math.random().toString(36).substr(2, 9)}`; let showControls = false; let showVolumeSlider = false; + let statsInterval = null; // Get muted/volume from stream object with defaults $: muted = stream.muted !== undefined ? stream.muted : true; @@ -68,6 +69,7 @@ if (viewerToken && actualStreamKey) { initializePlayer(); + startStatsPolling(); } else { error = 'Could not get stream access'; } @@ -75,7 +77,31 @@ loading = false; }); + function startStatsPolling() { + // Poll stats every 5 seconds to update viewer count for aggregation + statsInterval = setInterval(async () => { + try { + const response = await fetch(`/api/realms/${stream.realmId}/stats`); + if (response.ok) { + const data = await response.json(); + if (data.success && data.stats) { + // Update the store with this stream's viewer count + streamTiles.setViewerCount(stream.realmId, data.stats.connections || 0); + isLive = data.stats.is_live || false; + } + } + } catch (e) { + console.error('Failed to fetch stats for tile:', stream.name, e); + } + }, 5000); + } + onDestroy(() => { + if (statsInterval) { + clearInterval(statsInterval); + } + // Clear this stream's viewer count from the store + streamTiles.setViewerCount(stream.realmId, 0); if (player) { try { player.remove(); diff --git a/frontend/src/lib/components/chat/ChatTerminal.svelte b/frontend/src/lib/components/chat/ChatTerminal.svelte index 0dd99cd..434473a 100644 --- a/frontend/src/lib/components/chat/ChatTerminal.svelte +++ b/frontend/src/lib/components/chat/ChatTerminal.svelte @@ -232,21 +232,21 @@ calendarDate.getFullYear() === today.getFullYear(); } + $: calendarDays = getCalendarDays(calendarDate); + $: calendarMonthYear = calendarDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' }); + $: calendarHolidays = getHolidaysForMonth(calendarDate.getFullYear(), calendarDate.getMonth()); + function isHoliday(day) { if (!day) return false; - return calendarHolidays.has(day); + return calendarHolidays?.has(day) ?? false; } function getHolidayName(day) { if (!day) return null; - const info = calendarHolidays.get(day); + const info = calendarHolidays?.get(day); return info ? info.name : null; } - $: calendarDays = getCalendarDays(calendarDate); - $: calendarMonthYear = calendarDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' }); - $: calendarHolidays = getHolidaysForMonth(calendarDate.getFullYear(), calendarDate.getMonth()); - // Timezone definitions const timezones = [ { label: 'UTC', zone: 'UTC' }, diff --git a/frontend/src/lib/components/watch/YouTubePlayer.svelte b/frontend/src/lib/components/watch/YouTubePlayer.svelte index 3173ef8..1215d36 100644 --- a/frontend/src/lib/components/watch/YouTubePlayer.svelte +++ b/frontend/src/lib/components/watch/YouTubePlayer.svelte @@ -27,10 +27,10 @@ const SYNC_CHECK_INTERVAL = 1000; // Check sync every second (matches server sync interval) const RESTART_GRACE_PERIOD = 3000; // 3 seconds grace period during video restart - const DRIFT_THRESHOLD = 2; // Seek if drift > 2 seconds (CyTube-style tight sync) + const DRIFT_THRESHOLD = 5; // Seek if drift > 5 seconds (more lenient to avoid jitter) const MIN_SYNC_INTERVAL = 1000; // Minimum 1 second between sync checks (server pushes every 1s) - const CONTROLLER_SEEK_DEBOUNCE = 2000; // Debounce controller seeks by 2 seconds - const SEEK_RATE_LIMIT = 2000; // Minimum 2 seconds between seeks + const CONTROLLER_SEEK_DEBOUNCE = 5000; // Debounce controller seeks by 5 seconds + const SEEK_RATE_LIMIT = 3000; // Minimum 3 seconds between seeks const POST_LEAD_IN_GRACE = 500; // 500ms grace period after lead-in ends // Load YouTube IFrame API diff --git a/frontend/src/lib/stores/ebookReader.js b/frontend/src/lib/stores/ebookReader.js index 6ec5376..f7f2e7b 100644 --- a/frontend/src/lib/stores/ebookReader.js +++ b/frontend/src/lib/stores/ebookReader.js @@ -50,8 +50,8 @@ function loadState() { return { ...defaultState, ...parsed, - // Don't persist currentBook or showToc - currentBook: null, + // Restore currentBook if it was persisted + currentBook: parsed.currentBook || null, showToc: false }; } @@ -68,7 +68,8 @@ function saveState(state) { try { const toSave = { enabled: state.enabled, - minimized: state.minimized + minimized: state.minimized, + currentBook: state.currentBook }; localStorage.setItem(STORAGE_KEY, JSON.stringify(toSave)); } catch (e) { @@ -96,14 +97,18 @@ function createEbookReaderStore() { console.error('Invalid book object - missing id or filePath'); return; } - update(state => ({ - ...state, - enabled: true, - currentBook: book, - progress: 0, - showToc: false, - minimized: false - })); + update(state => { + const newState = { + ...state, + enabled: true, + currentBook: book, + progress: 0, + showToc: false, + minimized: false + }; + saveState(newState); + return newState; + }); }, // Close the reader (optionally save position first) @@ -113,13 +118,15 @@ function createEbookReaderStore() { if (cfi && state.currentBook?.id) { persistPosition(state.currentBook.id, cfi); } - return { + const newState = { ...state, enabled: false, currentBook: null, progress: 0, showToc: false }; + saveState(newState); + return newState; }); }, diff --git a/frontend/src/lib/stores/streamTiles.js b/frontend/src/lib/stores/streamTiles.js index e086f2b..e89d17e 100644 --- a/frontend/src/lib/stores/streamTiles.js +++ b/frontend/src/lib/stores/streamTiles.js @@ -6,6 +6,7 @@ const STORAGE_KEY = 'streamTiles'; const defaultState = { enabled: false, streams: [], // Array of { streamKey, name, username, realmId, offlineImageUrl?, muted: true, volume: 0.5 } + viewerCounts: {}, // Map of realmId -> viewerCount for aggregating across tiles // Grid sizing: percentage splits for resizable dividers horizontalSplit: 50, // Percentage for left/right split (2 streams side by side) verticalSplit: 50 // Percentage for top/bottom split (2x2 grid) @@ -138,6 +139,18 @@ function createTileStore() { return newState; }), + // Update viewer count for a specific stream (used for aggregating across tiles) + setViewerCount: (realmId, count) => update(s => { + const newViewerCounts = { ...s.viewerCounts, [realmId]: count }; + // Don't persist viewer counts to storage - they're ephemeral + return { ...s, viewerCounts: newViewerCounts }; + }), + + // Get total viewer count across all tile streams + getTotalViewerCount: (state) => { + return Object.values(state.viewerCounts).reduce((sum, count) => sum + (count || 0), 0); + }, + clear: () => { const newState = { ...defaultState }; saveToStorage(newState); diff --git a/frontend/src/routes/[realm]/live/+page.svelte b/frontend/src/routes/[realm]/live/+page.svelte index e80f355..34a1834 100644 --- a/frontend/src/routes/[realm]/live/+page.svelte +++ b/frontend/src/routes/[realm]/live/+page.svelte @@ -256,13 +256,13 @@ function startHeartbeat() { heartbeatInterval = setInterval(async () => { - if (streamKey && viewerToken) { + if (streamKey && viewerToken && realm) { try { - const response = await fetch(`/api/stream/heartbeat/${streamKey}`, { + const response = await fetch(`/api/stream/heartbeat/${realm.id}/${streamKey}`, { method: 'POST', credentials: 'include' }); - + if (!response.ok) { console.error('Heartbeat failed, getting new token'); await getViewerToken(); @@ -623,6 +623,11 @@ $: totalStreams = hasTiledStreams ? tiledStreams.length + 1 : 1; // +1 for main stream $: gridClass = totalStreams === 2 ? 'grid-2' : totalStreams >= 3 ? 'grid-4' : ''; + // Aggregated viewer count across all streams (main + tiles) + $: tileViewerCount = Object.values($streamTiles.viewerCounts).reduce((sum, count) => sum + (count || 0), 0); + $: mainViewerCount = stats.isLive ? stats.connections : (realm?.viewerCount || 0); + $: totalViewerCount = hasTiledStreams && $streamTiles.enabled ? mainViewerCount + tileViewerCount : mainViewerCount; + // Track previous layout to detect changes let prevTotalStreams = 1; @@ -1459,7 +1464,7 @@

{realm.displayName || realm.name}

- {stats.isLive ? stats.connections : realm.viewerCount} viewers + {totalViewerCount} viewers {#if stats.isLive && stats.bitrate > 0} {formatBitrate(stats.bitrate)} diff --git a/frontend/src/routes/chat/terminal/+page.svelte b/frontend/src/routes/chat/terminal/+page.svelte index 6c44da0..a0a29d2 100644 --- a/frontend/src/routes/chat/terminal/+page.svelte +++ b/frontend/src/routes/chat/terminal/+page.svelte @@ -130,21 +130,21 @@ calendarDate.getFullYear() === today.getFullYear(); } + $: calendarDays = getCalendarDays(calendarDate); + $: calendarMonthYear = calendarDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' }); + $: calendarHolidays = getHolidaysForMonth(calendarDate.getFullYear(), calendarDate.getMonth()); + function isHoliday(day) { if (!day) return false; - return calendarHolidays.has(day); + return calendarHolidays?.has(day) ?? false; } function getHolidayName(day) { if (!day) return null; - const info = calendarHolidays.get(day); + const info = calendarHolidays?.get(day); return info ? info.name : null; } - $: calendarDays = getCalendarDays(calendarDate); - $: calendarMonthYear = calendarDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' }); - $: calendarHolidays = getHolidaysForMonth(calendarDate.getFullYear(), calendarDate.getMonth()); - // Timezone definitions const timezones = [ { label: 'UTC', zone: 'UTC' }, diff --git a/frontend/src/routes/watch/[id]/+page.svelte b/frontend/src/routes/watch/[id]/+page.svelte index ab5bcdc..edecc59 100644 --- a/frontend/src/routes/watch/[id]/+page.svelte +++ b/frontend/src/routes/watch/[id]/+page.svelte @@ -224,6 +224,7 @@ controls autoplay playsinline + on:loadedmetadata={() => { if (videoElement) videoElement.volume = 0.6; }} >