blob: 559500097ca80b2993a3e7513755e780bb97ff0c [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 */
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050018import { CallAction, WebSocketMessageType } from 'jami-web-common';
19import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
simonf929a362022-11-18 16:53:45 -050020import { Navigate } from 'react-router-dom';
21
22import { useUrlParams } from '../hooks/useUrlParams';
23import { CallRouteParams } from '../router';
simonf9d78f22022-11-25 15:47:15 -050024import { SetState, WithChildren } from '../utils/utils';
simonf929a362022-11-18 16:53:45 -050025import { ConversationContext } from './ConversationProvider';
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050026import { WebRtcContext } from './WebRtcProvider';
simonf929a362022-11-18 16:53:45 -050027import { WebSocketContext } from './WebSocketProvider';
28
29export type CallRole = 'caller' | 'receiver';
30
31export enum CallStatus {
simonff1cb352022-11-24 15:15:26 -050032 Default,
simonf929a362022-11-18 16:53:45 -050033 Ringing,
34 Connecting,
35 InCall,
36}
37
38export interface ICallContext {
39 mediaDevices: Record<MediaDeviceKind, MediaDeviceInfo[]>;
40
41 localStream: MediaStream | undefined;
42 remoteStream: MediaStream | undefined; // TODO: should be an array of participants. find a way to map MediaStream id to contactid https://stackoverflow.com/a/68663155/6592293
43
44 isAudioOn: boolean;
45 setAudioStatus: (isOn: boolean) => void;
46 isVideoOn: boolean;
47 setVideoStatus: (isOn: boolean) => void;
simonf9d78f22022-11-25 15:47:15 -050048 isChatShown: boolean;
49 setIsChatShown: SetState<boolean>;
simonf929a362022-11-18 16:53:45 -050050 callRole: CallRole;
51 callStatus: CallStatus;
52
53 acceptCall: () => void;
54}
55
56const defaultCallContext: ICallContext = {
57 mediaDevices: {
58 audioinput: [],
59 audiooutput: [],
60 videoinput: [],
61 },
62
63 localStream: undefined,
64 remoteStream: undefined,
65
66 isAudioOn: false,
67 setAudioStatus: () => {},
68 isVideoOn: false,
69 setVideoStatus: () => {},
simonf9d78f22022-11-25 15:47:15 -050070 isChatShown: false,
71 setIsChatShown: () => {},
simonf929a362022-11-18 16:53:45 -050072 callRole: 'caller',
simonff1cb352022-11-24 15:15:26 -050073 callStatus: CallStatus.Default,
simonf929a362022-11-18 16:53:45 -050074
75 acceptCall: () => {},
76};
77
78export const CallContext = createContext<ICallContext>(defaultCallContext);
79
80export default ({ children }: WithChildren) => {
81 const {
82 queryParams: { role: callRole },
simonff1cb352022-11-24 15:15:26 -050083 state: routeState,
simonf929a362022-11-18 16:53:45 -050084 } = useUrlParams<CallRouteParams>();
simonf929a362022-11-18 16:53:45 -050085 const webSocket = useContext(WebSocketContext);
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050086 const { webRtcConnection, remoteStreams, sendWebRtcOffer, isConnected } = useContext(WebRtcContext);
87 const { conversationId, conversation } = useContext(ConversationContext);
simonf929a362022-11-18 16:53:45 -050088
89 const [mediaDevices, setMediaDevices] = useState<Record<MediaDeviceKind, MediaDeviceInfo[]>>(
90 defaultCallContext.mediaDevices
91 );
92 const [localStream, setLocalStream] = useState<MediaStream>();
93
94 const [isAudioOn, setIsAudioOn] = useState(false);
95 const [isVideoOn, setIsVideoOn] = useState(false);
simonf9d78f22022-11-25 15:47:15 -050096 const [isChatShown, setIsChatShown] = useState(false);
simonff1cb352022-11-24 15:15:26 -050097 const [callStatus, setCallStatus] = useState(routeState?.callStatus);
simonf929a362022-11-18 16:53:45 -050098
simonff1cb352022-11-24 15:15:26 -050099 // TODO: This logic will have to change to support multiple people in a call. Could we move this logic to the server?
100 // The client could make a single request with the conversationId, and the server would be tasked with sending
101 // all the individual requests to the members of the conversation.
simonf929a362022-11-18 16:53:45 -0500102 const contactUri = useMemo(() => conversation.getFirstMember().contact.getUri(), [conversation]);
103
104 useEffect(() => {
105 // TODO: Wait until status is `InCall` before getting devices
106 navigator.mediaDevices.enumerateDevices().then((devices) => {
107 const newMediaDevices: Record<MediaDeviceKind, MediaDeviceInfo[]> = {
108 audioinput: [],
109 audiooutput: [],
110 videoinput: [],
111 };
112
113 for (const device of devices) {
114 newMediaDevices[device.kind].push(device);
115 }
116
117 setMediaDevices(newMediaDevices);
118 });
119 }, []);
120
121 useEffect(() => {
122 // TODO: Only ask media permission once the call has been accepted
123 try {
124 // TODO: When toggling mute on/off, the camera flickers
125 // https://git.jami.net/savoirfairelinux/jami-web/-/issues/90
126 navigator.mediaDevices
127 .getUserMedia({
128 audio: true, // TODO: Set both to false by default
129 video: true,
130 })
131 .then((stream) => {
132 for (const track of stream.getTracks()) {
133 // TODO: Set default from isVideoOn and isMicOn values
134 track.enabled = false;
135 }
136 setLocalStream(stream);
137 });
138 } catch (e) {
139 // TODO: Better handle user denial
140 console.error('Could not get media devices:', e);
141 }
142 }, [setLocalStream]);
143
144 useEffect(() => {
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500145 if (localStream && webRtcConnection) {
simonf929a362022-11-18 16:53:45 -0500146 for (const track of localStream.getTracks()) {
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500147 webRtcConnection.addTrack(track, localStream);
simonf929a362022-11-18 16:53:45 -0500148 }
149 }
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500150 }, [localStream, webRtcConnection]);
simonf929a362022-11-18 16:53:45 -0500151
simonff1cb352022-11-24 15:15:26 -0500152 useEffect(() => {
153 if (!webSocket) {
154 return;
155 }
simonf929a362022-11-18 16:53:45 -0500156
simonff1cb352022-11-24 15:15:26 -0500157 if (callRole === 'caller' && callStatus === CallStatus.Default) {
158 const callBegin: CallAction = {
159 contactId: contactUri,
160 conversationId,
161 };
simonf929a362022-11-18 16:53:45 -0500162
simonff1cb352022-11-24 15:15:26 -0500163 console.info('Sending CallBegin', callBegin);
164 webSocket.send(WebSocketMessageType.CallBegin, callBegin);
165 setCallStatus(CallStatus.Ringing);
166 }
167 }, [webSocket, callRole, callStatus, contactUri, conversationId]);
simonf929a362022-11-18 16:53:45 -0500168
169 useEffect(() => {
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500170 if (!webSocket || !webRtcConnection) {
simonf929a362022-11-18 16:53:45 -0500171 return;
172 }
173
174 if (callRole === 'caller' && callStatus === CallStatus.Ringing) {
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500175 const callAcceptListener = (_data: CallAction) => {
176 console.info('Received event on CallAccept');
simonf929a362022-11-18 16:53:45 -0500177 setCallStatus(CallStatus.Connecting);
178
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500179 webRtcConnection
simonf929a362022-11-18 16:53:45 -0500180 .createOffer({
181 offerToReceiveAudio: true,
182 offerToReceiveVideo: true,
183 })
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500184 .then((sdp) => {
185 sendWebRtcOffer(sdp);
simonf929a362022-11-18 16:53:45 -0500186 });
187 };
188
189 webSocket.bind(WebSocketMessageType.CallAccept, callAcceptListener);
190
191 return () => {
192 webSocket.unbind(WebSocketMessageType.CallAccept, callAcceptListener);
193 };
194 }
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500195 }, [callRole, webSocket, webRtcConnection, sendWebRtcOffer, callStatus]);
simonf929a362022-11-18 16:53:45 -0500196
197 useEffect(() => {
198 if (callStatus === CallStatus.Connecting && isConnected) {
199 console.info('Changing call status to InCall');
200 setCallStatus(CallStatus.InCall);
201 }
202 }, [isConnected, callStatus]);
203
204 const acceptCall = useCallback(() => {
205 if (!webSocket) {
206 throw new Error('Could not accept call');
207 }
208
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500209 const callAccept: CallAction = {
210 contactId: contactUri,
211 conversationId,
simonf929a362022-11-18 16:53:45 -0500212 };
213
214 console.info('Sending CallAccept', callAccept);
215 webSocket.send(WebSocketMessageType.CallAccept, callAccept);
216 setCallStatus(CallStatus.Connecting);
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500217 }, [webSocket, contactUri, conversationId]);
simonf929a362022-11-18 16:53:45 -0500218
simonff1cb352022-11-24 15:15:26 -0500219 const setAudioStatus = useCallback(
220 (isOn: boolean) => {
221 if (!localStream) {
222 return;
223 }
224
225 for (const track of localStream.getAudioTracks()) {
226 track.enabled = isOn;
227 }
228
229 setIsAudioOn(isOn);
230 },
231 [localStream]
232 );
233
234 const setVideoStatus = useCallback(
235 (isOn: boolean) => {
236 if (!localStream) {
237 return;
238 }
239
240 for (const track of localStream.getVideoTracks()) {
241 track.enabled = isOn;
242 }
243
244 setIsVideoOn(isOn);
245 },
246 [localStream]
247 );
248
249 if (!callRole || callStatus === undefined) {
250 console.error('Invalid route. Redirecting...');
simonf929a362022-11-18 16:53:45 -0500251 return <Navigate to={'/'} />;
252 }
253
254 return (
255 <CallContext.Provider
256 value={{
257 mediaDevices,
258 localStream,
259 remoteStream: remoteStreams?.at(-1),
260 isAudioOn,
261 setAudioStatus,
262 isVideoOn,
263 setVideoStatus,
simonf9d78f22022-11-25 15:47:15 -0500264 isChatShown,
265 setIsChatShown,
simonf929a362022-11-18 16:53:45 -0500266 callRole,
267 callStatus,
268 acceptCall,
269 }}
270 >
271 {children}
272 </CallContext.Provider>
273 );
274};