346 lines
12 KiB
TypeScript
346 lines
12 KiB
TypeScript
import React, { useState, useEffect, useRef } from 'react';
|
|
import { Button } from 'avanza-ui';
|
|
import './App.css';
|
|
import StudioPortal from './components/Portal/StudioPortal';
|
|
import { isAllowedOrigin } from './utils/postMessage';
|
|
import { Room } from 'livekit-client';
|
|
|
|
function App() {
|
|
const [isConnected, setIsConnected] = useState(false);
|
|
const [credentials, setCredentials] = useState({
|
|
serverUrl: import.meta.env.VITE_LIVEKIT_URL || 'ws://localhost:7880',
|
|
token: '',
|
|
roomName: 'Studio Demo',
|
|
});
|
|
|
|
const [tempToken, setTempToken] = useState('');
|
|
const autoAttemptRef = useRef(false);
|
|
// external LiveKit Room managed by App when token is received
|
|
const roomRef = useRef<Room | null>(null);
|
|
const messageSourceRef = useRef<Window | null>(null);
|
|
// store the last validated origin that sent a token so we can ACK securely
|
|
const lastValidatedOrigin = useRef<string | null>(null);
|
|
|
|
// Called when the LiveKit room reports connected
|
|
const handleRoomConnected = () => {
|
|
setIsConnected(true);
|
|
// send connected ACK to the validated origin if available
|
|
try {
|
|
const payload = { type: 'LIVEKIT_ACK', status: 'connected' };
|
|
const targetOrigin = lastValidatedOrigin.current || '*';
|
|
// Prefer replying directly to the message source window if available
|
|
if (messageSourceRef.current && typeof (messageSourceRef.current as any).postMessage === 'function') {
|
|
try { (messageSourceRef.current as any).postMessage(payload, targetOrigin); } catch (e) { /* ignore */ }
|
|
}
|
|
if (window.opener && !window.opener.closed) {
|
|
try { window.opener.postMessage(payload, targetOrigin); } catch (e) { /* ignore */ }
|
|
}
|
|
if (window.parent && window.parent !== window) {
|
|
try { window.parent.postMessage(payload, targetOrigin); } catch (e) { /* ignore */ }
|
|
}
|
|
} catch (e) { /* ignore */ }
|
|
};
|
|
|
|
const handleRoomDisconnected = () => {
|
|
setIsConnected(false);
|
|
try {
|
|
const payload = { type: 'LIVEKIT_ACK', status: 'disconnected' };
|
|
const targetOrigin = lastValidatedOrigin.current || '*';
|
|
if (window.opener && !window.opener.closed) {
|
|
try { window.opener.postMessage(payload, targetOrigin); } catch (e) { /* ignore */ }
|
|
}
|
|
if (window.parent && window.parent !== window) {
|
|
try { window.parent.postMessage(payload, targetOrigin); } catch (e) { /* ignore */ }
|
|
}
|
|
} catch (e) {}
|
|
// disconnect and clear app-managed room
|
|
try { if (roomRef.current) { roomRef.current.disconnect(); roomRef.current = null; } } catch(e){}
|
|
};
|
|
|
|
// Cleanup app-managed room on unmount
|
|
useEffect(() => {
|
|
return () => {
|
|
try { if (roomRef.current) { roomRef.current.disconnect(); roomRef.current = null; } } catch(e){}
|
|
};
|
|
}, []);
|
|
|
|
// Listen for LIVEKIT_TOKEN posted via postMessage (handshake flow)
|
|
useEffect(() => {
|
|
// central token handler used by both message events and custom events
|
|
const handleIncomingToken = async (payload: any, origin?: string | null, source?: any) => {
|
|
try {
|
|
const originToUse = origin || null;
|
|
if (!isAllowedOrigin(originToUse)) {
|
|
return;
|
|
}
|
|
// store validated origin and message source for ACKs
|
|
if (originToUse) lastValidatedOrigin.current = originToUse;
|
|
if (source) messageSourceRef.current = source;
|
|
|
|
if (payload?.url) setCredentials(prev => ({ ...prev, serverUrl: String(payload.url) }));
|
|
const receivedToken = String(payload.token || payload?.token);
|
|
setTempToken(receivedToken);
|
|
|
|
// cleanup previous room if exists
|
|
try { if (roomRef.current) { roomRef.current.disconnect(); roomRef.current = null; } } catch(e){}
|
|
|
|
const newRoom = new Room();
|
|
roomRef.current = newRoom;
|
|
try {
|
|
const sUrl = payload?.url || credentials.serverUrl;
|
|
await newRoom.connect(sUrl, receivedToken);
|
|
setCredentials(prev => ({ ...prev, token: receivedToken }));
|
|
handleRoomConnected();
|
|
} catch (err) {
|
|
console.error('App-managed room connect failed', err);
|
|
}
|
|
|
|
if (!autoAttemptRef.current) {
|
|
autoAttemptRef.current = true;
|
|
setTimeout(() => { if (receivedToken) handleConnectWithToken(receivedToken); }, 60);
|
|
}
|
|
} catch (err) { console.debug('handleIncomingToken error', err); }
|
|
};
|
|
|
|
function onMessage(e: MessageEvent) {
|
|
try {
|
|
const d = e.data || {};
|
|
if (d?.type === 'LIVEKIT_TOKEN' && d.token) {
|
|
// call central handler and pass origin/source
|
|
handleIncomingToken(d, e.origin || null, e.source);
|
|
}
|
|
} catch (err) { console.debug('postMessage in App error', err) }
|
|
}
|
|
window.addEventListener('message', onMessage);
|
|
|
|
// Also listen for the custom event dispatched by main.tsx
|
|
function onCustomToken(e: any) {
|
|
try {
|
|
const detail = e?.detail || (window as any).__AVANZACAST_PENDING_TOKEN || null;
|
|
// attempt to recover origin/source from globals set by main.tsx if present
|
|
const lastMsg = (window as any).__AVZ_LAST_MSG_SOURCE || null;
|
|
const origin = lastMsg?.origin || null;
|
|
const source = lastMsg?.source || null;
|
|
if (detail && detail.token) handleIncomingToken(detail, origin, source);
|
|
} catch(err) { console.debug('custom token handler error', err); }
|
|
}
|
|
window.addEventListener('avz:livekit:token', onCustomToken as EventListener);
|
|
|
|
return () => {
|
|
window.removeEventListener('message', onMessage);
|
|
window.removeEventListener('avz:livekit:token', onCustomToken as EventListener);
|
|
};
|
|
}, []);
|
|
|
|
|
|
function handleConnectWithToken(tokenVal: string) {
|
|
if (tokenVal && tokenVal.trim()) {
|
|
setCredentials(prev => ({ ...prev, token: tokenVal }));
|
|
setIsConnected(true);
|
|
}
|
|
}
|
|
|
|
const handleConnect = () => {
|
|
if (tempToken.trim()) {
|
|
setCredentials(prev => ({ ...prev, token: tempToken }));
|
|
setIsConnected(true);
|
|
}
|
|
};
|
|
|
|
// Update a global #status element and notify opener/parent when connected — helps E2E tests detect ACK/state
|
|
useEffect(() => {
|
|
try {
|
|
const setStatus = (txt: string) => {
|
|
try {
|
|
let el = document.getElementById('status');
|
|
if (!el) {
|
|
el = document.createElement('div');
|
|
el.id = 'status';
|
|
el.style.position = 'fixed';
|
|
el.style.right = '12px';
|
|
el.style.top = '12px';
|
|
el.style.padding = '8px 12px';
|
|
el.style.background = 'rgba(16,185,129,0.12)';
|
|
el.style.color = '#10B981';
|
|
el.style.borderRadius = '6px';
|
|
el.style.zIndex = '9999';
|
|
document.body.appendChild(el);
|
|
}
|
|
el.textContent = txt;
|
|
} catch (e) { /* ignore */ }
|
|
};
|
|
|
|
if (isConnected) {
|
|
setStatus('Conectado');
|
|
} else {
|
|
setStatus('Esperando token...');
|
|
}
|
|
} catch (e) {}
|
|
}, [isConnected]);
|
|
|
|
if (!isConnected) {
|
|
return (
|
|
<div className="studio-theme" style={{
|
|
minHeight: '100vh',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
padding: '24px'
|
|
}}>
|
|
<div style={{
|
|
maxWidth: '500px',
|
|
width: '100%',
|
|
background: 'var(--studio-bg-secondary)',
|
|
padding: '32px',
|
|
borderRadius: 'var(--studio-radius-lg)',
|
|
boxShadow: 'var(--studio-shadow-lg)'
|
|
}}>
|
|
<h1 style={{
|
|
fontSize: 'var(--studio-text-2xl)',
|
|
fontWeight: 'var(--studio-font-bold)',
|
|
marginBottom: '8px',
|
|
color: 'var(--studio-text-primary)'
|
|
}}>
|
|
Studio Panel - AvanzaCast
|
|
</h1>
|
|
<p style={{
|
|
fontSize: 'var(--studio-text-base)',
|
|
color: 'var(--studio-text-muted)',
|
|
marginBottom: '24px'
|
|
}}>
|
|
Ingresa tus credenciales de LiveKit para comenzar
|
|
</p>
|
|
|
|
<div style={{ marginBottom: '16px' }}>
|
|
<label style={{
|
|
display: 'block',
|
|
fontSize: 'var(--studio-text-sm)',
|
|
fontWeight: 'var(--studio-font-medium)',
|
|
marginBottom: '8px',
|
|
color: 'var(--studio-text-secondary)'
|
|
}}>
|
|
LiveKit Server URL
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={credentials.serverUrl}
|
|
onChange={(e) => setCredentials(prev => ({ ...prev, serverUrl: e.target.value }))}
|
|
style={{
|
|
width: '100%',
|
|
padding: '10px 12px',
|
|
background: 'var(--studio-bg-primary)',
|
|
border: '1px solid var(--studio-border)',
|
|
borderRadius: 'var(--studio-radius-md)',
|
|
color: 'var(--studio-text-primary)',
|
|
fontSize: 'var(--studio-text-base)',
|
|
fontFamily: 'var(--studio-font-family)'
|
|
}}
|
|
placeholder="ws://localhost:7880"
|
|
/>
|
|
</div>
|
|
|
|
<div style={{ marginBottom: '16px' }}>
|
|
<label style={{
|
|
display: 'block',
|
|
fontSize: 'var(--studio-text-sm)',
|
|
fontWeight: 'var(--studio-font-medium)',
|
|
marginBottom: '8px',
|
|
color: 'var(--studio-text-secondary)'
|
|
}}>
|
|
Room Name
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={credentials.roomName}
|
|
onChange={(e) => setCredentials(prev => ({ ...prev, roomName: e.target.value }))}
|
|
style={{
|
|
width: '100%',
|
|
padding: '10px 12px',
|
|
background: 'var(--studio-bg-primary)',
|
|
border: '1px solid var(--studio-border)',
|
|
borderRadius: 'var(--studio-radius-md)',
|
|
color: 'var(--studio-text-primary)',
|
|
fontSize: 'var(--studio-text-base)',
|
|
fontFamily: 'var(--studio-font-family)'
|
|
}}
|
|
placeholder="Studio Demo"
|
|
/>
|
|
</div>
|
|
|
|
<div style={{ marginBottom: '24px' }}>
|
|
<label style={{
|
|
display: 'block',
|
|
fontSize: 'var(--studio-text-sm)',
|
|
fontWeight: 'var(--studio-font-medium)',
|
|
marginBottom: '8px',
|
|
color: 'var(--studio-text-secondary)'
|
|
}}>
|
|
Access Token
|
|
</label>
|
|
<textarea
|
|
value={tempToken}
|
|
onChange={(e) => setTempToken(e.target.value)}
|
|
rows={4}
|
|
style={{
|
|
width: '100%',
|
|
padding: '10px 12px',
|
|
background: 'var(--studio-bg-primary)',
|
|
border: '1px solid var(--studio-border)',
|
|
borderRadius: 'var(--studio-radius-md)',
|
|
color: 'var(--studio-text-primary)',
|
|
fontSize: 'var(--studio-text-sm)',
|
|
fontFamily: 'monospace',
|
|
resize: 'vertical'
|
|
}}
|
|
placeholder="Paste your LiveKit token here..."
|
|
/>
|
|
</div>
|
|
|
|
<Button
|
|
variant="primary"
|
|
fullWidth
|
|
onClick={handleConnect}
|
|
disabled={!tempToken.trim()}
|
|
>
|
|
Conectar al Estudio
|
|
</Button>
|
|
|
|
<div style={{
|
|
marginTop: '24px',
|
|
padding: '16px',
|
|
background: 'var(--studio-bg-tertiary)',
|
|
borderRadius: 'var(--studio-radius-md)',
|
|
fontSize: 'var(--studio-text-sm)',
|
|
color: 'var(--studio-text-muted)'
|
|
}}>
|
|
<strong style={{ color: 'var(--studio-text-secondary)' }}>Nota:</strong> Necesitas un token válido de LiveKit para conectar.
|
|
<br />
|
|
<a
|
|
href="https://docs.livekit.io/home/get-started/authentication/"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
style={{ color: 'var(--studio-accent)', textDecoration: 'none' }}
|
|
>
|
|
Aprende cómo generar un token →
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// When connected (we have token), show the StreamYard-like portal
|
|
return (
|
|
<StudioPortal
|
|
serverUrl={credentials.serverUrl}
|
|
token={credentials.token}
|
|
roomName={credentials.roomName}
|
|
onRoomConnected={handleRoomConnected}
|
|
onRoomDisconnected={handleRoomDisconnected}
|
|
room={roomRef.current || undefined}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export default App;
|