Files
DiscordClone/Frontend/Electron/src/contexts/VoiceContext.jsx

207 lines
6.7 KiB
JavaScript

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 (
<VoiceContext.Provider value={{
activeChannelId,
activeChannelName,
connectionState,
connectToVoice,
disconnectVoice,
room,
token,
voiceStates,
activeSpeakers,
isMuted,
isDeafened,
toggleMute,
toggleDeafen,
isScreenSharing,
setScreenSharing
}}>
{children}
{room && (
<LiveKitRoom
room={room}
style={{ position: 'absolute', width: 0, height: 0, overflow: 'hidden' }}
>
<RoomAudioRenderer muted={isDeafened} />
</LiveKitRoom>
)}
</VoiceContext.Provider>
);
};