feat: add call management features with useCall and useRoomCallMembers hooks
- Implemented useCall hook for managing call state and actions. - Created useRoomCallMembers hook to track active call members in a room. - Added useRtcConfig for fetching RTC configurations and LiveKit JWT. - Developed Audio settings component for managing audio devices and settings. - Introduced device selection and screen sharing options in the Audio settings. - Persisted audio settings in localStorage for user preferences.
This commit is contained in:
238
src/app/features/call/useCall.ts
Normal file
238
src/app/features/call/useCall.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
import { createContext, useContext, useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { CallService } from './CallService';
|
||||
import { ActiveCall, CallEvent, CallEventType, CallState, CallType } from './types';
|
||||
import { fetchWellKnownWithRTC, getLiveKitFocus } from './useRtcConfig';
|
||||
|
||||
interface CallContextValue {
|
||||
callService: CallService | null;
|
||||
activeCall: ActiveCall | null;
|
||||
callSupported: boolean;
|
||||
callSupportLoading: boolean;
|
||||
startCall: (roomId: string, callType: CallType) => Promise<void>;
|
||||
endCall: () => Promise<void>;
|
||||
toggleMute: () => boolean;
|
||||
toggleVideo: () => boolean;
|
||||
toggleScreenShare: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
const CallContext = createContext<CallContextValue | null>(null);
|
||||
|
||||
/**
|
||||
* Hook to access the call context
|
||||
* @returns The call context value
|
||||
*/
|
||||
export function useCall(): CallContextValue {
|
||||
const context = useContext(CallContext);
|
||||
if (!context) {
|
||||
throw new Error('useCall must be used within a CallProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export const CallProvider = CallContext.Provider;
|
||||
|
||||
interface UseCallServiceOptions {
|
||||
livekitServiceUrl?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to initialize and manage the call service
|
||||
* Should be used at the app level to provide call context
|
||||
* @param options - Optional configuration including livekitServiceUrl from client config
|
||||
*/
|
||||
export function useCallService(options?: UseCallServiceOptions): CallContextValue {
|
||||
const mx = useMatrixClient();
|
||||
const [callService, setCallService] = useState<CallService | null>(null);
|
||||
const [activeCall, setActiveCall] = useState<ActiveCall | null>(null);
|
||||
const [callSupported, setCallSupported] = useState(false);
|
||||
const [callSupportLoading, setCallSupportLoading] = useState(true);
|
||||
|
||||
const configuredLivekitUrl = options?.livekitServiceUrl;
|
||||
|
||||
useEffect(() => {
|
||||
let unsubscribe: (() => void) | undefined;
|
||||
let service: CallService | undefined;
|
||||
|
||||
const initCallService = async () => {
|
||||
const baseUrl = mx.getHomeserverUrl();
|
||||
const accessToken = mx.getAccessToken();
|
||||
const userId = mx.getSafeUserId();
|
||||
const deviceId = mx.getDeviceId();
|
||||
|
||||
if (!accessToken || !deviceId) {
|
||||
setCallSupportLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let livekitServiceUrl: string | undefined = configuredLivekitUrl;
|
||||
|
||||
// If no configured URL, try to get from well-known
|
||||
if (!livekitServiceUrl) {
|
||||
const wellKnown = await fetchWellKnownWithRTC(baseUrl);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Call service init - well-known:', wellKnown);
|
||||
const livekitFocus = getLiveKitFocus(wellKnown);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Call service init - LiveKit focus:', livekitFocus);
|
||||
livekitServiceUrl = livekitFocus?.livekit_service_url;
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Call service init - Using configured LiveKit URL:', livekitServiceUrl);
|
||||
}
|
||||
|
||||
if (!livekitServiceUrl) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Call service init - No LiveKit service found, calls not supported');
|
||||
setCallSupported(false);
|
||||
setCallSupportLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
service = new CallService(
|
||||
{
|
||||
livekitServiceUrl,
|
||||
homeserverBaseUrl: baseUrl,
|
||||
accessToken,
|
||||
userId,
|
||||
deviceId,
|
||||
},
|
||||
mx
|
||||
);
|
||||
|
||||
setCallService(service);
|
||||
setCallSupported(true);
|
||||
setCallSupportLoading(false);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Call service init - Success! Calls are supported');
|
||||
|
||||
unsubscribe = service.on((event: CallEvent) => {
|
||||
// Update state for any event that might change the call state
|
||||
if (event.type === CallEventType.StateChanged ||
|
||||
event.type === CallEventType.ScreenShareStarted ||
|
||||
event.type === CallEventType.ScreenShareStopped ||
|
||||
event.type === CallEventType.RemoteStreamAdded ||
|
||||
event.type === CallEventType.RemoteStreamRemoved) {
|
||||
const newCall = service?.getActiveCall();
|
||||
if (newCall) {
|
||||
// Deep copy Maps to ensure React detects changes
|
||||
setActiveCall({
|
||||
...newCall,
|
||||
remoteStreams: new Map(newCall.remoteStreams),
|
||||
remoteScreenTracks: new Map(newCall.remoteScreenTracks),
|
||||
remoteScreenStreams: new Map(newCall.remoteScreenStreams),
|
||||
});
|
||||
} else {
|
||||
setActiveCall(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Call service init - Error:', error);
|
||||
setCallSupported(false);
|
||||
setCallSupportLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
initCallService();
|
||||
|
||||
return () => {
|
||||
unsubscribe?.();
|
||||
service?.dispose();
|
||||
};
|
||||
}, [mx, configuredLivekitUrl]);
|
||||
|
||||
const startCall = useCallback(
|
||||
async (roomId: string, callType: CallType) => {
|
||||
if (!callService) {
|
||||
throw new Error('Call service not initialized');
|
||||
}
|
||||
await callService.startCall(roomId, callType);
|
||||
},
|
||||
[callService]
|
||||
);
|
||||
|
||||
const endCall = useCallback(async () => {
|
||||
if (!callService) {
|
||||
return;
|
||||
}
|
||||
await callService.endCall();
|
||||
}, [callService]);
|
||||
|
||||
const toggleMute = useCallback(() => {
|
||||
if (!callService) {
|
||||
return false;
|
||||
}
|
||||
const newState = callService.toggleMute();
|
||||
const call = callService.getActiveCall();
|
||||
if (call) {
|
||||
setActiveCall({ ...call });
|
||||
}
|
||||
return newState;
|
||||
}, [callService]);
|
||||
|
||||
const toggleVideo = useCallback(() => {
|
||||
if (!callService) {
|
||||
return false;
|
||||
}
|
||||
const newState = callService.toggleVideo();
|
||||
const call = callService.getActiveCall();
|
||||
if (call) {
|
||||
setActiveCall({ ...call });
|
||||
}
|
||||
return newState;
|
||||
}, [callService]);
|
||||
|
||||
const toggleScreenShare = useCallback(async () => {
|
||||
if (!callService) {
|
||||
return false;
|
||||
}
|
||||
const newState = await callService.toggleScreenShare();
|
||||
const call = callService.getActiveCall();
|
||||
if (call) {
|
||||
setActiveCall({ ...call });
|
||||
}
|
||||
return newState;
|
||||
}, [callService]);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
callService,
|
||||
activeCall,
|
||||
callSupported,
|
||||
callSupportLoading,
|
||||
startCall,
|
||||
endCall,
|
||||
toggleMute,
|
||||
toggleVideo,
|
||||
toggleScreenShare,
|
||||
}),
|
||||
[callService, activeCall, callSupported, callSupportLoading, startCall, endCall, toggleMute, toggleVideo, toggleScreenShare]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to check if a call is active in a specific room
|
||||
*/
|
||||
export function useRoomCall(roomId: string) {
|
||||
const { activeCall, startCall, endCall, callSupported, callSupportLoading } = useCall();
|
||||
|
||||
const isInCall = activeCall?.roomId === roomId;
|
||||
const callState = isInCall ? activeCall.state : CallState.Idle;
|
||||
|
||||
const startVoiceCall = useCallback(() => startCall(roomId, CallType.Voice), [roomId, startCall]);
|
||||
const startVideoCall = useCallback(() => startCall(roomId, CallType.Video), [roomId, startCall]);
|
||||
|
||||
return {
|
||||
isInCall,
|
||||
callState,
|
||||
activeCall: isInCall ? activeCall : null,
|
||||
startVoiceCall,
|
||||
startVideoCall,
|
||||
endCall,
|
||||
callSupported,
|
||||
callSupportLoading,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user