blob: 31b66731e9f946dc6b47c420869f9e9e5ba8f199 [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;
Gabriel Rochone382a302022-11-23 12:37:04 -050054 callStartTime: Date | 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);
Gabriel Rochone382a302022-11-23 12:37:04 -0500106 const [callStartTime, setCallStartTime] = useState<Date | 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);
166
167 getUserMedia()
168 .then(() => {
169 const callAccept: CallAction = {
170 contactId: contactUri,
171 conversationId,
172 };
173
174 setIsVideoOn(withVideoOn);
175 setCallStatus(CallStatus.Connecting);
176 console.info('Sending CallAccept', callAccept);
177 webSocket.send(WebSocketMessageType.CallAccept, callAccept);
178 })
179 .catch((e) => {
180 console.error(e);
181 setCallStatus(CallStatus.PermissionsDenied);
182 });
183 },
184 [webSocket, getUserMedia, contactUri, conversationId]
185 );
186
187 useEffect(() => {
simonf929a362022-11-18 16:53:45 -0500188 if (callRole === 'caller' && callStatus === CallStatus.Ringing) {
simonaccd8022022-11-24 15:04:53 -0500189 const callAcceptListener = (data: CallAction) => {
190 console.info('Received event on CallAccept', data);
Charliec18d6402022-11-27 13:01:04 -0500191 if (data.conversationId !== conversationId) {
192 console.warn('Wrong incoming conversationId, ignoring action');
193 return;
194 }
195
simonf929a362022-11-18 16:53:45 -0500196 setCallStatus(CallStatus.Connecting);
197
simon9076a9a2022-11-29 17:13:01 -0500198 sendWebRtcOffer();
simonf929a362022-11-18 16:53:45 -0500199 };
200
201 webSocket.bind(WebSocketMessageType.CallAccept, callAcceptListener);
202
203 return () => {
204 webSocket.unbind(WebSocketMessageType.CallAccept, callAcceptListener);
205 };
206 }
simon9076a9a2022-11-29 17:13:01 -0500207 }, [callRole, webSocket, sendWebRtcOffer, callStatus, conversationId]);
simonf929a362022-11-18 16:53:45 -0500208
simon9076a9a2022-11-29 17:13:01 -0500209 const endCall = useCallback(() => {
210 const callEnd: CallAction = {
211 contactId: contactUri,
212 conversationId,
213 };
MichelleSS55164202022-11-25 18:36:14 -0500214
simon9076a9a2022-11-29 17:13:01 -0500215 console.info('Sending CallEnd', callEnd);
216 closeConnection();
217 webSocket.send(WebSocketMessageType.CallEnd, callEnd);
simonaccd8022022-11-24 15:04:53 -0500218 navigate(`/conversation/${conversationId}`);
simon9076a9a2022-11-29 17:13:01 -0500219 // TODO: write in chat that the call ended
220 }, [webSocket, contactUri, conversationId, closeConnection, navigate]);
simonaccd8022022-11-24 15:04:53 -0500221
222 useEffect(() => {
simonaccd8022022-11-24 15:04:53 -0500223 const callEndListener = (data: CallAction) => {
224 console.info('Received event on CallEnd', data);
Charliec18d6402022-11-27 13:01:04 -0500225 if (data.conversationId !== conversationId) {
226 console.warn('Wrong incoming conversationId, ignoring action');
227 return;
228 }
229
simon9076a9a2022-11-29 17:13:01 -0500230 closeConnection();
231 navigate(`/conversation/${conversationId}`);
simonaccd8022022-11-24 15:04:53 -0500232 // TODO: write in chat that the call ended
233 };
234
235 webSocket.bind(WebSocketMessageType.CallEnd, callEndListener);
236 return () => {
237 webSocket.unbind(WebSocketMessageType.CallEnd, callEndListener);
238 };
simon9076a9a2022-11-29 17:13:01 -0500239 }, [webSocket, navigate, conversationId, closeConnection]);
simonaccd8022022-11-24 15:04:53 -0500240
simonf929a362022-11-18 16:53:45 -0500241 useEffect(() => {
Charlieb837e8f2022-11-28 19:18:46 -0500242 if (
243 callStatus === CallStatus.Connecting &&
244 (iceConnectionState === 'connected' || iceConnectionState === 'completed')
245 ) {
simonf929a362022-11-18 16:53:45 -0500246 console.info('Changing call status to InCall');
247 setCallStatus(CallStatus.InCall);
Gabriel Rochone382a302022-11-23 12:37:04 -0500248 setCallStartTime(new Date());
simonf929a362022-11-18 16:53:45 -0500249 }
simon9076a9a2022-11-29 17:13:01 -0500250 }, [iceConnectionState, callStatus]);
simonaccd8022022-11-24 15:04:53 -0500251
MichelleSS55164202022-11-25 18:36:14 -0500252 useEffect(() => {
Charlie380dc5e2022-11-29 16:51:42 -0500253 if (iceConnectionState === 'disconnected' || iceConnectionState === 'failed') {
254 console.info('ICE connection disconnected or failed, ending call');
Charlieb837e8f2022-11-28 19:18:46 -0500255 endCall();
256 }
simon9076a9a2022-11-29 17:13:01 -0500257 }, [iceConnectionState, callStatus, isVideoOn, endCall]);
Charlieb837e8f2022-11-28 19:18:46 -0500258
259 useEffect(() => {
MichelleSS55164202022-11-25 18:36:14 -0500260 const checkStatusTimeout = () => {
261 if (callStatus !== CallStatus.InCall) {
262 endCall();
simonff1cb352022-11-24 15:15:26 -0500263 }
MichelleSS55164202022-11-25 18:36:14 -0500264 };
265 const timeoutId = setTimeout(checkStatusTimeout, callTimeoutMs);
simonff1cb352022-11-24 15:15:26 -0500266
MichelleSS55164202022-11-25 18:36:14 -0500267 return () => {
268 clearTimeout(timeoutId);
269 };
270 }, [callStatus, endCall]);
simonff1cb352022-11-24 15:15:26 -0500271
Charlieb837e8f2022-11-28 19:18:46 -0500272 useEffect(() => {
273 navigate('.', {
274 replace: true,
275 state: {},
276 });
277 }, [navigate]);
278
simonff1cb352022-11-24 15:15:26 -0500279 if (!callRole || callStatus === undefined) {
280 console.error('Invalid route. Redirecting...');
simonf929a362022-11-18 16:53:45 -0500281 return <Navigate to={'/'} />;
282 }
283
284 return (
285 <CallContext.Provider
286 value={{
simonf929a362022-11-18 16:53:45 -0500287 isAudioOn,
simon9076a9a2022-11-29 17:13:01 -0500288 setIsAudioOn,
simonf929a362022-11-18 16:53:45 -0500289 isVideoOn,
simon9076a9a2022-11-29 17:13:01 -0500290 setIsVideoOn,
simonf9d78f22022-11-25 15:47:15 -0500291 isChatShown,
292 setIsChatShown,
simon2a5cf142022-11-25 15:47:35 -0500293 isFullscreen,
294 setIsFullscreen,
simonf929a362022-11-18 16:53:45 -0500295 callRole,
296 callStatus,
Gabriel Rochone382a302022-11-23 12:37:04 -0500297 callStartTime,
simonf929a362022-11-18 16:53:45 -0500298 acceptCall,
simonaccd8022022-11-24 15:04:53 -0500299 endCall,
simonf929a362022-11-18 16:53:45 -0500300 }}
301 >
simon9076a9a2022-11-29 17:13:01 -0500302 {callStatus === CallStatus.PermissionsDenied ? <CallPermissionDenied /> : children}
simonf929a362022-11-18 16:53:45 -0500303 </CallContext.Provider>
304 );
305};