import React, { useState, useEffect } from "react"; import { MdMoreVert, MdVideocam, MdPersonAdd, MdEdit, MdOpenInNew, MdDelete, } from "react-icons/md"; import { Dropdown } from "./Dropdown"; import { FaYoutube, FaFacebook, FaTwitch, FaLinkedin } from "react-icons/fa"; import { SkeletonTable } from "./Skeleton"; import styles from "./TransmissionsTable.module.css"; import InviteGuestsModal from "./InviteGuestsModal"; import { NewTransmissionModal } from "@shared/components"; import type { Transmission } from "@shared/types"; import useStudioLauncher from "../hooks/useStudioLauncher"; import useStudioMessageListener from "../hooks/useStudioMessageListener"; import StudioPortal from "../features/studio/StudioPortal"; interface Props { transmissions: Transmission[]; onDelete: (id: string) => void; onUpdate: (t: Transmission) => void; isLoading?: boolean; } const platformIcons: Record = { YouTube: , Facebook: , Twitch: , LinkedIn: , Generico: , // Logo genérico para transmisiones sin destino }; const TransmissionsTable: React.FC = (props) => { const { transmissions, onDelete, onUpdate, isLoading } = props; const [activeTab, setActiveTab] = useState<"upcoming" | "past">("upcoming"); const [inviteOpen, setInviteOpen] = useState(false); const [inviteLink, setInviteLink] = useState(undefined); const [editOpen, setEditOpen] = useState(false); const [editTransmission, setEditTransmission] = useState< Transmission | undefined >(undefined); const { openStudio, loadingId: launcherLoadingId, error: launcherError, } = useStudioLauncher(); const [loadingId, setLoadingId] = useState(null); const [studioSession, setStudioSession] = useState<{ serverUrl?: string; token?: string; room?: string; } | null>(null); const [validating, setValidating] = useState(false); const [connectError, setConnectError] = useState(null); const [currentAttempt, setCurrentAttempt] = useState( null, ); // Listen for external postMessage events carrying a LIVEKIT_TOKEN payload. useStudioMessageListener((msg) => { try { if (msg && msg.token) { const serverUrl = msg.url || (import.meta.env.VITE_LIVEKIT_WS_URL as string) || ""; // start validating token and open StudioPortal overlay setValidating(true); setConnectError(null); setStudioSession({ serverUrl, token: msg.token, room: msg.room || "external", }); } } catch (e) { /* ignore */ } }); // Auto-open studio if token is present in URL (INCLUDE_TOKEN_IN_REDIRECT flow) useEffect(() => { try { if (typeof window === "undefined") return; const params = new URLSearchParams(window.location.search); const tokenParam = params.get("token"); if (tokenParam) { const serverParam = params.get("serverUrl") || params.get("url") || (import.meta.env.VITE_LIVEKIT_WS_URL as string) || ""; const roomParam = params.get("room") || "external"; setConnectError(null); setValidating(true); setStudioSession({ serverUrl: serverParam, token: tokenParam, room: roomParam, }); } } catch (e) { /* ignore */ } }, []); const handleEdit = (t: Transmission) => { setEditTransmission(t); setEditOpen(true); }; // Filtrado por fechas const filtered = transmissions.filter((t: Transmission) => { // Si es "Próximamente" o no tiene fecha programada, siempre va a "upcoming" if (!t.scheduled || t.scheduled === "Próximamente") return activeTab === "upcoming"; const scheduledDate = new Date(t.scheduled); const now = new Date(); if (activeTab === "upcoming") { return scheduledDate >= now; } else { return scheduledDate < now; } }); const openStudioForTransmission = async (t: Transmission) => { if (loadingId || launcherLoadingId) return; setLoadingId(t.id); setCurrentAttempt(t); setValidating(true); try { const userRaw = localStorage.getItem("avanzacast_user") || "Demo User"; const user = userRaw; const room = t.id || "avanzacast-studio"; const result = await openStudio({ room, username: user }); if (!result) { // don't throw here — surface error in UI instead and stop loading setConnectError( "No se pudo abrir el estudio (launcher devolvió resultado vacío)", ); setValidating(false); setLoadingId(null); return; } const resAny: any = result as any; // If the token-server returns a direct studioUrl (redirect) but no token/id, navigate there if (resAny && resAny.studioUrl && !resAny.token && !resAny.id) { try { const target = resAny.studioUrl; try { window.location.href = target; return; } catch (e) { try { window.location.assign(target); return; } catch (e2) { /* ignore */ } } } catch (e) { /* ignore navigation errors */ } } // If backend returned a session id, persist it and navigate to broadcastPanel/:id so the Studio route picks it if (resAny && resAny.id) { try { const storeKey = (import.meta.env.VITE_STUDIO_SESSION_KEY as string) || "avanzacast_studio_session"; sessionStorage.setItem(storeKey, JSON.stringify(resAny)); try { window.dispatchEvent( new CustomEvent("AVZ_STUDIO_SESSION", { detail: resAny }), ); } catch (e) { /* ignore */ } } catch (e) { /* ignore storage errors */ } // Prefer explicit VITE_BROADCASTPANEL_URL; if not provided and we're running on localhost, // default to the public host so the redirect goes to the deployed Broadcast Panel. const envBroadcast = (import.meta.env.VITE_BROADCASTPANEL_URL as string) || ""; let BROADCAST_BASE = envBroadcast || (typeof window !== "undefined" ? window.location.origin : ""); if ( !envBroadcast && typeof window !== "undefined" && window.location.hostname === "localhost" ) { BROADCAST_BASE = "https://avanzacast-broadcastpanel.bfzqqk.easypanel.host"; } // Usar ruta amigable /studio/:id para redirigir al Studio Portal que validará el token/room const target = `${BROADCAST_BASE.replace(/\/$/, "")}/studio/${encodeURIComponent(resAny.id)}`; try { window.location.href = target; return; } catch (e) { try { window.location.assign(target); } catch (e2) { /* ignore */ } } } // If app is configured as integrated, ensure we open StudioPortal overlay immediately const INTEGRATED = import.meta.env.VITE_STUDIO_INTEGRATED === "true" || import.meta.env.VITE_STUDIO_INTEGRATED === "1" || false; if (INTEGRATED && resAny && resAny.token) { const serverUrl = resAny.url || resAny.studioUrl || (import.meta.env.VITE_LIVEKIT_WS_URL as string) || ""; setStudioSession({ serverUrl, token: resAny.token, room: resAny.room || room, }); setLoadingId(null); return; } const serverUrl = resAny.url || resAny.studioUrl || (import.meta.env.VITE_LIVEKIT_WS_URL as string) || ""; if (resAny.token) { setStudioSession({ serverUrl, token: resAny.token, room: resAny.room || room, }); } else { setValidating(false); } setLoadingId(null); } catch (err: any) { console.error("[BroadcastPanel] Error entrando al estudio:", err); setConnectError( err?.message || "No fue posible entrar al estudio. Revisa el servidor de tokens.", ); setValidating(false); setLoadingId(null); } }; const closeStudio = () => { try { setStudioSession(null); } catch (e) {} }; if (isLoading) { return (
); } return (
{!filtered || filtered.length === 0 ? (
{activeTab === "upcoming" ? "No hay transmisiones programadas todavía." : "No hay transmisiones anteriores."}
) : (
{filtered.map((t: Transmission) => ( ))}
Título Creado Programado
{platformIcons[t.platform] || platformIcons["YouTube"]}
{t.title}
{t.platform === "Generico" ? "Solo grabación" : t.platform || "YouTube"}
{t.createdAt || "---"} {t.scheduled && t.scheduled !== "Próximamente" ? t.scheduled : "---"}
{launcherError && ( // Mostrar modal claro si el hook de launcher reporta un error

Error al iniciar el estudio

{launcherError}

)} } items={[ { label: "Agregar invitados", icon: , onClick: () => { setInviteLink(`https://streamyard.com/${t.id}`); setInviteOpen(true); }, }, { label: "Editar", icon: , onClick: () => handleEdit(t), }, { divider: true, label: "", disabled: false }, { label: "Ver en YouTube", icon: , onClick: () => { /* abrir */ }, }, { divider: true, label: "", disabled: false }, { label: "Eliminar transmisión", icon: , onClick: () => onDelete(t.id), containerProps: { className: styles.deleteItem }, labelProps: { className: styles.dangerLabel }, }, ]} /> setInviteOpen(false)} link={inviteLink || ""} />
)} { setEditOpen(false); setEditTransmission(undefined); }} onCreate={() => {}} onUpdate={onUpdate} transmission={editTransmission} /> {studioSession && (
{ setValidating(false); /* keep portal open */ }} onRoomDisconnected={() => { closeStudio(); }} onRoomConnectError={(err) => { setValidating(false); setConnectError( String(err?.message || err || "Error al conectar"), ); }} />
)} {validating && (
Validando token, por favor espera...
)} {connectError && (

Error al conectar al estudio

{connectError}

)}
); }; export default TransmissionsTable;