import { PublicUser } from "@repo/db"; import { dispatchSocket } from "dispatch/socket"; import { serverApi } from "_helpers/axios"; import { handleActiveSpeakerChange, handleDisconnect, handleLocalTrackUnpublished, handleTrackSubscribed, handleTrackUnsubscribed, } from "_helpers/liveKitEventHandler"; import { ConnectionQuality, Room, RoomEvent } from "livekit-client"; import { pilotSocket } from "pilot/socket"; import { create } from "zustand"; let interval: NodeJS.Timeout; type TalkState = { micDeviceId: string | null; micVolume: number; isTalking: boolean; source: string; state: "connecting" | "connected" | "disconnected" | "error"; message: string | null; connectionQuality: ConnectionQuality; remoteParticipants: number; toggleTalking: () => void; setMic: (micDeviceId: string | null, volume: number) => void; connect: (roomName: string) => void; disconnect: () => void; room: Room | null; }; const getToken = async (roomName: string) => { const response = await serverApi.get(`/livekit/token?roomName=${roomName}`); const data = response.data; return data.token; }; export const useAudioStore = create((set, get) => ({ isTalking: false, message: null, micDeviceId: null, micVolume: 1, state: "disconnected", source: "", remoteParticipants: 0, connectionQuality: ConnectionQuality.Unknown, room: null, setMic: (micDeviceId, micVolume) => { set({ micDeviceId, micVolume }); }, toggleTalking: () => { const { room, isTalking, micDeviceId, micVolume } = get(); if (!room) return; // Todo: use micVolume room.localParticipant.setMicrophoneEnabled(!isTalking, { deviceId: micDeviceId ?? undefined, }); if (!isTalking) { // If old status was not talking, we need to emit the PTT event if (pilotSocket.connected) { pilotSocket.emit("ptt", { shouldTransmit: true, channel: room.name, }); } if (dispatchSocket.connected) dispatchSocket.emit("ptt", { shouldTransmit: true, channel: room.name, }); } set((state) => ({ isTalking: !state.isTalking, source: "web-app" })); }, connect: async (roomName) => { set({ state: "connecting" }); console.log("Connecting to room: ", roomName); try { // Clean old room const connectedRoom = get().room; if (interval) clearInterval(interval); if (connectedRoom) { connectedRoom.disconnect(); connectedRoom.removeAllListeners(); } const url = process.env.NEXT_PUBLIC_LIVEKIT_URL; if (!url) return console.error("NEXT_PUBLIC_LIVEKIT_URL not set"); const token = await getToken(roomName); if (!token) throw new Error("Fehlende Berechtigung"); 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, {}); set({ room }); 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" }); } } }, disconnect: () => { get().room?.disconnect(); }, })); interface PTTData { shouldTransmit: boolean; source: string; } const handlePTT = (data: PTTData) => { console.log("PTT", data); const { shouldTransmit, source } = data; const { room } = useAudioStore.getState(); if (!room) return; useAudioStore.setState({ isTalking: shouldTransmit, source, }); if (shouldTransmit) { room.localParticipant.setMicrophoneEnabled(true); } else { room.localParticipant.setMicrophoneEnabled(false); } }; const handleOtherPTT = (data: { publicUser: PublicUser; channel: string; source: string }) => { const currentChannel = useAudioStore.getState().room?.name; console.log("Other PTT", data); if (data.channel === currentChannel) useAudioStore.setState({ source: data.source, }); }; pilotSocket.on("ptt", handlePTT); pilotSocket.on("other-ptt", handleOtherPTT); dispatchSocket.on("ptt", handlePTT); dispatchSocket.on("other-ptt", handleOtherPTT);