import { dispatchSocket } from "(app)/dispatch/socket"; import { handleDisconnect, handleLocalTrackUnpublished, handleTrackSubscribed, handleTrackUnsubscribed, } from "_helpers/liveKitEventHandler"; import { ConnectionQuality, LocalTrackPublication, Participant, Room, RoomEvent, RpcInvocationData, Track, } from "livekit-client"; import { pilotSocket } from "(app)/pilot/socket"; import { create } from "zustand"; import axios from "axios"; import { useDispatchConnectionStore } from "_store/dispatch/connectionStore"; import { changeDispatcherAPI } from "_querys/dispatcher"; import { getRadioStream } from "_helpers/radioEffect"; let interval: NodeJS.Timeout; type TalkState = { settings: { micDeviceId: string | null; micVolume: number; radioVolume: number; dmeVolume: number; }; isTalking: boolean; transmitBlocked: boolean; removeMessage: () => void; state: "connecting" | "connected" | "disconnected" | "error"; message: string | null; connectionQuality: ConnectionQuality; remoteParticipants: number; toggleTalking: () => void; setSettings: (settings: Partial) => void; connect: (roomName: string, role: string) => void; disconnect: () => void; speakingParticipants: Participant[]; addSpeakingParticipant: (participant: Participant) => void; removeSpeakingParticipant: (speakingParticipants: Participant) => void; room: Room | null; localRadioTrack: LocalTrackPublication | undefined; }; const getToken = async (roomName: string) => { const response = await axios.get(`/api/livekit-token?roomName=${roomName}`); const data = response.data; return data.token; }; export const useAudioStore = create((set, get) => ({ isTalking: false, localRadioTrack: undefined, transmitBlocked: false, message: null, speakingParticipants: [], micVolume: 1, settings: { micDeviceId: null, micVolume: 1, radioVolume: 0.8, dmeVolume: 0.8, }, state: "disconnected" as const, remoteParticipants: 0, connectionQuality: ConnectionQuality.Unknown, room: null, addSpeakingParticipant: (participant) => { set((state) => { if (!state.speakingParticipants.some((p) => p.identity === participant.identity)) { return { speakingParticipants: [...state.speakingParticipants, participant] }; } return state; }); }, removeMessage: () => { set({ message: null }); }, removeSpeakingParticipant: (participant) => { const newSpeaktingParticipants = get().speakingParticipants.filter( (p) => !(p.identity === participant.identity), ); set(() => ({ speakingParticipants: newSpeaktingParticipants, })); if (newSpeaktingParticipants.length === 0 && get().transmitBlocked) { get().room?.localParticipant.setMicrophoneEnabled(true); set({ transmitBlocked: false, message: null, isTalking: true }); } }, setSettings: (newSettings) => { const oldSettings = get().settings; set((s) => ({ settings: { ...s.settings, ...newSettings, }, })); if ( get().state === "connected" && (oldSettings.micDeviceId !== newSettings.micDeviceId || oldSettings.micVolume !== newSettings.micVolume) ) { const { room, disconnect, connect } = get(); const role = room?.localParticipant.attributes.role; console.log(role); if (room?.name || role) { disconnect(); connect(room?.name || "", role || "user"); } } }, toggleTalking: () => { const { room, isTalking, speakingParticipants, transmitBlocked } = get(); if (!room) return; if (speakingParticipants.length > 0 && !isTalking && !transmitBlocked) { // Wenn andere sprechen, nicht reden set({ message: "Rufgruppe besetzt", transmitBlocked: true, }); return; } else if (!isTalking && transmitBlocked) { set({ message: null, transmitBlocked: false, }); return; } room.localParticipant.setMicrophoneEnabled(!isTalking); set((state) => ({ isTalking: !state.isTalking, transmitBlocked: false })); }, connect: async (roomName, role) => { set({ state: "connecting" }); 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, async () => { const dispatchState = useDispatchConnectionStore.getState(); if (dispatchState.status === "connected" && dispatchState.connectedDispatcher?.id) { changeDispatcherAPI(dispatchState.connectedDispatcher?.id, { zone: roomName, }); } const inputStream = await navigator.mediaDevices.getUserMedia({ audio: { deviceId: get().settings.micDeviceId ?? undefined, noiseSuppression: true, }, }); // Funk-Effekt anwenden const radioStream = getRadioStream(inputStream, get().settings.micVolume); if (!radioStream) throw new Error("Konnte Funkstream nicht erzeugen"); const [track] = radioStream.getAudioTracks(); if (!track) throw new Error("Konnte Audio-Track nicht erzeugen"); const publishedTrack = await room.localParticipant.publishTrack(track, { name: "radio-audio", source: Track.Source.Microphone, }); await publishedTrack.mute(); set({ localRadioTrack: publishedTrack }); 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.LocalTrackUnpublished, handleLocalTrackUnpublished); await room.connect(url, token, {}); room.localParticipant.setAttributes({ role, }); set({ room }); room.registerRpcMethod("force-mute", async (data: RpcInvocationData) => { const { by } = JSON.parse(data.payload); room.localParticipant.setMicrophoneEnabled(false); useAudioStore.setState({ isTalking: false, message: `Ruf beendet durch ${by || "eine unsichtbare Macht"}`, }); return `Hello, ${data.callerIdentity}!`; }); 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) => { const { shouldTransmit } = data; const { room, speakingParticipants } = useAudioStore.getState(); if (!room) return; if (speakingParticipants.length > 0 && shouldTransmit) { // Wenn andere sprechen, nicht reden useAudioStore.setState({ message: "Rufgruppe besetzt", transmitBlocked: true, }); return; } useAudioStore.setState({ isTalking: shouldTransmit, transmitBlocked: false, }); if (shouldTransmit) { room.localParticipant.setMicrophoneEnabled(true); } else { room.localParticipant.setMicrophoneEnabled(false); } }; pilotSocket.on("ptt", handlePTT); dispatchSocket.on("ptt", handlePTT);