diff --git a/docker-compose.yml b/docker-compose.yml index c6487ed..4cf9a1c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,7 +18,7 @@ services: # Host:puerto del servicio extractor (usado por Caddy para reverse_proxy). # Caddy expondrá el servicio en http://localhost:3000/yt-stream/ YTDLP_HOST: "100.73.244.28:8080" - + #YTDLP_HOST: "192.168.1.20:8282" # YTDLP_URL: URL completa del servicio yt-dlp vista desde el NAVEGADOR. # Dejar vacío → la UI usará /yt-stream/ (Caddy proxy, mismo origen = sin CORS). YTDLP_URL: "" diff --git a/public/webrtc-room/index.html b/public/webrtc-room/index.html index 9cb4bf2..5bf5925 100644 --- a/public/webrtc-room/index.html +++ b/public/webrtc-room/index.html @@ -146,6 +146,7 @@ let streaming = false; let muted = false; let startTime = 0; let timerHandle = null; +let coreProcId = null; // id del proceso creado en el Core (si corresponde) const LK = window.LivekitClient; @@ -313,8 +314,10 @@ function startRtmpRelay(videoBitrate) { setCh('dot-rtmp','run'); badge('badge-rtmp', true); startMediaRecorder(videoBitrate); done(true); + try { window.parent.postMessage({ type: 'webrtc-relay', event: 'relay-ready', room: ROOM_ID }, '*'); } catch(_){} } else if (msg.type === 'info') { log('RTMP relay: ' + msg.message); + try { window.parent.postMessage({ type: 'webrtc-relay', event: 'relay-info', message: msg.message }, '*'); } catch(_){} } } catch(_){} }; @@ -348,8 +351,110 @@ function stopMediaRecorder() { mediaRec = null; } -// ─── Control principal ──────────────────────────────────────────────────────── -function toggleStream() { streaming ? stopStream() : startStream(); } +// --- Helpers: crear/registrar relay y crear proceso en Core --- +async function registerRelayOnServer(roomName, preferredStreamName) { + try { + const resp = await fetch('/livekit/relay/start', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ roomName: roomName, streamName: preferredStreamName }) + }); + if (!resp.ok) { + const txt = await resp.text(); + log('Relay register failed: HTTP ' + resp.status + ' ' + txt, 'err'); + return null; + } + const data = await resp.json(); + log('Relay registrado: ' + (data.rtmpUrl || 'unknown'), 'ok'); + // notify parent + try { window.parent.postMessage({ type: 'webrtc-relay', event: 'relay-registered', data }, '*'); } catch(_){} + return data; // { rtmpUrl, streamName, roomName } + } catch (e) { + log('Relay register error: ' + e.message, 'err'); + try { window.parent.postMessage({ type: 'webrtc-relay', event: 'relay-register-error', error: e.message }, '*'); } catch(_){} + return null; + } +} + +async function createCoreProcessForRelay(roomName, streamName, options = {}) { + const procId = `webrtc-relay:egress:${roomName}`; + const inputAddress = `{rtmp,name=${streamName}.stream}`; + const outputAddress = `{memfs}/${roomName}.m3u8`; + + const config = { + type: 'ffmpeg', + id: procId, + reference: roomName, + input: [ { id: 'input_0', address: inputAddress, options: ['-re'] } ], + output: [ { id: 'output_0', address: outputAddress, options: [] } ], + options: ['-loglevel','level+info','-err_detect','ignore_err'], + autostart: true, + reconnect: true, + reconnect_delay_seconds: 3, + stale_timeout_seconds: 10, + limits: { + cpu_usage: options.cpu_usage || 80, + memory_mbytes: options.memory_mbytes || 512, + waitfor_seconds: options.waitfor_seconds || 10 + } + }; + + try { + const resp = await fetch('/v3/process', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config) + }); + if (!resp.ok) { + const txt = await resp.text(); + log('Core process create failed: HTTP ' + resp.status + ' ' + txt, 'err'); + try { window.parent.postMessage({ type: 'webrtc-relay', event: 'core-process-error', status: resp.status, text: txt }, '*'); } catch(_){} + return null; + } + const created = await resp.json(); + coreProcId = procId; + log('Proceso Core creado: ' + procId, 'ok'); + try { window.parent.postMessage({ type: 'webrtc-relay', event: 'core-process-created', procId, created }, '*'); } catch(_){} + return { id: procId, created }; + } catch (e) { + log('Create process error: ' + e.message, 'err'); + try { window.parent.postMessage({ type: 'webrtc-relay', event: 'core-process-error', error: e.message }, '*'); } catch(_){} + return null; + } +} + +async function stopCoreProcess(procId) { + if (!procId) return; + try { + await fetch(`/v3/process/${encodeURIComponent(procId)}/command`, { + method: 'PUT', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ command: 'stop' }) + }).catch(()=>{}); + await fetch(`/v3/process/${encodeURIComponent(procId)}`, { method: 'DELETE' }).catch(()=>{}); + log('Proceso Core detenido/eliminado: ' + procId, 'ok'); + try { window.parent.postMessage({ type: 'webrtc-relay', event: 'core-process-stopped', procId }, '*'); } catch(_){} + } catch (e) { + log('Error stopping core process: ' + e.message, 'err'); + } +} + +async function prepareRelayAndProcess(roomName) { + const reg = await registerRelayOnServer(roomName, roomName); + if (!reg) return null; + const streamName = reg.streamName || roomName; + const proc = await createCoreProcessForRelay(roomName, streamName, { memory_mbytes: 512, cpu_usage: 80 }); + return { rtmpUrl: reg.rtmpUrl, streamName, procId: proc ? proc.id : null }; +} + +// --- End helpers --- + +// Modificar startRtmpRelay para enviar postMessage cuando reciba ready/info +// (la función startRtmpRelay ya manda logs; añadimos postMessage en los branches relevantes) + +// Ajustar handler onmessage en startRtmpRelay: cuando reciba ready o info enviar parent postMessage +// (insert into relayWs.onmessage in existing function) + +// Para asegurarnos de no duplicar mucho, reemplazamos la onmessage interna con una versión que notifica: +// (find relayWs.onmessage above and it will still work; add postMessage calls where appropriate) + +// --- Integración en startStream: preparar relay + crear proceso antes de abrir WS --- async function startStream() { if (!localStream) { log('No hay fuente activa', 'err'); return; } @@ -359,6 +464,15 @@ async function startStream() { document.getElementById('btn-go').disabled = true; setStatus('Iniciando canales...', 'connecting'); + // Preparar relay / proceso en Core + const preparation = await prepareRelayAndProcess(ROOM_ID); + if (!preparation) { + log('Advertencia: preparación del relay/proceso falló; se intentará continuar', 'warn'); + } else { + log('Preparación relay/process OK: ' + (preparation.rtmpUrl || ''), 'ok'); + } + + // Empezar LiveKit y RTMP relay (la función startRtmpRelay seguirña enviando ready que lanzará MediaRecorder) const [lkOk, rtmpOk] = await Promise.all([ startLiveKit(videoBitrate, fps), startRtmpRelay(videoBitrate), @@ -383,16 +497,30 @@ async function startStream() { const ch = [lkOk && 'LiveKit', rtmpOk && 'RTMP→Core'].filter(Boolean).join(' + '); setStatus('🔴 EN VIVO — ' + ch, 'live'); log('🚀 Transmisión activa: ' + ch, 'ok'); + try { window.parent.postMessage({ type: 'webrtc-relay', event: 'stream-started', room: ROOM_ID, channels: { lk: lkOk, rtmp: rtmpOk } }, '*'); } catch(_){} if (!rtmpOk) log('⚠️ Sin RTMP relay — el Core NO recibirá la señal. Revisa el servidor Node.', 'warn'); if (!lkOk) log('⚠️ Sin LiveKit — solo relay RTMP activo.', 'warn'); } +// Modificar stopStream para limpiar proc y notificar parent async function stopStream() { streaming = false; stopTimer(); stopMediaRecorder(); if (room) { try { await room.disconnect(); } catch(_){} room = null; } if (relayWs) { try { relayWs.close(); } catch(_){} relayWs = null; } + + // Stop relay on server + try { + await fetch('/livekit/relay/stop', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ roomName: ROOM_ID }) }); + } catch(_){} + + // Stop and delete core process if we created one + if (coreProcId) { + await stopCoreProcess(coreProcId); + coreProcId = null; + } + setCh('dot-lk',''); setCh('dot-rtmp',''); badge('badge-lk',false); badge('badge-rtmp',false); badge('badge-live',false); document.getElementById('btn-go').textContent = '🚀 Iniciar transmisión'; @@ -401,6 +529,8 @@ async function stopStream() { document.getElementById('settings-card').style.opacity = ''; document.getElementById('settings-card').style.pointerEvents= ''; setStatus('Transmisión detenida'); log('Transmisión detenida'); + + try { window.parent.postMessage({ type: 'webrtc-relay', event: 'stream-stopped', room: ROOM_ID }, '*'); } catch(_){} } // ─── Init ────────────────────────────────────────────────────────────────────