blob: c32704b88e2f70ea1d590320cf531ef52f623804 [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';
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050019import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
simonaccd8022022-11-24 15:04:53 -050020import { Navigate, useNavigate } from 'react-router-dom';
simonf929a362022-11-18 16:53:45 -050021
simonf353ef42022-11-28 23:14:53 -050022import LoadingPage from '../components/Loading';
simonf929a362022-11-18 16:53:45 -050023import { useUrlParams } from '../hooks/useUrlParams';
simon9076a9a2022-11-29 17:13:01 -050024import CallPermissionDenied from '../pages/CallPermissionDenied';
simonf929a362022-11-18 16:53:45 -050025import { CallRouteParams } from '../router';
MichelleSS55164202022-11-25 18:36:14 -050026import { callTimeoutMs } from '../utils/constants';
simonf9d78f22022-11-25 15:47:15 -050027import { SetState, WithChildren } from '../utils/utils';
simonf929a362022-11-18 16:53:45 -050028import { ConversationContext } from './ConversationProvider';
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050029import { WebRtcContext } from './WebRtcProvider';
simonf353ef42022-11-28 23:14:53 -050030import { IWebSocketContext, WebSocketContext } 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
43export interface ICallContext {
simonf929a362022-11-18 16:53:45 -050044 isAudioOn: boolean;
simon9076a9a2022-11-29 17:13:01 -050045 setIsAudioOn: SetState<boolean>;
simonf929a362022-11-18 16:53:45 -050046 isVideoOn: boolean;
simon9076a9a2022-11-29 17:13:01 -050047 setIsVideoOn: SetState<boolean>;
simonf9d78f22022-11-25 15:47:15 -050048 isChatShown: boolean;
49 setIsChatShown: SetState<boolean>;
simon2a5cf142022-11-25 15:47:35 -050050 isFullscreen: boolean;
51 setIsFullscreen: SetState<boolean>;
simonf929a362022-11-18 16:53:45 -050052 callRole: CallRole;
53 callStatus: CallStatus;
Misha Krieger-Raynauldd0cc3e32022-11-29 19:59:31 -050054 callStartTime: number | undefined;
simonf929a362022-11-18 16:53:45 -050055
MichelleSS55164202022-11-25 18:36:14 -050056 acceptCall: (withVideoOn: boolean) => void;
simonaccd8022022-11-24 15:04:53 -050057 endCall: () => void;
simonf929a362022-11-18 16:53:45 -050058}
59
60const defaultCallContext: ICallContext = {
simonf929a362022-11-18 16:53:45 -050061 isAudioOn: false,
simon9076a9a2022-11-29 17:13:01 -050062 setIsAudioOn: () => {},
simonf929a362022-11-18 16:53:45 -050063 isVideoOn: false,
simon9076a9a2022-11-29 17:13:01 -050064 setIsVideoOn: () => {},
simonf9d78f22022-11-25 15:47:15 -050065 isChatShown: false,
66 setIsChatShown: () => {},
simon2a5cf142022-11-25 15:47:35 -050067 isFullscreen: false,
68 setIsFullscreen: () => {},
simonf929a362022-11-18 16:53:45 -050069 callRole: 'caller',
simonff1cb352022-11-24 15:15:26 -050070 callStatus: CallStatus.Default,
Gabriel Rochone382a302022-11-23 12:37:04 -050071 callStartTime: undefined,
simonf929a362022-11-18 16:53:45 -050072
MichelleSS55164202022-11-25 18:36:14 -050073 acceptCall: (_: boolean) => {},
simonaccd8022022-11-24 15:04:53 -050074 endCall: () => {},
simonf929a362022-11-18 16:53:45 -050075};
76
77export const CallContext = createContext<ICallContext>(defaultCallContext);
78
79export default ({ children }: WithChildren) => {
simonf353ef42022-11-28 23:14:53 -050080 const webSocket = useContext(WebSocketContext);
simonf353ef42022-11-28 23:14:53 -050081
simon9076a9a2022-11-29 17:13:01 -050082 if (!webSocket) {
simonf353ef42022-11-28 23:14:53 -050083 return <LoadingPage />;
84 }
85
simon9076a9a2022-11-29 17:13:01 -050086 return <CallProvider webSocket={webSocket}>{children}</CallProvider>;
simonf353ef42022-11-28 23:14:53 -050087};
88
89const CallProvider = ({
90 children,
91 webSocket,
simonf353ef42022-11-28 23:14:53 -050092}: WithChildren & {
93 webSocket: IWebSocketContext;
simonf353ef42022-11-28 23:14:53 -050094}) => {
Charlieb837e8f2022-11-28 19:18:46 -050095 const { state: routeState } = useUrlParams<CallRouteParams>();
simon9076a9a2022-11-29 17:13:01 -050096 const { localStream, sendWebRtcOffer, iceConnectionState, closeConnection, getUserMedia } = useContext(WebRtcContext);
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050097 const { conversationId, conversation } = useContext(ConversationContext);
simonaccd8022022-11-24 15:04:53 -050098 const navigate = useNavigate();
simonf929a362022-11-18 16:53:45 -050099
simonf929a362022-11-18 16:53:45 -0500100 const [isAudioOn, setIsAudioOn] = useState(false);
101 const [isVideoOn, setIsVideoOn] = useState(false);
simonf9d78f22022-11-25 15:47:15 -0500102 const [isChatShown, setIsChatShown] = useState(false);
simon2a5cf142022-11-25 15:47:35 -0500103 const [isFullscreen, setIsFullscreen] = useState(false);
simonff1cb352022-11-24 15:15:26 -0500104 const [callStatus, setCallStatus] = useState(routeState?.callStatus);
Charlieb837e8f2022-11-28 19:18:46 -0500105 const [callRole] = useState(routeState?.role);
Misha Krieger-Raynauldd0cc3e32022-11-29 19:59:31 -0500106 const [callStartTime, setCallStartTime] = useState<number | undefined>(undefined);
simonf929a362022-11-18 16:53:45 -0500107
simonff1cb352022-11-24 15:15:26 -0500108 // TODO: This logic will have to change to support multiple people in a call. Could we move this logic to the server?
109 // The client could make a single request with the conversationId, and the server would be tasked with sending
110 // all the individual requests to the members of the conversation.
simonf929a362022-11-18 16:53:45 -0500111 const contactUri = useMemo(() => conversation.getFirstMember().contact.getUri(), [conversation]);
112
113 useEffect(() => {
simon9076a9a2022-11-29 17:13:01 -0500114 if (localStream) {
115 for (const track of localStream.getAudioTracks()) {
116 track.enabled = isAudioOn;
117 }
simonfeaa1db2022-11-26 20:13:18 -0500118 }
simon9076a9a2022-11-29 17:13:01 -0500119 }, [isAudioOn, localStream]);
simonf929a362022-11-18 16:53:45 -0500120
121 useEffect(() => {
simonf353ef42022-11-28 23:14:53 -0500122 if (localStream) {
MichelleSS55164202022-11-25 18:36:14 -0500123 for (const track of localStream.getVideoTracks()) {
simon9076a9a2022-11-29 17:13:01 -0500124 track.enabled = isVideoOn;
MichelleSS55164202022-11-25 18:36:14 -0500125 }
simonff1cb352022-11-24 15:15:26 -0500126 }
simon9076a9a2022-11-29 17:13:01 -0500127 }, [isVideoOn, localStream]);
simonf929a362022-11-18 16:53:45 -0500128
129 useEffect(() => {
simon2a5cf142022-11-25 15:47:35 -0500130 const onFullscreenChange = () => {
131 setIsFullscreen(document.fullscreenElement !== null);
132 };
133
134 document.addEventListener('fullscreenchange', onFullscreenChange);
135 return () => {
136 document.removeEventListener('fullscreenchange', onFullscreenChange);
137 };
138 }, []);
139
140 useEffect(() => {
simon9076a9a2022-11-29 17:13:01 -0500141 if (callRole === 'caller' && callStatus === CallStatus.Default) {
142 setCallStatus(CallStatus.Loading);
143 getUserMedia()
144 .then(() => {
145 const callBegin: CallBegin = {
146 contactId: contactUri,
147 conversationId,
148 withVideoOn: routeState?.isVideoOn ?? false,
149 };
150
151 setCallStatus(CallStatus.Ringing);
152 setIsVideoOn(routeState?.isVideoOn ?? false);
153 console.info('Sending CallBegin', callBegin);
154 webSocket.send(WebSocketMessageType.CallBegin, callBegin);
155 })
156 .catch((e) => {
157 console.error(e);
158 setCallStatus(CallStatus.PermissionsDenied);
159 });
160 }
161 }, [webSocket, getUserMedia, callRole, callStatus, contactUri, conversationId, routeState]);
162
163 const acceptCall = useCallback(
164 (withVideoOn: boolean) => {
165 setCallStatus(CallStatus.Loading);
simon9076a9a2022-11-29 17:13:01 -0500166 getUserMedia()
167 .then(() => {
168 const callAccept: CallAction = {
169 contactId: contactUri,
170 conversationId,
171 };
172
173 setIsVideoOn(withVideoOn);
174 setCallStatus(CallStatus.Connecting);
175 console.info('Sending CallAccept', callAccept);
176 webSocket.send(WebSocketMessageType.CallAccept, callAccept);
177 })
178 .catch((e) => {
179 console.error(e);
180 setCallStatus(CallStatus.PermissionsDenied);
181 });
182 },
183 [webSocket, getUserMedia, contactUri, conversationId]
184 );
185
186 useEffect(() => {
simonf929a362022-11-18 16:53:45 -0500187 if (callRole === 'caller' && callStatus === CallStatus.Ringing) {
simonaccd8022022-11-24 15:04:53 -0500188 const callAcceptListener = (data: CallAction) => {
189 console.info('Received event on CallAccept', data);
Charliec18d6402022-11-27 13:01:04 -0500190 if (data.conversationId !== conversationId) {
191 console.warn('Wrong incoming conversationId, ignoring action');
192 return;
193 }
194
simonf929a362022-11-18 16:53:45 -0500195 setCallStatus(CallStatus.Connecting);
196
simon9076a9a2022-11-29 17:13:01 -0500197 sendWebRtcOffer();
simonf929a362022-11-18 16:53:45 -0500198 };
199
200 webSocket.bind(WebSocketMessageType.CallAccept, callAcceptListener);
201
202 return () => {
203 webSocket.unbind(WebSocketMessageType.CallAccept, callAcceptListener);
204 };
205 }
simon9076a9a2022-11-29 17:13:01 -0500206 }, [callRole, webSocket, sendWebRtcOffer, callStatus, conversationId]);
simonf929a362022-11-18 16:53:45 -0500207
simon9076a9a2022-11-29 17:13:01 -0500208 const endCall = useCallback(() => {
209 const callEnd: CallAction = {
210 contactId: contactUri,
211 conversationId,
212 };
MichelleSS55164202022-11-25 18:36:14 -0500213
simon9076a9a2022-11-29 17:13:01 -0500214 console.info('Sending CallEnd', callEnd);
215 closeConnection();
216 webSocket.send(WebSocketMessageType.CallEnd, callEnd);
simonaccd8022022-11-24 15:04:53 -0500217 navigate(`/conversation/${conversationId}`);
simon9076a9a2022-11-29 17:13:01 -0500218 // TODO: write in chat that the call ended
219 }, [webSocket, contactUri, conversationId, closeConnection, navigate]);
simonaccd8022022-11-24 15:04:53 -0500220
221 useEffect(() => {
simonaccd8022022-11-24 15:04:53 -0500222 const callEndListener = (data: CallAction) => {
223 console.info('Received event on CallEnd', data);
Charliec18d6402022-11-27 13:01:04 -0500224 if (data.conversationId !== conversationId) {
225 console.warn('Wrong incoming conversationId, ignoring action');
226 return;
227 }
228
simon9076a9a2022-11-29 17:13:01 -0500229 closeConnection();
230 navigate(`/conversation/${conversationId}`);
simonaccd8022022-11-24 15:04:53 -0500231 // TODO: write in chat that the call ended
232 };
233
234 webSocket.bind(WebSocketMessageType.CallEnd, callEndListener);
235 return () => {
236 webSocket.unbind(WebSocketMessageType.CallEnd, callEndListener);
237 };
simon9076a9a2022-11-29 17:13:01 -0500238 }, [webSocket, navigate, conversationId, closeConnection]);
simonaccd8022022-11-24 15:04:53 -0500239
simonf929a362022-11-18 16:53:45 -0500240 useEffect(() => {
Charlieb837e8f2022-11-28 19:18:46 -0500241 if (
242 callStatus === CallStatus.Connecting &&
243 (iceConnectionState === 'connected' || iceConnectionState === 'completed')
244 ) {
simonf929a362022-11-18 16:53:45 -0500245 console.info('Changing call status to InCall');
246 setCallStatus(CallStatus.InCall);
Misha Krieger-Raynauldd0cc3e32022-11-29 19:59:31 -0500247 setCallStartTime(Date.now());
simonf929a362022-11-18 16:53:45 -0500248 }
simon9076a9a2022-11-29 17:13:01 -0500249 }, [iceConnectionState, callStatus]);
simonaccd8022022-11-24 15:04:53 -0500250
MichelleSS55164202022-11-25 18:36:14 -0500251 useEffect(() => {
Charlie380dc5e2022-11-29 16:51:42 -0500252 if (iceConnectionState === 'disconnected' || iceConnectionState === 'failed') {
253 console.info('ICE connection disconnected or failed, ending call');
Charlieb837e8f2022-11-28 19:18:46 -0500254 endCall();
255 }
simon9076a9a2022-11-29 17:13:01 -0500256 }, [iceConnectionState, callStatus, isVideoOn, endCall]);
Charlieb837e8f2022-11-28 19:18:46 -0500257
258 useEffect(() => {
MichelleSS55164202022-11-25 18:36:14 -0500259 const checkStatusTimeout = () => {
260 if (callStatus !== CallStatus.InCall) {
261 endCall();
simonff1cb352022-11-24 15:15:26 -0500262 }
MichelleSS55164202022-11-25 18:36:14 -0500263 };
264 const timeoutId = setTimeout(checkStatusTimeout, callTimeoutMs);
simonff1cb352022-11-24 15:15:26 -0500265
MichelleSS55164202022-11-25 18:36:14 -0500266 return () => {
267 clearTimeout(timeoutId);
268 };
269 }, [callStatus, endCall]);
simonff1cb352022-11-24 15:15:26 -0500270
Charlieb837e8f2022-11-28 19:18:46 -0500271 useEffect(() => {
272 navigate('.', {
273 replace: true,
274 state: {},
275 });
276 }, [navigate]);
277
simonff1cb352022-11-24 15:15:26 -0500278 if (!callRole || callStatus === undefined) {
279 console.error('Invalid route. Redirecting...');
simonf929a362022-11-18 16:53:45 -0500280 return <Navigate to={'/'} />;
281 }
282
283 return (
284 <CallContext.Provider
285 value={{
simonf929a362022-11-18 16:53:45 -0500286 isAudioOn,
simon9076a9a2022-11-29 17:13:01 -0500287 setIsAudioOn,
simonf929a362022-11-18 16:53:45 -0500288 isVideoOn,
simon9076a9a2022-11-29 17:13:01 -0500289 setIsVideoOn,
simonf9d78f22022-11-25 15:47:15 -0500290 isChatShown,
291 setIsChatShown,
simon2a5cf142022-11-25 15:47:35 -0500292 isFullscreen,
293 setIsFullscreen,
simonf929a362022-11-18 16:53:45 -0500294 callRole,
295 callStatus,
Gabriel Rochone382a302022-11-23 12:37:04 -0500296 callStartTime,
simonf929a362022-11-18 16:53:45 -0500297 acceptCall,
simonaccd8022022-11-24 15:04:53 -0500298 endCall,
simonf929a362022-11-18 16:53:45 -0500299 }}
300 >
simon9076a9a2022-11-29 17:13:01 -0500301 {callStatus === CallStatus.PermissionsDenied ? <CallPermissionDenied /> : children}
simonf929a362022-11-18 16:53:45 -0500302 </CallContext.Provider>
303 );
304};