"use client"; import { useCallback, useEffect, useState } from "react"; import { Box, Spinner, Center, Text, IconButton } from "@chakra-ui/react"; import { useRouter, useParams } from "next/navigation"; import { LiveKitRoom as LKRoom, VideoConference, RoomAudioRenderer, } from "@livekit/components-react"; // LiveKit component styles — imported in the global layout to avoid // Next.js CSS import restrictions in client components. // See: app/[roomName]/layout.tsx import type { components } from "../../reflector-api"; import { useAuth } from "../../lib/AuthProvider"; import { useRoomJoinMeeting } from "../../lib/apiHooks"; import { assertMeetingId } from "../../lib/types"; import { ConsentDialogButton, RecordingIndicator, useConsentDialog, } from "../../lib/consent"; import { useEmailTranscriptDialog } from "../../lib/emailTranscript"; import { featureEnabled } from "../../lib/features"; import { LuMail } from "react-icons/lu"; type Meeting = components["schemas"]["Meeting"]; type Room = components["schemas"]["RoomDetails"]; interface LiveKitRoomProps { meeting: Meeting; room: Room; } /** * Extract LiveKit WebSocket URL, room name, and token from the room_url. * * The backend returns room_url like: ws://host:7880?room=&token= * We split these for the LiveKit React SDK. */ function parseLiveKitUrl(roomUrl: string): { serverUrl: string; roomName: string | null; token: string | null; } { try { const url = new URL(roomUrl); const token = url.searchParams.get("token"); const roomName = url.searchParams.get("room"); url.searchParams.delete("token"); url.searchParams.delete("room"); // Strip trailing slash and leftover ? from URL API const serverUrl = url.toString().replace(/[?/]+$/, ""); return { serverUrl, roomName, token }; } catch { return { serverUrl: roomUrl, roomName: null, token: null }; } } export default function LiveKitRoom({ meeting, room }: LiveKitRoomProps) { const router = useRouter(); const params = useParams(); const auth = useAuth(); const authLastUserId = auth.lastUserId; const roomName = params?.roomName as string; const meetingId = assertMeetingId(meeting.id); const joinMutation = useRoomJoinMeeting(); const [joinedMeeting, setJoinedMeeting] = useState(null); const [connectionError, setConnectionError] = useState(false); // ── Consent dialog (same hooks as Daily/Whereby) ────────── const { showConsentButton, showRecordingIndicator } = useConsentDialog({ meetingId, recordingType: meeting.recording_type, skipConsent: room.skip_consent, }); // ── Email transcript dialog ─────────────────────────────── const userEmail = auth.status === "authenticated" || auth.status === "refreshing" ? auth.user.email : null; const { showEmailModal } = useEmailTranscriptDialog({ meetingId, userEmail, }); const showEmailFeature = featureEnabled("emailTranscript"); // ── Join meeting via backend API to get token ───────────── useEffect(() => { if (authLastUserId === undefined || !meeting?.id || !roomName) return; let cancelled = false; async function join() { try { const result = await joinMutation.mutateAsync({ params: { path: { room_name: roomName, meeting_id: meeting.id }, }, }); if (!cancelled) setJoinedMeeting(result); } catch (err) { console.error("Failed to join LiveKit meeting:", err); if (!cancelled) setConnectionError(true); } } join(); return () => { cancelled = true; }; }, [meeting?.id, roomName, authLastUserId]); const handleDisconnected = useCallback(() => { router.push("/browse"); }, [router]); // ── Loading / error states ──────────────────────────────── if (connectionError) { return (
Failed to connect to meeting
); } if (!joinedMeeting) { return (
); } const { serverUrl, roomName: lkRoomName, token, } = parseLiveKitUrl(joinedMeeting.room_url); if ( serverUrl && !serverUrl.startsWith("ws://") && !serverUrl.startsWith("wss://") ) { console.warn( `LiveKit serverUrl has unexpected scheme: ${serverUrl}. Expected ws:// or wss://`, ); } if (!token || !lkRoomName) { return (
{!token ? "No access token received from server" : "No room name received from server"}
); } // ── Render ──────────────────────────────────────────────── // The token already encodes the room name (in VideoGrants.room), // so LiveKit SDK joins the correct room from the token alone. return ( {/* ── Floating overlay buttons (consent, email, extensible) ── */} {showConsentButton && ( )} {showRecordingIndicator && } {showEmailFeature && ( )} ); }