blob: a17e738f4290b48c8aa0ecd8f2b98ccbb44185a0 [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';
24import { 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 {
32 Ringing,
33 Connecting,
34 InCall,
35}
36
37export interface ICallContext {
38 mediaDevices: Record<MediaDeviceKind, MediaDeviceInfo[]>;
39
40 localStream: MediaStream | undefined;
41 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
42
43 isAudioOn: boolean;
44 setAudioStatus: (isOn: boolean) => void;
45 isVideoOn: boolean;
46 setVideoStatus: (isOn: boolean) => void;
47 callRole: CallRole;
48 callStatus: CallStatus;
49
50 acceptCall: () => void;
51}
52
53const defaultCallContext: ICallContext = {
54 mediaDevices: {
55 audioinput: [],
56 audiooutput: [],
57 videoinput: [],
58 },
59
60 localStream: undefined,
61 remoteStream: undefined,
62
63 isAudioOn: false,
64 setAudioStatus: () => {},
65 isVideoOn: false,
66 setVideoStatus: () => {},
67 callRole: 'caller',
68 callStatus: CallStatus.Ringing,
69
70 acceptCall: () => {},
71};
72
73export const CallContext = createContext<ICallContext>(defaultCallContext);
74
75export default ({ children }: WithChildren) => {
76 const {
77 queryParams: { role: callRole },
78 } = useUrlParams<CallRouteParams>();
simonf929a362022-11-18 16:53:45 -050079 const webSocket = useContext(WebSocketContext);
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050080 const { webRtcConnection, remoteStreams, sendWebRtcOffer, isConnected } = useContext(WebRtcContext);
81 const { conversationId, conversation } = useContext(ConversationContext);
simonf929a362022-11-18 16:53:45 -050082
83 const [mediaDevices, setMediaDevices] = useState<Record<MediaDeviceKind, MediaDeviceInfo[]>>(
84 defaultCallContext.mediaDevices
85 );
86 const [localStream, setLocalStream] = useState<MediaStream>();
87
88 const [isAudioOn, setIsAudioOn] = useState(false);
89 const [isVideoOn, setIsVideoOn] = useState(false);
90 const [callStatus, setCallStatus] = useState(CallStatus.Ringing);
91
92 // TODO: This logic will have to change to support multiple people in a call
93 const contactUri = useMemo(() => conversation.getFirstMember().contact.getUri(), [conversation]);
94
95 useEffect(() => {
96 // TODO: Wait until status is `InCall` before getting devices
97 navigator.mediaDevices.enumerateDevices().then((devices) => {
98 const newMediaDevices: Record<MediaDeviceKind, MediaDeviceInfo[]> = {
99 audioinput: [],
100 audiooutput: [],
101 videoinput: [],
102 };
103
104 for (const device of devices) {
105 newMediaDevices[device.kind].push(device);
106 }
107
108 setMediaDevices(newMediaDevices);
109 });
110 }, []);
111
112 useEffect(() => {
113 // TODO: Only ask media permission once the call has been accepted
114 try {
115 // TODO: When toggling mute on/off, the camera flickers
116 // https://git.jami.net/savoirfairelinux/jami-web/-/issues/90
117 navigator.mediaDevices
118 .getUserMedia({
119 audio: true, // TODO: Set both to false by default
120 video: true,
121 })
122 .then((stream) => {
123 for (const track of stream.getTracks()) {
124 // TODO: Set default from isVideoOn and isMicOn values
125 track.enabled = false;
126 }
127 setLocalStream(stream);
128 });
129 } catch (e) {
130 // TODO: Better handle user denial
131 console.error('Could not get media devices:', e);
132 }
133 }, [setLocalStream]);
134
135 useEffect(() => {
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500136 if (localStream && webRtcConnection) {
simonf929a362022-11-18 16:53:45 -0500137 for (const track of localStream.getTracks()) {
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500138 webRtcConnection.addTrack(track, localStream);
simonf929a362022-11-18 16:53:45 -0500139 }
140 }
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500141 }, [localStream, webRtcConnection]);
simonf929a362022-11-18 16:53:45 -0500142
143 const setAudioStatus = useCallback(
144 (isOn: boolean) => {
145 if (!localStream) {
146 return;
147 }
148
149 for (const track of localStream.getAudioTracks()) {
150 track.enabled = isOn;
151 }
152
153 setIsAudioOn(isOn);
154 },
155 [localStream]
156 );
157
158 const setVideoStatus = useCallback(
159 (isOn: boolean) => {
160 if (!localStream) {
161 return;
162 }
163
164 for (const track of localStream.getVideoTracks()) {
165 track.enabled = isOn;
166 }
167
168 setIsVideoOn(isOn);
169 },
170 [localStream]
171 );
172
173 useEffect(() => {
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500174 if (!webSocket || !webRtcConnection) {
simonf929a362022-11-18 16:53:45 -0500175 return;
176 }
177
178 if (callRole === 'caller' && callStatus === CallStatus.Ringing) {
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500179 const callAcceptListener = (_data: CallAction) => {
180 console.info('Received event on CallAccept');
simonf929a362022-11-18 16:53:45 -0500181 setCallStatus(CallStatus.Connecting);
182
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500183 webRtcConnection
simonf929a362022-11-18 16:53:45 -0500184 .createOffer({
185 offerToReceiveAudio: true,
186 offerToReceiveVideo: true,
187 })
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500188 .then((sdp) => {
189 sendWebRtcOffer(sdp);
simonf929a362022-11-18 16:53:45 -0500190 });
191 };
192
193 webSocket.bind(WebSocketMessageType.CallAccept, callAcceptListener);
194
195 return () => {
196 webSocket.unbind(WebSocketMessageType.CallAccept, callAcceptListener);
197 };
198 }
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500199 }, [callRole, webSocket, webRtcConnection, sendWebRtcOffer, callStatus]);
simonf929a362022-11-18 16:53:45 -0500200
201 useEffect(() => {
202 if (callStatus === CallStatus.Connecting && isConnected) {
203 console.info('Changing call status to InCall');
204 setCallStatus(CallStatus.InCall);
205 }
206 }, [isConnected, callStatus]);
207
208 const acceptCall = useCallback(() => {
209 if (!webSocket) {
210 throw new Error('Could not accept call');
211 }
212
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500213 const callAccept: CallAction = {
214 contactId: contactUri,
215 conversationId,
simonf929a362022-11-18 16:53:45 -0500216 };
217
218 console.info('Sending CallAccept', callAccept);
219 webSocket.send(WebSocketMessageType.CallAccept, callAccept);
220 setCallStatus(CallStatus.Connecting);
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500221 }, [webSocket, contactUri, conversationId]);
simonf929a362022-11-18 16:53:45 -0500222
223 if (!callRole) {
224 console.error('Call role not defined. Redirecting...');
225 return <Navigate to={'/'} />;
226 }
227
228 return (
229 <CallContext.Provider
230 value={{
231 mediaDevices,
232 localStream,
233 remoteStream: remoteStreams?.at(-1),
234 isAudioOn,
235 setAudioStatus,
236 isVideoOn,
237 setVideoStatus,
238 callRole,
239 callStatus,
240 acceptCall,
241 }}
242 >
243 {children}
244 </CallContext.Provider>
245 );
246};