import React from 'react'; import { Trans } from '@lingui/macro'; import Grid from '@mui/material/Grid'; import Icon from '@mui/icons-material/ScreenShare'; import TextField from '@mui/material/TextField'; import Typography from '@mui/material/Typography'; import ToggleButton from '@mui/material/ToggleButton'; import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; import Button from '@mui/material/Button'; import CircularProgress from '@mui/material/CircularProgress'; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import RefreshIcon from '@mui/icons-material/Refresh'; import CloudUploadIcon from '@mui/icons-material/CloudUpload'; import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord'; import Chip from '@mui/material/Chip'; import * as S from '../../Sources/WebRTCRoom'; /** Extrae el puerto de una dirección del tipo ":8555" o "host:8555" → "8555" */ function extractPort(address, fallback) { if (!address) return fallback; const m = address.match(/:(\d+)$/); return m ? m[1] : fallback; } /** Construye la URL base WHIP a partir del config del Core */ function buildWhipBaseUrl(coreConfig) { const whip = coreConfig?.whip; if (!whip?.enable) return ''; const port = extractPort(whip.address, '8555'); const hosts = coreConfig?.host?.name; const host = Array.isArray(hosts) && hosts.length > 0 ? hosts[0] : window.location.hostname; return `http://${host}:${port}/whip`; } const initSettings = (initialSettings, config) => { return S.func.initSettings(initialSettings, config); }; function Source(props) { const config = { channelid: 'external', ...(props.config || {}) }; const settings = initSettings(props.settings, config); const streamKey = settings.streamKey || config.channelid || 'external'; const [mode, setMode] = React.useState(settings.mode || 'whip'); const [copied, setCopied] = React.useState(false); // Core WHIP config const [coreConfig, setCoreConfig] = React.useState(null); const [configLoad, setConfigLoad] = React.useState(false); const [configErr, setConfigErr] = React.useState(''); // Active streams polling const [active, setActive] = React.useState(false); const [localKey, setLocalKey] = React.useState(streamKey); const origin = window.location.origin; const host = settings.relayHost || window.location.host; const roomUrl = `${origin}/webrtc-room/?room=${encodeURIComponent(localKey)}&host=${encodeURIComponent(host)}`; const whipBase = coreConfig ? buildWhipBaseUrl(coreConfig) : ''; const token = coreConfig?.whip?.token || ''; const whipEnabled = coreConfig?.whip?.enable === true; const whipUrl = whipBase ? `${whipBase}/${localKey}${token ? '?token=' + token : ''}` : ''; const makeInputs = () => { const s = { ...settings, streamKey: localKey, mode }; return S.func.createInputs(s, config); }; const handleChange = (m, key) => { const k = key !== undefined ? key : localKey; props.onChange(S.id, { ...settings, streamKey: k, mode: m }, makeInputs(), false); }; // Carga la config del Core (GET /api/v3/config) const fetchCoreConfig = React.useCallback(async () => { setConfigLoad(true); setConfigErr(''); try { const res = await fetch('/api/v3/config'); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); setCoreConfig(data); } catch (err) { setConfigErr(err.message); } finally { setConfigLoad(false); } }, []); // Poll GET /api/v3/whip para ver si el stream está activo const pollWhipStreams = React.useCallback(async () => { try { const res = await fetch('/api/v3/whip'); if (!res.ok) return; const streams = await res.json(); setActive(Array.isArray(streams) && streams.some((s) => s.name === localKey)); } catch (_) {} }, [localKey]); React.useEffect(() => { fetchCoreConfig(); }, [fetchCoreConfig]); // Poll cada 5s cuando modo = whip React.useEffect(() => { if (mode !== 'whip') return; pollWhipStreams(); const id = setInterval(pollWhipStreams, 5000); return () => clearInterval(id); }, [mode, pollWhipStreams]); React.useEffect(() => { handleChange(mode); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleMode = (_e, newMode) => { if (!newMode) return; setMode(newMode); handleChange(newMode); }; const handleKeyChange = (e) => { const k = e.target.value; setLocalKey(k); handleChange(mode, k); }; const handleCopy = (text) => () => { if (text && navigator.clipboard) { navigator.clipboard.writeText(text).then(() => { setCopied(text); setTimeout(() => setCopied(false), 2000); }); } }; return ( {/* ── Mode toggle ── */} WHIP / OBS Sala WebRTC (navegador) {/* ── WHIP mode ── */} {mode === 'whip' && ( <> {configLoad && ( Cargando config WHIP del Core… )} {configErr && ( ⚠️ {configErr} )} {!configLoad && coreConfig && !whipEnabled && ( ⚠️ El servidor WHIP del Core está deshabilitado. Actívalo en Ajustes → WHIP Server. )} {!configLoad && coreConfig && whipEnabled && ( <> {/* Stream Key */} Stream Key} value={localKey} onChange={handleKeyChange} helperText={OBS enviará a esta clave. Usa el ID del canal o escribe una personalizada.} /> {/* URL de publicación para OBS */} URL para OBS → Configuración → Emisión → Servicio: Custom (WHIP)
Clave de stream en OBS: dejar vacía (ya va en la URL)} />
{/* Estado en tiempo real */} {active ? ( } label={Transmitiendo} size="small" style={{ backgroundColor: 'rgba(39,174,96,0.15)', color: '#27ae60', border: '1px solid #27ae60' }} /> ) : ( } label={Esperando publicador…} size="small" style={{ backgroundColor: 'rgba(255,255,255,0.05)', color: 'rgba(255,255,255,0.5)' }} /> )} {/* Token warning */} {token && ( 🔑 El servidor requiere token. Ya está incluido en la URL anterior. )} )} )} {/* ── Room mode ── */} {mode === 'room' && (
URL de la sala (compartir con el presentador)} value={roomUrl} InputProps={{ readOnly: true, style: { fontFamily: 'monospace', fontSize: '0.78rem' } }} helperText={Room ID: {localKey}} />
)}
); } Source.defaultProps = { knownDevices: [], settings: {}, config: null, skills: null, onChange: function (type, settings, inputs, ready) {}, onRefresh: function () {}, }; function SourceIcon(props) { return ; } const id = 'webrtcroom'; const type = 'webrtcroom'; const name = WebRTC Room; const capabilities = ['audio', 'video']; export { id, type, name, capabilities, SourceIcon as icon, Source as component };