207 lines
6.7 KiB
JavaScript
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>
|
|
);
|
|
};
|