restreamer-ui-v2/src/setupProxy.js
Cesar Mendivil bc97ee0a68 feat(nginx): add Docker setup for Nginx with templated configuration
- Create Dockerfile for Nginx with envsubst for dynamic configuration.
- Add djmaster.conf.template for Nginx configuration with upstream services.
- Implement docker-entrypoint.sh to substitute environment variables in the Nginx config.
- Add README.md in nginx-examples for guidance on using the Nginx template.
- Include djmaster.conf.template in nginx-examples for local setup.
- Introduce utility functions for fetching YouTube video snippets and titles.
2026-03-18 10:35:28 -07:00

247 lines
8.6 KiB
JavaScript

const { createProxyMiddleware } = require('http-proxy-middleware');
const path = require('path');
const fs = require('fs');
/**
* Development proxy (CRA - only active with `npm start`).
*
* .env / .env.local:
* REACT_APP_CORE_URL=https://restreamer.nextream.sytes.net
* REACT_APP_YTDLP_URL=http://192.168.1.20:8282
* REACT_APP_FB_SERVER_URL=http://localhost:3002 (optional, default shown)
*/
const CORE_TARGET = process.env.REACT_APP_CORE_URL || 'http://localhost:8080';
const YTDLP_TARGET = process.env.REACT_APP_YTDLP_URL || 'http://localhost:8282';
const YTDLP_TITLES_TARGET = process.env.REACT_APP_YTDLP_URL_TITLES || 'http://localhost:8080';
const FB_SERVER_TARGET = process.env.REACT_APP_FB_SERVER_URL || 'http://localhost:3002';
// Dirección LOCAL del servidor egress/whip para el proxy de desarrollo.
// DISTINTO de REACT_APP_WHIP_SERVER_URL (que es la URL pública para el frontend).
// En dev: http://localhost:3005. En prod Caddy proxea directamente, esto no se usa.
const WHIP_SERVER_TARGET = process.env.WHIP_API_TARGET || 'http://localhost:3005';
// WHIP ingest directo a livekit-ingress (en dev OBS no puede acceder via CRA, pero lo mapeamos)
const LIVEKIT_INGRESS_TARGET = process.env.LIVEKIT_INGRESS_INTERNAL_URL || 'http://192.168.1.20:8088';
console.log('\n[setupProxy] ─────────────────────────────────────');
console.log(`[setupProxy] Core → ${CORE_TARGET}`);
console.log(`[setupProxy] yt-dlp → ${YTDLP_TARGET}`);
console.log(`[setupProxy] yt-titles → ${YTDLP_TITLES_TARGET}`);
console.log(`[setupProxy] fb-server → ${FB_SERVER_TARGET}`);
console.log(`[setupProxy] whip/egress → ${WHIP_SERVER_TARGET}`);
console.log('[setupProxy] ─────────────────────────────────────\n');
let coreUrl;
let coreTarget = CORE_TARGET;
try {
coreUrl = new URL(CORE_TARGET);
} catch (e) {
console.error(`[setupProxy] Invalid REACT_APP_CORE_URL: "${CORE_TARGET}" — falling back to http://localhost:8080`);
coreTarget = 'http://localhost:8080';
coreUrl = new URL(coreTarget);
}
// Shared proxy instance for all Core paths (/api, /memfs, /diskfs)
const coreProxy = createProxyMiddleware({
target: coreTarget,
changeOrigin: true,
secure: false,
ws: false,
onProxyReq: (proxyReq) => {
proxyReq.setHeader('Host', coreUrl.host);
},
onError: (err, req, res) => {
console.error(`[setupProxy] Core proxy error: ${err.code}${err.message}`);
if (!res.headersSent) {
res.writeHead(502, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Proxy error', target: CORE_TARGET, message: err.message }));
}
},
});
module.exports = function (app) {
// Restreamer Core REST API
app.use('/api', coreProxy);
// Restreamer Core in-memory FS (HLS playlists, snapshots)
app.use('/memfs', coreProxy);
// Restreamer Core disk FS
app.use('/diskfs', coreProxy);
// yt-dlp stream extractor: /yt-stream/{VIDEO_ID} → /stream/{VIDEO_ID}
// yt-dlp puede tardar hasta 30-60s en extraer la URL — timeout extendido
app.use(
'/yt-stream',
createProxyMiddleware({
target: YTDLP_TARGET,
changeOrigin: true,
secure: false,
ws: false,
proxyTimeout: 120000, // 120s — yt-dlp puede tardar bastante
timeout: 120000,
pathRewrite: { '^/yt-stream': '/stream' },
onError: (err, req, res) => {
console.error(`[setupProxy] yt-dlp proxy error: ${err.code}${err.message}`);
if (!res.headersSent) {
res.writeHead(502, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Proxy error', target: YTDLP_TARGET, message: err.message }));
}
},
}),
);
// yt-dlp titles extractor: /yt-titles/{VIDEO_ID} → /info/{VIDEO_ID}
app.use(
'/yt-titles',
createProxyMiddleware({
target: YTDLP_TITLES_TARGET,
changeOrigin: true,
secure: false,
ws: false,
proxyTimeout: 15000,
timeout: 15000,
pathRewrite: { '^/yt-titles': '/info' },
onError: (err, req, res) => {
console.error(`[setupProxy] yt-titles proxy error: ${err.code}${err.message}`);
if (!res.headersSent) {
res.writeHead(502, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Proxy error', target: YTDLP_TITLES_TARGET, message: err.message }));
}
},
}),
);
// Facebook OAuth server + WebRTC relay: /fb-server/* → http://localhost:3002/*
app.use(
'/fb-server',
createProxyMiddleware({
target: FB_SERVER_TARGET,
changeOrigin: true,
secure: false,
ws: true,
pathRewrite: { '^/fb-server': '' },
onError: (err, req, res) => {
console.error(`[setupProxy] fb-server proxy error: ${err.code}${err.message}`);
if (!res.headersSent) {
res.writeHead(502, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'fb-server unavailable', message: err.message }));
}
},
}),
);
// LiveKit token endpoint: /livekit/* → http://localhost:3002/livekit/*
app.use(
'/livekit',
createProxyMiddleware({
target: FB_SERVER_TARGET,
changeOrigin: true,
secure: false,
ws: false,
onError: (err, req, res) => {
console.error(`[setupProxy] livekit proxy error: ${err.code}${err.message}`);
if (!res.headersSent) {
res.writeHead(502, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'livekit endpoint unavailable', message: err.message }));
}
},
}),
);
// WebRTC relay WebSocket (legacy — mantenido por compatibilidad)
app.use(
'/webrtc-relay',
createProxyMiddleware({
target: FB_SERVER_TARGET,
changeOrigin: true,
secure: false,
ws: true,
onError: (err, req, res) => {
console.error(`[setupProxy] webrtc-relay proxy error: ${err.code}${err.message}`);
if (res && !res.headersSent) {
res.writeHead(502, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'webrtc-relay unavailable', message: err.message }));
}
},
}),
);
// OAuth2 callback: sirve el HTML de callback para YouTube y otras plataformas
app.get('/oauth2callback', (req, res) => {
const callbackFile = path.join(__dirname, '..', 'public', 'oauth2callback.html');
if (fs.existsSync(callbackFile)) {
res.sendFile(callbackFile);
} else {
res.status(404).send('oauth2callback.html not found');
}
});
// WHIP Ingress API: /api/whip/* → egress server
// En dev: http://localhost:3005 (egress server/index.js)
// En prod: Caddy proxea /api/whip/* → Node:3002 (restreamer-ui server/index.js)
app.use(
'/api/whip',
createProxyMiddleware({
target: WHIP_SERVER_TARGET,
changeOrigin: true,
secure: false,
onError: (err, req, res) => {
console.error(`[setupProxy] whip proxy error: ${err.code}${err.message}`);
if (!res.headersSent) {
res.writeHead(502, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Egress server unavailable', message: err.message }));
}
},
}),
);
// WHEP relay: /whep/* → egress server (Core hace pull del stream desde aquí)
// En dev: http://localhost:3005/whep/*
// En prod: Caddy proxea /whep/* → EGRESS_HOST (llmchats-whep.zuqtxy.easypanel.host)
app.use(
'/whep',
createProxyMiddleware({
target: WHIP_SERVER_TARGET,
changeOrigin: true,
secure: false,
onError: (err, req, res) => {
console.error(`[setupProxy] whep proxy error: ${err.code}${err.message}`);
if (!res.headersSent) {
res.writeHead(502, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Egress server unavailable', message: err.message }));
}
},
}),
);
// WHIP ingest: /w/* → livekit-ingress directamente
// En dev apunta a la IP interna del livekit-ingress (192.168.1.20:8088).
// En prod: Caddy proxea /w/* → LIVEKIT_INGRESS_HOST.
// OBS hace: POST https://<host>/w/<stream_key> con Content-Type: application/sdp
app.use(
'/w',
createProxyMiddleware({
target: LIVEKIT_INGRESS_TARGET,
changeOrigin: true,
secure: false,
onError: (err, req, res) => {
console.error(`[setupProxy] livekit-ingress proxy error: ${err.code}${err.message}`);
if (!res.headersSent) {
res.writeHead(502, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'LiveKit Ingress unavailable', message: err.message }));
}
},
}),
);
// Facebook OAuth2 callback popup
app.get('/oauth/facebook/callback.html', (req, res) => {
const callbackFile = path.join(__dirname, '..', 'public', 'oauth', 'facebook', 'callback.html');
if (fs.existsSync(callbackFile)) {
res.sendFile(callbackFile);
} else {
res.status(404).send('Facebook callback.html not found');
}
});
};