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.
This commit is contained in:
parent
543d6bc6af
commit
f57ce90c11
@ -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' });
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -19,7 +19,6 @@ const Sidebar: React.FC<SidebarProps> = ({ activeLink = 'inicio', onNavigate })
|
||||
|
||||
const navItems = [
|
||||
{ id: 'inicio', label: 'Inicio', icon: <MdHome size={20} /> },
|
||||
{ id: 'studio', label: 'Studio', icon: <MdVideocam size={20} /> },
|
||||
{ id: 'biblioteca', label: 'Biblioteca', icon: <MdVideoLibrary size={20} /> },
|
||||
{ id: 'destinos', label: 'Destinos', icon: <MdLink size={20} /> },
|
||||
{ id: 'miembros', label: 'Miembros', icon: <MdPeople size={20} /> },
|
||||
|
||||
@ -58,21 +58,37 @@ const TransmissionsTable: React.FC<Props> = ({ 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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
121
packages/studio-panel/src/components/ParticipantsPanel.tsx
Normal file
121
packages/studio-panel/src/components/ParticipantsPanel.tsx
Normal file
@ -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<ParticipantsPanelProps> = ({ roomName }) => {
|
||||
const liveParticipants = useParticipants()
|
||||
const [invited, setInvited] = useState<InvitedParticipant[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-white text-sm font-semibold mb-2">Conectados</h4>
|
||||
{liveParticipants.length === 0 ? (
|
||||
<p className="text-gray-400 text-xs">No hay participantes conectados.</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{liveParticipants.map((p: any) => (
|
||||
<div key={p.sid} className="flex items-center justify-between bg-gray-700 p-2 rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-full bg-gradient-to-br from-pink-500 to-red-500 flex items-center justify-center text-white">
|
||||
{p.identity?.charAt(0)?.toUpperCase()}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-white truncate">{p.identity}</p>
|
||||
<p className="text-xs text-gray-400">{p.isLocal ? 'Tú (presentador)' : 'Invitado'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button className="text-xs px-2 py-1 bg-gray-600 hover:bg-gray-500 rounded text-white">Silenciar</button>
|
||||
<button className="text-xs px-2 py-1 bg-red-600 hover:bg-red-700 rounded text-white">Remover</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-white text-sm font-semibold mb-2">Invitados</h4>
|
||||
{loading ? (
|
||||
<p className="text-gray-400 text-xs">Cargando...</p>
|
||||
) : error ? (
|
||||
<p className="text-red-400 text-xs">Error: {error}</p>
|
||||
) : invited.length === 0 ? (
|
||||
<p className="text-gray-400 text-xs">No hay invitados registrados.</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{invited.map((inv) => (
|
||||
<div key={inv.identity} className={`flex items-center justify-between bg-gray-700 p-2 rounded-lg ${isConnected(inv.identity) ? 'border-2 border-green-500' : ''}`}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-full bg-gray-600 flex items-center justify-center text-white">
|
||||
{inv.identity?.charAt(0)?.toUpperCase()}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-white truncate">{inv.identity}</p>
|
||||
<p className="text-xs text-gray-400">{isConnected(inv.identity) ? 'Conectado' : 'Esperando'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button className="text-xs px-2 py-1 bg-blue-600 hover:bg-blue-700 rounded text-white">Invitar</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ParticipantsPanel
|
||||
57
packages/studio-panel/src/components/PresentationPanel.tsx
Normal file
57
packages/studio-panel/src/components/PresentationPanel.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import React, { useRef, useState } from 'react'
|
||||
|
||||
interface PresentationPanelProps {
|
||||
onClose: () => void
|
||||
onShareScreen: () => void
|
||||
onShareFile: (file: File) => void
|
||||
}
|
||||
|
||||
const PresentationPanel: React.FC<PresentationPanelProps> = ({ onClose, onShareScreen, onShareFile }) => {
|
||||
const inputRef = useRef<HTMLInputElement | null>(null)
|
||||
const [uploading, setUploading] = useState(false)
|
||||
|
||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const f = e.target.files?.[0]
|
||||
if (!f) return
|
||||
setUploading(true)
|
||||
try {
|
||||
onShareFile(f)
|
||||
} finally {
|
||||
setUploading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-gray-800 p-4 rounded shadow-md">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-white text-sm font-semibold">Compartir presentación</h3>
|
||||
<button onClick={onClose} className="text-gray-300 text-sm">Cerrar</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
onClick={onShareScreen}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 rounded"
|
||||
>
|
||||
Compartir pantalla
|
||||
</button>
|
||||
|
||||
<div>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
accept="image/*,video/*,application/pdf"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
id="presentation-file"
|
||||
/>
|
||||
<label htmlFor="presentation-file" className="w-full block text-center bg-green-600 hover:bg-green-700 text-white py-2 rounded cursor-pointer">
|
||||
{uploading ? 'Subiendo...' : 'Subir archivo (imagen, video, pdf)'}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PresentationPanel
|
||||
@ -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<StudioProps> = ({ userName, roomName }) => {
|
||||
const [token, setToken] = useState<string>(DEMO_TOKEN)
|
||||
const [serverUrl, setServerUrl] = useState<string>('')
|
||||
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<StudioProps> = ({ 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<StudioProps> = ({ 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<StudioProps> = ({ 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<StudioProps> = ({ 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<StudioProps> = ({ 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<StudioProps> = ({ 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 (
|
||||
<div className="fixed inset-0 z-[9999] bg-gray-900 flex items-center justify-center">
|
||||
<div className="fixed inset-0 z-[9999] bg-gray-900 bg-opacity-90 flex items-center justify-center">
|
||||
{/* Modal */}
|
||||
<div role="dialog" aria-modal="true" className="bg-gray-800 p-8 rounded-lg max-w-md w-full mx-4 shadow-2xl">
|
||||
<div role="dialog" aria-modal="true" className="bg-gray-800 p-8 rounded-lg max-w-md w-full mx-4 shadow-2xl border border-red-500/50">
|
||||
<div className="flex items-center mb-4">
|
||||
<svg className="w-8 h-8 text-red-500 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg className="w-8 h-8 text-red-500 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<h3 className="text-xl text-white font-semibold">No se pudo conectar al estudio</h3>
|
||||
<h3 className="text-xl text-white font-semibold">Error de Conexión</h3>
|
||||
</div>
|
||||
<p className="text-gray-300 mb-3">No se obtuvo un token válido para autenticarse con el servidor de LiveKit.</p>
|
||||
<p className="text-gray-400 text-sm mb-6">Token obtenido: {token ? `${token.length} chars` : 'ninguno'}</p>
|
||||
<p className="text-gray-300 mb-3">No se pudo conectar al estudio porque no se obtuvo un token de autenticación válido.</p>
|
||||
<p className="text-gray-400 text-sm mb-6">Esto puede ocurrir si el servidor de tokens no está disponible o si el token ha expirado.</p>
|
||||
<div className="flex gap-3 justify-end">
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="py-2 px-4 bg-slate-600 hover:bg-slate-700 text-white rounded-lg font-medium"
|
||||
className="py-2 px-4 bg-slate-600 hover:bg-slate-700 text-white rounded-lg font-medium transition-colors"
|
||||
>
|
||||
Reintentar
|
||||
</button>
|
||||
<button
|
||||
onClick={goBackToBroadcast}
|
||||
className="py-2 px-4 bg-pink-600 hover:bg-pink-700 text-white rounded-lg font-medium"
|
||||
className="py-2 px-4 bg-pink-600 hover:bg-pink-700 text-white rounded-lg font-medium transition-colors"
|
||||
>
|
||||
Volver al Broadcast
|
||||
Volver al Panel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -213,26 +286,125 @@ const Studio: React.FC<StudioProps> = ({ userName, roomName }) => {
|
||||
<div className="flex flex-col h-screen bg-gray-900">
|
||||
{/* Banner de modo demo */}
|
||||
<div className="bg-yellow-600 text-black px-4 py-2 text-center text-sm font-semibold">
|
||||
⚠️ MODO DEMO - Servidor de tokens no disponible. Funcionalidad limitada.
|
||||
⚠️ MODO DEMO - No se encontró un token en sessionStorage. Funcionalidad de streaming deshabilitada.
|
||||
</div>
|
||||
|
||||
{/* Header superior */}
|
||||
<StudioHeader roomName={roomName} userName={userName} />
|
||||
|
||||
{/* Contenido principal */}
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
{/* Sidebar izquierdo - Escenas */}
|
||||
<StudioLeftSidebar />
|
||||
<div className="flex-1 overflow-hidden relative">
|
||||
{/* Sidebar izquierdo - Escenas (posición absoluta izquierda) */}
|
||||
<div
|
||||
className={`absolute left-0 top-0 bottom-0 z-20 transition-transform duration-300 ease-in-out ${
|
||||
showLeftPanel ? 'translate-x-0' : '-translate-x-full'
|
||||
}`}
|
||||
>
|
||||
<StudioLeftSidebar />
|
||||
</div>
|
||||
|
||||
{/* Botón toggle izquierdo (en el borde derecho del panel izquierdo) */}
|
||||
<button
|
||||
onClick={() => setShowLeftPanel(!showLeftPanel)}
|
||||
className="absolute top-1/2 -translate-y-1/2 z-30 transition-all duration-300"
|
||||
style={{ left: showLeftPanel ? '256px' : '0px' }}
|
||||
title={showLeftPanel ? 'Ocultar panel izquierdo' : 'Mostrar panel izquierdo'}
|
||||
>
|
||||
<svg width="16" height="101" viewBox="0 0 16 101" fill="none" xmlns="http://www.w3.org/2000/svg" className="hover:opacity-80 transition-opacity">
|
||||
<path d="M0 12C0 5.37258 5.37258 0 12 0H16V101H12C5.37258 101 0 95.6274 0 89V12Z" fill="#1F2937"/>
|
||||
<path d="M0.5 12C0.5 5.64873 5.64873 0.5 12 0.5H15.5V100.5H12C5.64873 100.5 0.5 95.3513 0.5 89V12Z" stroke="#1B1F291A"/>
|
||||
<rect x="13" y="42" width="3" height="17" rx="1.5" fill="white"/>
|
||||
<path
|
||||
d={showLeftPanel ? "M6 44L10 50.5L6 57" : "M10 44L6 50.5L10 57"}
|
||||
stroke="#1B1F29"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="transition-all duration-300"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Área central de video */}
|
||||
<StudioVideoArea isDemoMode={true} />
|
||||
<div className="w-full h-full overflow-hidden">
|
||||
<StudioVideoArea isDemoMode={true} layout={layout} mode={mode} />
|
||||
</div>
|
||||
|
||||
{/* Panel derecho - Ajustes */}
|
||||
<StudioRightPanel />
|
||||
{/* Panel derecho - Ajustes (posición absoluta derecha) */}
|
||||
<div
|
||||
className={`absolute right-0 top-0 bottom-0 z-20 transition-transform duration-300 ease-in-out ${
|
||||
showRightPanel ? 'translate-x-0' : 'translate-x-full'
|
||||
}`}
|
||||
>
|
||||
<StudioRightPanel roomName={(localStorage.getItem('avanzacast_studio_room') || roomName) as string} />
|
||||
</div>
|
||||
|
||||
{/* Botón toggle derecho (en el borde izquierdo del panel derecho) */}
|
||||
<button
|
||||
onClick={() => setShowRightPanel(!showRightPanel)}
|
||||
className="absolute top-1/2 -translate-y-1/2 z-30 transition-all duration-300"
|
||||
style={{ right: showRightPanel ? '320px' : '0px' }}
|
||||
title={showRightPanel ? 'Ocultar panel derecho' : 'Mostrar panel derecho'}
|
||||
>
|
||||
<svg width="16" height="101" viewBox="0 0 16 101" fill="none" xmlns="http://www.w3.org/2000/svg" className="hover:opacity-80 transition-opacity" style={{ transform: 'scaleX(-1)' }}>
|
||||
<path d="M0 12C0 5.37258 5.37258 0 12 0H16V101H12C5.37258 101 0 95.6274 0 89V12Z" fill="#1F2937"/>
|
||||
<path d="M0.5 12C0.5 5.64873 5.64873 0.5 12 0.5H15.5V100.5H12C5.64873 100.5 0.5 95.3513 0.5 89V12Z" stroke="#1B1F291A"/>
|
||||
<rect x="13" y="42" width="3" height="17" rx="1.5" fill="white"/>
|
||||
<path
|
||||
d={showRightPanel ? "M10 44L6 50.5L10 57" : "M6 44L10 50.5L6 57"}
|
||||
stroke="#1B1F29"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="transition-all duration-300"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Controles inferiores */}
|
||||
<StudioControls />
|
||||
{/* Controles inferiores */}
|
||||
<StudioControls
|
||||
onOpenPresentation={handleOpenPresentationPanel}
|
||||
onShareScreen={handleShareScreen}
|
||||
sharedPresentation={sharedPresentation}
|
||||
onClearPresentation={() => setSharedPresentation(null)}
|
||||
layout={layout}
|
||||
mode={mode}
|
||||
onChangeLayout={(l: 'grid' | 'focus') => setLayout(l)}
|
||||
onChangeMode={(m: 'video' | 'audio') => setMode(m)}
|
||||
/>
|
||||
{/* Presentation Panel modal */}
|
||||
{presentationOpen && (
|
||||
<div className="fixed right-6 bottom-24 z-50">
|
||||
<PresentationPanel onClose={handleClosePresentationPanel} onShareScreen={handleShareScreen} onShareFile={handleShareFile} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Shared presentation overlay */}
|
||||
{sharedPresentation && (
|
||||
<div className="fixed inset-0 z-40 flex items-center justify-center bg-black bg-opacity-80">
|
||||
<div className="max-w-6xl max-h-[80vh] w-full p-4">
|
||||
<div className="bg-gray-900 p-2 rounded">
|
||||
{sharedPresentation.type === 'file' ? (
|
||||
// Try to guess type by URL extension
|
||||
(sharedPresentation.url && sharedPresentation.url.endsWith('.pdf')) ? (
|
||||
<iframe src={sharedPresentation.url} className="w-full h-[70vh] border-0" />
|
||||
) : (sharedPresentation.url && sharedPresentation.url.match(/\.(mp4|webm|ogg)$/i)) ? (
|
||||
<video src={sharedPresentation.url} controls className="w-full h-[70vh] bg-black" />
|
||||
) : (
|
||||
<img src={sharedPresentation.url || ''} alt="presentation" className="w-full h-[70vh] object-contain" />
|
||||
)
|
||||
) : (
|
||||
// screen share is a blob URL
|
||||
<video src={sharedPresentation.url} autoPlay playsInline controls className="w-full h-[70vh] bg-black" />
|
||||
)}
|
||||
<div className="mt-2 text-right">
|
||||
<button onClick={() => setSharedPresentation(null)} className="px-4 py-2 bg-red-600 text-white rounded">Cerrar presentación</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -246,6 +418,8 @@ const Studio: React.FC<StudioProps> = ({ userName, roomName }) => {
|
||||
audio={true}
|
||||
token={token}
|
||||
serverUrl={serverUrl}
|
||||
onDisconnected={() => console.log('[LiveKit] Desconectado.')}
|
||||
onError={(e) => console.error('[LiveKit] Error de conexión:', e)}
|
||||
data-lk-theme="default"
|
||||
className="studio-container"
|
||||
>
|
||||
@ -254,19 +428,82 @@ const Studio: React.FC<StudioProps> = ({ userName, roomName }) => {
|
||||
<StudioHeader roomName={roomName} userName={userName} />
|
||||
|
||||
{/* Contenido principal */}
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
{/* Sidebar izquierdo - Escenas */}
|
||||
<StudioLeftSidebar />
|
||||
<div className="flex-1 overflow-hidden relative">
|
||||
{/* Sidebar izquierdo - Escenas (posición absoluta izquierda) */}
|
||||
<div
|
||||
className={`absolute left-0 top-0 bottom-0 z-20 transition-transform duration-300 ease-in-out ${
|
||||
showLeftPanel ? 'translate-x-0' : '-translate-x-full'
|
||||
}`}
|
||||
>
|
||||
<StudioLeftSidebar />
|
||||
</div>
|
||||
|
||||
{/* Botón toggle izquierdo (en el borde derecho del panel izquierdo) */}
|
||||
<button
|
||||
onClick={() => setShowLeftPanel(!showLeftPanel)}
|
||||
className="absolute top-1/2 -translate-y-1/2 z-30 transition-all duration-300"
|
||||
style={{ left: showLeftPanel ? '256px' : '0px' }}
|
||||
title={showLeftPanel ? 'Ocultar panel izquierdo' : 'Mostrar panel izquierdo'}
|
||||
>
|
||||
<svg width="16" height="101" viewBox="0 0 16 101" fill="none" xmlns="http://www.w3.org/2000/svg" className="hover:opacity-80 transition-opacity" style={{ transform: 'scaleX(-1)' }}>
|
||||
<path d="M0 12C0 5.37258 5.37258 0 12 0H16V101H12C5.37258 101 0 95.6274 0 89V12Z" fill="#1F2937"/>
|
||||
<path d="M0.5 12C0.5 5.64873 5.64873 0.5 12 0.5H15.5V100.5H12C5.64873 100.5 0.5 95.3513 0.5 89V12Z" stroke="#1B1F291A"/>
|
||||
<rect x="13" y="42" width="3" height="17" rx="1.5" fill="white"/>
|
||||
<path
|
||||
d={showLeftPanel ? "M6 44L10 50.5L6 57" : "M10 44L6 50.5L10 57"}
|
||||
stroke="#FFF"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="transition-all duration-300"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Área central de video */}
|
||||
<StudioVideoArea isDemoMode={false} />
|
||||
<div className="w-full h-full overflow-hidden">
|
||||
<StudioVideoArea isDemoMode={false} />
|
||||
</div>
|
||||
|
||||
{/* Panel derecho - Ajustes */}
|
||||
<StudioRightPanel />
|
||||
{/* Panel derecho - Ajustes (posición absoluta derecha) */}
|
||||
<div
|
||||
className={`absolute right-0 top-0 bottom-0 z-20 transition-transform duration-300 ease-in-out ${
|
||||
showRightPanel ? 'translate-x-0' : 'translate-x-full'
|
||||
}`}
|
||||
>
|
||||
<StudioRightPanel roomName={(localStorage.getItem('avanzacast_studio_room') || roomName) as string} />
|
||||
</div>
|
||||
|
||||
{/* Botón toggle derecho (en el borde izquierdo del panel derecho) */}
|
||||
<button
|
||||
onClick={() => setShowRightPanel(!showRightPanel)}
|
||||
className="absolute top-1/2 -translate-y-1/2 z-30 transition-all duration-300"
|
||||
style={{ right: showRightPanel ? '320px' : '0px' }}
|
||||
title={showRightPanel ? 'Ocultar panel derecho' : 'Mostrar panel derecho'}
|
||||
>
|
||||
<svg width="16" height="101" viewBox="0 0 16 101" fill="none" xmlns="http://www.w3.org/2000/svg" className="hover:opacity-80 transition-opacity" style={{ transform: 'scaleX(1)' }}>
|
||||
<path d="M0 12C0 5.37258 5.37258 0 12 0H16V101H12C5.37258 101 0 95.6274 0 89V12Z" fill="#1F2937"/>
|
||||
<path d="M0.5 12C0.5 5.64873 5.64873 0.5 12 0.5H15.5V100.5H12C5.64873 100.5 0.5 95.3513 0.5 89V12Z" stroke="#1B1F291A"/>
|
||||
<rect x="13" y="42" width="3" height="17" rx="1.5" fill="white"/>
|
||||
<path
|
||||
d={showRightPanel ? "M6 44L10 50.5L6 57" : "M10 44L6 50.5L10 57"}
|
||||
stroke="#FFF"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="transition-all duration-300"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Controles inferiores */}
|
||||
<StudioControls />
|
||||
<StudioControls
|
||||
onOpenPresentation={handleOpenPresentationPanel}
|
||||
onShareScreen={handleShareScreen}
|
||||
sharedPresentation={sharedPresentation}
|
||||
onClearPresentation={() => setSharedPresentation(null)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Renderizador de audio de la sala */}
|
||||
|
||||
@ -15,7 +15,18 @@ import {
|
||||
MdStop,
|
||||
} from 'react-icons/md'
|
||||
|
||||
const StudioControls: React.FC = () => {
|
||||
interface Props {
|
||||
onOpenPresentation?: () => void
|
||||
onShareScreen?: () => void
|
||||
sharedPresentation?: { type: 'screen' | 'file', url: string } | null
|
||||
onClearPresentation?: () => void
|
||||
layout?: 'grid' | 'focus'
|
||||
mode?: 'video' | 'audio'
|
||||
onChangeLayout?: (layout: 'grid'|'focus') => void
|
||||
onChangeMode?: (mode: 'video'|'audio') => void
|
||||
}
|
||||
|
||||
const StudioControls: React.FC<Props> = ({ onOpenPresentation, onShareScreen, sharedPresentation, onClearPresentation, layout, mode, onChangeLayout, onChangeMode }) => {
|
||||
const { localParticipant } = useLocalParticipant()
|
||||
const [micEnabled, setMicEnabled] = useState(true)
|
||||
const [cameraEnabled, setCameraEnabled] = useState(true)
|
||||
@ -103,7 +114,12 @@ const StudioControls: React.FC = () => {
|
||||
|
||||
{/* Compartir pantalla */}
|
||||
<button
|
||||
onClick={toggleScreenShare}
|
||||
onClick={() => {
|
||||
// Preferimos delegar a un handler externo (que abrirá el panel o iniciará screen share)
|
||||
if (onOpenPresentation) return onOpenPresentation()
|
||||
if (onShareScreen) return onShareScreen()
|
||||
toggleScreenShare()
|
||||
}}
|
||||
className={`control-button ${
|
||||
isScreenSharing ? 'bg-blue-600 hover:bg-blue-700' : 'bg-gray-700 hover:bg-gray-600'
|
||||
} p-3 rounded-lg transition-colors`}
|
||||
@ -112,6 +128,15 @@ const StudioControls: React.FC = () => {
|
||||
{isScreenSharing ? <MdStopScreenShare size={24} /> : <MdScreenShare size={24} />}
|
||||
</button>
|
||||
|
||||
{/* Botón rápido para abrir el panel de presentación (subir archivos) */}
|
||||
<button
|
||||
onClick={() => onOpenPresentation && onOpenPresentation()}
|
||||
className="control-button bg-green-600 hover:bg-green-700 p-3 rounded-lg transition-colors"
|
||||
title="Compartir presentación"
|
||||
>
|
||||
<MdPeople size={20} />
|
||||
</button>
|
||||
|
||||
<div className="w-px h-8 bg-gray-600 mx-2"></div>
|
||||
|
||||
{/* Layouts */}
|
||||
@ -122,6 +147,27 @@ const StudioControls: React.FC = () => {
|
||||
<MdViewComfy size={24} />
|
||||
</button>
|
||||
|
||||
{/* Layout selector */}
|
||||
<div className="ml-2 flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => onChangeLayout && onChangeLayout('grid')}
|
||||
className={`px-2 py-1 rounded ${layout === 'grid' ? 'bg-pink-600' : 'bg-gray-700'}`}
|
||||
title="Grid layout"
|
||||
>
|
||||
Grid
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onChangeLayout && onChangeLayout('focus')}
|
||||
className={`px-2 py-1 rounded ${layout === 'focus' ? 'bg-pink-600' : 'bg-gray-700'}`}
|
||||
title="Focus layout"
|
||||
>
|
||||
Focus
|
||||
</button>
|
||||
|
||||
<button onClick={() => onChangeMode && onChangeMode('video')} className={`px-2 py-1 rounded ${mode === 'video' ? 'bg-pink-600' : 'bg-gray-700'}`}>Video</button>
|
||||
<button onClick={() => onChangeMode && onChangeMode('audio')} className={`px-2 py-1 rounded ${mode === 'audio' ? 'bg-pink-600' : 'bg-gray-700'}`}>Audio</button>
|
||||
</div>
|
||||
|
||||
{/* Configuración */}
|
||||
<button
|
||||
className="control-button bg-gray-700 hover:bg-gray-600 p-3 rounded-lg transition-colors"
|
||||
@ -144,8 +190,14 @@ const StudioControls: React.FC = () => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Derecha - Salir */}
|
||||
{/* Derecha - Presentación / Salir */}
|
||||
<div>
|
||||
{sharedPresentation ? (
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-sm text-green-300">Presentación activa</span>
|
||||
<button onClick={() => onClearPresentation && onClearPresentation()} className="px-3 py-1 bg-gray-700 text-white rounded">Detener</button>
|
||||
</div>
|
||||
) : null}
|
||||
<button
|
||||
className="px-6 py-2 bg-red-600 hover:bg-red-700 text-white font-medium rounded-lg transition-colors"
|
||||
onClick={() => {
|
||||
|
||||
@ -53,7 +53,7 @@ const StudioLeftSidebar = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-64 bg-gray-800 border-r border-gray-700 flex flex-col">
|
||||
<div className="w-64 h-full bg-gray-800 border-r border-gray-700 flex flex-col">
|
||||
{/* Header de Escenas */}
|
||||
<div className="p-4 border-b border-gray-700">
|
||||
<h3 className="text-white font-semibold text-sm flex items-center gap-2">
|
||||
|
||||
@ -7,19 +7,22 @@ import {
|
||||
MdQrCode,
|
||||
MdTimer,
|
||||
MdSettings,
|
||||
MdClose
|
||||
MdClose,
|
||||
MdPeople,
|
||||
} from 'react-icons/md'
|
||||
import { COLOR_THEMES, DEMO_OVERLAYS, DEMO_BACKGROUNDS, DEMO_SOUNDS } from '../config/demo'
|
||||
import ParticipantsPanel from './ParticipantsPanel'
|
||||
|
||||
type TabType = 'brand' | 'multimedia' | 'sounds' | 'video' | 'qr' | 'countdown' | 'settings'
|
||||
type TabType = 'brand' | 'multimedia' | 'participants' | 'sounds' | 'video' | 'qr' | 'countdown' | 'settings'
|
||||
|
||||
const StudioRightPanel = () => {
|
||||
const StudioRightPanel = ({ roomName }: { roomName?: string }) => {
|
||||
const [activeTab, setActiveTab] = useState<TabType>('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' },
|
||||
@ -51,7 +54,7 @@ const StudioRightPanel = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-80 bg-gray-800 border-l border-gray-700 flex flex-col">
|
||||
<div className="w-80 h-full bg-gray-800 border-l border-gray-700 flex flex-col">
|
||||
{/* Header con tabs */}
|
||||
<div className="border-b border-gray-700">
|
||||
<div className="flex items-center justify-between px-3 py-2">
|
||||
@ -90,6 +93,7 @@ const StudioRightPanel = () => {
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{activeTab === 'brand' && <BrandTab />}
|
||||
{activeTab === 'multimedia' && <MultimediaTab />}
|
||||
{activeTab === 'participants' && <ParticipantsPanel roomName={roomName} />}
|
||||
{activeTab === 'sounds' && <SoundsTab />}
|
||||
{activeTab === 'video' && <VideoTab />}
|
||||
{activeTab === 'qr' && <QRTab />}
|
||||
|
||||
@ -1,30 +1,45 @@
|
||||
import { useParticipants, ParticipantTile } from '@livekit/components-react'
|
||||
import { MdAdd, MdPerson, MdMic, MdMicOff, MdVideocamOff } from 'react-icons/md'
|
||||
import React from 'react'
|
||||
import { DEMO_PARTICIPANTS } from '../config/demo'
|
||||
import VideoConference from '../prefabs/VideoConference'
|
||||
import AudioConference from '../prefabs/AudioConference'
|
||||
|
||||
interface StudioVideoAreaProps {
|
||||
isDemoMode?: boolean
|
||||
layout?: 'grid' | 'focus'
|
||||
mode?: 'video' | 'audio'
|
||||
}
|
||||
|
||||
const DemoParticipantTile: React.FC<{ participant: typeof DEMO_PARTICIPANTS[0] }> = ({ participant }) => (
|
||||
<div className="relative w-full h-full bg-gray-800 rounded-lg overflow-hidden flex items-center justify-center">
|
||||
{participant.isCameraEnabled ? <div className="w-20 h-20 rounded-full bg-gradient-to-br from-pink-500 to-red-500 flex items-center justify-center text-white text-3xl font-bold">{participant.name.charAt(0).toUpperCase()}</div> : <MdVideocamOff className="text-gray-600" size={40} />}
|
||||
<div className="absolute bottom-2 left-2">{participant.isMicrophoneEnabled ? <MdMic className="text-white" size={14} /> : <MdMicOff className="text-white" size={14} />}</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const DemoModeView: React.FC = () => (
|
||||
<div className="flex-1 flex flex-col bg-gray-950">
|
||||
<div className="flex-1 p-4"><div className="w-full h-full grid gap-2 grid-cols-2">{DEMO_PARTICIPANTS.map(p => <DemoParticipantTile key={p.id} participant={p} />)}</div></div>
|
||||
<div className="bg-gray-900 p-4"><button className="w-full bg-pink-600 text-white py-2 rounded">Presentar</button></div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const LiveKitModeView: React.FC = () => {
|
||||
const participants = useParticipants()
|
||||
return <div className="flex-1 bg-gray-950 flex items-center justify-center"><div className="text-white">LiveKit Mode</div></div>
|
||||
const DemoModeView: React.FC = () => {
|
||||
return (
|
||||
<div className="flex-1 flex flex-col bg-gray-950">
|
||||
<div className="flex-1 p-4 grid grid-cols-2 gap-2">
|
||||
{DEMO_PARTICIPANTS.map((p) => (
|
||||
<div key={p.id} className="bg-gray-800 rounded-lg p-3 flex flex-col items-center justify-center">
|
||||
<div className="w-20 h-20 rounded-full bg-gradient-to-br from-pink-500 to-red-500 flex items-center justify-center text-white text-2xl font-bold mb-2">
|
||||
{p.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<div className="text-white text-sm font-medium">{p.name}</div>
|
||||
<div className="text-gray-400 text-xs">{p.isMicrophoneEnabled ? 'Mic encendido' : 'Mic apagado'}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="bg-gray-900 border-t border-gray-800 p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<h4 className="text-white text-sm font-medium">Participantes en Studio</h4>
|
||||
<span className="text-gray-400 text-xs">({DEMO_PARTICIPANTS.length})</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const StudioVideoArea: React.FC<StudioVideoAreaProps> = ({ isDemoMode = false }) => isDemoMode ? <DemoModeView /> : <LiveKitModeView />
|
||||
const LiveKitModeView: React.FC<{ layout: 'grid' | 'focus'; mode: 'video' | 'audio' }> = ({ layout, mode }) => {
|
||||
if (mode === 'audio') return <AudioConference />
|
||||
return <VideoConference layout={layout} />
|
||||
}
|
||||
|
||||
const StudioVideoArea: React.FC<StudioVideoAreaProps> = ({ isDemoMode = false, layout = 'grid', mode = 'video' }) => {
|
||||
return isDemoMode ? <DemoModeView /> : <LiveKitModeView layout={layout} mode={mode} />
|
||||
}
|
||||
|
||||
export default StudioVideoArea
|
||||
|
||||
45
packages/studio-panel/src/components/StudioVideoArea.tsx.bak
Normal file
45
packages/studio-panel/src/components/StudioVideoArea.tsx.bak
Normal file
@ -0,0 +1,45 @@
|
||||
import React from 'react'
|
||||
import { DEMO_PARTICIPANTS } from '../config/demo'
|
||||
import VideoConference from '../prefabs/VideoConference'
|
||||
import AudioConference from '../prefabs/AudioConference'
|
||||
|
||||
interface StudioVideoAreaProps {
|
||||
isDemoMode?: boolean
|
||||
layout?: 'grid' | 'focus'
|
||||
mode?: 'video' | 'audio'
|
||||
}
|
||||
|
||||
const DemoModeView: React.FC = () => {
|
||||
return (
|
||||
<div className="flex-1 flex flex-col bg-gray-950">
|
||||
<div className="flex-1 p-4 grid grid-cols-2 gap-2">
|
||||
{DEMO_PARTICIPANTS.map((p) => (
|
||||
<div key={p.id} className="bg-gray-800 rounded-lg p-3 flex flex-col items-center justify-center">
|
||||
<div className="w-20 h-20 rounded-full bg-gradient-to-br from-pink-500 to-red-500 flex items-center justify-center text-white text-2xl font-bold mb-2">
|
||||
{p.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<div className="text-white text-sm font-medium">{p.name}</div>
|
||||
<div className="text-gray-400 text-xs">{p.isMicrophoneEnabled ? 'Mic encendido' : 'Mic apagado'}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="bg-gray-900 border-t border-gray-800 p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<h4 className="text-white text-sm font-medium">Participantes en Studio</h4>
|
||||
<span className="text-gray-400 text-xs">({DEMO_PARTICIPANTS.length})</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const LiveKitModeView: React.FC<{ layout: 'grid' | 'focus'; mode: 'video' | 'audio' }> = ({ layout, mode }) => {
|
||||
if (mode === 'audio') return <AudioConference />
|
||||
return <VideoConference layout={layout} />
|
||||
}
|
||||
|
||||
const StudioVideoArea: React.FC<StudioVideoAreaProps> = ({ isDemoMode = false, layout = 'grid', mode = 'video' }) => {
|
||||
return isDemoMode ? <DemoModeView /> : <LiveKitModeView layout={layout} mode={mode} />
|
||||
}
|
||||
|
||||
export default StudioVideoArea
|
||||
45
packages/studio-panel/src/components/StudioVideoArea.tsx.new
Normal file
45
packages/studio-panel/src/components/StudioVideoArea.tsx.new
Normal file
@ -0,0 +1,45 @@
|
||||
import React from 'react'
|
||||
import { DEMO_PARTICIPANTS } from '../config/demo'
|
||||
import VideoConference from '../prefabs/VideoConference'
|
||||
import AudioConference from '../prefabs/AudioConference'
|
||||
|
||||
interface StudioVideoAreaProps {
|
||||
isDemoMode?: boolean
|
||||
layout?: 'grid' | 'focus'
|
||||
mode?: 'video' | 'audio'
|
||||
}
|
||||
|
||||
const DemoModeView: React.FC = () => {
|
||||
return (
|
||||
<div className="flex-1 flex flex-col bg-gray-950">
|
||||
<div className="flex-1 p-4 grid grid-cols-2 gap-2">
|
||||
{DEMO_PARTICIPANTS.map((p) => (
|
||||
<div key={p.id} className="bg-gray-800 rounded-lg p-3 flex flex-col items-center justify-center">
|
||||
<div className="w-20 h-20 rounded-full bg-gradient-to-br from-pink-500 to-red-500 flex items-center justify-center text-white text-2xl font-bold mb-2">
|
||||
{p.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<div className="text-white text-sm font-medium">{p.name}</div>
|
||||
<div className="text-gray-400 text-xs">{p.isMicrophoneEnabled ? 'Mic encendido' : 'Mic apagado'}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="bg-gray-900 border-t border-gray-800 p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<h4 className="text-white text-sm font-medium">Participantes en Studio</h4>
|
||||
<span className="text-gray-400 text-xs">({DEMO_PARTICIPANTS.length})</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const LiveKitModeView: React.FC<{ layout: 'grid' | 'focus'; mode: 'video' | 'audio' }> = ({ layout, mode }) => {
|
||||
if (mode === 'audio') return <AudioConference />
|
||||
return <VideoConference layout={layout} />
|
||||
}
|
||||
|
||||
const StudioVideoArea: React.FC<StudioVideoAreaProps> = ({ isDemoMode = false, layout = 'grid', mode = 'video' }) => {
|
||||
return isDemoMode ? <DemoModeView /> : <LiveKitModeView layout={layout} mode={mode} />
|
||||
}
|
||||
|
||||
export default StudioVideoArea
|
||||
23
packages/studio-panel/src/prefabs/AudioConference.tsx
Normal file
23
packages/studio-panel/src/prefabs/AudioConference.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import React from 'react'
|
||||
import { Track } from 'livekit-client'
|
||||
import { GridLayout, TrackLoop, ParticipantAudioTile, useTracks } from '@livekit/components-react'
|
||||
|
||||
interface AudioConferenceProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const AudioConference: React.FC<AudioConferenceProps> = ({ className }) => {
|
||||
const audioTracks = useTracks([{ source: Track.Source.Microphone, withPlaceholder: true }])
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<GridLayout tracks={audioTracks as any}>
|
||||
<TrackLoop tracks={audioTracks as any}>
|
||||
<ParticipantAudioTile />
|
||||
</TrackLoop>
|
||||
</GridLayout>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AudioConference
|
||||
36
packages/studio-panel/src/prefabs/Chat.tsx
Normal file
36
packages/studio-panel/src/prefabs/Chat.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import React, { useState } from 'react'
|
||||
import { useChat } from '@livekit/components-react'
|
||||
|
||||
interface ChatProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const Chat: React.FC<ChatProps> = ({ className }) => {
|
||||
const { chatMessages, send, isSending } = useChat()
|
||||
const [text, setText] = useState('')
|
||||
|
||||
const sendMessage = async () => {
|
||||
if (!text) return
|
||||
await send(text)
|
||||
setText('')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col h-full ${className || ''}`}>
|
||||
<div className="flex-1 overflow-auto p-2">
|
||||
{chatMessages.map((m) => (
|
||||
<div key={m.timestamp} className="mb-2">
|
||||
<div className="text-xs text-gray-400">{m.from?.identity || 'anon'}</div>
|
||||
<div className="text-sm text-white">{m.message}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="p-2 border-t border-gray-700 flex gap-2">
|
||||
<input className="flex-1 bg-gray-900 text-white p-2 rounded" value={text} onChange={(e) => setText(e.target.value)} />
|
||||
<button className="bg-pink-500 text-white px-3 py-2 rounded" onClick={sendMessage} disabled={isSending}>Enviar</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Chat
|
||||
33
packages/studio-panel/src/prefabs/ControlBar.tsx
Normal file
33
packages/studio-panel/src/prefabs/ControlBar.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import React from 'react'
|
||||
import { useLocalParticipant, TrackToggle, StartAudio } from '@livekit/components-react'
|
||||
import { Track } from 'livekit-client'
|
||||
|
||||
interface ControlBarProps {
|
||||
onLeave?: () => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
const ControlBar: React.FC<ControlBarProps> = ({ onLeave, className }) => {
|
||||
const { localParticipant } = useLocalParticipant()
|
||||
|
||||
return (
|
||||
<div className={`flex items-center gap-3 p-3 bg-gray-800 ${className || ''}`}>
|
||||
<button
|
||||
className="bg-red-600 text-white px-3 py-2 rounded"
|
||||
onClick={() => {
|
||||
if (onLeave) onLeave()
|
||||
}}
|
||||
>
|
||||
Salir
|
||||
</button>
|
||||
<div className="flex gap-2 items-center">
|
||||
<StartAudio label="Permitir audio" />
|
||||
<TrackToggle source={Track.Source.Camera} />
|
||||
<TrackToggle source={Track.Source.Microphone} />
|
||||
</div>
|
||||
<div className="ml-auto text-sm text-gray-300">{localParticipant?.identity}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ControlBar
|
||||
34
packages/studio-panel/src/prefabs/PreJoin.tsx
Normal file
34
packages/studio-panel/src/prefabs/PreJoin.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import React from 'react'
|
||||
import { useStartVideo, useMediaDevices } from '@livekit/components-react'
|
||||
|
||||
interface PreJoinProps {
|
||||
onJoin?: () => void
|
||||
}
|
||||
|
||||
const PreJoin: React.FC<PreJoinProps> = ({ onJoin }) => {
|
||||
const videoDevices = useMediaDevices({ kind: 'videoinput' })
|
||||
const { mergedProps, canPlayVideo } = useStartVideo({ props: { className: 'bg-gray-700 text-white px-3 py-1 rounded' } })
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-gray-900 rounded-lg">
|
||||
<h3 className="text-white text-lg mb-4">Antes de entrar</h3>
|
||||
<div className="mb-4">
|
||||
<div className="w-full h-48 bg-black rounded-md overflow-hidden flex items-center justify-center text-gray-400">
|
||||
<div>
|
||||
<div className="mb-2">Vista previa no activa</div>
|
||||
<div className="text-xs text-gray-400">Pulsa "Iniciar cámara" para ver la previsualización</div>
|
||||
<div className="mt-2 flex gap-2">
|
||||
<button {...mergedProps} disabled={!canPlayVideo}>Iniciar cámara</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4 text-sm text-gray-300">Dispositivos de video detectados: {videoDevices.length}</div>
|
||||
<div className="flex gap-2">
|
||||
<button className="bg-pink-500 text-white px-4 py-2 rounded" onClick={onJoin}>Entrar</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PreJoin
|
||||
36
packages/studio-panel/src/prefabs/VideoConference.tsx
Normal file
36
packages/studio-panel/src/prefabs/VideoConference.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import React from 'react'
|
||||
import { Track } from 'livekit-client'
|
||||
import { GridLayout, FocusLayout, ParticipantTile, TrackLoop, useTracks } from '@livekit/components-react'
|
||||
|
||||
interface VideoConferenceProps {
|
||||
layout?: 'grid' | 'focus'
|
||||
className?: string
|
||||
}
|
||||
|
||||
const VideoConference: React.FC<VideoConferenceProps> = ({ layout = 'grid', className }) => {
|
||||
// request camera tracks (with placeholders for participants without camera)
|
||||
const cameraTracks = useTracks([{ source: Track.Source.Camera, withPlaceholder: true }])
|
||||
|
||||
if (layout === 'focus') {
|
||||
// FocusLayout accepts a single trackRef (we can pass the first track as default)
|
||||
return (
|
||||
<div className={className}>
|
||||
<FocusLayout trackRef={cameraTracks[0] as any}>
|
||||
{/* children are not required here; FocusLayout will render the passed trackRef */}
|
||||
</FocusLayout>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<GridLayout tracks={cameraTracks as any}>
|
||||
<TrackLoop tracks={cameraTracks as any}>
|
||||
<ParticipantTile />
|
||||
</TrackLoop>
|
||||
</GridLayout>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default VideoConference
|
||||
5
packages/studio-panel/src/prefabs/index.ts
Normal file
5
packages/studio-panel/src/prefabs/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export { default as VideoConference } from './VideoConference'
|
||||
export { default as AudioConference } from './AudioConference'
|
||||
export { default as PreJoin } from './PreJoin'
|
||||
export { default as ControlBar } from './ControlBar'
|
||||
export { default as Chat } from './Chat'
|
||||
@ -23,3 +23,12 @@ nohup: no se tendrá en cuenta la entrada
|
||||
11:06:22 p.m. [vite] hmr update /src/components/Studio.tsx, /src/index.css
|
||||
11:07:13 p.m. [vite] hmr update /src/components/Studio.tsx, /src/index.css
|
||||
11:11:12 p.m. [vite] hmr update /src/components/Studio.tsx, /src/index.css
|
||||
11:18:06 p.m. [vite] hmr update /src/components/Studio.tsx, /src/index.css
|
||||
11:23:16 p.m. [vite] hmr update /src/components/Studio.tsx, /src/index.css
|
||||
11:23:38 p.m. [vite] hmr update /src/components/Studio.tsx, /src/index.css
|
||||
11:24:03 p.m. [vite] hmr update /src/components/Studio.tsx, /src/index.css
|
||||
11:25:07 p.m. [vite] hmr update /src/components/Studio.tsx, /src/index.css
|
||||
11:25:59 p.m. [vite] hmr update /src/components/Studio.tsx, /src/index.css
|
||||
11:30:54 p.m. [vite] hmr update /src/components/Studio.tsx, /src/index.css
|
||||
11:31:36 p.m. [vite] hmr update /src/components/Studio.tsx, /src/index.css
|
||||
11:35:59 p.m. [vite] hmr update /src/components/Studio.tsx, /src/index.css
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import path from 'path'
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
// Cargar variables de entorno del directorio raíz
|
||||
@ -7,9 +8,27 @@ export default defineConfig(({ mode }) => {
|
||||
|
||||
return {
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
'@shared': path.resolve(__dirname, '../../shared'),
|
||||
'@avanzacast/shared-utils': path.resolve(__dirname, '../../shared/utils'),
|
||||
'@avanzacast/shared-types': path.resolve(__dirname, '../../shared/types'),
|
||||
'@avanzacast/shared-hooks': path.resolve(__dirname, '../../shared/hooks'),
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 3020,
|
||||
host: true,
|
||||
fs: {
|
||||
// Allow serving files from the shared folder (monorepo)
|
||||
allow: [
|
||||
path.resolve(__dirname),
|
||||
path.resolve(__dirname, '../../shared')
|
||||
],
|
||||
// Disable strict fs checking so imports from outside project root work
|
||||
strict: false
|
||||
},
|
||||
// Usar HTTP en dev para pruebas E2E locales (localhost permite getUserMedia sin HTTPS en muchos navegadores)
|
||||
watch: { usePolling: true }
|
||||
},
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
// vite.config.ts
|
||||
import { defineConfig, loadEnv } from "file:///home/xesar/Documentos/Nextream/AvanzaCast/node_modules/vite/dist/node/index.js";
|
||||
import react from "file:///home/xesar/Documentos/Nextream/AvanzaCast/node_modules/@vitejs/plugin-react/dist/index.mjs";
|
||||
var vite_config_default = defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, "../../", "");
|
||||
return {
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 3020,
|
||||
host: true,
|
||||
// Usar HTTP en dev para pruebas E2E locales (localhost permite getUserMedia sin HTTPS en muchos navegadores)
|
||||
watch: { usePolling: true }
|
||||
},
|
||||
define: {
|
||||
// Exponer variables de entorno al cliente
|
||||
"import.meta.env.VITE_LIVEKIT_URL": JSON.stringify(env.LIVEKIT_URL),
|
||||
"import.meta.env.VITE_LIVEKIT_API_KEY": JSON.stringify(env.LIVEKIT_API_KEY),
|
||||
"import.meta.env.VITE_LIVEKIT_API_SECRET": JSON.stringify(env.LIVEKIT_API_SECRET)
|
||||
}
|
||||
};
|
||||
});
|
||||
export {
|
||||
vite_config_default as default
|
||||
};
|
||||
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvaG9tZS94ZXNhci9Eb2N1bWVudG9zL05leHRyZWFtL0F2YW56YUNhc3QvcGFja2FnZXMvc3R1ZGlvLXBhbmVsXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvaG9tZS94ZXNhci9Eb2N1bWVudG9zL05leHRyZWFtL0F2YW56YUNhc3QvcGFja2FnZXMvc3R1ZGlvLXBhbmVsL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9ob21lL3hlc2FyL0RvY3VtZW50b3MvTmV4dHJlYW0vQXZhbnphQ2FzdC9wYWNrYWdlcy9zdHVkaW8tcGFuZWwvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcsIGxvYWRFbnYgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHJlYWN0IGZyb20gJ0B2aXRlanMvcGx1Z2luLXJlYWN0J1xuXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoKHsgbW9kZSB9KSA9PiB7XG4gIC8vIENhcmdhciB2YXJpYWJsZXMgZGUgZW50b3JubyBkZWwgZGlyZWN0b3JpbyByYVx1MDBFRHpcbiAgY29uc3QgZW52ID0gbG9hZEVudihtb2RlLCAnLi4vLi4vJywgJycpXG4gIFxuICByZXR1cm4ge1xuICAgIHBsdWdpbnM6IFtyZWFjdCgpXSxcbiAgICBzZXJ2ZXI6IHtcbiAgICAgIHBvcnQ6IDMwMjAsXG4gICAgICBob3N0OiB0cnVlLFxuICAgICAgLy8gVXNhciBIVFRQIGVuIGRldiBwYXJhIHBydWViYXMgRTJFIGxvY2FsZXMgKGxvY2FsaG9zdCBwZXJtaXRlIGdldFVzZXJNZWRpYSBzaW4gSFRUUFMgZW4gbXVjaG9zIG5hdmVnYWRvcmVzKVxuICAgICAgd2F0Y2g6IHsgdXNlUG9sbGluZzogdHJ1ZSB9XG4gICAgfSxcbiAgICBkZWZpbmU6IHtcbiAgICAgIC8vIEV4cG9uZXIgdmFyaWFibGVzIGRlIGVudG9ybm8gYWwgY2xpZW50ZVxuICAgICAgJ2ltcG9ydC5tZXRhLmVudi5WSVRFX0xJVkVLSVRfVVJMJzogSlNPTi5zdHJpbmdpZnkoZW52LkxJVkVLSVRfVVJMKSxcbiAgICAgICdpbXBvcnQubWV0YS5lbnYuVklURV9MSVZFS0lUX0FQSV9LRVknOiBKU09OLnN0cmluZ2lmeShlbnYuTElWRUtJVF9BUElfS0VZKSxcbiAgICAgICdpbXBvcnQubWV0YS5lbnYuVklURV9MSVZFS0lUX0FQSV9TRUNSRVQnOiBKU09OLnN0cmluZ2lmeShlbnYuTElWRUtJVF9BUElfU0VDUkVUKSxcbiAgICB9LFxuICB9XG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUFrWCxTQUFTLGNBQWMsZUFBZTtBQUN4WixPQUFPLFdBQVc7QUFFbEIsSUFBTyxzQkFBUSxhQUFhLENBQUMsRUFBRSxLQUFLLE1BQU07QUFFeEMsUUFBTSxNQUFNLFFBQVEsTUFBTSxVQUFVLEVBQUU7QUFFdEMsU0FBTztBQUFBLElBQ0wsU0FBUyxDQUFDLE1BQU0sQ0FBQztBQUFBLElBQ2pCLFFBQVE7QUFBQSxNQUNOLE1BQU07QUFBQSxNQUNOLE1BQU07QUFBQTtBQUFBLE1BRU4sT0FBTyxFQUFFLFlBQVksS0FBSztBQUFBLElBQzVCO0FBQUEsSUFDQSxRQUFRO0FBQUE7QUFBQSxNQUVOLG9DQUFvQyxLQUFLLFVBQVUsSUFBSSxXQUFXO0FBQUEsTUFDbEUsd0NBQXdDLEtBQUssVUFBVSxJQUFJLGVBQWU7QUFBQSxNQUMxRSwyQ0FBMkMsS0FBSyxVQUFVLElBQUksa0JBQWtCO0FBQUEsSUFDbEY7QUFBQSxFQUNGO0FBQ0YsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K
|
||||
@ -16,5 +16,5 @@
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src", "packages/*/src"]
|
||||
"include": ["src", "packages/*/src", "types"]
|
||||
}
|
||||
|
||||
13
types/env.d.ts
vendored
Normal file
13
types/env.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_TOKEN_SERVER_URL?: string
|
||||
readonly VITE_LIVEKIT_URL?: string
|
||||
readonly VITE_STUDIO_URL?: string
|
||||
readonly VITE_BROADCAST_URL?: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
|
||||
declare module '*.module.css'
|
||||
declare module '*.css'
|
||||
Loading…
x
Reference in New Issue
Block a user