From 381e8b79b0967dcdc182d627848fe99abc297935 Mon Sep 17 00:00:00 2001 From: doomtube Date: Sun, 11 Jan 2026 20:08:40 -0500 Subject: [PATCH] fixes lol --- backend/src/controllers/AudioController.cpp | 4 +- backend/src/controllers/PyramidController.cpp | 20 ++++++++ backend/src/controllers/PyramidController.h | 7 +-- backend/src/services/RestreamService.cpp | 27 +++++++--- backend/src/services/RestreamService.h | 2 + docker-compose.prod.yml | 1 + docker-compose.yml | 1 + .../src/lib/components/StreamPlayer.svelte | 35 ++++++++++--- .../lib/components/StreamTileOverlay.svelte | 35 ++++++++++--- .../lib/components/WaveformBackground.svelte | 47 +++++++++++------ .../lib/components/chat/ChatTerminal.svelte | 11 ++-- .../components/pyramid/PyramidCanvas.svelte | 33 ++++++++++-- frontend/src/routes/[realm]/live/+page.svelte | 40 +++++++++++---- .../src/routes/audio/realm/[id]/+page.svelte | 29 +++++------ .../src/routes/chat/terminal/+page.svelte | 11 ++-- openresty/nginx.conf | 50 +++++++++++++++++++ ovenmediaengine/Server.xml | 11 ++-- 17 files changed, 282 insertions(+), 82 deletions(-) diff --git a/backend/src/controllers/AudioController.cpp b/backend/src/controllers/AudioController.cpp index 4f56788..1f05866 100644 --- a/backend/src/controllers/AudioController.cpp +++ b/backend/src/controllers/AudioController.cpp @@ -115,8 +115,8 @@ namespace { return ""; } - // Compute 200 peaks from samples - const int numPeaks = 200; + // Compute 500 peaks from samples for finer waveform detail + const int numPeaks = 500; std::vector peaks(numPeaks, 0.0f); size_t samplesPerPeak = samples.size() / numPeaks; if (samplesPerPeak == 0) samplesPerPeak = 1; diff --git a/backend/src/controllers/PyramidController.cpp b/backend/src/controllers/PyramidController.cpp index 9a93aa1..c89d39b 100644 --- a/backend/src/controllers/PyramidController.cpp +++ b/backend/src/controllers/PyramidController.cpp @@ -15,6 +15,8 @@ std::mutex PyramidWebSocketController::connectionsMutex_; // ==================== Validation Helpers ==================== bool PyramidController::isValidPixelPosition(int faceId, int x, int y) { + constexpr int FACE_SIZE = 200; + // Face ID must be 0-4 if (faceId < 0 || faceId > 4) return false; @@ -673,7 +675,25 @@ void PyramidWebSocketController::handlePlacePixel(const WebSocketConnectionPtr & int y = msg["y"].asInt(); std::string color = msg["color"].asString(); + // Validate pixel position + if (!PyramidController::isValidPixelPosition(faceId, x, y)) { + Json::Value error; + error["type"] = "error"; + error["message"] = "Invalid pixel position"; + Json::StreamWriterBuilder builder; + wsConnPtr->send(Json::writeString(builder, error)); + return; + } + // Validate and uppercase color + if (!PyramidController::isValidColor(color)) { + Json::Value error; + error["type"] = "error"; + error["message"] = "Invalid color format (use #RRGGBB)"; + Json::StreamWriterBuilder builder; + wsConnPtr->send(Json::writeString(builder, error)); + return; + } std::transform(color.begin(), color.end(), color.begin(), ::toupper); // Use the REST endpoint logic for actual placement diff --git a/backend/src/controllers/PyramidController.h b/backend/src/controllers/PyramidController.h index 410767b..2843222 100644 --- a/backend/src/controllers/PyramidController.h +++ b/backend/src/controllers/PyramidController.h @@ -56,12 +56,13 @@ public: const std::string &x, const std::string &y); + // Static validation methods (also used by WebSocket controller) + static bool isValidPixelPosition(int faceId, int x, int y); + static bool isValidColor(const std::string &color); + private: static constexpr int DAILY_PIXEL_LIMIT = 1000; static constexpr int FACE_SIZE = 200; - - bool isValidPixelPosition(int faceId, int x, int y); - bool isValidColor(const std::string &color); }; // WebSocket controller for real-time pixel updates diff --git a/backend/src/services/RestreamService.cpp b/backend/src/services/RestreamService.cpp index 64b8901..5534999 100644 --- a/backend/src/services/RestreamService.cpp +++ b/backend/src/services/RestreamService.cpp @@ -16,6 +16,12 @@ std::string RestreamService::getBaseUrl() { return "http://ovenmediaengine:8081"; } +// Use openresty proxy for push operations to avoid URL encoding issues +// Drogon encodes ':' as '%3A' but OME expects literal colon in path +std::string RestreamService::getPushProxyUrl() { + return "http://openresty:80"; +} + std::string RestreamService::getApiToken() { const char* envToken = std::getenv("OME_API_TOKEN"); if (!envToken || strlen(envToken) == 0) { @@ -28,6 +34,10 @@ HttpClientPtr RestreamService::getClient() { return HttpClient::newHttpClient(getBaseUrl()); } +HttpClientPtr RestreamService::getPushProxyClient() { + return HttpClient::newHttpClient(getPushProxyUrl()); +} + HttpRequestPtr RestreamService::createRequest(HttpMethod method, const std::string& path) { auto request = HttpRequest::newHttpRequest(); request->setMethod(method); @@ -106,12 +116,13 @@ void RestreamService::startPush(const std::string& sourceStreamKey, const Restre body["protocol"] = "rtmp"; body["url"] = fullUrl; - // Use Drogon HttpClient instead of curl for security - auto request = createJsonRequest(drogon::Post, "/v1/vhosts/default/apps/app:startPush", body); + // Use openresty proxy to avoid URL encoding issues with colon in path + // Drogon encodes ':' as '%3A' but OME expects literal colon + auto request = createJsonRequest(drogon::Post, "/ome-internal/push/start", body); - LOG_INFO << "Sending HTTP request for push start"; + LOG_INFO << "Sending HTTP request for push start via proxy"; - getClient()->sendRequest(request, + getPushProxyClient()->sendRequest(request, [this, callback, pushId, sourceStreamKey, destId](ReqResult result, const HttpResponsePtr& response) { if (result != ReqResult::Ok || !response) { std::string error = "Failed to connect to OME API"; @@ -187,12 +198,12 @@ void RestreamService::stopPush(const std::string& sourceStreamKey, int64_t desti Json::Value body; body["id"] = pushId; - // Use Drogon HttpClient instead of curl for security - auto request = createJsonRequest(drogon::Post, "/v1/vhosts/default/apps/app:stopPush", body); + // Use openresty proxy to avoid URL encoding issues with colon in path + auto request = createJsonRequest(drogon::Post, "/ome-internal/push/stop", body); - LOG_INFO << "Sending HTTP request for push stop"; + LOG_INFO << "Sending HTTP request for push stop via proxy"; - getClient()->sendRequest(request, + getPushProxyClient()->sendRequest(request, [this, callback, pushId, sourceStreamKey, destinationId](ReqResult result, const HttpResponsePtr& response) { // Remove from tracking regardless of result { diff --git a/backend/src/services/RestreamService.h b/backend/src/services/RestreamService.h index 1d13695..13bd257 100644 --- a/backend/src/services/RestreamService.h +++ b/backend/src/services/RestreamService.h @@ -57,8 +57,10 @@ private: RestreamService& operator=(const RestreamService&) = delete; std::string getBaseUrl(); + std::string getPushProxyUrl(); // Openresty proxy for push API (avoids URL encoding issues) std::string getApiToken(); drogon::HttpClientPtr getClient(); + drogon::HttpClientPtr getPushProxyClient(); // Client for push proxy drogon::HttpRequestPtr createRequest(drogon::HttpMethod method, const std::string& path); drogon::HttpRequestPtr createJsonRequest(drogon::HttpMethod method, const std::string& path, const Json::Value& body); diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index e653b3e..4429c76 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -53,6 +53,7 @@ services: environment: OME_API_PORT: 8081 OME_API_ACCESS_TOKEN: ${OME_API_TOKEN} + OME_ICE_CANDIDATES: ${OME_ICE_CANDIDATES:-*} networks: - backend - frontend diff --git a/docker-compose.yml b/docker-compose.yml index 6fd6df7..c15654d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -46,6 +46,7 @@ services: environment: OME_API_PORT: 8081 OME_API_ACCESS_TOKEN: ${OME_API_TOKEN} + OME_ICE_CANDIDATES: ${OME_ICE_CANDIDATES:-*} networks: - backend - frontend diff --git a/frontend/src/lib/components/StreamPlayer.svelte b/frontend/src/lib/components/StreamPlayer.svelte index 473f525..0f27097 100644 --- a/frontend/src/lib/components/StreamPlayer.svelte +++ b/frontend/src/lib/components/StreamPlayer.svelte @@ -172,14 +172,33 @@ debug: false, enableWorker: true, lowLatencyMode: true, - backBufferLength: 90, - // Increased retry settings for LLHLS resilience - fragLoadingMaxRetry: 6, - fragLoadingRetryDelay: 1000, - manifestLoadingMaxRetry: 4, - levelLoadingMaxRetry: 4, - maxBufferLength: 30, - maxBufferHole: 0.5, + + // Buffer Management - optimized for low latency + maxBufferLength: 4, + maxMaxBufferLength: 6, + backBufferLength: 10, + + // Live Edge Sync - key for reducing latency + liveSyncDurationCount: 3, + liveMaxLatencyDurationCount: 5, + liveSyncDuration: 3, + liveMaxLatencyDuration: 6, + maxLiveSyncPlaybackRate: 1.5, + + // Faster Recovery - reduced retry delays + fragLoadingMaxRetry: 4, + fragLoadingRetryDelay: 200, + fragLoadingMaxRetryTimeout: 4000, + manifestLoadingMaxRetry: 3, + manifestLoadingRetryDelay: 200, + levelLoadingMaxRetry: 3, + levelLoadingRetryDelay: 200, + + // Startup optimization + startFragPrefetch: true, + testBandwidth: false, + maxBufferHole: 0.1, + xhrSetup: function(xhr, url) { let finalUrl = url; diff --git a/frontend/src/lib/components/StreamTileOverlay.svelte b/frontend/src/lib/components/StreamTileOverlay.svelte index f0d42ae..0bc010e 100644 --- a/frontend/src/lib/components/StreamTileOverlay.svelte +++ b/frontend/src/lib/components/StreamTileOverlay.svelte @@ -161,14 +161,33 @@ debug: false, enableWorker: true, lowLatencyMode: true, - backBufferLength: 30, - // Increased retry settings for LLHLS resilience - fragLoadingMaxRetry: 6, - fragLoadingRetryDelay: 1000, - manifestLoadingMaxRetry: 4, - levelLoadingMaxRetry: 4, - maxBufferLength: 30, - maxBufferHole: 0.5, + + // Buffer Management - optimized for low latency + maxBufferLength: 4, + maxMaxBufferLength: 6, + backBufferLength: 10, + + // Live Edge Sync - key for reducing latency + liveSyncDurationCount: 3, + liveMaxLatencyDurationCount: 5, + liveSyncDuration: 3, + liveMaxLatencyDuration: 6, + maxLiveSyncPlaybackRate: 1.5, + + // Faster Recovery - reduced retry delays + fragLoadingMaxRetry: 4, + fragLoadingRetryDelay: 200, + fragLoadingMaxRetryTimeout: 4000, + manifestLoadingMaxRetry: 3, + manifestLoadingRetryDelay: 200, + levelLoadingMaxRetry: 3, + levelLoadingRetryDelay: 200, + + // Startup optimization + startFragPrefetch: true, + testBandwidth: false, + maxBufferHole: 0.1, + xhrSetup: function(xhr, url) { let finalUrl = url; diff --git a/frontend/src/lib/components/WaveformBackground.svelte b/frontend/src/lib/components/WaveformBackground.svelte index 25f1b69..ec3f1b9 100644 --- a/frontend/src/lib/components/WaveformBackground.svelte +++ b/frontend/src/lib/components/WaveformBackground.svelte @@ -19,6 +19,9 @@ /** @type {string} Waveform color */ export let color = '#ec4899'; + /** @type {string} Color for the played portion */ + export let playedColor = '#f472b6'; + /** @type {number} Opacity of the waveform (0-1) */ export let opacity = 0.15; @@ -32,9 +35,8 @@ let peaks = []; let loading = true; - // Calculate scroll position based on playback - $: progress = duration > 0 ? currentTime / duration : 0; - $: translateX = isCurrentTrack && isPlaying ? -(progress * 50) : 0; + // Calculate progress percentage (0-100) + $: progress = duration > 0 ? (currentTime / duration) * 100 : 0; async function fetchWaveform() { if (!audioId && !waveformPath) { @@ -91,34 +93,52 @@ > {#each peaks as peak, i} - {@const barWidth = 400 / peaks.length} + {@const barWidth = 1000 / peaks.length} {@const barHeight = peak * 45} {@const x = i * barWidth} + {@const barProgress = (x / 1000) * 100} + {@const isPlayed = isCurrentTrack && barProgress < progress} + {@const barColor = isPlayed ? playedColor : color} + {@const barOpacity = isPlayed ? 0.6 : 1} {/each} + + + {#if isCurrentTrack && progress > 0} + + {/if} {/if} @@ -133,8 +153,7 @@ } .waveform-svg { - width: 200%; + width: 100%; height: 100%; - transition: transform 0.1s linear; } diff --git a/frontend/src/lib/components/chat/ChatTerminal.svelte b/frontend/src/lib/components/chat/ChatTerminal.svelte index 434473a..6b6d6cf 100644 --- a/frontend/src/lib/components/chat/ChatTerminal.svelte +++ b/frontend/src/lib/components/chat/ChatTerminal.svelte @@ -24,6 +24,7 @@ let showCalendar = false; let calendarDate = new Date(); let timeInterval; + let calendarContainerEl; // Tab navigation - includes audio, ebooks, games, and treasury let activeTab = 'terminal'; @@ -308,7 +309,11 @@ }); - showCalendar = false} /> + { + if (calendarContainerEl && !calendarContainerEl.contains(e.target)) { + showCalendar = false; + } +}} /> {#if isOpen && $isAuthenticated}
-
- diff --git a/frontend/src/lib/components/pyramid/PyramidCanvas.svelte b/frontend/src/lib/components/pyramid/PyramidCanvas.svelte index 2836d3e..62ff4e9 100644 --- a/frontend/src/lib/components/pyramid/PyramidCanvas.svelte +++ b/frontend/src/lib/components/pyramid/PyramidCanvas.svelte @@ -20,6 +20,24 @@ let hoveredFace = null; let hoveredPixel = null; + // Validate pixel position (matches backend validation) + function isValidPixelPosition(faceId, x, y) { + // X and Y must be within face bounds + if (x < 0 || x >= FACE_SIZE || y < 0 || y >= FACE_SIZE) return false; + + // For triangular faces (1-4), validate within triangle bounds + if (faceId > 0) { + // Triangle with base at bottom (y=199), apex at top (y=0) + const halfWidth = Math.floor((y + 1) / 2); + const center = Math.floor(FACE_SIZE / 2); + const minX = center - halfWidth; + const maxX = center + halfWidth; + if (x < minX || x > maxX) return false; + } + + return true; + } + // Unsubscribe function for store let unsubscribePyramid; @@ -274,11 +292,18 @@ const x = Math.floor(uv.x * FACE_SIZE); const y = Math.floor((1 - uv.y) * FACE_SIZE); - hoveredFace = faceId; - hoveredPixel = { faceId, x, y }; + // Only set hover if position is valid (especially for triangular faces) + if (isValidPixelPosition(faceId, x, y)) { + hoveredFace = faceId; + hoveredPixel = { faceId, x, y }; - dispatch('hover', { faceId, x, y }); - pyramidStore.setHoveredPixel({ faceId, x, y }); + dispatch('hover', { faceId, x, y }); + pyramidStore.setHoveredPixel({ faceId, x, y }); + } else { + hoveredFace = null; + hoveredPixel = null; + pyramidStore.setHoveredPixel(null); + } } } else { hoveredFace = null; diff --git a/frontend/src/routes/[realm]/live/+page.svelte b/frontend/src/routes/[realm]/live/+page.svelte index 34a1834..9157f07 100644 --- a/frontend/src/routes/[realm]/live/+page.svelte +++ b/frontend/src/routes/[realm]/live/+page.svelte @@ -426,20 +426,42 @@ sources: sources, webrtcConfig: { timeoutMaxRetry: 4, - connectionTimeout: 10000 + connectionTimeout: 10000, + iceServers: [ + { urls: 'stun:stun.l.google.com:19302' } + ] }, hlsConfig: { debug: false, enableWorker: true, lowLatencyMode: true, - backBufferLength: 90, - // Increased retry settings for LLHLS resilience - fragLoadingMaxRetry: 6, - fragLoadingRetryDelay: 1000, - manifestLoadingMaxRetry: 4, - levelLoadingMaxRetry: 4, - maxBufferLength: 30, - maxBufferHole: 0.5, + + // Buffer Management - optimized for low latency + maxBufferLength: 4, + maxMaxBufferLength: 6, + backBufferLength: 10, + + // Live Edge Sync - key for reducing latency + liveSyncDurationCount: 3, + liveMaxLatencyDurationCount: 5, + liveSyncDuration: 3, + liveMaxLatencyDuration: 6, + maxLiveSyncPlaybackRate: 1.5, + + // Faster Recovery - reduced retry delays + fragLoadingMaxRetry: 4, + fragLoadingRetryDelay: 200, + fragLoadingMaxRetryTimeout: 4000, + manifestLoadingMaxRetry: 3, + manifestLoadingRetryDelay: 200, + levelLoadingMaxRetry: 3, + levelLoadingRetryDelay: 200, + + // Startup optimization + startFragPrefetch: true, + testBandwidth: false, + maxBufferHole: 0.1, + xhrSetup: function(xhr, url) { let finalUrl = url; diff --git a/frontend/src/routes/audio/realm/[id]/+page.svelte b/frontend/src/routes/audio/realm/[id]/+page.svelte index 38464b0..6562f3d 100644 --- a/frontend/src/routes/audio/realm/[id]/+page.svelte +++ b/frontend/src/routes/audio/realm/[id]/+page.svelte @@ -39,13 +39,10 @@ return date.toLocaleDateString(); } - function isInPlaylist(audioId) { - return $audioPlaylist.queue.some(t => t.id === audioId); - } - - function isCurrentlyPlaying(audioId) { - return $currentTrack?.id === audioId && $audioPlaylist.isPlaying; - } + // Reactive sets for tracking playlist state - these update when stores change + $: playlistIds = new Set($audioPlaylist.queue.map(t => t.id)); + $: currentPlayingId = $audioPlaylist.isPlaying ? $currentTrack?.id : null; + $: currentTrackId = $currentTrack?.id; function handlePlayClick(audio) { if ($currentTrack?.id === audio.id) { @@ -91,7 +88,7 @@ } function togglePlaylist(audio) { - if (isInPlaylist(audio.id)) { + if (playlistIds.has(audio.id)) { audioPlaylist.removeTrack(audio.id); } else { audioPlaylist.addTrack({ @@ -135,7 +132,7 @@ function addAllToPlaylist() { audioFiles.forEach(audio => { - if (!isInPlaylist(audio.id)) { + if (!playlistIds.has(audio.id)) { addToPlaylist(audio); } }); @@ -521,7 +518,7 @@ isPlaying={$audioPlaylist.isPlaying} currentTime={$audioPlaylist.currentTime} duration={$audioPlaylist.duration} - isCurrentTrack={$currentTrack?.id === audio.id} + isCurrentTrack={currentTrackId === audio.id} /> {/if} {index + 1} @@ -546,19 +543,19 @@
{#if $isAuthenticated} diff --git a/openresty/nginx.conf b/openresty/nginx.conf index a536d7b..a8069b8 100644 --- a/openresty/nginx.conf +++ b/openresty/nginx.conf @@ -84,6 +84,10 @@ http { server ovenmediaengine:8080; } + upstream ome_api { + server ovenmediaengine:8081; + } + # Rate limiting zones limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s; limit_req_zone $binary_remote_addr zone=auth_limit:10m rate=3r/m; @@ -735,6 +739,34 @@ http { return 403; } + # OME Push API proxy (internal network only - fixes URL encoding of colon in path) + # Backend calls this instead of OME directly to avoid %3A encoding issue + location = /ome-internal/push/start { + # Only allow requests from Docker internal network + allow 172.16.0.0/12; + allow 10.0.0.0/8; + allow 192.168.0.0/16; + deny all; + + proxy_pass http://ome_api/v1/vhosts/default/apps/app:startPush; + proxy_set_header Host ovenmediaengine:8081; + proxy_set_header Content-Type application/json; + proxy_pass_request_headers on; + } + + location = /ome-internal/push/stop { + # Only allow requests from Docker internal network + allow 172.16.0.0/12; + allow 10.0.0.0/8; + allow 192.168.0.0/16; + deny all; + + proxy_pass http://ome_api/v1/vhosts/default/apps/app:stopPush; + proxy_set_header Host ovenmediaengine:8081; + proxy_set_header Content-Type application/json; + proxy_pass_request_headers on; + } + # Public stickers endpoint - no authentication required (guests need stickers for chat) location = /api/admin/stickers { # CORS headers @@ -925,6 +957,23 @@ http { # WebRTC Signaling proxy for OvenMediaEngine # Handles wss:// → ws:// translation so OME doesn't need TLS certificates location /webrtc/ { + # CORS headers for WebSocket upgrade + add_header Access-Control-Allow-Origin $cors_origin always; + add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always; + add_header Access-Control-Allow-Headers "Content-Type, Upgrade, Connection" always; + add_header Access-Control-Allow-Credentials "true" always; + + # Handle preflight OPTIONS requests + if ($request_method = 'OPTIONS') { + add_header Access-Control-Allow-Origin $cors_origin always; + add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always; + add_header Access-Control-Allow-Headers "Content-Type, Upgrade, Connection" always; + add_header Access-Control-Allow-Credentials "true" always; + add_header Content-Length 0; + add_header Content-Type text/plain; + return 204; + } + # Proxy to OvenMediaEngine WebRTC signaling port proxy_pass http://ovenmediaengine:3333/; proxy_http_version 1.1; @@ -937,6 +986,7 @@ http { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Origin $http_origin; # WebRTC signaling needs long timeouts proxy_read_timeout 3600s; diff --git a/ovenmediaengine/Server.xml b/ovenmediaengine/Server.xml index c3375b9..bc4852c 100644 --- a/ovenmediaengine/Server.xml +++ b/ovenmediaengine/Server.xml @@ -33,7 +33,8 @@ 3333 - *:10000-10009/udp + + ${env:OME_ICE_CANDIDATES:*}:10000-10009/udp @@ -100,9 +101,11 @@ - 0.5 - 3 - 10 + + 0.2 + 1 + 5 + 0.6 http://localhost