diff --git a/apps/dispatch/.env.example b/apps/dispatch/.env.example index 4f75f0b4..3e284d52 100644 --- a/apps/dispatch/.env.example +++ b/apps/dispatch/.env.example @@ -1,7 +1,9 @@ -NEXTAUTH_SECRET= -NEXTAUTH_COOKIE_PREFIX= +NEXTAUTH_SECRET=dispatch +NEXTAUTH_COOKIE_PREFIX=DISPATCH +NEXT_PUBLIC_DISPATCH_SERVER_URL=http://localhost:3002 NEXTAUTH_URL=http://localhost:3001 -NEXT_PUBLIC_PUBLIC_URL=http://localhost:3001 NEXT_PUBLIC_HUB_URL=http://localhost:3000 +NEXT_PUBLIC_PUBLIC_URL=http://localhost:3001 NEXT_PUBLIC_SERVICE_ID=1 -NEXT_PUBLIC_DISPATCH_SERVER_URL=http://localhost:3002 \ No newline at end of file +DATABASE_URL=postgresql://persistant-data:persistant-data-pw@localhost:5432/var +NEXT_PUBLIC_LIVEKIT_URL=ws://localhost:7880 \ No newline at end of file diff --git a/apps/dispatch/app/(dispatch)/_components/navbar/_components/Audio.tsx b/apps/dispatch/app/(dispatch)/_components/navbar/_components/Audio.tsx index 22d3337b..cddd89e4 100644 --- a/apps/dispatch/app/(dispatch)/_components/navbar/_components/Audio.tsx +++ b/apps/dispatch/app/(dispatch)/_components/navbar/_components/Audio.tsx @@ -6,6 +6,7 @@ import { Disc, Mic, PlugZap, + ServerCrash, ShieldQuestion, Signal, SignalLow, @@ -16,8 +17,7 @@ import { import { useAudioStore } from "_store/audioStore"; import { cn } from "helpers/cn"; import { ConnectionQuality } from "livekit-client"; - -const ROOMS = ["LST_01", "LST_02", "LST_03", "LST_04", "LST_05"]; +import { ROOMS } from "data/livekitRooms"; export const Audio = () => { const connection = useConnectionStore(); @@ -30,6 +30,7 @@ export const Audio = () => { disconnect, remoteParticipants, room, + message, } = useAudioStore(); const [selectedRoom, setSelectedRoom] = useState("LST_01"); @@ -50,16 +51,21 @@ export const Audio = () => { return ( <>
+ {state === "error" && ( +
{message}
+ )} {state === "connected" && ( @@ -112,6 +119,20 @@ export const Audio = () => { ))} +
  • + +
  • )} diff --git a/apps/dispatch/app/_store/audioStore.ts b/apps/dispatch/app/_store/audioStore.ts index eb747b26..026c54b2 100644 --- a/apps/dispatch/app/_store/audioStore.ts +++ b/apps/dispatch/app/_store/audioStore.ts @@ -12,7 +12,8 @@ let interval: NodeJS.Timeout; type TalkState = { isTalking: boolean; - state: "connecting" | "connected" | "disconnected"; + state: "connecting" | "connected" | "disconnected" | "error"; + message: string | null; connectionQuality: ConnectionQuality; remoteParticipants: number; toggleTalking: () => void; @@ -21,60 +22,73 @@ type TalkState = { disconnect: () => void; room: Room | null; }; -const getToken = async (roomName: string) => { - const response = await fetch( - `${process.env.NEXT_PUBLIC_DISPATCH_SERVER_URL}/livekit/token?roomName=${roomName}`, - ); +const getToken = async () => { + const response = await fetch(`/api/livekit/token`); const data = await response.json(); return data.token; }; export const useAudioStore = create((set, get) => ({ isTalking: false, + message: null, state: "disconnected", remoteParticipants: 0, connectionQuality: ConnectionQuality.Unknown, room: null, toggleTalking: () => set((state) => ({ isTalking: !state.isTalking })), connect: async (roomName) => { - const connectedRoom = get().room; - if (interval) clearInterval(interval); - if (connectedRoom) { - connectedRoom.disconnect(); - connectedRoom.removeAllListeners(); + try { + // Clean old room + const connectedRoom = get().room; + if (interval) clearInterval(interval); + if (connectedRoom) { + connectedRoom.disconnect(); + connectedRoom.removeAllListeners(); + } + + set({ state: "connecting" }); + const url = process.env.NEXT_PUBLIC_LIVEKIT_URL; + if (!url) return console.error("NEXT_PUBLIC_LIVEKIT_URL not set"); + + const token = await getToken(); + if (!token) throw new Error("Fehlende Berechtigung"); + console.log("Token: ", token); + const room = new Room(); + await room.prepareConnection(url, token); + room + // Connection events + .on(RoomEvent.Connected, () => { + set({ state: "connected", room, message: null }); + }) + .on(RoomEvent.Disconnected, () => { + set({ state: "disconnected" }); + + handleDisconnect(); + }) + .on(RoomEvent.ConnectionQualityChanged, (connectionQuality) => + set({ connectionQuality }), + ) + + // Track events + .on(RoomEvent.TrackSubscribed, handleTrackSubscribed) + .on(RoomEvent.TrackUnsubscribed, handleTrackUnsubscribed) + .on(RoomEvent.ActiveSpeakersChanged, handleActiveSpeakerChange) + .on(RoomEvent.LocalTrackUnpublished, handleLocalTrackUnpublished); + await room.connect(url, token); + interval = setInterval(() => { + set({ + remoteParticipants: + room.numParticipants === 0 ? 0 : room.numParticipants - 1, // Unreliable and delayed + }); + }, 500); + } catch (error: Error | unknown) { + console.error("Error occured: ", error); + if (error instanceof Error) { + set({ state: "error", message: error.message }); + } else { + set({ state: "error", message: "Unknown error" }); + } } - set({ state: "connecting" }); - const url = process.env.NEXT_PUBLIC_LIVEKIT_URL; - if (!url) return console.error("NEXT_PUBLIC_LIVEKIT_URL not set"); - const token = await getToken(roomName); - const room = new Room(); - await room.prepareConnection(url, token); - room - // Connection events - .on(RoomEvent.Connected, () => { - set({ state: "connected", room }); - }) - .on(RoomEvent.Disconnected, () => { - set({ state: "disconnected" }); - - handleDisconnect(); - }) - .on(RoomEvent.ConnectionQualityChanged, (connectionQuality) => - set({ connectionQuality }), - ) - - // Track events - .on(RoomEvent.TrackSubscribed, handleTrackSubscribed) - .on(RoomEvent.TrackUnsubscribed, handleTrackUnsubscribed) - .on(RoomEvent.ActiveSpeakersChanged, handleActiveSpeakerChange) - .on(RoomEvent.LocalTrackUnpublished, handleLocalTrackUnpublished); - await room.connect(url, token); - interval = setInterval(() => { - set({ - remoteParticipants: - room.numParticipants === 0 ? 0 : room.numParticipants - 1, // Unreliable and delayed - }); - }, 500); }, disconnect: () => { get().room?.disconnect(); diff --git a/apps/dispatch/app/api/livekit/token/route.ts b/apps/dispatch/app/api/livekit/token/route.ts new file mode 100644 index 00000000..05a6b8fd --- /dev/null +++ b/apps/dispatch/app/api/livekit/token/route.ts @@ -0,0 +1,44 @@ +import { getServerSession } from "api/auth/[...nextauth]/auth"; +import { ROOMS } from "data/livekitRooms"; +import { AccessToken } from "livekit-server-sdk"; +import { prisma } from "../../../../../../packages/database/prisma/client"; + +if (!process.env.LIVEKIT_API_KEY) throw new Error("LIVEKIT_API_KEY not set"); +if (!process.env.LIVEKIT_API_SECRET) + throw new Error("LIVEKIT_API_SECRET not set"); + +export const GET = async () => { + const session = await getServerSession(); + + console.log(session?.user.permissions); + if (!session) + return Response.json({ message: "Unauthorized" }, { status: 401 }); + const user = await prisma.user.findUnique({ + where: { + id: session.user.id, + }, + }); + console.log(user); + + if (!user || !user.permissions.includes("AUDIO")) + return Response.json({ message: "Missing permissions" }, { status: 401 }); + + const participantName = user.publicId; + + const at = new AccessToken( + process.env.LIVEKIT_API_KEY, + process.env.LIVEKIT_API_SECRET, + { + identity: participantName, + // Token to expire after 10 minutes + ttl: "1d", + }, + ); + ROOMS.forEach((roomName) => { + at.addGrant({ roomJoin: true, room: roomName, canPublish: true }); + }); + + const token = await at.toJwt(); + + return Response.json({ token }); +}; diff --git a/apps/dispatch/app/data/livekitRooms.ts b/apps/dispatch/app/data/livekitRooms.ts new file mode 100644 index 00000000..1bbb1ae0 --- /dev/null +++ b/apps/dispatch/app/data/livekitRooms.ts @@ -0,0 +1 @@ +export const ROOMS = ["LST_01", "LST_02", "LST_03", "LST_04", "LST_05"]; diff --git a/apps/dispatch/package.json b/apps/dispatch/package.json index 53258c9a..a9f2a693 100644 --- a/apps/dispatch/package.json +++ b/apps/dispatch/package.json @@ -18,6 +18,7 @@ "@tailwindcss/postcss": "^4.0.14", "leaflet": "^1.9.4", "livekit-client": "^2.9.7", + "livekit-server-sdk": "^2.10.2", "lucide-react": "^0.482.0", "next": "^15.1.0", "next-auth": "^4.24.11", diff --git a/package-lock.json b/package-lock.json index 0489b47d..47a7b384 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "@tailwindcss/postcss": "^4.0.14", "leaflet": "^1.9.4", "livekit-client": "^2.9.7", + "livekit-server-sdk": "^2.10.2", "lucide-react": "^0.482.0", "next": "^15.1.0", "next-auth": "^4.24.11", diff --git a/packages/database/prisma/schema/user.prisma b/packages/database/prisma/schema/user.prisma index f2637216..df2db4de 100644 --- a/packages/database/prisma/schema/user.prisma +++ b/packages/database/prisma/schema/user.prisma @@ -11,7 +11,8 @@ enum BADGES { enum PERMISSION { ADMIN_EVENT ADMIN_USER - SUSPENDED + AUDIO + AUDIO_ADMIN PILOT DISPO }