From f57ce90c11b743dbc95a32bcdc05674311756c89 Mon Sep 17 00:00:00 2001 From: Cesar Mendivil Date: Fri, 7 Nov 2025 14:29:14 -0700 Subject: [PATCH] feat: Enhance StudioControls with presentation and layout features - Added props for handling presentation and screen sharing actions. - Implemented buttons for opening presentation panel and changing layouts (grid/focus) and modes (video/audio). - Updated UI to reflect active presentations and added functionality to clear presentations. fix: Adjust StudioLeftSidebar and StudioRightPanel for full height - Modified styles to ensure both sidebars occupy full height of the container. feat: Introduce ParticipantsPanel for managing participants in the conference - Created ParticipantsPanel component to display connected and invited participants. - Integrated API calls to fetch invited participants and handle connection status. feat: Implement PresentationPanel for sharing presentations - Developed PresentationPanel component to handle file uploads and screen sharing. refactor: Update StudioVideoArea to support layout and mode changes - Refactored StudioVideoArea to accept layout and mode props, rendering appropriate conference views. feat: Add AudioConference and VideoConference prefabs for audio and video handling - Created AudioConference and VideoConference components to manage respective media streams. feat: Introduce Chat component for real-time messaging - Developed Chat component to facilitate messaging between participants. feat: Implement ControlBar for user controls in the conference - Created ControlBar component for managing participant actions like leaving the conference and toggling audio/video. feat: Add PreJoin component for pre-conference setup - Developed PreJoin component to allow users to preview video before joining the conference. chore: Update Vite configuration for better module resolution - Enhanced Vite config to include path aliases for easier imports across the project. chore: Add TypeScript definitions for environment variables - Created env.d.ts to define types for environment variables used in the project. --- packages/backend-api/src/index.ts | 22 +- packages/broadcast-panel/.env.example | 2 +- packages/broadcast-panel/LIVEKIT_SETUP.md | 2 +- packages/broadcast-panel/docker-compose.yml | 2 +- .../src/components/Sidebar.tsx | 1 - .../src/components/TransmissionsTable.tsx | 36 +- packages/broadcast-panel/vite.config.ts | 2 +- .../src/components/ParticipantsPanel.tsx | 121 ++++++ .../src/components/PresentationPanel.tsx | 57 +++ .../studio-panel/src/components/Studio.tsx | 349 +++++++++++++++--- .../src/components/StudioControls.tsx | 58 ++- .../src/components/StudioLeftSidebar.tsx | 2 +- .../src/components/StudioRightPanel.tsx | 12 +- .../src/components/StudioVideoArea.tsx | 55 ++- .../src/components/StudioVideoArea.tsx.bak | 45 +++ .../src/components/StudioVideoArea.tsx.new | 45 +++ .../src/prefabs/AudioConference.tsx | 23 ++ packages/studio-panel/src/prefabs/Chat.tsx | 36 ++ .../studio-panel/src/prefabs/ControlBar.tsx | 33 ++ packages/studio-panel/src/prefabs/PreJoin.tsx | 34 ++ .../src/prefabs/VideoConference.tsx | 36 ++ packages/studio-panel/src/prefabs/index.ts | 5 + packages/studio-panel/vite-dev.log | 9 + packages/studio-panel/vite.config.ts | 19 + ...s.timestamp-1762497626946-f3a7a31a30d1.mjs | 25 ++ tsconfig.json | 2 +- types/env.d.ts | 13 + 27 files changed, 945 insertions(+), 101 deletions(-) create mode 100644 packages/studio-panel/src/components/ParticipantsPanel.tsx create mode 100644 packages/studio-panel/src/components/PresentationPanel.tsx create mode 100644 packages/studio-panel/src/components/StudioVideoArea.tsx.bak create mode 100644 packages/studio-panel/src/components/StudioVideoArea.tsx.new create mode 100644 packages/studio-panel/src/prefabs/AudioConference.tsx create mode 100644 packages/studio-panel/src/prefabs/Chat.tsx create mode 100644 packages/studio-panel/src/prefabs/ControlBar.tsx create mode 100644 packages/studio-panel/src/prefabs/PreJoin.tsx create mode 100644 packages/studio-panel/src/prefabs/VideoConference.tsx create mode 100644 packages/studio-panel/src/prefabs/index.ts create mode 100644 packages/studio-panel/vite.config.ts.timestamp-1762497626946-f3a7a31a30d1.mjs create mode 100644 types/env.d.ts diff --git a/packages/backend-api/src/index.ts b/packages/backend-api/src/index.ts index 1024ec6..3018928 100644 --- a/packages/backend-api/src/index.ts +++ b/packages/backend-api/src/index.ts @@ -10,8 +10,15 @@ const PORT = process.env.PORT || 4000; // Middleware app.use(helmet()); +const allowedOrigins = process.env.FRONTEND_URLS?.split(',') || ['http://localhost:3000']; +// Always allow our local dev studio ports to avoid CORS blockers during development +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') +} + app.use(cors({ - origin: process.env.FRONTEND_URLS?.split(',') || ['http://localhost:3000'], + origin: allowedOrigins, credentials: true, })); app.use(express.json()); @@ -37,6 +44,19 @@ app.get('/api/v1', (req, res) => { }); }); +// 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; + + // If no roomName provided, return a list of rooms (empty list for now) + if (!roomName) { + return res.json({ rooms: [] }); + } + + // Placeholder: return empty participants list for the requested room + return res.json({ room: { name: roomName, participants: [] } }); +}); + // 404 handler app.use((req, res) => { res.status(404).json({ error: 'Not found' }); diff --git a/packages/broadcast-panel/.env.example b/packages/broadcast-panel/.env.example index ccb0526..d3a2509 100644 --- a/packages/broadcast-panel/.env.example +++ b/packages/broadcast-panel/.env.example @@ -1,6 +1,6 @@ # LiveKit Configuration VITE_LIVEKIT_WS_URL=wss://livekit-server.bfzqqk.easypanel.host -VITE_TOKEN_SERVER_URL=http://localhost:3010 +VITE_TOKEN_SERVER_URL=https://avanzacast-studio.bfzqqk.easypanel.host # Application Configuration VITE_APP_NAME=AvanzaCast Broadcast Panel diff --git a/packages/broadcast-panel/LIVEKIT_SETUP.md b/packages/broadcast-panel/LIVEKIT_SETUP.md index 0875e64..d523967 100644 --- a/packages/broadcast-panel/LIVEKIT_SETUP.md +++ b/packages/broadcast-panel/LIVEKIT_SETUP.md @@ -124,7 +124,7 @@ LIVEKIT_URL=wss://your-project.livekit.cloud ## Componentes de LiveKit Utilizados -- `LiveKitRoom`: Contenedor principal para la sala de video +- ` `: Contenedor principal para la sala de video - `VideoConference`: Componente de conferencia de video todo-en-uno - `ParticipantTile`: Miniatura de video de participante individual - `ControlBar`: Barra de controles personalizable diff --git a/packages/broadcast-panel/docker-compose.yml b/packages/broadcast-panel/docker-compose.yml index bfbc8cf..1a64f99 100644 --- a/packages/broadcast-panel/docker-compose.yml +++ b/packages/broadcast-panel/docker-compose.yml @@ -21,7 +21,7 @@ services: - NODE_ENV=development - DOCKER_ENV=true - VITE_LIVEKIT_WS_URL=wss://livekit-server.bfzqqk.easypanel.host - - VITE_TOKEN_SERVER_URL=http://localhost:3010 + - VITE_TOKEN_SERVER_URL=${VITE_TOKEN_SERVER_URL:-https://avanzacast-studio.bfzqqk.easypanel.host} networks: - avanzacast-network restart: unless-stopped diff --git a/packages/broadcast-panel/src/components/Sidebar.tsx b/packages/broadcast-panel/src/components/Sidebar.tsx index bdbbd00..eef45d3 100644 --- a/packages/broadcast-panel/src/components/Sidebar.tsx +++ b/packages/broadcast-panel/src/components/Sidebar.tsx @@ -19,7 +19,6 @@ const Sidebar: React.FC = ({ activeLink = 'inicio', onNavigate }) const navItems = [ { id: 'inicio', label: 'Inicio', icon: }, - { id: 'studio', label: 'Studio', icon: }, { id: 'biblioteca', label: 'Biblioteca', icon: }, { id: 'destinos', label: 'Destinos', icon: }, { id: 'miembros', label: 'Miembros', icon: }, diff --git a/packages/broadcast-panel/src/components/TransmissionsTable.tsx b/packages/broadcast-panel/src/components/TransmissionsTable.tsx index 1456229..5c3ad9c 100644 --- a/packages/broadcast-panel/src/components/TransmissionsTable.tsx +++ b/packages/broadcast-panel/src/components/TransmissionsTable.tsx @@ -58,21 +58,37 @@ const TransmissionsTable: React.FC = ({ transmissions, onDelete, onUpdate const userRaw = localStorage.getItem('avanzacast_user') || 'Demo User' const user = encodeURIComponent(userRaw) const room = encodeURIComponent(t.id || 'avanzacast-studio') - const tokenRes = await fetch(`http://localhost:3010/api/token?room=${room}&username=${user}`) + + 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 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') const tokenData = await tokenRes.json() - // Guardar token, serverUrl, room, user en sessionStorage - sessionStorage.setItem('avanzacast_studio_token', tokenData.token) - sessionStorage.setItem('avanzacast_studio_serverUrl', tokenData.serverUrl) - sessionStorage.setItem('avanzacast_studio_room', decodeURIComponent(room)) - sessionStorage.setItem('avanzacast_studio_user', decodeURIComponent(user)) + console.log('[BroadcastPanel] Token recibido:', { + tokenLength: tokenData.token?.length || 0, + serverUrl: tokenData.serverUrl, + hasToken: !!tokenData.token, + hasServerUrl: !!tokenData.serverUrl + }) - // Redirigir a studio-panel en la misma pestaña - const shortId = Math.random().toString(36).slice(2, 10) - window.location.href = `http://localhost:3020/${shortId}` + // Pasar solo token, room y user por URL (serverUrl se lee del .env en studio-panel) + const params = new URLSearchParams({ + token: tokenData.token, + room: decodeURIComponent(room), + user: decodeURIComponent(user) + }) + + console.log('[BroadcastPanel] Redirigiendo con parámetros en URL...') + + // Redirigir a studio-panel en la misma pestaña con los datos en la URL + const STUDIO_URL = import.meta.env.VITE_STUDIO_URL || 'https://avanzacast-studio.bfzqqk.easypanel.host' + const shortId = Math.random().toString(36).slice(2, 10) + window.location.href = `${STUDIO_URL.replace(/\/$/, '')}/${shortId}?${params.toString()}` } catch (err) { - console.error('Error entrando al estudio', err) + console.error('[BroadcastPanel] Error entrando al estudio:', err) alert('No fue posible entrar al estudio. Revisa el servidor de tokens.') setLoadingId(null) } diff --git a/packages/broadcast-panel/vite.config.ts b/packages/broadcast-panel/vite.config.ts index 9b27be6..928d544 100644 --- a/packages/broadcast-panel/vite.config.ts +++ b/packages/broadcast-panel/vite.config.ts @@ -35,7 +35,7 @@ export default defineConfig({ } }, server: { - port: 5173, + port: 5175, host: true, fs: { // Allow serving files from the shared folder when mounted in Docker diff --git a/packages/studio-panel/src/components/ParticipantsPanel.tsx b/packages/studio-panel/src/components/ParticipantsPanel.tsx new file mode 100644 index 0000000..f9c2c89 --- /dev/null +++ b/packages/studio-panel/src/components/ParticipantsPanel.tsx @@ -0,0 +1,121 @@ +import React, { useEffect, useState } from 'react' +import { useParticipants } from '@livekit/components-react' +import apiClient from '@avanzacast/shared-utils/api' + +interface InvitedParticipant { + identity: string + id?: string + metadata?: any +} + +interface ParticipantsPanelProps { + roomName?: string +} + +const ParticipantsPanel: React.FC = ({ roomName }) => { + const liveParticipants = useParticipants() + const [invited, setInvited] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + useEffect(() => { + if (!roomName) return + let mounted = true + const fetchInvited = async () => { + setLoading(true) + setError(null) + try { + const result = await apiClient.get(`/livekit/rooms?roomName=${encodeURIComponent(roomName as string)}`) + if (result.success) { + const json: any = result.data || {} + const list = (json?.room?.participants) || [] + if (mounted) setInvited(list) + } else { + // Fallback: try Next.js app running on localhost:3000 (some environments expose the route there) + try { + const nextUrl = `http://localhost:3000/api/livekit/rooms?roomName=${encodeURIComponent(roomName as string)}` + const resp = await fetch(nextUrl, { headers: { 'Content-Type': 'application/json' } }) + const data = await resp.json() + const list = (data?.room?.participants) || [] + if (mounted) setInvited(list) + } catch (nfErr) { + throw new Error(result.error?.message || 'API error') + } + } + } catch (e: any) { + console.warn('[ParticipantsPanel] error fetching invited', e) + if (mounted) setError(String(e?.message || e)) + } finally { + if (mounted) setLoading(false) + } + } + + fetchInvited() + return () => { mounted = false } + }, [roomName]) + + const isConnected = (identity: string) => liveParticipants.some((p: any) => p.identity === identity) + + return ( +
+
+

Conectados

+ {liveParticipants.length === 0 ? ( +

No hay participantes conectados.

+ ) : ( +
+ {liveParticipants.map((p: any) => ( +
+
+
+ {p.identity?.charAt(0)?.toUpperCase()} +
+
+

{p.identity}

+

{p.isLocal ? 'Tú (presentador)' : 'Invitado'}

+
+
+
+ + +
+
+ ))} +
+ )} +
+ +
+

Invitados

+ {loading ? ( +

Cargando...

+ ) : error ? ( +

Error: {error}

+ ) : invited.length === 0 ? ( +

No hay invitados registrados.

+ ) : ( +
+ {invited.map((inv) => ( +
+
+
+ {inv.identity?.charAt(0)?.toUpperCase()} +
+
+

{inv.identity}

+

{isConnected(inv.identity) ? 'Conectado' : 'Esperando'}

+
+
+
+ +
+
+ ))} +
+ )} +
+
+ ) +} + +export default ParticipantsPanel diff --git a/packages/studio-panel/src/components/PresentationPanel.tsx b/packages/studio-panel/src/components/PresentationPanel.tsx new file mode 100644 index 0000000..381e7f2 --- /dev/null +++ b/packages/studio-panel/src/components/PresentationPanel.tsx @@ -0,0 +1,57 @@ +import React, { useRef, useState } from 'react' + +interface PresentationPanelProps { + onClose: () => void + onShareScreen: () => void + onShareFile: (file: File) => void +} + +const PresentationPanel: React.FC = ({ onClose, onShareScreen, onShareFile }) => { + const inputRef = useRef(null) + const [uploading, setUploading] = useState(false) + + const handleFileChange = async (e: React.ChangeEvent) => { + const f = e.target.files?.[0] + if (!f) return + setUploading(true) + try { + onShareFile(f) + } finally { + setUploading(false) + } + } + + return ( +
+
+

Compartir presentación

+ +
+ +
+ + +
+ + +
+
+
+ ) +} + +export default PresentationPanel diff --git a/packages/studio-panel/src/components/Studio.tsx b/packages/studio-panel/src/components/Studio.tsx index c4d94d0..5e49cf4 100644 --- a/packages/studio-panel/src/components/Studio.tsx +++ b/packages/studio-panel/src/components/Studio.tsx @@ -6,6 +6,7 @@ import StudioLeftSidebar from './StudioLeftSidebar' import StudioVideoArea from './StudioVideoArea' import StudioRightPanel from './StudioRightPanel' import StudioControls from './StudioControls' +import PresentationPanel from './PresentationPanel' import { DEMO_TOKEN } from '../config/demo' interface StudioProps { @@ -15,8 +16,15 @@ interface StudioProps { const Studio: React.FC = ({ userName, roomName }) => { const [token, setToken] = useState(DEMO_TOKEN) + const [serverUrl, setServerUrl] = useState('') const [isConnecting, setIsConnecting] = useState(true) const [isDemoMode, setIsDemoMode] = useState(true) // Iniciar en modo demo por defecto + const [presentationOpen, setPresentationOpen] = useState(false) + const [sharedPresentation, setSharedPresentation] = useState<{ type: 'screen' | 'file', url?: string, stream?: MediaStream } | null>(null) + const [layout, setLayout] = useState<'grid' | 'focus'>('grid') + const [mode, setMode] = useState<'video' | 'audio'>('video') + const [showLeftPanel, setShowLeftPanel] = useState(true) + const [showRightPanel, setShowRightPanel] = useState(true) // Utility: heurística ligera para validar token en cliente const isTokenLikelyValid = (t?: string) => { @@ -51,26 +59,83 @@ const Studio: React.FC = ({ userName, roomName }) => { } }, [token]) - useEffect(() => { - // Leer token y datos desde sessionStorage (guardado por broadcast-panel) + // Presentation handlers + const handleOpenPresentationPanel = () => setPresentationOpen(true) + const handleClosePresentationPanel = () => setPresentationOpen(false) + + const handleShareScreen = async () => { try { - const storedToken = sessionStorage.getItem('avanzacast_studio_token') - const storedServerUrl = sessionStorage.getItem('avanzacast_studio_serverUrl') - const storedRoom = sessionStorage.getItem('avanzacast_studio_room') - const storedUser = sessionStorage.getItem('avanzacast_studio_user') + // Solicitar MediaStream para screen sharing + // Note: this must be called after a user gesture + const stream = await (navigator.mediaDevices as any).getDisplayMedia({ video: true }) + // Guardar el objeto MediaStream en el estado para reproducirlo localmente + setSharedPresentation({ type: 'screen', stream }) + setPresentationOpen(false) + } catch (e) { + console.warn('[Studio] Error al compartir pantalla', e) + } + } - console.log('[Studio] sessionStorage:', { storedToken: storedToken ? `${storedToken.length} chars` : 'none', storedServerUrl, storedRoom, storedUser }) + const handleShareFile = async (file: File) => { + // Para MVP: crear URL local y mostrar en el área central + const url = URL.createObjectURL(file) + setSharedPresentation({ type: 'file', url }) + setPresentationOpen(false) + } - if (storedToken && storedServerUrl) { + useEffect(() => { + // Leer token, room y user desde URL query params (pasados por broadcast-panel) + // El serverUrl se lee del .env + try { + const urlParams = new URLSearchParams(window.location.search) + const storedToken = urlParams.get('token') + const storedRoom = urlParams.get('room') + const storedUser = urlParams.get('user') + + // ServerUrl siempre viene del .env + const envServerUrl = import.meta.env.VITE_LIVEKIT_URL || 'wss://livekit-server.bfzqqk.easypanel.host' + + // Log detallado para depuración + console.log('[Studio] Leyendo datos de URL params:', { + token: storedToken ? `recibido (${storedToken.length} caracteres)` : 'no encontrado', + room: storedRoom || 'no encontrado', + user: storedUser || 'no encontrado', + }) + console.log('[Studio] ServerUrl desde .env:', envServerUrl) + + if (storedToken) { + console.log('[Studio] Token encontrado en URL. Configurando para conexión real.') setToken(storedToken) + setServerUrl(envServerUrl) setIsDemoMode(false) + + // Guardar en localStorage para que persista en recargas + localStorage.setItem('avanzacast_studio_token', storedToken) + localStorage.setItem('avanzacast_studio_serverUrl', envServerUrl) + if (storedRoom) localStorage.setItem('avanzacast_studio_room', storedRoom) + if (storedUser) localStorage.setItem('avanzacast_studio_user', storedUser) + + // Limpiar URL para que no se vea el token + window.history.replaceState({}, '', window.location.pathname) } else { - console.log('⚠️ No se encontró token en sessionStorage. Usando modo DEMO...') - setToken(DEMO_TOKEN) - setIsDemoMode(true) + // Intentar leer de localStorage (si ya se guardó antes) + const cachedToken = localStorage.getItem('avanzacast_studio_token') + const cachedServerUrl = localStorage.getItem('avanzacast_studio_serverUrl') + + if (cachedToken && cachedServerUrl) { + console.log('[Studio] Token y Server URL encontrados en localStorage. Configurando para conexión real.') + setToken(cachedToken) + setServerUrl(cachedServerUrl) + setIsDemoMode(false) + } else { + console.warn('⚠️ No se encontró token en URL ni localStorage. Usando modo DEMO...') + setToken(DEMO_TOKEN) + setServerUrl(envServerUrl) + setIsDemoMode(true) + } } } catch (err) { - console.error('Error leyendo sessionStorage:', err) + console.error('Error crítico leyendo datos:', err) setToken(DEMO_TOKEN) setIsDemoMode(true) } finally { @@ -85,18 +150,19 @@ const Studio: React.FC = ({ userName, roomName }) => { if (w && !w.__AVZ_WS_WRAPPED) { const OriginalWS = w.WebSocket function WrappedWebSocket(url: string, protocols?: string | string[]) { + console.log(`[WS Monitor] new WebSocket(${url})`) // @ts-ignore const sock = protocols ? new OriginalWS(url, protocols) : new OriginalWS(url) try { - sock.addEventListener('open', (ev: Event) => console.log('[WS Monitor] open', { url, type: (ev && (ev as any).type) || 'open' })) - sock.addEventListener('close', (ev: any) => console.log('[WS Monitor] close', { url, code: ev.code, reason: ev.reason, wasClean: ev.wasClean })) - sock.addEventListener('error', (ev: any) => console.error('[WS Monitor] error', { url, ev })) + sock.addEventListener('open', (ev: Event) => console.log('[WS Monitor] Evento: open', { url, type: (ev && (ev as any).type) || 'open' })) + sock.addEventListener('close', (ev: any) => console.log('[WS Monitor] Evento: close', { url, code: ev.code, reason: ev.reason, wasClean: ev.wasClean })) + sock.addEventListener('error', (ev: any) => console.error('[WS Monitor] Evento: error', { url, ev })) sock.addEventListener('message', (ev: any) => { // don't log binary payloads try { - const data = typeof ev.data === 'string' ? ev.data : '[binary]' + const data = typeof ev.data === 'string' ? ev.data : '[payload binario]' // limit length - console.log('[WS Monitor] message', { url, data: data && data.slice ? data.slice(0, 200) : data }) + console.log('[WS Monitor] Evento: message', { url, data: data && data.slice ? data.slice(0, 250) : data }) } catch (e) { // ignore } @@ -109,7 +175,7 @@ const Studio: React.FC = ({ userName, roomName }) => { WrappedWebSocket.prototype = OriginalWS.prototype w.WebSocket = WrappedWebSocket w.__AVZ_WS_WRAPPED = true - console.log('[Studio] WebSocket wrapped for debug: will log open/close/error/messages') + console.log('[Studio] WebSocket envuelto para depuración: se registrarán todos los eventos de WS.') } } catch (e) { // ignore failures in exotic environments @@ -118,8 +184,8 @@ const Studio: React.FC = ({ userName, roomName }) => { // Global error capture to help debugging connection failures useEffect(() => { - const onErr = (ev: ErrorEvent) => console.error('[Studio][global error]', ev.message, ev.error) - const onReject = (ev: PromiseRejectionEvent) => console.error('[Studio][unhandledrejection]', ev.reason) + const onErr = (ev: ErrorEvent) => console.error('[Studio][Error Global]', { message: ev.message, error: ev.error, filename: ev.filename, lineno: ev.lineno }) + const onReject = (ev: PromiseRejectionEvent) => console.error('[Studio][Promesa Rechazada]', ev.reason) window.addEventListener('error', onErr) window.addEventListener('unhandledrejection', onReject) return () => { @@ -132,8 +198,8 @@ const Studio: React.FC = ({ userName, roomName }) => { // NOTE: development WebSocket monitor removed to keep hooks stable during HMR useEffect(() => { - console.log('[Studio] rendering LiveKitRoom, token_len=', token ? token.length : 0) - return () => console.log('[Studio] unmount LiveKitRoom') + console.log(`[Studio] Renderizando LiveKitRoom. Longitud del token: ${token ? token.length : 0}. ¿Es válido?: ${isTokenLikelyValid(token)}`) + return () => console.log('[Studio] Desmontando LiveKitRoom') }, [token]) if (isConnecting) { @@ -148,58 +214,65 @@ const Studio: React.FC = ({ userName, roomName }) => { } // Heurística para no montar LiveKitRoom con tokens de demo o cortos - const serverUrl = import.meta.env.VITE_LIVEKIT_URL || 'wss://livekit-server.bfzqqk.easypanel.host' const hasValidToken = isTokenLikelyValid(token) + const hasValidServerUrl = serverUrl && serverUrl.startsWith('wss://') - if (!hasValidToken) { + console.log('[Studio] Validación antes de renderizar:', { hasValidToken, hasValidServerUrl, serverUrl }) + + if (!hasValidToken || !hasValidServerUrl) { // Mostrar diagnóstico más útil cuando el token es claramente inválido/corto - const preview = token ? `${token.slice(0, 6)}...${token.slice(-6)}` : 'none' - console.log('[Studio] token not valid, will not mount LiveKitRoom. preview=', preview, ' len=', token ? token.length : 0) + const preview = token ? `${token.slice(0, 6)}...${token.slice(-6)}` : 'ninguno' + console.error(`[Studio] Token o ServerUrl inválidos, no se montará LiveKitRoom.`) + console.error(`[Studio] Token preview: ${preview}, Longitud: ${token ? token.length : 0}`) + console.error(`[Studio] ServerUrl: ${serverUrl || 'ninguno'}`) const broadcastUrl = import.meta.env.VITE_BROADCAST_URL || 'http://localhost:5175' const goBackToBroadcast = () => { - // Limpiar sessionStorage + // Limpiar localStorage (por si acaso quedaron datos) try { - sessionStorage.removeItem('avanzacast_studio_token') - sessionStorage.removeItem('avanzacast_studio_serverUrl') - sessionStorage.removeItem('avanzacast_studio_room') - sessionStorage.removeItem('avanzacast_studio_user') + console.log('[Studio] Limpiando localStorage antes de volver.') + localStorage.removeItem('avanzacast_studio_token') + localStorage.removeItem('avanzacast_studio_serverUrl') + localStorage.removeItem('avanzacast_studio_room') + localStorage.removeItem('avanzacast_studio_user') } catch (e) {} // Intentar volver atrás en el historial si hay entrada previa if (window.history.length > 1) { + console.log('[Studio] Volviendo atrás en el historial.') window.history.back() } else { // Redirigir al broadcast + console.log(`[Studio] No hay historial, redirigiendo a ${broadcastUrl}`) window.location.href = broadcastUrl } } // Modal bloqueante: overlay completo que no permite interacción con UI return ( -
+
{/* Modal */} -
+
- + -

No se pudo conectar al estudio

+

Error de Conexión

-

No se obtuvo un token válido para autenticarse con el servidor de LiveKit.

-

Token obtenido: {token ? `${token.length} chars` : 'ninguno'}

+

No se pudo conectar al estudio porque no se obtuvo un token de autenticación válido.

+

Esto puede ocurrir si el servidor de tokens no está disponible o si el token ha expirado.

@@ -213,26 +286,125 @@ const Studio: React.FC = ({ userName, roomName }) => {
{/* Banner de modo demo */}
- ⚠️ MODO DEMO - Servidor de tokens no disponible. Funcionalidad limitada. + ⚠️ MODO DEMO - No se encontró un token en sessionStorage. Funcionalidad de streaming deshabilitada.
{/* Header superior */} {/* Contenido principal */} -
- {/* Sidebar izquierdo - Escenas */} - +
+ {/* Sidebar izquierdo - Escenas (posición absoluta izquierda) */} +
+ +
+ + {/* Botón toggle izquierdo (en el borde derecho del panel izquierdo) */} + {/* Área central de video */} - +
+ +
- {/* Panel derecho - Ajustes */} - + {/* Panel derecho - Ajustes (posición absoluta derecha) */} +
+ +
+ + {/* Botón toggle derecho (en el borde izquierdo del panel derecho) */} +
- {/* Controles inferiores */} - + {/* Controles inferiores */} + setSharedPresentation(null)} + layout={layout} + mode={mode} + onChangeLayout={(l: 'grid' | 'focus') => setLayout(l)} + onChangeMode={(m: 'video' | 'audio') => setMode(m)} + /> + {/* Presentation Panel modal */} + {presentationOpen && ( +
+ +
+ )} + + {/* Shared presentation overlay */} + {sharedPresentation && ( +
+
+
+ {sharedPresentation.type === 'file' ? ( + // Try to guess type by URL extension + (sharedPresentation.url && sharedPresentation.url.endsWith('.pdf')) ? ( +