Add WebRTC relay integration and core process management for streaming

This commit is contained in:
Cesar Mendivil 2026-03-10 11:34:34 -07:00
parent 6dc2fdad57
commit 71b3bd9e1d
2 changed files with 133 additions and 3 deletions

View File

@ -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: ""

View File

@ -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 ────────────────────────────────────────────────────────────────────