blob: 1a8c5b0664c96d7344960b5d41bab3d759a9d0d2 [file] [log] [blame]
simonf929a362022-11-18 16:53:45 -05001/*
2 * Copyright (C) 2022 Savoir-faire Linux Inc.
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU Affero General Public License as
6 * published by the Free Software Foundation; either version 3 of the
7 * License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Affero General Public License for more details.
13 *
14 * You should have received a copy of the GNU Affero General Public
15 * License along with this program. If not, see
16 * <https://www.gnu.org/licenses/>.
17 */
MichelleSS55164202022-11-25 18:36:14 -050018import { CallAction, CallBegin, WebSocketMessageType } from 'jami-web-common';
simon5c677962022-12-02 16:51:54 -050019import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
simonf929a362022-11-18 16:53:45 -050020
simon5c677962022-12-02 16:51:54 -050021import { createOptionalContext } from '../hooks/createOptionalContext';
idillon07d31cc2022-12-06 22:40:14 -050022import { ConversationMember } from '../models/conversation-member';
MichelleSS55164202022-11-25 18:36:14 -050023import { callTimeoutMs } from '../utils/constants';
simon1e2bf342022-12-02 12:19:40 -050024import { AsyncSetState, SetState, WithChildren } from '../utils/utils';
idilloncf42c652023-01-31 18:56:17 -050025import { useWebRtcManager } from '../webrtc/WebRtcManager';
26import { useAuthContext } from './AuthProvider';
simon5c677962022-12-02 16:51:54 -050027import { CallData, CallManagerContext } from './CallManagerProvider';
28import ConditionalContextProvider from './ConditionalContextProvider';
idillon27dab022023-02-02 17:55:47 -050029import { useUserMediaContext } from './UserMediaProvider';
idillona4b96ab2023-02-01 15:30:12 -050030import { IWebSocketContext, useWebSocketContext } from './WebSocketProvider';
simonf929a362022-11-18 16:53:45 -050031
32export type CallRole = 'caller' | 'receiver';
33
34export enum CallStatus {
simonff1cb352022-11-24 15:15:26 -050035 Default,
simon9076a9a2022-11-29 17:13:01 -050036 Loading,
simonf929a362022-11-18 16:53:45 -050037 Ringing,
38 Connecting,
39 InCall,
simon9076a9a2022-11-29 17:13:01 -050040 PermissionsDenied,
simonf929a362022-11-18 16:53:45 -050041}
42
simon1e2bf342022-12-02 12:19:40 -050043export enum VideoStatus {
44 Off,
45 Camera,
46 ScreenShare,
47}
48
simonf929a362022-11-18 16:53:45 -050049export interface ICallContext {
idilloncf42c652023-01-31 18:56:17 -050050 remoteStreams: readonly MediaStream[];
51
simonf929a362022-11-18 16:53:45 -050052 isAudioOn: boolean;
simon9076a9a2022-11-29 17:13:01 -050053 setIsAudioOn: SetState<boolean>;
simon1e2bf342022-12-02 12:19:40 -050054 videoStatus: VideoStatus;
55 updateVideoStatus: AsyncSetState<VideoStatus>;
simonf9d78f22022-11-25 15:47:15 -050056 isChatShown: boolean;
57 setIsChatShown: SetState<boolean>;
simon2a5cf142022-11-25 15:47:35 -050058 isFullscreen: boolean;
59 setIsFullscreen: SetState<boolean>;
simonf929a362022-11-18 16:53:45 -050060 callRole: CallRole;
61 callStatus: CallStatus;
Misha Krieger-Raynauldd0cc3e32022-11-29 19:59:31 -050062 callStartTime: number | undefined;
simonf929a362022-11-18 16:53:45 -050063
MichelleSS55164202022-11-25 18:36:14 -050064 acceptCall: (withVideoOn: boolean) => void;
simonaccd8022022-11-24 15:04:53 -050065 endCall: () => void;
simonf929a362022-11-18 16:53:45 -050066}
67
simon5c677962022-12-02 16:51:54 -050068const optionalCallContext = createOptionalContext<ICallContext>('CallContext');
69export const useCallContext = optionalCallContext.useOptionalContext;
simonf929a362022-11-18 16:53:45 -050070
71export default ({ children }: WithChildren) => {
idillona4b96ab2023-02-01 15:30:12 -050072 const webSocket = useWebSocketContext();
idillon07d31cc2022-12-06 22:40:14 -050073 const { callMembers, callData, exitCall } = useContext(CallManagerContext);
simonf353ef42022-11-28 23:14:53 -050074
simon5c677962022-12-02 16:51:54 -050075 const dependencies = useMemo(
76 () => ({
77 webSocket,
idillon07d31cc2022-12-06 22:40:14 -050078 callMembers,
simon5c677962022-12-02 16:51:54 -050079 callData,
80 exitCall,
81 conversationId: callData?.conversationId,
82 }),
idilloncf42c652023-01-31 18:56:17 -050083 [webSocket, callMembers, callData, exitCall]
simon5c677962022-12-02 16:51:54 -050084 );
simonf353ef42022-11-28 23:14:53 -050085
simone35acc22022-12-02 16:51:12 -050086 return (
simon5c677962022-12-02 16:51:54 -050087 <ConditionalContextProvider
88 Context={optionalCallContext.Context}
89 initialValue={undefined}
90 dependencies={dependencies}
91 useProviderValue={CallProvider}
92 >
simone35acc22022-12-02 16:51:12 -050093 {children}
simon5c677962022-12-02 16:51:54 -050094 </ConditionalContextProvider>
simone35acc22022-12-02 16:51:12 -050095 );
simonf353ef42022-11-28 23:14:53 -050096};
97
98const CallProvider = ({
idillon07d31cc2022-12-06 22:40:14 -050099 callMembers,
simon5c677962022-12-02 16:51:54 -0500100 callData,
101 exitCall,
simone35acc22022-12-02 16:51:12 -0500102 conversationId,
simonf353ef42022-11-28 23:14:53 -0500103 webSocket,
simon5c677962022-12-02 16:51:54 -0500104}: {
simonf353ef42022-11-28 23:14:53 -0500105 webSocket: IWebSocketContext;
idillon07d31cc2022-12-06 22:40:14 -0500106 callMembers: ConversationMember[];
simon5c677962022-12-02 16:51:54 -0500107 callData: CallData;
108 exitCall: () => void;
simone35acc22022-12-02 16:51:12 -0500109 conversationId: string;
simon5c677962022-12-02 16:51:54 -0500110}): ICallContext => {
idillon27dab022023-02-02 17:55:47 -0500111 const {
112 localStream,
113 updateLocalStream,
114 screenShareLocalStream,
115 updateScreenShare,
116 setAudioInputDeviceId,
117 setVideoDeviceId,
118 stopMedias,
119 } = useUserMediaContext();
idilloncf42c652023-01-31 18:56:17 -0500120 const { account } = useAuthContext();
121 const webRtcManager = useWebRtcManager();
122
123 // TODO: This logic will have to change to support multiple people in a call. Could we move this logic to the server?
124 // The client could make a single request with the conversationId, and the server would be tasked with sending
125 // all the individual requests to the members of the conversation.
126 const contactUri = callMembers[0]?.contact.uri;
127 const connectionInfos = webRtcManager.connectionsInfos[contactUri];
128 const remoteStreams = connectionInfos?.remoteStreams;
129 const iceConnectionState = connectionInfos?.iceConnectionState;
simonf929a362022-11-18 16:53:45 -0500130
simonf929a362022-11-18 16:53:45 -0500131 const [isAudioOn, setIsAudioOn] = useState(false);
simon1e2bf342022-12-02 12:19:40 -0500132 const [videoStatus, setVideoStatus] = useState(VideoStatus.Off);
simonf9d78f22022-11-25 15:47:15 -0500133 const [isChatShown, setIsChatShown] = useState(false);
simon2a5cf142022-11-25 15:47:35 -0500134 const [isFullscreen, setIsFullscreen] = useState(false);
simone35acc22022-12-02 16:51:12 -0500135 const [callStatus, setCallStatus] = useState(CallStatus.Default);
136 const [callRole] = useState(callData?.role);
Misha Krieger-Raynauldd0cc3e32022-11-29 19:59:31 -0500137 const [callStartTime, setCallStartTime] = useState<number | undefined>(undefined);
simonf929a362022-11-18 16:53:45 -0500138
idilloncf42c652023-01-31 18:56:17 -0500139 // TODO: Replace this by a callback
140 useEffect(() => {
141 if (callData.role === 'receiver' && contactUri && localStream) {
142 webRtcManager.addConnection(webSocket, account, contactUri, callData, localStream, screenShareLocalStream);
143 }
144 }, [account, callData, contactUri, localStream, screenShareLocalStream, webRtcManager, webSocket]);
145
idilloncf42c652023-01-31 18:56:17 -0500146 // TODO: Transform the effect into a callback
147 const updateLocalStreams = webRtcManager.updateLocalStreams;
148 useEffect(() => {
149 if ((!localStream && !screenShareLocalStream) || !updateLocalStreams) {
150 return;
151 }
152
153 updateLocalStreams(localStream, screenShareLocalStream);
154 }, [localStream, screenShareLocalStream, updateLocalStreams]);
155
156 const sendWebRtcOffer = useCallback(async () => {
157 if (contactUri) {
158 webRtcManager.addConnection(webSocket, account, contactUri, callData, localStream, screenShareLocalStream);
159 }
160 }, [account, callData, contactUri, localStream, screenShareLocalStream, webRtcManager, webSocket]);
161
162 const closeConnection = useCallback(() => {
idillon27dab022023-02-02 17:55:47 -0500163 stopMedias();
idilloncf42c652023-01-31 18:56:17 -0500164 webRtcManager.clean();
idillon27dab022023-02-02 17:55:47 -0500165 }, [stopMedias, webRtcManager]);
simonf929a362022-11-18 16:53:45 -0500166
idillon27dab022023-02-02 17:55:47 -0500167 // Tracks logic should be moved into UserMediaProvider
simon492e8402022-11-29 16:48:37 -0500168 useEffect(() => {
simon9076a9a2022-11-29 17:13:01 -0500169 if (localStream) {
170 for (const track of localStream.getAudioTracks()) {
171 track.enabled = isAudioOn;
simon492e8402022-11-29 16:48:37 -0500172 const deviceId = track.getSettings().deviceId;
173 if (deviceId) {
174 setAudioInputDeviceId(deviceId);
175 }
simon9076a9a2022-11-29 17:13:01 -0500176 }
simonfeaa1db2022-11-26 20:13:18 -0500177 }
idillon27dab022023-02-02 17:55:47 -0500178 }, [isAudioOn, localStream, setAudioInputDeviceId]);
simonf929a362022-11-18 16:53:45 -0500179
idillon27dab022023-02-02 17:55:47 -0500180 // Tracks logic should be moved into UserMediaProvider
simonf929a362022-11-18 16:53:45 -0500181 useEffect(() => {
simonf353ef42022-11-28 23:14:53 -0500182 if (localStream) {
MichelleSS55164202022-11-25 18:36:14 -0500183 for (const track of localStream.getVideoTracks()) {
simon1e2bf342022-12-02 12:19:40 -0500184 track.enabled = videoStatus === VideoStatus.Camera;
simon492e8402022-11-29 16:48:37 -0500185 const deviceId = track.getSettings().deviceId;
186 if (deviceId) {
187 setVideoDeviceId(deviceId);
188 }
MichelleSS55164202022-11-25 18:36:14 -0500189 }
simonff1cb352022-11-24 15:15:26 -0500190 }
idillon27dab022023-02-02 17:55:47 -0500191 }, [videoStatus, localStream, setVideoDeviceId]);
simon1e2bf342022-12-02 12:19:40 -0500192
idillon27dab022023-02-02 17:55:47 -0500193 // Track logic should be moved into UserMediaProvider
simon1e2bf342022-12-02 12:19:40 -0500194 const updateVideoStatus = useCallback(
195 async (newStatus: ((prevState: VideoStatus) => VideoStatus) | VideoStatus) => {
196 if (typeof newStatus === 'function') {
197 newStatus = newStatus(videoStatus);
198 }
199
200 const stream = await updateScreenShare(newStatus === VideoStatus.ScreenShare);
201 if (stream) {
202 for (const track of stream.getTracks()) {
203 track.addEventListener('ended', () => {
204 console.warn('Browser ended screen sharing');
205 updateVideoStatus(VideoStatus.Off);
206 });
207 }
208 }
209
210 setVideoStatus(newStatus);
211 },
212 [videoStatus, updateScreenShare]
213 );
simonf929a362022-11-18 16:53:45 -0500214
215 useEffect(() => {
simon2a5cf142022-11-25 15:47:35 -0500216 const onFullscreenChange = () => {
217 setIsFullscreen(document.fullscreenElement !== null);
218 };
219
220 document.addEventListener('fullscreenchange', onFullscreenChange);
221 return () => {
222 document.removeEventListener('fullscreenchange', onFullscreenChange);
223 };
224 }, []);
225
226 useEffect(() => {
simon9076a9a2022-11-29 17:13:01 -0500227 if (callRole === 'caller' && callStatus === CallStatus.Default) {
simone35acc22022-12-02 16:51:12 -0500228 const withVideoOn = callData?.withVideoOn ?? false;
simon9076a9a2022-11-29 17:13:01 -0500229 setCallStatus(CallStatus.Loading);
simon492e8402022-11-29 16:48:37 -0500230 updateLocalStream()
simon9076a9a2022-11-29 17:13:01 -0500231 .then(() => {
232 const callBegin: CallBegin = {
233 contactId: contactUri,
234 conversationId,
simon492e8402022-11-29 16:48:37 -0500235 withVideoOn,
simon9076a9a2022-11-29 17:13:01 -0500236 };
237
238 setCallStatus(CallStatus.Ringing);
simon1e2bf342022-12-02 12:19:40 -0500239 setVideoStatus(withVideoOn ? VideoStatus.Camera : VideoStatus.Off);
simon9076a9a2022-11-29 17:13:01 -0500240 console.info('Sending CallBegin', callBegin);
241 webSocket.send(WebSocketMessageType.CallBegin, callBegin);
242 })
243 .catch((e) => {
244 console.error(e);
245 setCallStatus(CallStatus.PermissionsDenied);
246 });
247 }
simone35acc22022-12-02 16:51:12 -0500248 }, [webSocket, updateLocalStream, callRole, callStatus, contactUri, conversationId, callData]);
simon9076a9a2022-11-29 17:13:01 -0500249
250 const acceptCall = useCallback(
251 (withVideoOn: boolean) => {
252 setCallStatus(CallStatus.Loading);
simon492e8402022-11-29 16:48:37 -0500253 updateLocalStream()
simon9076a9a2022-11-29 17:13:01 -0500254 .then(() => {
255 const callAccept: CallAction = {
256 contactId: contactUri,
257 conversationId,
258 };
259
simon1e2bf342022-12-02 12:19:40 -0500260 setVideoStatus(withVideoOn ? VideoStatus.Camera : VideoStatus.Off);
simon9076a9a2022-11-29 17:13:01 -0500261 setCallStatus(CallStatus.Connecting);
262 console.info('Sending CallAccept', callAccept);
263 webSocket.send(WebSocketMessageType.CallAccept, callAccept);
264 })
265 .catch((e) => {
266 console.error(e);
267 setCallStatus(CallStatus.PermissionsDenied);
268 });
269 },
simon492e8402022-11-29 16:48:37 -0500270 [webSocket, updateLocalStream, contactUri, conversationId]
simon9076a9a2022-11-29 17:13:01 -0500271 );
272
273 useEffect(() => {
simonf929a362022-11-18 16:53:45 -0500274 if (callRole === 'caller' && callStatus === CallStatus.Ringing) {
simonaccd8022022-11-24 15:04:53 -0500275 const callAcceptListener = (data: CallAction) => {
276 console.info('Received event on CallAccept', data);
Charliec18d6402022-11-27 13:01:04 -0500277 if (data.conversationId !== conversationId) {
278 console.warn('Wrong incoming conversationId, ignoring action');
279 return;
280 }
281
simonf929a362022-11-18 16:53:45 -0500282 setCallStatus(CallStatus.Connecting);
283
simon9076a9a2022-11-29 17:13:01 -0500284 sendWebRtcOffer();
simonf929a362022-11-18 16:53:45 -0500285 };
286
287 webSocket.bind(WebSocketMessageType.CallAccept, callAcceptListener);
288
289 return () => {
290 webSocket.unbind(WebSocketMessageType.CallAccept, callAcceptListener);
291 };
292 }
simon9076a9a2022-11-29 17:13:01 -0500293 }, [callRole, webSocket, sendWebRtcOffer, callStatus, conversationId]);
simonf929a362022-11-18 16:53:45 -0500294
simon9076a9a2022-11-29 17:13:01 -0500295 const endCall = useCallback(() => {
296 const callEnd: CallAction = {
297 contactId: contactUri,
298 conversationId,
299 };
MichelleSS55164202022-11-25 18:36:14 -0500300
simon9076a9a2022-11-29 17:13:01 -0500301 console.info('Sending CallEnd', callEnd);
302 closeConnection();
303 webSocket.send(WebSocketMessageType.CallEnd, callEnd);
simone35acc22022-12-02 16:51:12 -0500304 exitCall();
simon9076a9a2022-11-29 17:13:01 -0500305 // TODO: write in chat that the call ended
simone35acc22022-12-02 16:51:12 -0500306 }, [webSocket, contactUri, conversationId, closeConnection, exitCall]);
simonaccd8022022-11-24 15:04:53 -0500307
308 useEffect(() => {
simonaccd8022022-11-24 15:04:53 -0500309 const callEndListener = (data: CallAction) => {
310 console.info('Received event on CallEnd', data);
Charliec18d6402022-11-27 13:01:04 -0500311 if (data.conversationId !== conversationId) {
312 console.warn('Wrong incoming conversationId, ignoring action');
313 return;
314 }
315
simon9076a9a2022-11-29 17:13:01 -0500316 closeConnection();
simone35acc22022-12-02 16:51:12 -0500317 exitCall();
simonaccd8022022-11-24 15:04:53 -0500318 // TODO: write in chat that the call ended
319 };
320
321 webSocket.bind(WebSocketMessageType.CallEnd, callEndListener);
322 return () => {
323 webSocket.unbind(WebSocketMessageType.CallEnd, callEndListener);
324 };
simone35acc22022-12-02 16:51:12 -0500325 }, [webSocket, exitCall, conversationId, closeConnection]);
simonaccd8022022-11-24 15:04:53 -0500326
simonf929a362022-11-18 16:53:45 -0500327 useEffect(() => {
Charlieb837e8f2022-11-28 19:18:46 -0500328 if (
329 callStatus === CallStatus.Connecting &&
330 (iceConnectionState === 'connected' || iceConnectionState === 'completed')
331 ) {
simonf929a362022-11-18 16:53:45 -0500332 console.info('Changing call status to InCall');
333 setCallStatus(CallStatus.InCall);
Misha Krieger-Raynauldd0cc3e32022-11-29 19:59:31 -0500334 setCallStartTime(Date.now());
simonf929a362022-11-18 16:53:45 -0500335 }
simon9076a9a2022-11-29 17:13:01 -0500336 }, [iceConnectionState, callStatus]);
simonaccd8022022-11-24 15:04:53 -0500337
MichelleSS55164202022-11-25 18:36:14 -0500338 useEffect(() => {
Charlie380dc5e2022-11-29 16:51:42 -0500339 if (iceConnectionState === 'disconnected' || iceConnectionState === 'failed') {
340 console.info('ICE connection disconnected or failed, ending call');
Charlieb837e8f2022-11-28 19:18:46 -0500341 endCall();
342 }
simon1e2bf342022-12-02 12:19:40 -0500343 }, [iceConnectionState, callStatus, videoStatus, endCall]);
Charlieb837e8f2022-11-28 19:18:46 -0500344
345 useEffect(() => {
MichelleSS55164202022-11-25 18:36:14 -0500346 const checkStatusTimeout = () => {
347 if (callStatus !== CallStatus.InCall) {
348 endCall();
simonff1cb352022-11-24 15:15:26 -0500349 }
MichelleSS55164202022-11-25 18:36:14 -0500350 };
351 const timeoutId = setTimeout(checkStatusTimeout, callTimeoutMs);
simonff1cb352022-11-24 15:15:26 -0500352
MichelleSS55164202022-11-25 18:36:14 -0500353 return () => {
354 clearTimeout(timeoutId);
355 };
356 }, [callStatus, endCall]);
simonff1cb352022-11-24 15:15:26 -0500357
simon5c677962022-12-02 16:51:54 -0500358 return useMemo(
359 () => ({
idilloncf42c652023-01-31 18:56:17 -0500360 remoteStreams,
simon5c677962022-12-02 16:51:54 -0500361 isAudioOn,
362 setIsAudioOn,
363 videoStatus,
364 updateVideoStatus,
365 isChatShown,
366 setIsChatShown,
367 isFullscreen,
368 setIsFullscreen,
369 callRole,
370 callStatus,
371 callStartTime,
372 acceptCall,
373 endCall,
374 }),
375 [
idilloncf42c652023-01-31 18:56:17 -0500376 remoteStreams,
simon5c677962022-12-02 16:51:54 -0500377 isAudioOn,
simon5c677962022-12-02 16:51:54 -0500378 videoStatus,
idillon27dab022023-02-02 17:55:47 -0500379 setIsAudioOn,
simon5c677962022-12-02 16:51:54 -0500380 updateVideoStatus,
381 isChatShown,
idillon27dab022023-02-02 17:55:47 -0500382 setIsChatShown,
simon5c677962022-12-02 16:51:54 -0500383 isFullscreen,
idillon27dab022023-02-02 17:55:47 -0500384 setIsFullscreen,
simon5c677962022-12-02 16:51:54 -0500385 callRole,
386 callStatus,
387 callStartTime,
388 acceptCall,
389 endCall,
390 ]
simonf929a362022-11-18 16:53:45 -0500391 );
392};