import React, { createContext, useContext, useState, useEffect, useRef } from 'react'; import { Room, RoomEvent } from 'livekit-client'; import { LiveKitRoom, RoomAudioRenderer } from '@livekit/components-react'; import { useQuery, useConvex } from 'convex/react'; import { api } from '../../../../convex/_generated/api'; import '@livekit/components-styles'; import joinSound from '../assets/sounds/join_call.mp3'; import leaveSound from '../assets/sounds/leave_call.mp3'; import muteSound from '../assets/sounds/mute.mp3'; import unmuteSound from '../assets/sounds/unmute.mp3'; import deafenSound from '../assets/sounds/deafen.mp3'; import undeafenSound from '../assets/sounds/undeafen.mp3'; const soundMap = { join: joinSound, leave: leaveSound, mute: muteSound, unmute: unmuteSound, deafen: deafenSound, undeafen: undeafenSound }; const VoiceContext = createContext(); export const useVoice = () => useContext(VoiceContext); function playSound(type) { const src = soundMap[type]; if (!src) return; const audio = new Audio(src); audio.volume = 0.5; audio.play().catch(e => console.error("Sound play failed", e)); } export const VoiceProvider = ({ children }) => { const [activeChannelId, setActiveChannelId] = useState(null); const [activeChannelName, setActiveChannelName] = useState(null); const [connectionState, setConnectionState] = useState('disconnected'); const [room, setRoom] = useState(null); const [token, setToken] = useState(null); const [activeSpeakers, setActiveSpeakers] = useState(new Set()); const [isMuted, setIsMuted] = useState(false); const [isDeafened, setIsDeafened] = useState(false); const [isScreenSharing, setIsScreenSharingLocal] = useState(false); const convex = useConvex(); const voiceStates = useQuery(api.voiceState.getAll) || {}; async function updateVoiceState(fields) { const userId = localStorage.getItem('userId'); if (!userId || !activeChannelId) return; try { await convex.mutation(api.voiceState.updateState, { userId, ...fields }); } catch (e) { console.error('Failed to update voice state:', e); } } const connectToVoice = async (channelId, channelName, userId) => { if (activeChannelId === channelId) return; if (room) await room.disconnect(); setActiveChannelId(channelId); setActiveChannelName(channelName); setConnectionState('connecting'); try { const { token: lkToken } = await convex.action(api.voice.getToken, { channelId, userId, username: localStorage.getItem('username') || 'Unknown' }); if (!lkToken) throw new Error('Failed to get token'); setToken(lkToken); const newRoom = new Room({ adaptiveStream: true, dynacast: true, autoSubscribe: true, audioCaptureDefaults: { autoGainControl: true, echoCancellation: true, noiseSuppression: false, channelCount: 1, sampleRate: 48000, }, publishDefaults: { audioPreset: { maxBitrate: 96_000 }, dtx: false, red: true, screenShareEncoding: { maxBitrate: 10_000_000, maxFramerate: 60, }, screenShareSimulcastLayers: [], }, }); await newRoom.connect(import.meta.env.VITE_LIVEKIT_URL, lkToken); await newRoom.localParticipant.setMicrophoneEnabled(!isMuted && !isDeafened); setRoom(newRoom); setConnectionState('connected'); window.voiceRoom = newRoom; playSound('join'); await convex.mutation(api.voiceState.join, { channelId, userId, username: localStorage.getItem('username') || 'Unknown', isMuted, isDeafened, }); newRoom.on(RoomEvent.Disconnected, async (reason) => { console.warn('Voice Room Disconnected. Reason:', reason); playSound('leave'); setConnectionState('disconnected'); setActiveChannelId(null); setRoom(null); setToken(null); setActiveSpeakers(new Set()); try { await convex.mutation(api.voiceState.leave, { userId }); } catch (e) { console.error('Failed to leave voice state:', e); } }); newRoom.on(RoomEvent.ActiveSpeakersChanged, (speakers) => { setActiveSpeakers(new Set(speakers.map(p => p.identity))); }); } catch (err) { console.error('Voice Connection Failed:', err); setConnectionState('error'); setActiveChannelId(null); } }; const disconnectVoice = () => { console.log('User manually disconnected voice'); if (room) room.disconnect(); }; const toggleMute = async () => { const nextState = !isMuted; setIsMuted(nextState); playSound(nextState ? 'mute' : 'unmute'); if (room) { room.localParticipant.setMicrophoneEnabled(!nextState); } await updateVoiceState({ isMuted: nextState }); }; const toggleDeafen = async () => { const nextState = !isDeafened; setIsDeafened(nextState); playSound(nextState ? 'deafen' : 'undeafen'); if (room && !isMuted) { room.localParticipant.setMicrophoneEnabled(!nextState); } await updateVoiceState({ isDeafened: nextState }); }; const setScreenSharing = async (active) => { setIsScreenSharingLocal(active); await updateVoiceState({ isScreenSharing: active }); }; return ( {children} {room && ( )} ); };