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/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,
]