feat: add environment configuration files and update WHIP server URL handling

This commit is contained in:
Cesar Mendivil 2026-03-20 16:21:44 -07:00
parent bc97ee0a68
commit 62a6697d7d
9 changed files with 420 additions and 15 deletions

21
.env.example Normal file
View File

@ -0,0 +1,21 @@
# Environment variables for local development
# Set your YouTube Data API v3 key here (do NOT commit real keys to public repos)
YOUTUBE_API_KEY="AIzaSyABiXKk-1tcoR0wQnccZfutBDi0ijTr0Ko"
REACT_APP_CORE_URL=https://restreamer.nextream.sytes.net
#REACT_APP_YTDLP_URL=http://100.73.244.28:8080
REACT_APP_YTDLP_URL=http://144.217.82.82:8282
REACT_APP_YTDLP_URL_TITLES=http://100.73.244.28:8080
# URL que la UI usa para construir el input WHEP del Core.
# Apunta al mismo dominio del UI — Caddy proxea /whep/* → egress server.
REACT_APP_WHIP_SERVER_URL=https://djmaster.nextream.sytes.net
# Vars REACT_APP_ expuestas al browser (CRA las inyecta en el bundle).
# La UI llama a IngressClient directamente sin intermediario Node.
REACT_APP_LIVEKIT_API_KEY=APIBTqTGxf9htMK
REACT_APP_LIVEKIT_API_SECRET=0dOHWPffwneaPg7OYpe4PeAes21zLJfeYJB9cKzSTtXW
REACT_APP_LIVEKIT_WS_URL=wss://livekit-server.nextream.sytes.net
LIVEKIT_INGRESS_INTERNAL_URL=http://192.168.1.20:8088
LIVEKIT_INGRESS_HOST=192.168.1.20:8088
UI_BASE_URL=https://djmaster.nextream.sytes.net
# Destino local del proxy de desarrollo para /api/whip/* y /whep/* (CRA setupProxy).
# NO confundir con REACT_APP_WHIP_SERVER_URL (esa es la URL pública para el frontend).
WHIP_API_TARGET=http://localhost:3005

View File

@ -1,8 +1,8 @@
# Local overrides (gitignored). Put real secrets here on your development machine.
REACT_APP_YOUTUBE_API_KEY="AIzaSyABiXKk-1tcoR0wQnccZfutBDi0ijTr0Ko"
REACT_APP_CORE_URL=https://restreamer.nextream.sytes.net
REACT_APP_WHIP_BASE_URL=http://192.168.1.15:8555
REACT_APP_YTDLP_URL=http://144.217.82.82:8282
REACT_APP_CORE_URL=http://192.168.1.20:8080
REACT_APP_WHIP_BASE_URL=http://192.168.1.20:8555
REACT_APP_YTDLP_URL=http://192.168.1.20:8282
REACT_APP_YTDLP_URL_TITLES=http://100.73.244.28:8080
REACT_APP_FB_SERVER_URL=http://localhost:3002
REACT_APP_LIVEKIT_API_KEY=APIBTqTGxf9htMK

View File

@ -0,0 +1,121 @@
# djmaster HTTP-only Nginx template
# Use this when another front-facing proxy/terminator handles TLS and you want
# Nginx to listen on port 80 (cleartext) and proxy to local backends.
# Map for websocket upgrade handling
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name ${UI_HOST};
client_max_body_size 512M;
client_body_timeout 300s;
keepalive_timeout 65;
# Frontend (UI) — proxy to your UI backend (webpack dev or static server)
# Set UI_BACKEND_HOST to 192.168.1.15 (or 127.0.0.1) when rendering
location / {
proxy_pass http://${UI_BACKEND_HOST}:3000;
proxy_set_header Host $host;
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 X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_buffering off;
proxy_read_timeout 120s;
}
# WebSocket endpoint (HMR / app ws) — ensure upgrades are forwarded
location /ws {
proxy_pass http://${UI_BACKEND_HOST}:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 3600s;
proxy_buffering off;
}
# Microserver for OAuth and config persistence
location /fb-server/ {
proxy_pass http://${MICROSERVER_HOST}:3002/;
proxy_set_header Host $host;
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_http_version 1.1;
proxy_buffering off;
proxy_read_timeout 120s;
}
# WHIP Ingest API and WHEP relay (egress). If your egress listens elsewhere,
# adjust ${WHIP_HOST} and ports accordingly.
location /api/whip/ {
proxy_pass http://${WHIP_HOST}:3005/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_buffering off;
proxy_read_timeout 300s;
}
location /whep/ {
proxy_pass http://${WHIP_HOST}:3005/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_buffering off;
proxy_read_timeout 300s;
}
# WHIP ingest (POST with SDP) — forward to livekit-ingress/internal target
location /w/ {
proxy_pass http://${LIVEKIT_INGRESS_HOST}/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_buffering off;
proxy_read_timeout 300s;
}
# yt-dlp stream extractor and metadata proxies
location /yt-stream/ {
proxy_pass http://${YTDLP_HOST}/;
proxy_set_header Host ${YTDLP_HOST};
proxy_http_version 1.1;
proxy_buffering off;
proxy_read_timeout 300s;
}
location /yt-titles/ {
proxy_pass http://${YTDLP_TITLES_HOST}/;
proxy_set_header Host ${YTDLP_TITLES_HOST};
proxy_http_version 1.1;
proxy_buffering off;
proxy_read_timeout 120s;
}
# Basic security headers
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header Referrer-Policy "no-referrer-when-downgrade";
add_header X-XSS-Protection "1; mode=block";
}

View File

@ -21,13 +21,51 @@
* FB_SERVER_URL = '' (leave empty Caddy proxies /fb-server localhost:3002)
*/
window.__RESTREAMER_CONFIG__ = {
CORE_ADDRESS: '',
CORE_ADDRESS: 'http://192.168.1.20:8080',
YTDLP_URL: '',
YTDLP_TITLES_URL: '',
FB_SERVER_URL: '',
// URL pública del servidor egress (WHIP ingest + WHEP relay).
// Ej: 'https://llmchats-whep.zuqtxy.easypanel.host'
WHIP_SERVER_URL: '',
// Se fija explícitamente para respetar las variables locales.
WHIP_SERVER_URL: 'http://192.168.1.20:8555',
// Optional: override the WHIP port used when deriving from CORE_ADDRESS
WHIP_PORT: '',
};
// Derive WHIP_SERVER_URL at runtime if not explicitly provided.
(function () {
try {
var cfg = window.__RESTREAMER_CONFIG__ || {};
// helper to derive from a base URL and a port
function deriveFrom(base, port) {
if (!base) return '';
try {
var u = new URL(base);
var p = port && port.length ? port : '8555';
return u.protocol + '//' + u.hostname + (p ? ':' + p : '');
} catch (e) {
return '';
}
}
if ((!cfg.WHIP_SERVER_URL || cfg.WHIP_SERVER_URL.length === 0)) {
// prefer explicit CORE_ADDRESS runtime value
if (cfg.CORE_ADDRESS && cfg.CORE_ADDRESS.length) {
var derived = deriveFrom(cfg.CORE_ADDRESS, cfg.WHIP_PORT);
if (derived) cfg.WHIP_SERVER_URL = derived;
} else if (window && window.location && window.location.origin) {
// fallback to current origin
var derived2 = deriveFrom(window.location.origin, cfg.WHIP_PORT);
if (derived2) cfg.WHIP_SERVER_URL = derived2;
}
}
window.__RESTREAMER_CONFIG__ = cfg;
} catch (e) {
// no-op
}
})();

View File

@ -0,0 +1,186 @@
#!/usr/bin/env node
/**
* check-whip-obs-live.js
*
* Verifica de forma E2E que un publisher WHIP (ej. OBS) está enviando al
* endpoint proporcionado. Realiza:
* - OPTIONS al endpoint WHIP (comprobación básica)
* - GET al endpoint /whip/<key>/sdp (si está disponible)
* - Polling a Core: GET <CORE_URL>/api/v3/whip para detectar el stream activo
*
* Uso:
* node scripts/check-whip-obs-live.js --url "http://192.168.1.15:8555/whip/06a2..." --token heavy666 --core http://192.168.1.15:8080 --timeout 30
*/
'use strict';
const http = require('http');
const https = require('https');
function parseArgs() {
const out = {};
const argv = process.argv.slice(2);
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a.startsWith('--')) {
const k = a.replace(/^--/, '');
const v = (argv[i+1] && !argv[i+1].startsWith('--')) ? argv[++i] : 'true';
out[k] = v;
}
}
return out;
}
function request(url, { method = 'GET', headers = {}, body, timeout = 15000 } = {}) {
return new Promise((resolve, reject) => {
try {
const u = new URL(url);
const lib = u.protocol === 'https:' ? https : http;
const raw = body;
const opts = {
hostname: u.hostname,
port: u.port || (u.protocol === 'https:' ? 443 : 80),
path: u.pathname + (u.search || ''),
method,
headers: { ...headers },
timeout,
rejectUnauthorized: false,
};
const req = lib.request(opts, (res) => {
let buf = '';
res.on('data', (c) => (buf += c));
res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: buf }));
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(new Error('timeout')); });
if (raw) req.write(raw);
req.end();
} catch (e) {
reject(e);
}
});
}
async function main() {
const args = parseArgs();
const WHIP_URL = args.url || process.env.WHIP_URL;
const TOKEN = args.token || process.env.WHIP_TOKEN || '';
const CORE_URL = args.core || process.env.CORE_URL || process.env.REACT_APP_CORE_URL || 'http://127.0.0.1:8080';
const TIMEOUT = parseInt(args.timeout || process.env.WHIP_CHECK_TIMEOUT || '30', 10);
const INTERVAL = parseInt(args.interval || process.env.WHIP_CHECK_INTERVAL || '3', 10);
if (!WHIP_URL) {
console.error('Usage: node scripts/check-whip-obs-live.js --url <WHIP_URL> [--token <token>] [--core <CORE_URL>]');
process.exit(2);
}
console.log('\nWHIP E2E live check');
console.log(` WHIP URL: ${WHIP_URL}`);
if (TOKEN) console.log(` token: ${TOKEN}`);
console.log(` Core: ${CORE_URL}`);
console.log(` Poll timeout: ${TIMEOUT}s (interval ${INTERVAL}s)`);
// Extract stream key
let streamKey = null;
try {
const u = new URL(WHIP_URL);
const parts = u.pathname.split('/').filter(Boolean);
streamKey = parts[parts.length - 1];
} catch (e) {
console.error('Invalid WHIP_URL:', e.message);
process.exit(2);
}
// OPTIONS (OBS does this first)
try {
const urlWithToken = TOKEN ? (WHIP_URL + (WHIP_URL.includes('?') ? '&' : '?') + 'token=' + encodeURIComponent(TOKEN)) : WHIP_URL;
console.log('\n1) OPTIONS → WHIP endpoint (discovery)');
const opt = await request(urlWithToken, {
method: 'OPTIONS',
headers: {
'Origin': args.origin || 'http://localhost:3000',
'Access-Control-Request-Method': 'POST',
'Access-Control-Request-Headers': 'content-type',
},
timeout: 10000,
});
console.log(` HTTP ${opt.status}`);
console.log(' Allow:', opt.headers['allow'] || opt.headers['access-control-allow-methods'] || '(none)');
} catch (e) {
console.error(' OPTIONS failed:', e.message);
}
// GET /whip/<key>/sdp (relay SDP) — may or may not be implemented
try {
const base = (() => { const u = new URL(WHIP_URL); return u.origin; })();
const sdpUrl = base + '/whip/' + encodeURIComponent(streamKey) + '/sdp' + (TOKEN ? ('?token=' + encodeURIComponent(TOKEN)) : '');
console.log('\n2) GET /whip/<key>/sdp (relay SDP)');
const r = await request(sdpUrl, { method: 'GET', timeout: 8000 });
console.log(` GET ${sdpUrl} → HTTP ${r.status}`);
if (r.body && r.body.length) console.log(' Body length:', r.body.length);
} catch (e) {
console.error(' GET /sdp failed:', e.message);
}
// Poll Core /api/v3/whip for active publishers
console.log('\n3) Poll Core /api/v3/whip looking for active stream key');
const start = Date.now();
let found = false;
const whipApi = (CORE_URL.replace(/\/$/, '') + '/api/v3/whip');
// Build optional auth header
const headers = {};
if (process.env.CORE_AUTH) {
headers['Authorization'] = process.env.CORE_AUTH;
} else if (process.env.CORE_USER && process.env.CORE_PASS) {
const token = Buffer.from(process.env.CORE_USER + ':' + process.env.CORE_PASS).toString('base64');
headers['Authorization'] = 'Basic ' + token;
}
while ((Date.now() - start) / 1000 < TIMEOUT) {
try {
const res = await request(whipApi, { method: 'GET', headers, timeout: 5000 });
if (res.status === 200) {
let list = [];
try { list = JSON.parse(res.body); } catch (e) { /* ignore */ }
if (Array.isArray(list)) {
const hit = list.find((it) => String(it.name) === String(streamKey));
if (hit) {
console.log(` ✅ Stream detected in Core /api/v3/whip: ${streamKey}`);
console.log(' → published_at:', hit.published_at || '(unknown)');
found = true;
break;
} else {
process.stdout.write('.');
}
} else {
process.stdout.write('?');
}
} else {
process.stdout.write('x');
}
} catch (e) {
process.stdout.write('!');
}
await new Promise((r) => setTimeout(r, INTERVAL * 1000));
}
console.log('');
if (!found) {
console.error(`\n✗ Stream not detected within ${TIMEOUT}s. Si OBS ya está enviando, verifica:`);
console.error('- Que el stream key usado en OBS coincida exactamente');
console.error('- Que Core esté exponiendo /api/v3/whip sin autenticación o proveas credenciales');
console.error('- Logs del Core/egress para errores de ingest');
process.exit(1);
}
console.log('\nE2E check completo — stream activo.');
process.exit(0);
}
main().catch((e) => {
console.error('Fatal:', e.message || e);
process.exit(2);
});

View File

@ -37,9 +37,11 @@ if (urlParams.has('address')) {
window.location.pathname.replace(/\/ui\/.*$/, '');
} else if (process.env.REACT_APP_CORE_URL && process.env.REACT_APP_CORE_URL.trim() !== '') {
// 4. .env / .env.local — CRA injects at build time.
// Use window.location.origin so all /api/* and /memfs/* calls go through
// the CRA dev proxy (setupProxy.js) which forwards them to REACT_APP_CORE_URL.
address = window.location.origin;
// Prefer the explicit REACT_APP_CORE_URL build-time value so the UI
// connects to the configured Core directly instead of relying on
// window.location.origin. This ensures the UI respects the user's
// REACT_APP_CORE_URL setting from .env.local or the build environment.
address = process.env.REACT_APP_CORE_URL.trim().replace(/\/$/, '');
} else {
// 5. Same-origin production (Core and UI on the same host/port)
address = window.location.origin;

View File

@ -142,7 +142,11 @@ class API {
}
}
// Avoid noisy console.error for expected 404 responses (e.g. probe cleanup).
// Keep logging for other HTTP errors.
if (response.status !== 404) {
this._error(res.err.message);
}
return res;
}

View File

@ -15,6 +15,28 @@ import * as Version from '../version';
import API from './api';
import { anonymize } from './anonymizer';
// Helper: derive WHIP base URL
function computeWhipBaseFromEnv() {
// If explicit override provided, use it
try {
const envOverride = (process && process.env && process.env.REACT_APP_WHIP_BASE_URL) ? process.env.REACT_APP_WHIP_BASE_URL : '';
if (envOverride && envOverride.length) return envOverride.replace(/\/$/, '');
// Otherwise derive from REACT_APP_CORE_URL by reusing hostname and swapping port
const core = (process && process.env && process.env.REACT_APP_CORE_URL) ? process.env.REACT_APP_CORE_URL : '';
if (core && core.length) {
const coreUrl = new URL(core);
const whipPort = (process && process.env && process.env.REACT_APP_WHIP_PORT) ? process.env.REACT_APP_WHIP_PORT : '8555';
const portSuffix = whipPort ? ':' + whipPort : '';
return coreUrl.protocol + '//' + coreUrl.hostname + portSuffix;
}
} catch (e) {
// fallthrough
}
return '';
}
class Restreamer {
constructor(address) {
try {
@ -990,10 +1012,9 @@ class Restreamer {
return null;
}
// Allow overriding the public host:port via REACT_APP_WHIP_BASE_URL.
// Useful when the Core reports the wrong public IP (e.g. behind NAT)
// and you want the OBS URL to show a specific host, e.g. a LAN IP.
const override = process.env.REACT_APP_WHIP_BASE_URL;
// Allow overriding the public host:port via REACT_APP_WHIP_BASE_URL
// or derive it from REACT_APP_CORE_URL (reuse domain, change port).
const override = computeWhipBaseFromEnv();
if (override && val) {
const base = override.replace(/\/$/, '') + '/whip/';
val.base_publish_url = base;
@ -1010,8 +1031,8 @@ class Restreamer {
return null;
}
// Same host override as WhipUrl().
const override = process.env.REACT_APP_WHIP_BASE_URL;
// Same host override as WhipUrl() — consider env override or derive from CORE_URL.
const override = computeWhipBaseFromEnv();
if (override && val) {
const base = override.replace(/\/$/, '') + '/whip/';
val.publish_url = base + name;

View File

@ -118,6 +118,18 @@ function getWhipServerUrl() {
if (process.env.REACT_APP_WHIP_SERVER_URL) {
return String(process.env.REACT_APP_WHIP_SERVER_URL).replace(/\/+$/, '');
}
// If WHIP server URL not provided, derive from REACT_APP_CORE_URL by reusing
// the core's hostname and swapping the port (default WHIP port 8555).
if (process.env.REACT_APP_CORE_URL) {
try {
const coreUrl = new URL(process.env.REACT_APP_CORE_URL);
const whipPort = process.env.REACT_APP_WHIP_PORT || '8555';
return coreUrl.protocol + '//' + coreUrl.hostname + (whipPort ? ':' + whipPort : '');
} catch (e) {
// ignore and fallback
}
}
// Fallback: mismo origen del UI (útil si egress corre detrás del mismo proxy)
const loc = window.location;
return (loc.origin || `${loc.protocol}//${loc.host}`);