Fix: Use dynamic URLs for all frontend connections
- CSP: Allow WebSocket/HTTP connections to any domain (for production) - Nakama: Detect host/SSL from browser location instead of hardcoded localhost - WebSocket: Dynamic protocol/host detection for stream and watch sync - HLS/LLHLS/WebRTC: Dynamic URLs in live page and stream components - RTMP/SRT: Show actual domain in my-realms settings page - Forums: Use numeric forum ID for banner/title-color API calls 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e26fd346f3
commit
118629549e
9 changed files with 121 additions and 28 deletions
|
|
@ -12,6 +12,17 @@
|
||||||
|
|
||||||
const STREAM_PORT = import.meta.env.VITE_STREAM_PORT || '8088';
|
const STREAM_PORT = import.meta.env.VITE_STREAM_PORT || '8088';
|
||||||
|
|
||||||
|
// Helper for dynamic host detection
|
||||||
|
function getStreamHost() {
|
||||||
|
if (!browser) return 'localhost';
|
||||||
|
return window.location.hostname;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStreamProtocol() {
|
||||||
|
if (!browser) return 'http';
|
||||||
|
return window.location.protocol === 'https:' ? 'https' : 'http';
|
||||||
|
}
|
||||||
|
|
||||||
let player;
|
let player;
|
||||||
let playerElement;
|
let playerElement;
|
||||||
let viewerToken = null;
|
let viewerToken = null;
|
||||||
|
|
@ -111,10 +122,12 @@
|
||||||
function initializePlayer() {
|
function initializePlayer() {
|
||||||
if (!playerElement || !window.OvenPlayer || !viewerToken || !actualStreamKey) return;
|
if (!playerElement || !window.OvenPlayer || !viewerToken || !actualStreamKey) return;
|
||||||
|
|
||||||
|
const host = getStreamHost();
|
||||||
|
const proto = getStreamProtocol();
|
||||||
const sources = [
|
const sources = [
|
||||||
{
|
{
|
||||||
type: 'hls',
|
type: 'hls',
|
||||||
file: `http://localhost:${STREAM_PORT}/app/${actualStreamKey}/llhls.m3u8?token=${viewerToken}`,
|
file: `${proto}://${host}:${STREAM_PORT}/app/${actualStreamKey}/llhls.m3u8?token=${viewerToken}`,
|
||||||
label: 'LLHLS'
|
label: 'LLHLS'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,17 @@
|
||||||
|
|
||||||
const STREAM_PORT = import.meta.env.VITE_STREAM_PORT || '8088';
|
const STREAM_PORT = import.meta.env.VITE_STREAM_PORT || '8088';
|
||||||
|
|
||||||
|
// Helper for dynamic host detection
|
||||||
|
function getStreamHost() {
|
||||||
|
if (!browser) return 'localhost';
|
||||||
|
return window.location.hostname;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStreamProtocol() {
|
||||||
|
if (!browser) return 'http';
|
||||||
|
return window.location.protocol === 'https:' ? 'https' : 'http';
|
||||||
|
}
|
||||||
|
|
||||||
let players = {};
|
let players = {};
|
||||||
let viewerTokens = {};
|
let viewerTokens = {};
|
||||||
let offlineStreams = {}; // Track which streams are offline
|
let offlineStreams = {}; // Track which streams are offline
|
||||||
|
|
@ -127,10 +138,12 @@
|
||||||
|
|
||||||
const isMuted = $streamTiles.unmutedStream !== stream.streamKey;
|
const isMuted = $streamTiles.unmutedStream !== stream.streamKey;
|
||||||
|
|
||||||
|
const host = getStreamHost();
|
||||||
|
const proto = getStreamProtocol();
|
||||||
const sources = [
|
const sources = [
|
||||||
{
|
{
|
||||||
type: 'hls',
|
type: 'hls',
|
||||||
file: `http://localhost:${STREAM_PORT}/app/${stream.streamKey}/llhls.m3u8?token=${token}`,
|
file: `${proto}://${host}:${STREAM_PORT}/app/${stream.streamKey}/llhls.m3u8?token=${token}`,
|
||||||
label: 'LLHLS'
|
label: 'LLHLS'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,33 @@
|
||||||
import { writable, derived } from 'svelte/store';
|
import { writable, derived } from 'svelte/store';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
// Nakama configuration from environment
|
// Nakama configuration - dynamically detect from browser location for production
|
||||||
const NAKAMA_SERVER_KEY = import.meta.env.VITE_NAKAMA_SERVER_KEY || 'defaultkey';
|
const NAKAMA_SERVER_KEY = import.meta.env.VITE_NAKAMA_SERVER_KEY || 'defaultkey';
|
||||||
const NAKAMA_HOST = import.meta.env.VITE_NAKAMA_HOST || 'localhost';
|
|
||||||
const NAKAMA_PORT = import.meta.env.VITE_NAKAMA_PORT || '80';
|
// Dynamically detect host/protocol from browser location
|
||||||
const NAKAMA_USE_SSL = import.meta.env.VITE_NAKAMA_USE_SSL === 'true';
|
// This ensures production uses the correct domain and SSL settings
|
||||||
|
function getNakamaConfig() {
|
||||||
|
if (!browser) {
|
||||||
|
// SSR fallback - use env vars or defaults
|
||||||
|
return {
|
||||||
|
host: import.meta.env.VITE_NAKAMA_HOST || 'localhost',
|
||||||
|
port: import.meta.env.VITE_NAKAMA_PORT || '80',
|
||||||
|
useSSL: import.meta.env.VITE_NAKAMA_USE_SSL === 'true'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Browser: use current page's host/protocol
|
||||||
|
const isSSL = window.location.protocol === 'https:';
|
||||||
|
return {
|
||||||
|
host: window.location.hostname,
|
||||||
|
port: isSSL ? '443' : (window.location.port || '80'),
|
||||||
|
useSSL: isSSL
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const nakamaConfig = getNakamaConfig();
|
||||||
|
const NAKAMA_HOST = nakamaConfig.host;
|
||||||
|
const NAKAMA_PORT = nakamaConfig.port;
|
||||||
|
const NAKAMA_USE_SSL = nakamaConfig.useSSL;
|
||||||
|
|
||||||
// Polling interval for games lists (ms)
|
// Polling interval for games lists (ms)
|
||||||
export const GAMES_POLL_INTERVAL = 30000;
|
export const GAMES_POLL_INTERVAL = 30000;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,16 @@
|
||||||
import { writable, derived, get } from 'svelte/store';
|
import { writable, derived, get } from 'svelte/store';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
const WS_URL = import.meta.env.VITE_WS_URL || 'ws://localhost/ws';
|
// Dynamically detect WebSocket URL from browser location
|
||||||
|
function getWsUrl() {
|
||||||
|
if (!browser) {
|
||||||
|
return import.meta.env.VITE_WS_URL || 'ws://localhost/ws';
|
||||||
|
}
|
||||||
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
return `${protocol}//${window.location.host}/ws`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WS_URL = getWsUrl();
|
||||||
const SYNC_INTERVAL = 5000; // Sync every 5 seconds (server pushes every 1s anyway)
|
const SYNC_INTERVAL = 5000; // Sync every 5 seconds (server pushes every 1s anyway)
|
||||||
const DRIFT_THRESHOLD = 2; // Seek if drift > 2 seconds (CyTube-style tight sync)
|
const DRIFT_THRESHOLD = 2; // Seek if drift > 2 seconds (CyTube-style tight sync)
|
||||||
const LEAD_IN_DURATION = 3000; // 3 seconds lead-in for buffering
|
const LEAD_IN_DURATION = 3000; // 3 seconds lead-in for buffering
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
let ws = null;
|
let ws = null;
|
||||||
let reconnectTimeout = null;
|
let reconnectTimeout = null;
|
||||||
let reconnectAttempts = 0;
|
let reconnectAttempts = 0;
|
||||||
|
|
@ -5,7 +7,17 @@ const MAX_RECONNECT_ATTEMPTS = 10;
|
||||||
const BASE_RECONNECT_DELAY = 1000; // 1 second
|
const BASE_RECONNECT_DELAY = 1000; // 1 second
|
||||||
const MAX_RECONNECT_DELAY = 30000; // 30 seconds
|
const MAX_RECONNECT_DELAY = 30000; // 30 seconds
|
||||||
|
|
||||||
const WS_URL = import.meta.env.VITE_WS_URL || 'ws://localhost/ws';
|
// Dynamically detect WebSocket URL from browser location
|
||||||
|
// This ensures production uses wss:// and the correct host
|
||||||
|
function getWebSocketURL() {
|
||||||
|
if (!browser) {
|
||||||
|
return import.meta.env.VITE_WS_URL || 'ws://localhost/ws';
|
||||||
|
}
|
||||||
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
return `${protocol}//${window.location.host}/ws`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WS_URL = getWebSocketURL();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate exponential backoff delay with jitter
|
* Calculate exponential backoff delay with jitter
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,23 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const STREAM_PORT = import.meta.env.VITE_STREAM_PORT || '8088';
|
const STREAM_PORT = import.meta.env.VITE_STREAM_PORT || '8088';
|
||||||
|
const WEBRTC_PORT = import.meta.env.VITE_WEBRTC_PORT || '3333';
|
||||||
|
|
||||||
|
// Helper functions for dynamic host/protocol detection
|
||||||
|
function getStreamHost() {
|
||||||
|
if (!browser) return 'localhost';
|
||||||
|
return window.location.hostname;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStreamProtocol(secure = false) {
|
||||||
|
if (!browser) return secure ? 'https' : 'http';
|
||||||
|
return window.location.protocol === 'https:' ? 'https' : 'http';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWsProtocol() {
|
||||||
|
if (!browser) return 'ws';
|
||||||
|
return window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||||
|
}
|
||||||
|
|
||||||
let player;
|
let player;
|
||||||
let realm = null;
|
let realm = null;
|
||||||
|
|
@ -297,21 +314,26 @@
|
||||||
const sources = [];
|
const sources = [];
|
||||||
|
|
||||||
if (streamKey) {
|
if (streamKey) {
|
||||||
|
// Dynamic URLs based on current page host/protocol
|
||||||
|
const host = getStreamHost();
|
||||||
|
const httpProto = getStreamProtocol();
|
||||||
|
const wsProto = getWsProtocol();
|
||||||
|
|
||||||
// Add all sources - LLHLS first (default), then HLS, then WebRTC as fallback
|
// Add all sources - LLHLS first (default), then HLS, then WebRTC as fallback
|
||||||
sources.push(
|
sources.push(
|
||||||
{
|
{
|
||||||
type: 'hls',
|
type: 'hls',
|
||||||
file: `http://localhost:${STREAM_PORT}/app/${streamKey}/llhls.m3u8?token=${viewerToken}`,
|
file: `${httpProto}://${host}:${STREAM_PORT}/app/${streamKey}/llhls.m3u8?token=${viewerToken}`,
|
||||||
label: 'LLHLS (Low Latency)'
|
label: 'LLHLS (Low Latency)'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'hls',
|
type: 'hls',
|
||||||
file: `http://localhost:${STREAM_PORT}/app/${streamKey}/ts:playlist.m3u8?token=${viewerToken}`,
|
file: `${httpProto}://${host}:${STREAM_PORT}/app/${streamKey}/ts:playlist.m3u8?token=${viewerToken}`,
|
||||||
label: 'HLS (Standard)'
|
label: 'HLS (Standard)'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'webrtc',
|
type: 'webrtc',
|
||||||
file: `ws://localhost:3333/app/${streamKey}`,
|
file: `${wsProto}://${host}:${WEBRTC_PORT}/app/${streamKey}`,
|
||||||
label: 'WebRTC (Ultra Low Latency)'
|
label: 'WebRTC (Ultra Low Latency)'
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -244,7 +244,7 @@
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('banner', file);
|
formData.append('banner', file);
|
||||||
|
|
||||||
const response = await fetch(`/api/forums/${$page.params.slug}/banner`, {
|
const response = await fetch(`/api/forums/${forum.id}/banner`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
body: formData
|
body: formData
|
||||||
|
|
@ -274,7 +274,7 @@
|
||||||
bannerError = '';
|
bannerError = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/forums/${$page.params.slug}/banner`, {
|
const response = await fetch(`/api/forums/${forum.id}/banner`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
|
|
@ -351,7 +351,7 @@
|
||||||
bannerError = '';
|
bannerError = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/forums/${$page.params.slug}/banner/position`, {
|
const response = await fetch(`/api/forums/${forum.id}/banner/position`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
|
|
@ -396,7 +396,7 @@
|
||||||
bannerError = '';
|
bannerError = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/forums/${$page.params.slug}/title-color`, {
|
const response = await fetch(`/api/forums/${forum.id}/title-color`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
import { auth, isAuthenticated, isStreamer, isRestreamer, isUploader, isWatchCreator } from '$lib/stores/auth';
|
import { auth, isAuthenticated, isStreamer, isRestreamer, isUploader, isWatchCreator } from '$lib/stores/auth';
|
||||||
import { siteSettings } from '$lib/stores/siteSettings';
|
import { siteSettings } from '$lib/stores/siteSettings';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { connectWebSocket, disconnectWebSocket } from '$lib/websocket';
|
import { connectWebSocket, disconnectWebSocket } from '$lib/websocket';
|
||||||
|
|
||||||
|
// Dynamic host for streaming URLs (use current domain in production)
|
||||||
|
$: streamHost = browser ? window.location.hostname : 'localhost';
|
||||||
|
|
||||||
let realms = [];
|
let realms = [];
|
||||||
let loading = true;
|
let loading = true;
|
||||||
let error = '';
|
let error = '';
|
||||||
|
|
@ -2968,13 +2972,13 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="stream-info-row">
|
<div class="stream-info-row">
|
||||||
<span class="stream-info-label">RTMP URL:</span>
|
<span class="stream-info-label">RTMP URL:</span>
|
||||||
<span class="stream-key">rtmp://localhost:1935/app/{realm.streamKey}</span>
|
<span class="stream-key">rtmp://{streamHost}:1935/app/{realm.streamKey}</span>
|
||||||
<button on:click={() => copyToClipboard(`rtmp://localhost:1935/app/${realm.streamKey}`)}>Copy</button>
|
<button on:click={() => copyToClipboard(`rtmp://${streamHost}:1935/app/${realm.streamKey}`)}>Copy</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="stream-info-row">
|
<div class="stream-info-row">
|
||||||
<span class="stream-info-label">SRT URL:</span>
|
<span class="stream-info-label">SRT URL:</span>
|
||||||
<span class="stream-key">srt://localhost:9999?streamid={encodeURIComponent(`srt://localhost:9999/app/${realm.streamKey}`)}</span>
|
<span class="stream-key">srt://{streamHost}:9999?streamid={encodeURIComponent(`srt://${streamHost}:9999/app/${realm.streamKey}`)}</span>
|
||||||
<button on:click={() => copyToClipboard(`srt://localhost:9999?streamid=${encodeURIComponent(`srt://localhost:9999/app/${realm.streamKey}`)}`)}>Copy</button>
|
<button on:click={() => copyToClipboard(`srt://${streamHost}:9999?streamid=${encodeURIComponent(`srt://${streamHost}:9999/app/${realm.streamKey}`)}`)}>Copy</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,15 +22,13 @@ const config = {
|
||||||
'font-src': ["'self'", 'data:', 'https://cdnjs.cloudflare.com'],
|
'font-src': ["'self'", 'data:', 'https://cdnjs.cloudflare.com'],
|
||||||
'connect-src': [
|
'connect-src': [
|
||||||
"'self'",
|
"'self'",
|
||||||
'ws://localhost:*',
|
'ws://*:*', // Allow any WebSocket
|
||||||
'wss://localhost:*',
|
'wss://*:*', // Allow any secure WebSocket
|
||||||
'http://localhost:*',
|
'http://*:*', // Allow any HTTP (for dev and streaming)
|
||||||
'ws://127.0.0.1:*',
|
'https://*:*', // Allow any HTTPS
|
||||||
'wss://127.0.0.1:*',
|
|
||||||
'http://127.0.0.1:*',
|
|
||||||
'https://www.youtube.com'
|
'https://www.youtube.com'
|
||||||
],
|
],
|
||||||
'media-src': ["'self'", 'blob:', 'http://localhost:*'],
|
'media-src': ["'self'", 'blob:', 'http://*:*', 'https://*:*'],
|
||||||
'frame-src': ["'self'", 'blob:', 'https://www.youtube.com'],
|
'frame-src': ["'self'", 'blob:', 'https://www.youtube.com'],
|
||||||
'object-src': ["'none'"],
|
'object-src': ["'none'"],
|
||||||
'frame-ancestors': ["'none'"],
|
'frame-ancestors': ["'none'"],
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue