From 396a803b1cf7464b9109162fca3b4dde11cff029 Mon Sep 17 00:00:00 2001 From: Cesar Mendivil Date: Fri, 7 Nov 2025 15:42:53 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20Integrar=20LiveKit=20y=20actualizar=20U?= =?UTF-8?q?RLs=20en=20el=20panel=20de=20transmisi=C3=B3n=20y=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 1 + packages/backend-api/package.json | 1 + packages/backend-api/src/index.ts | 53 ++ .../broadcast-panel/src/components/Studio.tsx | 2 +- .../src/components/TransmissionsTable.tsx | 2 +- packages/broadcast-panel/vite.config.ts | 5 + .../src/components/StudioRightPanel.tsx | 730 +++++++++--------- shared/utils/api.ts | 2 +- 8 files changed, 409 insertions(+), 387 deletions(-) diff --git a/package-lock.json b/package-lock.json index 82a399a..fb46ad6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11875,6 +11875,7 @@ "ioredis": "^5.3.2", "joi": "^17.11.0", "jsonwebtoken": "^9.0.2", + "livekit-server-sdk": "^2.14.0", "socket.io": "^4.6.2", "stripe": "^14.9.0", "winston": "^3.11.0" diff --git a/packages/backend-api/package.json b/packages/backend-api/package.json index 0967c2e..b32639e 100644 --- a/packages/backend-api/package.json +++ b/packages/backend-api/package.json @@ -25,6 +25,7 @@ "ioredis": "^5.3.2", "joi": "^17.11.0", "jsonwebtoken": "^9.0.2", + "livekit-server-sdk": "^2.14.0", "socket.io": "^4.6.2", "stripe": "^14.9.0", "winston": "^3.11.0" diff --git a/packages/backend-api/src/index.ts b/packages/backend-api/src/index.ts index 3018928..a35db1a 100644 --- a/packages/backend-api/src/index.ts +++ b/packages/backend-api/src/index.ts @@ -15,6 +15,9 @@ const allowedOrigins = process.env.FRONTEND_URLS?.split(',') || ['http://localho if (process.env.NODE_ENV !== 'production') { if (!allowedOrigins.includes('http://localhost:3020')) allowedOrigins.push('http://localhost:3020') if (!allowedOrigins.includes('http://localhost:3021')) allowedOrigins.push('http://localhost:3021') + if (!allowedOrigins.includes('http://localhost:5175')) allowedOrigins.push('http://localhost:5175') + if (!allowedOrigins.includes('https://avanzacast-studio.bfzqqk.easypanel.host')) allowedOrigins.push('https://avanzacast-studio.bfzqqk.easypanel.host') + if (!allowedOrigins.includes('https://avanzacast-broadcastpanel.bfzqqk.easypanel.host')) allowedOrigins.push('https://avanzacast-broadcastpanel.bfzqqk.easypanel.host') } app.use(cors({ @@ -44,6 +47,56 @@ app.get('/api/v1', (req, res) => { }); }); +// LiveKit token generation endpoint +app.get('/api/token', async (req, res) => { + const { room, username } = req.query; + + if (!room || typeof room !== 'string') { + return res.status(400).json({ error: 'Room name is required' }); + } + + if (!username || typeof username !== 'string') { + return res.status(400).json({ error: 'Username is required' }); + } + + // TODO: Implement actual LiveKit token generation + // For now, return a placeholder response + const LIVEKIT_API_KEY = process.env.LIVEKIT_API_KEY; + const LIVEKIT_API_SECRET = process.env.LIVEKIT_API_SECRET; + + if (!LIVEKIT_API_KEY || !LIVEKIT_API_SECRET) { + console.error('⚠️ LIVEKIT_API_KEY and LIVEKIT_API_SECRET must be set in environment variables'); + return res.status(500).json({ error: 'LiveKit credentials not configured' }); + } + + try { + // Import AccessToken from livekit-server-sdk + const { AccessToken } = await import('livekit-server-sdk'); + + const at = new AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET, { + identity: username, + name: username, + }); + + at.addGrant({ + room, + roomJoin: true, + canPublish: true, + canSubscribe: true, + }); + + const token = await at.toJwt(); + + return res.json({ + token, + url: process.env.LIVEKIT_URL || 'ws://localhost:7880', + }); + } catch (error) { + console.error('Error generating LiveKit token:', error); + return res.status(500).json({ error: 'Failed to generate token' }); + } +}); + // Minimal LiveKit-related endpoints (placeholder implementation) app.get('/api/v1/livekit/rooms', (req, res) => { const roomName = typeof req.query.roomName === 'string' ? req.query.roomName : undefined; diff --git a/packages/broadcast-panel/src/components/Studio.tsx b/packages/broadcast-panel/src/components/Studio.tsx index 95dfe66..1ae53d0 100644 --- a/packages/broadcast-panel/src/components/Studio.tsx +++ b/packages/broadcast-panel/src/components/Studio.tsx @@ -15,7 +15,7 @@ const Studio: React.FC = () => { localStorage.setItem('avanzacast_room', roomName) // Redirigir al studio-panel (puerto 3001) - const studioUrl = `http://localhost:3001?user=${encodeURIComponent(userName)}&room=${encodeURIComponent(roomName)}` + const studioUrl = `https://avanzacast-studio.bfzqqk.easypanel.host?user=${encodeURIComponent(userName)}&room=${encodeURIComponent(roomName)}` window.location.href = studioUrl }, []) diff --git a/packages/broadcast-panel/src/components/TransmissionsTable.tsx b/packages/broadcast-panel/src/components/TransmissionsTable.tsx index 5c3ad9c..375f25f 100644 --- a/packages/broadcast-panel/src/components/TransmissionsTable.tsx +++ b/packages/broadcast-panel/src/components/TransmissionsTable.tsx @@ -61,7 +61,7 @@ const TransmissionsTable: React.FC = ({ transmissions, onDelete, onUpdate console.log('[BroadcastPanel] Solicitando token:', { room: decodeURIComponent(room), user: decodeURIComponent(user) }) - const TOKEN_SERVER = import.meta.env.VITE_TOKEN_SERVER_URL || 'https://avanzacast-studio.bfzqqk.easypanel.host' + const TOKEN_SERVER = import.meta.env.VITE_TOKEN_SERVER_URL || 'https://avanzacast-servertokens.bfzqqk.easypanel.host' const tokenUrl = `${TOKEN_SERVER.replace(/\/$/, '')}/api/token?room=${room}&username=${user}` const tokenRes = await fetch(tokenUrl) if (!tokenRes.ok) throw new Error('No se pudo obtener token') diff --git a/packages/broadcast-panel/vite.config.ts b/packages/broadcast-panel/vite.config.ts index 928d544..89c8aff 100644 --- a/packages/broadcast-panel/vite.config.ts +++ b/packages/broadcast-panel/vite.config.ts @@ -37,6 +37,11 @@ export default defineConfig({ server: { port: 5175, host: true, + allowedHosts: [ + 'localhost', + '.easypanel.host', + 'avanzacast-broadcastpanel.bfzqqk.easypanel.host' + ], fs: { // Allow serving files from the shared folder when mounted in Docker allow: [ diff --git a/packages/studio-panel/src/components/StudioRightPanel.tsx b/packages/studio-panel/src/components/StudioRightPanel.tsx index 51d112a..dc1470f 100644 --- a/packages/studio-panel/src/components/StudioRightPanel.tsx +++ b/packages/studio-panel/src/components/StudioRightPanel.tsx @@ -7,108 +7,307 @@ import { MdQrCode, MdTimer, MdSettings, - MdClose, MdPeople, + MdChat, + MdComment, } from 'react-icons/md' import { COLOR_THEMES, DEMO_OVERLAYS, DEMO_BACKGROUNDS, DEMO_SOUNDS } from '../config/demo' import ParticipantsPanel from './ParticipantsPanel' -type TabType = 'brand' | 'multimedia' | 'participants' | 'sounds' | 'video' | 'qr' | 'countdown' | 'settings' +type TabType = 'comments' | 'banners' | 'brand' | 'style' | 'notes' | 'participants' | 'chat' const StudioRightPanel = ({ roomName }: { roomName?: string }) => { const [activeTab, setActiveTab] = useState('brand') - const [isCollapsed, setIsCollapsed] = useState(false) const tabs = [ - { id: 'brand' as TabType, icon: MdBrush, label: 'Marca' }, - { id: 'multimedia' as TabType, icon: MdImage, label: 'Multimedia' }, - { id: 'participants' as TabType, icon: MdPeople, label: 'Personas' }, - { id: 'sounds' as TabType, icon: MdMusicNote, label: 'Sonidos' }, - { id: 'video' as TabType, icon: MdVideoLibrary, label: 'Videos' }, - { id: 'qr' as TabType, icon: MdQrCode, label: 'QR' }, - { id: 'countdown' as TabType, icon: MdTimer, label: 'Cuenta regresiva' }, - { id: 'settings' as TabType, icon: MdSettings, label: 'Ajustes' }, + { id: 'comments' as TabType, icon: MdComment, label: 'Comentarios', badge: 0 }, + { id: 'banners' as TabType, icon: MdImage, label: 'Banners', badge: 0 }, + { id: 'brand' as TabType, icon: MdBrush, label: 'Activos multimedia', badge: 0 }, + { id: 'style' as TabType, icon: MdBrush, label: 'Estilo', badge: 0 }, + { id: 'notes' as TabType, icon: MdTimer, label: 'Notas', badge: 0 }, + { id: 'participants' as TabType, icon: MdPeople, label: 'Personas', badge: 0 }, + { id: 'chat' as TabType, icon: MdChat, label: 'Chat privado', badge: 0 }, ] - if (isCollapsed) { - return ( -
+ return ( +
+ + + {/* Content Panel - Aside como en Streamyard */} + + {/* Tabs Column - Vertical tabs como en Streamyard */} +
{tabs.map((tab) => { const Icon = tab.icon + const isActive = activeTab === tab.id + return ( ) })}
- ) - } - - return ( -
- {/* Header con tabs */} -
-
-

Configuración

- -
- - {/* Tabs */} -
- {tabs.map((tab) => { - const Icon = tab.icon - return ( - - ) - })} -
-
- - {/* Contenido de tabs */} -
- {activeTab === 'brand' && } - {activeTab === 'multimedia' && } - {activeTab === 'participants' && } - {activeTab === 'sounds' && } - {activeTab === 'video' && } - {activeTab === 'qr' && } - {activeTab === 'countdown' && } - {activeTab === 'settings' && } -
) } -// Tab de Marca -const BrandTab = () => { +// Tab de Comentarios (Streamyard style) +const CommentsTab = () => { + return ( +
+ {/* Header */} +
+

Comentarios

+

+ Puedes destacar comentarios importantes para que recuerdes comentarlos durante el programa. +

+
+ + {/* Empty state */} +
+
+ +

No hay comentarios destacados

+ +
+
+ + {/* Input area */} +
+
+