fixes lol
Some checks failed
Build and Push / build-all (push) Has been cancelled

This commit is contained in:
doomtube 2026-01-11 20:08:40 -05:00
parent 58392b7d6a
commit 381e8b79b0
17 changed files with 282 additions and 82 deletions

View file

@ -115,8 +115,8 @@ namespace {
return ""; return "";
} }
// Compute 200 peaks from samples // Compute 500 peaks from samples for finer waveform detail
const int numPeaks = 200; const int numPeaks = 500;
std::vector<float> peaks(numPeaks, 0.0f); std::vector<float> peaks(numPeaks, 0.0f);
size_t samplesPerPeak = samples.size() / numPeaks; size_t samplesPerPeak = samples.size() / numPeaks;
if (samplesPerPeak == 0) samplesPerPeak = 1; if (samplesPerPeak == 0) samplesPerPeak = 1;

View file

@ -15,6 +15,8 @@ std::mutex PyramidWebSocketController::connectionsMutex_;
// ==================== Validation Helpers ==================== // ==================== Validation Helpers ====================
bool PyramidController::isValidPixelPosition(int faceId, int x, int y) { bool PyramidController::isValidPixelPosition(int faceId, int x, int y) {
constexpr int FACE_SIZE = 200;
// Face ID must be 0-4 // Face ID must be 0-4
if (faceId < 0 || faceId > 4) return false; if (faceId < 0 || faceId > 4) return false;
@ -673,7 +675,25 @@ void PyramidWebSocketController::handlePlacePixel(const WebSocketConnectionPtr &
int y = msg["y"].asInt(); int y = msg["y"].asInt();
std::string color = msg["color"].asString(); 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 // 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); std::transform(color.begin(), color.end(), color.begin(), ::toupper);
// Use the REST endpoint logic for actual placement // Use the REST endpoint logic for actual placement

View file

@ -56,12 +56,13 @@ public:
const std::string &x, const std::string &x,
const std::string &y); 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: private:
static constexpr int DAILY_PIXEL_LIMIT = 1000; static constexpr int DAILY_PIXEL_LIMIT = 1000;
static constexpr int FACE_SIZE = 200; 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 // WebSocket controller for real-time pixel updates

View file

@ -16,6 +16,12 @@ std::string RestreamService::getBaseUrl() {
return "http://ovenmediaengine:8081"; 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() { std::string RestreamService::getApiToken() {
const char* envToken = std::getenv("OME_API_TOKEN"); const char* envToken = std::getenv("OME_API_TOKEN");
if (!envToken || strlen(envToken) == 0) { if (!envToken || strlen(envToken) == 0) {
@ -28,6 +34,10 @@ HttpClientPtr RestreamService::getClient() {
return HttpClient::newHttpClient(getBaseUrl()); return HttpClient::newHttpClient(getBaseUrl());
} }
HttpClientPtr RestreamService::getPushProxyClient() {
return HttpClient::newHttpClient(getPushProxyUrl());
}
HttpRequestPtr RestreamService::createRequest(HttpMethod method, const std::string& path) { HttpRequestPtr RestreamService::createRequest(HttpMethod method, const std::string& path) {
auto request = HttpRequest::newHttpRequest(); auto request = HttpRequest::newHttpRequest();
request->setMethod(method); request->setMethod(method);
@ -106,12 +116,13 @@ void RestreamService::startPush(const std::string& sourceStreamKey, const Restre
body["protocol"] = "rtmp"; body["protocol"] = "rtmp";
body["url"] = fullUrl; body["url"] = fullUrl;
// Use Drogon HttpClient instead of curl for security // Use openresty proxy to avoid URL encoding issues with colon in path
auto request = createJsonRequest(drogon::Post, "/v1/vhosts/default/apps/app:startPush", body); // 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) { [this, callback, pushId, sourceStreamKey, destId](ReqResult result, const HttpResponsePtr& response) {
if (result != ReqResult::Ok || !response) { if (result != ReqResult::Ok || !response) {
std::string error = "Failed to connect to OME API"; 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; Json::Value body;
body["id"] = pushId; body["id"] = pushId;
// Use Drogon HttpClient instead of curl for security // Use openresty proxy to avoid URL encoding issues with colon in path
auto request = createJsonRequest(drogon::Post, "/v1/vhosts/default/apps/app:stopPush", body); 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) { [this, callback, pushId, sourceStreamKey, destinationId](ReqResult result, const HttpResponsePtr& response) {
// Remove from tracking regardless of result // Remove from tracking regardless of result
{ {

View file

@ -57,8 +57,10 @@ private:
RestreamService& operator=(const RestreamService&) = delete; RestreamService& operator=(const RestreamService&) = delete;
std::string getBaseUrl(); std::string getBaseUrl();
std::string getPushProxyUrl(); // Openresty proxy for push API (avoids URL encoding issues)
std::string getApiToken(); std::string getApiToken();
drogon::HttpClientPtr getClient(); drogon::HttpClientPtr getClient();
drogon::HttpClientPtr getPushProxyClient(); // Client for push proxy
drogon::HttpRequestPtr createRequest(drogon::HttpMethod method, const std::string& path); drogon::HttpRequestPtr createRequest(drogon::HttpMethod method, const std::string& path);
drogon::HttpRequestPtr createJsonRequest(drogon::HttpMethod method, const std::string& path, drogon::HttpRequestPtr createJsonRequest(drogon::HttpMethod method, const std::string& path,
const Json::Value& body); const Json::Value& body);

View file

@ -53,6 +53,7 @@ services:
environment: environment:
OME_API_PORT: 8081 OME_API_PORT: 8081
OME_API_ACCESS_TOKEN: ${OME_API_TOKEN} OME_API_ACCESS_TOKEN: ${OME_API_TOKEN}
OME_ICE_CANDIDATES: ${OME_ICE_CANDIDATES:-*}
networks: networks:
- backend - backend
- frontend - frontend

View file

@ -46,6 +46,7 @@ services:
environment: environment:
OME_API_PORT: 8081 OME_API_PORT: 8081
OME_API_ACCESS_TOKEN: ${OME_API_TOKEN} OME_API_ACCESS_TOKEN: ${OME_API_TOKEN}
OME_ICE_CANDIDATES: ${OME_ICE_CANDIDATES:-*}
networks: networks:
- backend - backend
- frontend - frontend

View file

@ -172,14 +172,33 @@
debug: false, debug: false,
enableWorker: true, enableWorker: true,
lowLatencyMode: true, lowLatencyMode: true,
backBufferLength: 90,
// Increased retry settings for LLHLS resilience // Buffer Management - optimized for low latency
fragLoadingMaxRetry: 6, maxBufferLength: 4,
fragLoadingRetryDelay: 1000, maxMaxBufferLength: 6,
manifestLoadingMaxRetry: 4, backBufferLength: 10,
levelLoadingMaxRetry: 4,
maxBufferLength: 30, // Live Edge Sync - key for reducing latency
maxBufferHole: 0.5, 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) { xhrSetup: function(xhr, url) {
let finalUrl = url; let finalUrl = url;

View file

@ -161,14 +161,33 @@
debug: false, debug: false,
enableWorker: true, enableWorker: true,
lowLatencyMode: true, lowLatencyMode: true,
backBufferLength: 30,
// Increased retry settings for LLHLS resilience // Buffer Management - optimized for low latency
fragLoadingMaxRetry: 6, maxBufferLength: 4,
fragLoadingRetryDelay: 1000, maxMaxBufferLength: 6,
manifestLoadingMaxRetry: 4, backBufferLength: 10,
levelLoadingMaxRetry: 4,
maxBufferLength: 30, // Live Edge Sync - key for reducing latency
maxBufferHole: 0.5, 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) { xhrSetup: function(xhr, url) {
let finalUrl = url; let finalUrl = url;

View file

@ -19,6 +19,9 @@
/** @type {string} Waveform color */ /** @type {string} Waveform color */
export let color = '#ec4899'; export let color = '#ec4899';
/** @type {string} Color for the played portion */
export let playedColor = '#f472b6';
/** @type {number} Opacity of the waveform (0-1) */ /** @type {number} Opacity of the waveform (0-1) */
export let opacity = 0.15; export let opacity = 0.15;
@ -32,9 +35,8 @@
let peaks = []; let peaks = [];
let loading = true; let loading = true;
// Calculate scroll position based on playback // Calculate progress percentage (0-100)
$: progress = duration > 0 ? currentTime / duration : 0; $: progress = duration > 0 ? (currentTime / duration) * 100 : 0;
$: translateX = isCurrentTrack && isPlaying ? -(progress * 50) : 0;
async function fetchWaveform() { async function fetchWaveform() {
if (!audioId && !waveformPath) { if (!audioId && !waveformPath) {
@ -91,34 +93,52 @@
> >
<svg <svg
class="waveform-svg" class="waveform-svg"
viewBox="0 0 400 100" viewBox="0 0 1000 100"
preserveAspectRatio="none" preserveAspectRatio="none"
style="transform: translateX({translateX}%);"
> >
<!-- Mirror waveform: bars extend up and down from center --> <!-- Mirror waveform: bars extend up and down from center -->
{#each peaks as peak, i} {#each peaks as peak, i}
{@const barWidth = 400 / peaks.length} {@const barWidth = 1000 / peaks.length}
{@const barHeight = peak * 45} {@const barHeight = peak * 45}
{@const x = i * barWidth} {@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}
<!-- Top bar (from center going up) --> <!-- Top bar (from center going up) -->
<rect <rect
x={x} x={x}
y={50 - barHeight} y={50 - barHeight}
width={barWidth * 0.8} width={barWidth * 0.7}
height={barHeight} height={barHeight}
fill={color} fill={barColor}
rx="1" opacity={barOpacity}
rx="0.5"
/> />
<!-- Bottom bar (from center going down) --> <!-- Bottom bar (from center going down) -->
<rect <rect
x={x} x={x}
y={50} y={50}
width={barWidth * 0.8} width={barWidth * 0.7}
height={barHeight} height={barHeight}
fill={color} fill={barColor}
rx="1" opacity={barOpacity}
rx="0.5"
/> />
{/each} {/each}
<!-- Playhead line -->
{#if isCurrentTrack && progress > 0}
<line
x1={progress * 10}
y1="5"
x2={progress * 10}
y2="95"
stroke={playedColor}
stroke-width="2"
stroke-linecap="round"
/>
{/if}
</svg> </svg>
</div> </div>
{/if} {/if}
@ -133,8 +153,7 @@
} }
.waveform-svg { .waveform-svg {
width: 200%; width: 100%;
height: 100%; height: 100%;
transition: transform 0.1s linear;
} }
</style> </style>

View file

@ -24,6 +24,7 @@
let showCalendar = false; let showCalendar = false;
let calendarDate = new Date(); let calendarDate = new Date();
let timeInterval; let timeInterval;
let calendarContainerEl;
// Tab navigation - includes audio, ebooks, games, and treasury // Tab navigation - includes audio, ebooks, games, and treasury
let activeTab = 'terminal'; let activeTab = 'terminal';
@ -308,7 +309,11 @@
}); });
</script> </script>
<svelte:window on:click={() => showCalendar = false} /> <svelte:window on:click={(e) => {
if (calendarContainerEl && !calendarContainerEl.contains(e.target)) {
showCalendar = false;
}
}} />
{#if isOpen && $isAuthenticated} {#if isOpen && $isAuthenticated}
<div <div
@ -325,8 +330,8 @@
<div class="terminal-header" on:mousedown={!isDocked ? startDrag : null}> <div class="terminal-header" on:mousedown={!isDocked ? startDrag : null}>
<TerminalTabBar {tabs} {activeTab} on:tabChange={handleTabChange} /> <TerminalTabBar {tabs} {activeTab} on:tabChange={handleTabChange} />
<div class="header-right"> <div class="header-right">
<div class="datetime-container"> <div class="datetime-container" bind:this={calendarContainerEl}>
<button class="datetime-button" on:click|stopPropagation={toggleCalendar} title="Show calendar"> <button class="datetime-button" on:click={toggleCalendar} title="Show calendar">
<span class="datetime-date">{formatDate(currentTime)}</span> <span class="datetime-date">{formatDate(currentTime)}</span>
<span class="datetime-time">{formatTime(currentTime)}</span> <span class="datetime-time">{formatTime(currentTime)}</span>
</button> </button>

View file

@ -20,6 +20,24 @@
let hoveredFace = null; let hoveredFace = null;
let hoveredPixel = 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 // Unsubscribe function for store
let unsubscribePyramid; let unsubscribePyramid;
@ -274,11 +292,18 @@
const x = Math.floor(uv.x * FACE_SIZE); const x = Math.floor(uv.x * FACE_SIZE);
const y = Math.floor((1 - uv.y) * FACE_SIZE); const y = Math.floor((1 - uv.y) * FACE_SIZE);
// Only set hover if position is valid (especially for triangular faces)
if (isValidPixelPosition(faceId, x, y)) {
hoveredFace = faceId; hoveredFace = faceId;
hoveredPixel = { faceId, x, y }; hoveredPixel = { faceId, x, y };
dispatch('hover', { faceId, x, y }); dispatch('hover', { faceId, x, y });
pyramidStore.setHoveredPixel({ faceId, x, y }); pyramidStore.setHoveredPixel({ faceId, x, y });
} else {
hoveredFace = null;
hoveredPixel = null;
pyramidStore.setHoveredPixel(null);
}
} }
} else { } else {
hoveredFace = null; hoveredFace = null;

View file

@ -426,20 +426,42 @@
sources: sources, sources: sources,
webrtcConfig: { webrtcConfig: {
timeoutMaxRetry: 4, timeoutMaxRetry: 4,
connectionTimeout: 10000 connectionTimeout: 10000,
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }
]
}, },
hlsConfig: { hlsConfig: {
debug: false, debug: false,
enableWorker: true, enableWorker: true,
lowLatencyMode: true, lowLatencyMode: true,
backBufferLength: 90,
// Increased retry settings for LLHLS resilience // Buffer Management - optimized for low latency
fragLoadingMaxRetry: 6, maxBufferLength: 4,
fragLoadingRetryDelay: 1000, maxMaxBufferLength: 6,
manifestLoadingMaxRetry: 4, backBufferLength: 10,
levelLoadingMaxRetry: 4,
maxBufferLength: 30, // Live Edge Sync - key for reducing latency
maxBufferHole: 0.5, 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) { xhrSetup: function(xhr, url) {
let finalUrl = url; let finalUrl = url;

View file

@ -39,13 +39,10 @@
return date.toLocaleDateString(); return date.toLocaleDateString();
} }
function isInPlaylist(audioId) { // Reactive sets for tracking playlist state - these update when stores change
return $audioPlaylist.queue.some(t => t.id === audioId); $: playlistIds = new Set($audioPlaylist.queue.map(t => t.id));
} $: currentPlayingId = $audioPlaylist.isPlaying ? $currentTrack?.id : null;
$: currentTrackId = $currentTrack?.id;
function isCurrentlyPlaying(audioId) {
return $currentTrack?.id === audioId && $audioPlaylist.isPlaying;
}
function handlePlayClick(audio) { function handlePlayClick(audio) {
if ($currentTrack?.id === audio.id) { if ($currentTrack?.id === audio.id) {
@ -91,7 +88,7 @@
} }
function togglePlaylist(audio) { function togglePlaylist(audio) {
if (isInPlaylist(audio.id)) { if (playlistIds.has(audio.id)) {
audioPlaylist.removeTrack(audio.id); audioPlaylist.removeTrack(audio.id);
} else { } else {
audioPlaylist.addTrack({ audioPlaylist.addTrack({
@ -135,7 +132,7 @@
function addAllToPlaylist() { function addAllToPlaylist() {
audioFiles.forEach(audio => { audioFiles.forEach(audio => {
if (!isInPlaylist(audio.id)) { if (!playlistIds.has(audio.id)) {
addToPlaylist(audio); addToPlaylist(audio);
} }
}); });
@ -521,7 +518,7 @@
isPlaying={$audioPlaylist.isPlaying} isPlaying={$audioPlaylist.isPlaying}
currentTime={$audioPlaylist.currentTime} currentTime={$audioPlaylist.currentTime}
duration={$audioPlaylist.duration} duration={$audioPlaylist.duration}
isCurrentTrack={$currentTrack?.id === audio.id} isCurrentTrack={currentTrackId === audio.id}
/> />
{/if} {/if}
<span class="audio-number">{index + 1}</span> <span class="audio-number">{index + 1}</span>
@ -546,19 +543,19 @@
<div class="audio-actions"> <div class="audio-actions">
<button <button
class="action-btn play" class="action-btn play"
class:playing={isCurrentlyPlaying(audio.id)} class:playing={currentPlayingId === audio.id}
on:click={() => handlePlayClick(audio)} on:click={() => handlePlayClick(audio)}
title={isCurrentlyPlaying(audio.id) ? 'Pause' : 'Play now'} title={currentPlayingId === audio.id ? 'Pause' : 'Play now'}
> >
{isCurrentlyPlaying(audio.id) ? '▮▮' : '▶'} {currentPlayingId === audio.id ? '▮▮' : '▶'}
</button> </button>
<button <button
class="action-btn" class="action-btn"
class:added={isInPlaylist(audio.id)} class:added={playlistIds.has(audio.id)}
on:click={() => togglePlaylist(audio)} on:click={() => togglePlaylist(audio)}
title={isInPlaylist(audio.id) ? 'Remove from playlist' : 'Add to playlist'} title={playlistIds.has(audio.id) ? 'Remove from playlist' : 'Add to playlist'}
> >
{isInPlaylist(audio.id) ? '✓' : '+'} {playlistIds.has(audio.id) ? '✓' : '+'}
</button> </button>
{#if $isAuthenticated} {#if $isAuthenticated}
<button <button

View file

@ -28,6 +28,7 @@
let showCalendar = false; let showCalendar = false;
let calendarDate = new Date(); let calendarDate = new Date();
let timeInterval; let timeInterval;
let calendarContainerEl;
const tabs = [ const tabs = [
{ id: 'terminal', label: 'Terminal' }, { id: 'terminal', label: 'Terminal' },
@ -196,7 +197,11 @@
}); });
</script> </script>
<svelte:window on:click={() => showCalendar = false} /> <svelte:window on:click={(e) => {
if (calendarContainerEl && !calendarContainerEl.contains(e.target)) {
showCalendar = false;
}
}} />
<svelte:head> <svelte:head>
<title>{$siteSettings.site_title} - Terminal</title> <title>{$siteSettings.site_title} - Terminal</title>
@ -219,8 +224,8 @@
<div class="popout-header"> <div class="popout-header">
<TerminalTabBar {tabs} {activeTab} on:tabChange={handleTabChange} /> <TerminalTabBar {tabs} {activeTab} on:tabChange={handleTabChange} />
<div class="header-right"> <div class="header-right">
<div class="datetime-container"> <div class="datetime-container" bind:this={calendarContainerEl}>
<button class="datetime-button" on:click|stopPropagation={toggleCalendar} title="Show calendar"> <button class="datetime-button" on:click={toggleCalendar} title="Show calendar">
<span class="datetime-date">{formatDate(currentTime)}</span> <span class="datetime-date">{formatDate(currentTime)}</span>
<span class="datetime-time">{formatTime(currentTime)}</span> <span class="datetime-time">{formatTime(currentTime)}</span>
</button> </button>

View file

@ -84,6 +84,10 @@ http {
server ovenmediaengine:8080; server ovenmediaengine:8080;
} }
upstream ome_api {
server ovenmediaengine:8081;
}
# Rate limiting zones # Rate limiting zones
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s; 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; limit_req_zone $binary_remote_addr zone=auth_limit:10m rate=3r/m;
@ -735,6 +739,34 @@ http {
return 403; 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) # Public stickers endpoint - no authentication required (guests need stickers for chat)
location = /api/admin/stickers { location = /api/admin/stickers {
# CORS headers # CORS headers
@ -925,6 +957,23 @@ http {
# WebRTC Signaling proxy for OvenMediaEngine # WebRTC Signaling proxy for OvenMediaEngine
# Handles wss:// ws:// translation so OME doesn't need TLS certificates # Handles wss:// ws:// translation so OME doesn't need TLS certificates
location /webrtc/ { 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 to OvenMediaEngine WebRTC signaling port
proxy_pass http://ovenmediaengine:3333/; proxy_pass http://ovenmediaengine:3333/;
proxy_http_version 1.1; proxy_http_version 1.1;
@ -937,6 +986,7 @@ http {
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Origin $http_origin;
# WebRTC signaling needs long timeouts # WebRTC signaling needs long timeouts
proxy_read_timeout 3600s; proxy_read_timeout 3600s;

View file

@ -33,7 +33,8 @@
<Port>3333</Port> <Port>3333</Port>
</Signalling> </Signalling>
<IceCandidates> <IceCandidates>
<IceCandidate>*:10000-10009/udp</IceCandidate> <!-- Use public IP/domain for NAT traversal -->
<IceCandidate>${env:OME_ICE_CANDIDATES:*}:10000-10009/udp</IceCandidate>
</IceCandidates> </IceCandidates>
</WebRTC> </WebRTC>
<Thumbnail> <Thumbnail>
@ -100,9 +101,11 @@
<Publishers> <Publishers>
<LLHLS> <LLHLS>
<ChunkDuration>0.5</ChunkDuration> <!-- Optimized for low latency: 200ms chunks, 1s segments -->
<SegmentDuration>3</SegmentDuration> <ChunkDuration>0.2</ChunkDuration>
<SegmentCount>10</SegmentCount> <SegmentDuration>1</SegmentDuration>
<SegmentCount>5</SegmentCount>
<PartHoldBack>0.6</PartHoldBack>
<CrossDomains> <CrossDomains>
<Url>http://localhost</Url> <Url>http://localhost</Url>
</CrossDomains> </CrossDomains>