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:///w/ 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'); } }); };