Transform CallProvider into 'CallManager' hook, review WebSocket messages for calls
- These changes allowed to remove more cascading effects. It is now possible to reactivate StrictMode. Downside is we lost the 'optional context' of CallProvider: the call logic will be loaded even if there is no call.
- The WebSocket messages have been changed so the client does not have to know the conversation members before a call. Previously, the client had to fetch the conversation members for a call, which was causing cascading effects.
- Accidentally, moving the handling of conversation members to the server added some logic for calls with more than two participants, but it is still not ready to work.
* CallProvider.tsx will be renamed in next commit in order to make it easier to track its file history
Change-Id: Iae711009adafce065ac3defc1c91c7ca0f37898c
diff --git a/client/src/components/CallButtons.tsx b/client/src/components/CallButtons.tsx
index d853cc1..0ee463e 100644
--- a/client/src/components/CallButtons.tsx
+++ b/client/src/components/CallButtons.tsx
@@ -20,7 +20,8 @@
import { styled } from '@mui/material/styles';
import { ChangeEvent, useMemo } from 'react';
-import { CallStatus, useCallContext, VideoStatus } from '../contexts/CallProvider';
+import { useCallManagerContext } from '../contexts/CallManagerProvider';
+import { CallStatus, VideoStatus } from '../contexts/CallProvider';
import { useUserMediaContext } from '../contexts/UserMediaProvider';
import {
ColoredRoundButton,
@@ -58,7 +59,7 @@
});
export const CallingChatButton = (props: ExpandableButtonProps) => {
- const { setIsChatShown } = useCallContext();
+ const { setIsChatShown } = useCallManagerContext();
return (
<CallButton
aria-label="chat"
@@ -72,7 +73,7 @@
};
export const CallingEndButton = (props: ExpandableButtonProps) => {
- const { endCall } = useCallContext();
+ const { endCall } = useCallManagerContext();
return (
<ColoredRoundButton
paletteColor={(theme) => theme.palette.error}
@@ -91,7 +92,7 @@
};
export const CallingFullScreenButton = (props: ExpandableButtonProps) => {
- const { setIsFullscreen } = useCallContext();
+ const { setIsFullscreen } = useCallManagerContext();
return (
<CallButton
aria-label="full screen"
@@ -123,7 +124,7 @@
};
const ToggleScreenShareIconButton = (props: IconButtonProps) => {
- const { videoStatus, updateVideoStatus } = useCallContext();
+ const { videoStatus, updateVideoStatus } = useCallManagerContext();
return (
<ToggleIconButton
@@ -194,7 +195,7 @@
};
const ToggleAudioCameraIconButton = (props: IconButtonProps) => {
- const { isAudioOn, setIsAudioOn } = useCallContext();
+ const { isAudioOn, setIsAudioOn } = useCallManagerContext();
return (
<ToggleIconButton
IconOn={MicroIcon}
@@ -220,7 +221,7 @@
};
const ToggleVideoCameraIconButton = (props: IconButtonProps) => {
- const { videoStatus, updateVideoStatus } = useCallContext();
+ const { videoStatus, updateVideoStatus } = useCallManagerContext();
return (
<ToggleIconButton
IconOn={VideoCameraIcon}
@@ -236,7 +237,7 @@
// Calling pending/receiving interface
export const CallingCancelButton = (props: IconButtonProps) => {
- const { endCall } = useCallContext();
+ const { endCall } = useCallManagerContext();
return (
<ColoredRoundButton
@@ -252,7 +253,7 @@
};
export const CallingAnswerAudioButton = (props: IconButtonProps) => {
- const { acceptCall, callStatus } = useCallContext();
+ const { acceptCall, callStatus } = useCallManagerContext();
return (
<ColoredRoundButton
@@ -269,7 +270,7 @@
};
export const CallingAnswerVideoButton = (props: IconButtonProps) => {
- const { acceptCall, callStatus } = useCallContext();
+ const { acceptCall, callStatus } = useCallManagerContext();
return (
<ColoredRoundButton
disabled={callStatus === CallStatus.Connecting || callStatus === CallStatus.Loading}
@@ -285,7 +286,7 @@
};
export const CallingRefuseButton = (props: IconButtonProps) => {
- const { endCall } = useCallContext();
+ const { endCall } = useCallManagerContext();
return (
<ColoredRoundButton
aria-label="refuse call"
diff --git a/client/src/components/CallChatDrawer.tsx b/client/src/components/CallChatDrawer.tsx
index d094554..4a58393 100644
--- a/client/src/components/CallChatDrawer.tsx
+++ b/client/src/components/CallChatDrawer.tsx
@@ -17,7 +17,7 @@
*/
import { Divider, Stack, Typography } from '@mui/material';
-import { useCallContext } from '../contexts/CallProvider';
+import { useCallManagerContext } from '../contexts/CallManagerProvider';
import { useConversationContext } from '../contexts/ConversationProvider';
import ChatInterface from '../pages/ChatInterface';
import { CloseButton } from './Button';
@@ -43,7 +43,7 @@
};
const CallChatDrawerHeader = () => {
- const { setIsChatShown } = useCallContext();
+ const { setIsChatShown } = useCallManagerContext();
const { conversationDisplayName } = useConversationContext();
return (
diff --git a/client/src/components/ConversationSummaryList.tsx b/client/src/components/ConversationSummaryList.tsx
index f8e3d22..c50a5f9 100644
--- a/client/src/components/ConversationSummaryList.tsx
+++ b/client/src/components/ConversationSummaryList.tsx
@@ -19,13 +19,13 @@
import dayjs from 'dayjs';
import { IConversationSummary } from 'jami-web-common';
import { QRCodeCanvas } from 'qrcode.react';
-import { useCallback, useContext, useMemo, useState } from 'react';
+import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { useAuthContext } from '../contexts/AuthProvider';
-import { CallManagerContext } from '../contexts/CallManagerProvider';
-import { CallStatus, useCallContext } from '../contexts/CallProvider';
+import { useCallManagerContext } from '../contexts/CallManagerProvider';
+import { CallStatus } from '../contexts/CallProvider';
import { useConversationDisplayNameShort } from '../hooks/useConversationDisplayName';
import { useUrlParams } from '../hooks/useUrlParams';
import { ConversationRouteParams } from '../router';
@@ -108,8 +108,7 @@
const SecondaryText = ({ conversationSummary, isSelected }: SecondaryTextProps) => {
const { account } = useAuthContext();
- const callContext = useCallContext(true);
- const { callData } = useContext(CallManagerContext);
+ const { callData, callStatus, isAudioOn } = useCallManagerContext();
const { t, i18n } = useTranslation();
const timeIndicator = useMemo(() => {
@@ -123,7 +122,7 @@
}, [conversationSummary, i18n]);
const lastMessageText = useMemo(() => {
- if (!callContext || !callData || callData.conversationId !== conversationSummary.id) {
+ if (!callData || callData.conversationId !== conversationSummary.id) {
const message = conversationSummary.lastMessage;
switch (message.type) {
case 'initial': {
@@ -149,16 +148,16 @@
}
}
- if (callContext.callStatus === CallStatus.InCall) {
- return callContext.isAudioOn ? t('ongoing_call_unmuted') : t('ongoing_call_muted');
+ if (callStatus === CallStatus.InCall) {
+ return isAudioOn ? t('ongoing_call_unmuted') : t('ongoing_call_muted');
}
- if (callContext.callStatus === CallStatus.Connecting) {
+ if (callStatus === CallStatus.Connecting) {
return t('connecting_call');
}
- return callContext.callRole === 'caller' ? t('outgoing_call') : t('incoming_call');
- }, [account, conversationSummary, callContext, callData, t, i18n]);
+ return callData.role === 'caller' ? t('outgoing_call') : t('incoming_call');
+ }, [account, conversationSummary, callData, callStatus, isAudioOn, t, i18n]);
return (
<Stack direction="row" spacing="5px">
@@ -186,7 +185,7 @@
contextMenuProps,
}: ConversationMenuProps) => {
const { t } = useTranslation();
- const { startCall } = useContext(CallManagerContext);
+ const { startCall } = useCallManagerContext();
const [isSwarm] = useState(true);
const detailsDialogHandler = useDialogHandler();
@@ -206,10 +205,7 @@
Icon: AudioCallIcon,
onClick: () => {
if (conversationId) {
- startCall({
- conversationId,
- role: 'caller',
- });
+ startCall(conversationId);
}
},
},
@@ -218,11 +214,7 @@
Icon: VideoCallIcon,
onClick: () => {
if (conversationId) {
- startCall({
- conversationId,
- role: 'caller',
- withVideoOn: true,
- });
+ startCall(conversationId, true);
}
},
},
diff --git a/client/src/components/ConversationView.tsx b/client/src/components/ConversationView.tsx
index 5f76add..b636ec3 100644
--- a/client/src/components/ConversationView.tsx
+++ b/client/src/components/ConversationView.tsx
@@ -16,10 +16,8 @@
* <https://www.gnu.org/licenses/>.
*/
import { Divider, Stack, Typography } from '@mui/material';
-import { useContext } from 'react';
-import { CallManagerContext } from '../contexts/CallManagerProvider';
-import { useCallContext } from '../contexts/CallProvider';
+import { useCallManagerContext } from '../contexts/CallManagerProvider';
import { useConversationContext } from '../contexts/ConversationProvider';
import CallInterface from '../pages/CallInterface';
import ChatInterface from '../pages/ChatInterface';
@@ -27,10 +25,9 @@
const ConversationView = () => {
const { conversationId } = useConversationContext();
- const callContext = useCallContext(true);
- const { callData } = useContext(CallManagerContext);
+ const { callData } = useCallManagerContext();
- if (callContext && callData?.conversationId === conversationId) {
+ if (callData?.conversationId === conversationId) {
return <CallInterface />;
}
@@ -49,7 +46,7 @@
const ConversationHeader = () => {
const { conversationId, conversationDisplayName } = useConversationContext();
- const { startCall } = useContext(CallManagerContext);
+ const { startCall } = useCallManagerContext();
return (
<Stack direction="row" padding="16px" overflow="hidden">
@@ -59,8 +56,8 @@
</Typography>
</Stack>
<Stack direction="row" spacing="20px">
- <StartAudioCallButton onClick={() => startCall({ conversationId, role: 'caller' })} />
- <StartVideoCallButton onClick={() => startCall({ conversationId, role: 'caller', withVideoOn: true })} />
+ <StartAudioCallButton onClick={() => startCall(conversationId)} />
+ <StartVideoCallButton onClick={() => startCall(conversationId, true)} />
<AddParticipantButton />
<ShowOptionsMenuButton />
</Stack>
diff --git a/client/src/components/VideoOverlay.tsx b/client/src/components/VideoOverlay.tsx
index 9307f9b..2093840 100644
--- a/client/src/components/VideoOverlay.tsx
+++ b/client/src/components/VideoOverlay.tsx
@@ -20,7 +20,7 @@
import Draggable, { DraggableEventHandler } from 'react-draggable';
import { useNavigate } from 'react-router-dom';
-import { useCallContext } from '../contexts/CallProvider';
+import { useCallManagerContext } from '../contexts/CallManagerProvider';
import { useUserMediaContext } from '../contexts/UserMediaProvider';
import { VideoElementWithSinkId } from '../utils/utils';
import VideoStream, { VideoStreamProps } from './VideoStream';
@@ -75,7 +75,7 @@
};
export const RemoteVideoOverlay = ({ callConversationId }: { callConversationId: string }) => {
- const { remoteStreams } = useCallContext();
+ const { remoteStreams } = useCallManagerContext();
const {
currentMediaDeviceIds: {
audiooutput: { id: audioOutDeviceId },
diff --git a/client/src/contexts/CallManagerProvider.tsx b/client/src/contexts/CallManagerProvider.tsx
index ec68765..8f44a1b 100644
--- a/client/src/contexts/CallManagerProvider.tsx
+++ b/client/src/contexts/CallManagerProvider.tsx
@@ -15,117 +15,28 @@
* License along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*/
-import { CallBegin, ConversationInfos, WebSocketMessageType } from 'jami-web-common';
-import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
-import { useTranslation } from 'react-i18next';
-import { useNavigate } from 'react-router-dom';
-
import { RemoteVideoOverlay } from '../components/VideoOverlay';
+import { createOptionalContext } from '../hooks/createOptionalContext';
import { useUrlParams } from '../hooks/useUrlParams';
-import { ConversationMember } from '../models/conversation-member';
import { ConversationRouteParams } from '../router';
-import { useConversationInfosQuery, useMembersQuery } from '../services/conversationQueries';
-import { SetState, WithChildren } from '../utils/utils';
-import { AlertSnackbarContext } from './AlertSnackbarProvider';
-import CallProvider, { CallRole } from './CallProvider';
-import { useWebSocketContext } from './WebSocketProvider';
+import { WithChildren } from '../utils/utils';
+import { ICallManager, useCallManager } from './CallProvider';
-export type CallData = {
- conversationId: string;
- role: CallRole;
- withVideoOn?: boolean;
-};
-
-type ICallManagerContext = {
- callData: CallData | undefined;
- callConversationInfos: ConversationInfos | undefined;
- callMembers: ConversationMember[] | undefined;
- startCall: SetState<CallData | undefined>;
- exitCall: () => void;
-};
-
-const defaultCallManagerContext: ICallManagerContext = {
- callData: undefined,
- callConversationInfos: undefined,
- callMembers: undefined,
- startCall: () => {},
- exitCall: () => {},
-};
-
-export const CallManagerContext = createContext<ICallManagerContext>(defaultCallManagerContext);
-CallManagerContext.displayName = 'CallManagerContext';
+const optionalCallManagerContext = createOptionalContext<ICallManager>('CallManagerContext');
+export const useCallManagerContext = optionalCallManagerContext.useOptionalContext;
export default ({ children }: WithChildren) => {
- const [callData, setCallData] = useState<CallData>();
- const { setAlertContent } = useContext(AlertSnackbarContext);
- const webSocket = useWebSocketContext();
- const navigate = useNavigate();
- const { data: conversationInfos } = useConversationInfosQuery(callData?.conversationId);
- const { data: members } = useMembersQuery(callData?.conversationId);
const { urlParams } = useUrlParams<ConversationRouteParams>();
- const { t } = useTranslation();
+ const callManager = useCallManager();
- const failStartCall = useCallback(() => {
- throw new Error('Cannot start call: Already in a call');
- }, []);
-
- const startCall = !callData ? setCallData : failStartCall;
-
- const exitCall = useCallback(() => {
- if (!callData) {
- return;
- }
-
- setCallData(undefined);
- // TODO: write in chat that the call ended
- }, [callData]);
-
- useEffect(() => {
- const callBeginListener = ({ conversationId, withVideoOn }: CallBegin) => {
- if (callData) {
- // TODO: Currently, we display a notification if already in a call.
- // In the future, we should handle receiving a call while already in another.
- setAlertContent({
- messageI18nKey: 'missed_incoming_call',
- messageI18nContext: { conversationId },
- severity: 'info',
- alertOpen: true,
- });
- return;
- }
-
- startCall({ conversationId: conversationId, role: 'receiver', withVideoOn });
- navigate(`/conversation/${conversationId}`);
- };
-
- webSocket.bind(WebSocketMessageType.CallBegin, callBeginListener);
-
- return () => {
- webSocket.unbind(WebSocketMessageType.CallBegin, callBeginListener);
- };
- }, [webSocket, navigate, startCall, callData, setAlertContent, t]);
-
- const value = useMemo(
- () => ({
- startCall,
- callData,
- callConversationInfos: conversationInfos,
- callMembers: members,
- exitCall,
- }),
- [startCall, callData, conversationInfos, members, exitCall]
- );
+ const { callData } = callManager;
return (
- <>
- <CallManagerContext.Provider value={value}>
- <CallProvider>
- {callData && callData.conversationId !== urlParams.conversationId && (
- <RemoteVideoOverlay callConversationId={callData.conversationId} />
- )}
- {children}
- </CallProvider>
- </CallManagerContext.Provider>
- </>
+ <optionalCallManagerContext.Context.Provider value={callManager}>
+ {callData && callData.conversationId !== urlParams.conversationId && (
+ <RemoteVideoOverlay callConversationId={callData.conversationId} />
+ )}
+ {children}
+ </optionalCallManagerContext.Context.Provider>
);
};
diff --git a/client/src/contexts/CallProvider.tsx b/client/src/contexts/CallProvider.tsx
index 1a8c5b0..61379a9 100644
--- a/client/src/contexts/CallProvider.tsx
+++ b/client/src/contexts/CallProvider.tsx
@@ -15,21 +15,27 @@
* License along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*/
-import { CallAction, CallBegin, WebSocketMessageType } from 'jami-web-common';
+import { ConversationInfos, WebSocketMessageTable, WebSocketMessageType } from 'jami-web-common';
import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
-import { createOptionalContext } from '../hooks/createOptionalContext';
import { ConversationMember } from '../models/conversation-member';
+import { useConversationInfosQuery, useMembersQuery } from '../services/conversationQueries';
import { callTimeoutMs } from '../utils/constants';
-import { AsyncSetState, SetState, WithChildren } from '../utils/utils';
+import { AsyncSetState, SetState } from '../utils/utils';
import { useWebRtcManager } from '../webrtc/WebRtcManager';
+import { AlertSnackbarContext } from './AlertSnackbarProvider';
import { useAuthContext } from './AuthProvider';
-import { CallData, CallManagerContext } from './CallManagerProvider';
-import ConditionalContextProvider from './ConditionalContextProvider';
import { useUserMediaContext } from './UserMediaProvider';
-import { IWebSocketContext, useWebSocketContext } from './WebSocketProvider';
+import { useWebSocketContext } from './WebSocketProvider';
-export type CallRole = 'caller' | 'receiver';
+export type CallRole = 'caller' | 'receiver' | undefined;
+
+export type CallData = {
+ conversationId: string;
+ role: CallRole;
+ withVideoOn?: boolean;
+};
export enum CallStatus {
Default,
@@ -46,7 +52,11 @@
ScreenShare,
}
-export interface ICallContext {
+export interface ICallManager {
+ callData: CallData | undefined;
+ callConversationInfos: ConversationInfos | undefined;
+ callMembers: ConversationMember[] | undefined;
+
remoteStreams: readonly MediaStream[];
isAudioOn: boolean;
@@ -57,57 +67,22 @@
setIsChatShown: SetState<boolean>;
isFullscreen: boolean;
setIsFullscreen: SetState<boolean>;
- callRole: CallRole;
callStatus: CallStatus;
callStartTime: number | undefined;
+ startCall: (conversationId: string, withVideoOn?: boolean) => void;
acceptCall: (withVideoOn: boolean) => void;
endCall: () => void;
}
-const optionalCallContext = createOptionalContext<ICallContext>('CallContext');
-export const useCallContext = optionalCallContext.useOptionalContext;
-
-export default ({ children }: WithChildren) => {
+export const useCallManager = () => {
+ const { setAlertContent } = useContext(AlertSnackbarContext);
+ const [callData, setCallData] = useState<CallData>();
const webSocket = useWebSocketContext();
- const { callMembers, callData, exitCall } = useContext(CallManagerContext);
+ const navigate = useNavigate();
+ const { data: callConversationInfos } = useConversationInfosQuery(callData?.conversationId);
+ const { data: callMembers } = useMembersQuery(callData?.conversationId);
- const dependencies = useMemo(
- () => ({
- webSocket,
- callMembers,
- callData,
- exitCall,
- conversationId: callData?.conversationId,
- }),
- [webSocket, callMembers, callData, exitCall]
- );
-
- return (
- <ConditionalContextProvider
- Context={optionalCallContext.Context}
- initialValue={undefined}
- dependencies={dependencies}
- useProviderValue={CallProvider}
- >
- {children}
- </ConditionalContextProvider>
- );
-};
-
-const CallProvider = ({
- callMembers,
- callData,
- exitCall,
- conversationId,
- webSocket,
-}: {
- webSocket: IWebSocketContext;
- callMembers: ConversationMember[];
- callData: CallData;
- exitCall: () => void;
- conversationId: string;
-}): ICallContext => {
const {
localStream,
updateLocalStream,
@@ -120,28 +95,43 @@
const { account } = useAuthContext();
const webRtcManager = useWebRtcManager();
- // TODO: This logic will have to change to support multiple people in a call. Could we move this logic to the server?
- // The client could make a single request with the conversationId, and the server would be tasked with sending
- // all the individual requests to the members of the conversation.
- const contactUri = callMembers[0]?.contact.uri;
- const connectionInfos = webRtcManager.connectionsInfos[contactUri];
- const remoteStreams = connectionInfos?.remoteStreams;
- const iceConnectionState = connectionInfos?.iceConnectionState;
-
const [isAudioOn, setIsAudioOn] = useState(false);
const [videoStatus, setVideoStatus] = useState(VideoStatus.Off);
const [isChatShown, setIsChatShown] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
const [callStatus, setCallStatus] = useState(CallStatus.Default);
- const [callRole] = useState(callData?.role);
const [callStartTime, setCallStartTime] = useState<number | undefined>(undefined);
- // TODO: Replace this by a callback
useEffect(() => {
- if (callData.role === 'receiver' && contactUri && localStream) {
- webRtcManager.addConnection(webSocket, account, contactUri, callData, localStream, screenShareLocalStream);
- }
- }, [account, callData, contactUri, localStream, screenShareLocalStream, webRtcManager, webSocket]);
+ const callInviteListener = ({ conversationId, withVideoOn }: WebSocketMessageTable['onCallInvite']) => {
+ if (callData) {
+ // TODO: Currently, we display a notification if already in a call.
+ // In the future, we should handle receiving a call while already in another.
+ setAlertContent({
+ messageI18nKey: 'missed_incoming_call',
+ messageI18nContext: { conversationId },
+ severity: 'info',
+ alertOpen: true,
+ });
+ return;
+ }
+
+ setCallData({ conversationId: conversationId, role: 'receiver', withVideoOn });
+ navigate(`/conversation/${conversationId}`);
+ };
+
+ webSocket.bind(WebSocketMessageType.onCallInvite, callInviteListener);
+
+ return () => {
+ webSocket.unbind(WebSocketMessageType.onCallInvite, callInviteListener);
+ };
+ }, [webSocket, navigate, callData, setAlertContent]);
+
+ const conversationId = callData?.conversationId;
+ const contactUri = callMembers?.[0]?.contact.uri;
+ const connectionInfos = contactUri ? webRtcManager.connectionsInfos[contactUri] : undefined;
+ const remoteStreams = useMemo(() => connectionInfos?.remoteStreams || [], [connectionInfos]);
+ const iceConnectionState = connectionInfos?.iceConnectionState;
// TODO: Transform the effect into a callback
const updateLocalStreams = webRtcManager.updateLocalStreams;
@@ -153,12 +143,6 @@
updateLocalStreams(localStream, screenShareLocalStream);
}, [localStream, screenShareLocalStream, updateLocalStreams]);
- const sendWebRtcOffer = useCallback(async () => {
- if (contactUri) {
- webRtcManager.addConnection(webSocket, account, contactUri, callData, localStream, screenShareLocalStream);
- }
- }, [account, callData, contactUri, localStream, screenShareLocalStream, webRtcManager, webSocket]);
-
const closeConnection = useCallback(() => {
stopMedias();
webRtcManager.clean();
@@ -223,57 +207,82 @@
};
}, []);
- useEffect(() => {
- if (callRole === 'caller' && callStatus === CallStatus.Default) {
- const withVideoOn = callData?.withVideoOn ?? false;
- setCallStatus(CallStatus.Loading);
- updateLocalStream()
- .then(() => {
- const callBegin: CallBegin = {
- contactId: contactUri,
- conversationId,
- withVideoOn,
- };
+ const startCall = useCallback(
+ (conversationId: string, withVideoOn = false) => {
+ setCallData({ conversationId, withVideoOn, role: 'caller' });
+ if (callStatus === CallStatus.Default) {
+ setCallStatus(CallStatus.Loading);
+ updateLocalStream()
+ .then(() => {
+ const callInvite: WebSocketMessageTable['sendCallInvite'] = {
+ conversationId: conversationId,
+ withVideoOn,
+ };
- setCallStatus(CallStatus.Ringing);
- setVideoStatus(withVideoOn ? VideoStatus.Camera : VideoStatus.Off);
- console.info('Sending CallBegin', callBegin);
- webSocket.send(WebSocketMessageType.CallBegin, callBegin);
- })
- .catch((e) => {
- console.error(e);
- setCallStatus(CallStatus.PermissionsDenied);
- });
- }
- }, [webSocket, updateLocalStream, callRole, callStatus, contactUri, conversationId, callData]);
+ setCallStatus(CallStatus.Ringing);
+ setVideoStatus(withVideoOn ? VideoStatus.Camera : VideoStatus.Off);
+ webSocket.send(WebSocketMessageType.sendCallInvite, callInvite);
+ })
+ .catch((e) => {
+ console.error(e);
+ setCallStatus(CallStatus.PermissionsDenied);
+ });
+ }
+ },
+ [webSocket, updateLocalStream, callStatus]
+ );
const acceptCall = useCallback(
(withVideoOn: boolean) => {
+ if (!callMembers || !conversationId) {
+ console.warn('acceptCall without callMembers or conversationId');
+ return;
+ }
setCallStatus(CallStatus.Loading);
updateLocalStream()
.then(() => {
- const callAccept: CallAction = {
- contactId: contactUri,
+ const callAccept: WebSocketMessageTable['sendCallJoin'] = {
conversationId,
};
setVideoStatus(withVideoOn ? VideoStatus.Camera : VideoStatus.Off);
setCallStatus(CallStatus.Connecting);
- console.info('Sending CallAccept', callAccept);
- webSocket.send(WebSocketMessageType.CallAccept, callAccept);
+ console.info('Sending CallJoin', callAccept);
+ webSocket.send(WebSocketMessageType.sendCallJoin, callAccept);
+ // TODO: move this to "onWebRtcOffer" listener so we don't add connections for non-connected members
+ callMembers.forEach((member) =>
+ webRtcManager.addConnection(
+ webSocket,
+ account,
+ member.contact.uri,
+ callData,
+ localStream,
+ screenShareLocalStream
+ )
+ );
})
.catch((e) => {
console.error(e);
setCallStatus(CallStatus.PermissionsDenied);
});
},
- [webSocket, updateLocalStream, contactUri, conversationId]
+ [
+ account,
+ callData,
+ conversationId,
+ localStream,
+ callMembers,
+ screenShareLocalStream,
+ updateLocalStream,
+ webRtcManager,
+ webSocket,
+ ]
);
useEffect(() => {
- if (callRole === 'caller' && callStatus === CallStatus.Ringing) {
- const callAcceptListener = (data: CallAction) => {
- console.info('Received event on CallAccept', data);
+ if (callData?.role === 'caller' && callStatus === CallStatus.Ringing) {
+ const callJoinListener = (data: WebSocketMessageTable['onCallJoin']) => {
+ console.info('Received event on CallJoin', data, callData);
if (data.conversationId !== conversationId) {
console.warn('Wrong incoming conversationId, ignoring action');
return;
@@ -281,48 +290,51 @@
setCallStatus(CallStatus.Connecting);
- sendWebRtcOffer();
+ webRtcManager.addConnection(webSocket, account, data.senderId, callData, localStream, screenShareLocalStream);
};
- webSocket.bind(WebSocketMessageType.CallAccept, callAcceptListener);
+ webSocket.bind(WebSocketMessageType.onCallJoin, callJoinListener);
return () => {
- webSocket.unbind(WebSocketMessageType.CallAccept, callAcceptListener);
+ webSocket.unbind(WebSocketMessageType.onCallJoin, callJoinListener);
};
}
- }, [callRole, webSocket, sendWebRtcOffer, callStatus, conversationId]);
+ }, [account, callData, callStatus, conversationId, localStream, screenShareLocalStream, webRtcManager, webSocket]);
const endCall = useCallback(() => {
- const callEnd: CallAction = {
- contactId: contactUri,
+ if (!conversationId) {
+ return;
+ }
+
+ const callExit: WebSocketMessageTable['sendCallExit'] = {
conversationId,
};
- console.info('Sending CallEnd', callEnd);
+ console.info('Sending CallExit', callExit);
closeConnection();
- webSocket.send(WebSocketMessageType.CallEnd, callEnd);
- exitCall();
+ webSocket.send(WebSocketMessageType.sendCallExit, callExit);
+ setCallData(undefined);
+ setCallStatus(CallStatus.Default);
// TODO: write in chat that the call ended
- }, [webSocket, contactUri, conversationId, closeConnection, exitCall]);
+ }, [webSocket, conversationId, closeConnection]);
useEffect(() => {
- const callEndListener = (data: CallAction) => {
- console.info('Received event on CallEnd', data);
+ const callExitListener = (data: WebSocketMessageTable['onCallExit']) => {
+ console.info('Received event on CallExit', data);
if (data.conversationId !== conversationId) {
console.warn('Wrong incoming conversationId, ignoring action');
return;
}
- closeConnection();
- exitCall();
+ endCall();
// TODO: write in chat that the call ended
};
- webSocket.bind(WebSocketMessageType.CallEnd, callEndListener);
+ webSocket.bind(WebSocketMessageType.onCallExit, callExitListener);
return () => {
- webSocket.unbind(WebSocketMessageType.CallEnd, callEndListener);
+ webSocket.unbind(WebSocketMessageType.onCallExit, callExitListener);
};
- }, [webSocket, exitCall, conversationId, closeConnection]);
+ }, [webSocket, endCall, conversationId]);
useEffect(() => {
if (
@@ -357,6 +369,9 @@
return useMemo(
() => ({
+ callData,
+ callConversationInfos,
+ callMembers,
remoteStreams,
isAudioOn,
setIsAudioOn,
@@ -366,13 +381,16 @@
setIsChatShown,
isFullscreen,
setIsFullscreen,
- callRole,
callStatus,
callStartTime,
+ startCall,
acceptCall,
endCall,
}),
[
+ callData,
+ callConversationInfos,
+ callMembers,
remoteStreams,
isAudioOn,
videoStatus,
@@ -382,9 +400,9 @@
setIsChatShown,
isFullscreen,
setIsFullscreen,
- callRole,
callStatus,
callStartTime,
+ startCall,
acceptCall,
endCall,
]
diff --git a/client/src/pages/CallInterface.tsx b/client/src/pages/CallInterface.tsx
index f66a180..2892bb7 100644
--- a/client/src/pages/CallInterface.tsx
+++ b/client/src/pages/CallInterface.tsx
@@ -48,7 +48,8 @@
import CallChatDrawer from '../components/CallChatDrawer';
import VideoOverlay from '../components/VideoOverlay';
import VideoStream from '../components/VideoStream';
-import { CallStatus, useCallContext, VideoStatus } from '../contexts/CallProvider';
+import { useCallManagerContext } from '../contexts/CallManagerProvider';
+import { CallStatus, VideoStatus } from '../contexts/CallProvider';
import { useConversationContext } from '../contexts/ConversationProvider';
import { useUserMediaContext } from '../contexts/UserMediaProvider';
import { formatCallDuration } from '../utils/dates×';
@@ -57,7 +58,7 @@
import CallPermissionDenied from './CallPermissionDenied';
export default () => {
- const { callStatus, isChatShown, isFullscreen } = useCallContext();
+ const { callStatus, isChatShown, isFullscreen } = useCallManagerContext();
const callInterfaceRef = useRef<HTMLDivElement>();
useEffect(() => {
@@ -99,7 +100,7 @@
audiooutput: { id: audioOutDeviceId },
},
} = useUserMediaContext();
- const { remoteStreams, videoStatus } = useCallContext();
+ const { remoteStreams, videoStatus } = useCallManagerContext();
const remoteVideoRef = useRef<VideoElementWithSinkId | null>(null);
const gridItemRef = useRef<HTMLDivElement | null>(null);
const [isLocalVideoZoomed, setIsLocalVideoZoomed] = useState(false);
@@ -156,7 +157,7 @@
};
const CallInterfaceInformation = () => {
- const { callStartTime } = useCallContext();
+ const { callStartTime } = useCallManagerContext();
const { conversationDisplayName } = useConversationContext();
const [elapsedTime, setElapsedTime] = useState<Duration>(
dayjs.duration(callStartTime ? Date.now() - callStartTime : 0)
diff --git a/client/src/pages/CallPending.tsx b/client/src/pages/CallPending.tsx
index c965d45..0ff1b9e 100644
--- a/client/src/pages/CallPending.tsx
+++ b/client/src/pages/CallPending.tsx
@@ -28,7 +28,8 @@
CallingRefuseButton,
} from '../components/CallButtons';
import ConversationAvatar from '../components/ConversationAvatar';
-import { CallStatus, useCallContext } from '../contexts/CallProvider';
+import { useCallManagerContext } from '../contexts/CallManagerProvider';
+import { CallStatus } from '../contexts/CallProvider';
import { useConversationContext } from '../contexts/ConversationProvider';
import { useUserMediaContext } from '../contexts/UserMediaProvider';
import { VideoElementWithSinkId } from '../utils/utils';
@@ -36,7 +37,7 @@
export const CallPending = () => {
const { conversationDisplayName } = useConversationContext();
const { localStream } = useUserMediaContext();
- const { callRole } = useCallContext();
+ const { callData } = useCallManagerContext();
const localVideoRef = useRef<VideoElementWithSinkId | null>(null);
useEffect(() => {
@@ -107,7 +108,7 @@
/>
</Box>
</Box>
- {callRole === 'caller' ? <CallPendingCallerInterface /> : <CallPendingReceiverInterface />}
+ {callData?.role === 'caller' ? <CallPendingCallerInterface /> : <CallPendingReceiverInterface />}
</Stack>
);
};
@@ -146,7 +147,7 @@
};
export const CallPendingCallerInterface = () => {
- const { callStatus } = useCallContext();
+ const { callStatus } = useCallManagerContext();
const { t } = useTranslation();
const { members } = useConversationContext();
const memberName = useMemo(() => members[0].getDisplayName(), [members]);
@@ -179,7 +180,7 @@
export const CallPendingReceiverInterface = () => {
const { state } = useLocation();
- const { callStatus } = useCallContext();
+ const { callStatus } = useCallManagerContext();
const { t } = useTranslation();
const { members } = useConversationContext();
diff --git a/client/src/webrtc/RtcPeerConnectionHandler.ts b/client/src/webrtc/RtcPeerConnectionHandler.ts
index 8d83e5d..ddf2f6f 100644
--- a/client/src/webrtc/RtcPeerConnectionHandler.ts
+++ b/client/src/webrtc/RtcPeerConnectionHandler.ts
@@ -16,9 +16,9 @@
* <https://www.gnu.org/licenses/>.
*/
-import { WebRtcIceCandidate, WebRtcSdp, WebSocketMessageType } from 'jami-web-common';
+import { WebRtcIceCandidate, WebRtcSdp, WebSocketMessageTable, WebSocketMessageType } from 'jami-web-common';
-import { CallData } from '../contexts/CallManagerProvider';
+import { CallData } from '../contexts/CallProvider';
import { IWebSocketContext } from '../contexts/WebSocketProvider';
import { Account } from '../models/account';
import { Listener } from '../utils/utils';
@@ -65,6 +65,7 @@
screenShareLocalStream: MediaStream | undefined,
listener: Listener
) {
+ console.log('constructor', callData);
this.listener = listener;
const iceServers = this.getIceServers(account);
this.connection = new RTCPeerConnection({ iceServers });
@@ -159,15 +160,15 @@
offerToReceiveVideo: true,
});
- const webRtcOffer: WebRtcSdp = {
- contactId: contactUri,
+ const webRtcOffer: WebSocketMessageTable['sendWebRtcOffer'] = {
+ receiverId: contactUri,
conversationId: conversationId,
sdp,
};
await this.connection.setLocalDescription(new RTCSessionDescription(sdp));
console.info('Sending WebRtcOffer', webRtcOffer);
- webSocket.send(WebSocketMessageType.WebRtcOffer, webRtcOffer);
+ webSocket.send(WebSocketMessageType.sendWebRtcOffer, webRtcOffer);
}
private setWebSocketListeners(webSocket: IWebSocketContext, conversationId: string, contactUri: string) {
@@ -177,15 +178,15 @@
offerToReceiveVideo: true,
});
- const webRtcAnswer: WebRtcSdp = {
- contactId: contactUri,
+ const webRtcAnswer: WebSocketMessageTable['sendWebRtcAnswer'] = {
+ receiverId: contactUri,
conversationId: conversationId,
sdp,
};
await this.connection.setLocalDescription(new RTCSessionDescription(sdp));
console.info('Sending WebRtcAnswer', webRtcAnswer);
- webSocket.send(WebSocketMessageType.WebRtcAnswer, webRtcAnswer);
+ webSocket.send(WebSocketMessageType.sendWebRtcAnswer, webRtcAnswer);
};
const addQueuedIceCandidates = async () => {
@@ -243,28 +244,28 @@
}
};
- webSocket.bind(WebSocketMessageType.WebRtcOffer, webRtcOfferListener);
- webSocket.bind(WebSocketMessageType.WebRtcAnswer, webRtcAnswerListener);
- webSocket.bind(WebSocketMessageType.WebRtcIceCandidate, webRtcIceCandidateListener);
+ webSocket.bind(WebSocketMessageType.onWebRtcOffer, webRtcOfferListener);
+ webSocket.bind(WebSocketMessageType.onWebRtcAnswer, webRtcAnswerListener);
+ webSocket.bind(WebSocketMessageType.onWebRtcIceCandidate, webRtcIceCandidateListener);
this.cleaningFunctions.push(() => {
- webSocket.unbind(WebSocketMessageType.WebRtcOffer, webRtcOfferListener);
- webSocket.unbind(WebSocketMessageType.WebRtcAnswer, webRtcAnswerListener);
- webSocket.unbind(WebSocketMessageType.WebRtcIceCandidate, webRtcIceCandidateListener);
+ webSocket.unbind(WebSocketMessageType.onWebRtcOffer, webRtcOfferListener);
+ webSocket.unbind(WebSocketMessageType.onWebRtcAnswer, webRtcAnswerListener);
+ webSocket.unbind(WebSocketMessageType.onWebRtcIceCandidate, webRtcIceCandidateListener);
});
}
private setConnectionListeners(webSocket: IWebSocketContext, conversationId: string, contactUri: string) {
this.connection.onicecandidate = (event: RTCPeerConnectionIceEvent) => {
if (event.candidate) {
- const webRtcIceCandidate: WebRtcIceCandidate = {
- contactId: contactUri,
+ const webRtcIceCandidate: WebSocketMessageTable['sendWebRtcIceCandidate'] = {
+ receiverId: contactUri,
conversationId: conversationId,
candidate: event.candidate,
};
// Send ice candidates as soon as they're found. This is called "trickle ice"
- webSocket.send(WebSocketMessageType.WebRtcIceCandidate, webRtcIceCandidate);
+ webSocket.send(WebSocketMessageType.sendWebRtcIceCandidate, webRtcIceCandidate);
}
};
diff --git a/client/src/webrtc/WebRtcManager.ts b/client/src/webrtc/WebRtcManager.ts
index afae8a0..67a614b 100644
--- a/client/src/webrtc/WebRtcManager.ts
+++ b/client/src/webrtc/WebRtcManager.ts
@@ -17,7 +17,7 @@
*/
import { useMemo, useRef, useSyncExternalStore } from 'react';
-import { CallData } from '../contexts/CallManagerProvider';
+import { CallData } from '../contexts/CallProvider';
import { IWebSocketContext } from '../contexts/WebSocketProvider';
import { Account } from '../models/account';
import { Listener } from '../utils/utils';
@@ -107,5 +107,7 @@
clean() {
Object.values(this.connections).forEach((connection) => connection.disconnect());
+ this.connections = {};
+ this.emitChange();
}
}