blob: 1ce3ff69de858bf1c420aa496109afb90fc34d79 [file] [log] [blame]
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -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 */
18
19import { WebRtcIceCandidate, WebRtcSdp, WebSocketMessageType } from 'jami-web-common';
20import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
21
simonf353ef42022-11-28 23:14:53 -050022import LoadingPage from '../components/Loading';
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050023import { WithChildren } from '../utils/utils';
simon71d1c0a2022-11-24 15:28:33 -050024import { useAuthContext } from './AuthProvider';
simon09fe4822022-11-30 23:36:25 -050025import { useConversationContext } from './ConversationProvider';
simonf353ef42022-11-28 23:14:53 -050026import { IWebSocketContext, WebSocketContext } from './WebSocketProvider';
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050027
simon492e8402022-11-29 16:48:37 -050028export type MediaDevicesInfo = Record<MediaDeviceKind, MediaDeviceInfo[]>;
29export type MediaInputKind = 'audio' | 'video';
30export type MediaInputIds = Record<MediaInputKind, string | false | undefined>;
31
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050032interface IWebRtcContext {
Charlieb837e8f2022-11-28 19:18:46 -050033 iceConnectionState: RTCIceConnectionState | undefined;
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050034
simon9076a9a2022-11-29 17:13:01 -050035 localStream: MediaStream | undefined;
simon1e2bf342022-12-02 12:19:40 -050036 screenShareLocalStream: MediaStream | undefined;
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050037 remoteStreams: readonly MediaStream[] | undefined;
simon492e8402022-11-29 16:48:37 -050038 getMediaDevices: () => Promise<MediaDevicesInfo>;
39 updateLocalStream: (mediaDeviceIds?: MediaInputIds) => Promise<void>;
simon1e2bf342022-12-02 12:19:40 -050040 updateScreenShare: (active: boolean) => Promise<MediaStream | undefined>;
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050041
simon9076a9a2022-11-29 17:13:01 -050042 sendWebRtcOffer: () => Promise<void>;
43 closeConnection: () => void;
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050044}
45
46const defaultWebRtcContext: IWebRtcContext = {
Charlieb837e8f2022-11-28 19:18:46 -050047 iceConnectionState: undefined,
simon9076a9a2022-11-29 17:13:01 -050048 localStream: undefined,
simon1e2bf342022-12-02 12:19:40 -050049 screenShareLocalStream: undefined,
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050050 remoteStreams: undefined,
simon492e8402022-11-29 16:48:37 -050051 getMediaDevices: async () => Promise.reject(),
52 updateLocalStream: async () => Promise.reject(),
simon1e2bf342022-12-02 12:19:40 -050053 updateScreenShare: async () => Promise.reject(),
simon492e8402022-11-29 16:48:37 -050054 sendWebRtcOffer: async () => Promise.reject(),
simon9076a9a2022-11-29 17:13:01 -050055 closeConnection: () => {},
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050056};
57
58export const WebRtcContext = createContext<IWebRtcContext>(defaultWebRtcContext);
59
60export default ({ children }: WithChildren) => {
simon71d1c0a2022-11-24 15:28:33 -050061 const { account } = useAuthContext();
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050062 const [webRtcConnection, setWebRtcConnection] = useState<RTCPeerConnection | undefined>();
simonf353ef42022-11-28 23:14:53 -050063 const webSocket = useContext(WebSocketContext);
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050064
65 useEffect(() => {
simon71d1c0a2022-11-24 15:28:33 -050066 if (!webRtcConnection && account) {
67 const iceServers: RTCIceServer[] = [];
68
69 if (account.getDetails()['TURN.enable'] === 'true') {
70 iceServers.push({
71 urls: 'turn:' + account.getDetails()['TURN.server'],
72 username: account.getDetails()['TURN.username'],
73 credential: account.getDetails()['TURN.password'],
74 });
75 }
76
77 if (account.getDetails()['STUN.enable'] === 'true') {
78 iceServers.push({
79 urls: 'stun:' + account.getDetails()['STUN.server'],
80 });
81 }
82
simon9076a9a2022-11-29 17:13:01 -050083 setWebRtcConnection(new RTCPeerConnection({ iceServers }));
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050084 }
simon71d1c0a2022-11-24 15:28:33 -050085 }, [account, webRtcConnection]);
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050086
simonf353ef42022-11-28 23:14:53 -050087 if (!webRtcConnection || !webSocket) {
88 return <LoadingPage />;
89 }
90
91 return (
92 <WebRtcProvider webRtcConnection={webRtcConnection} webSocket={webSocket}>
93 {children}
94 </WebRtcProvider>
95 );
96};
97
98const WebRtcProvider = ({
99 children,
100 webRtcConnection,
101 webSocket,
102}: WithChildren & {
103 webRtcConnection: RTCPeerConnection;
104 webSocket: IWebSocketContext;
105}) => {
simon09fe4822022-11-30 23:36:25 -0500106 const { conversation, conversationId } = useConversationContext();
simon9076a9a2022-11-29 17:13:01 -0500107 const [localStream, setLocalStream] = useState<MediaStream>();
simon1e2bf342022-12-02 12:19:40 -0500108 const [screenShareLocalStream, setScreenShareLocalStream] = useState<MediaStream>();
simonf353ef42022-11-28 23:14:53 -0500109 const [remoteStreams, setRemoteStreams] = useState<readonly MediaStream[]>();
Charlieb837e8f2022-11-28 19:18:46 -0500110 const [iceConnectionState, setIceConnectionState] = useState<RTCIceConnectionState | undefined>();
simon492e8402022-11-29 16:48:37 -0500111
112 const [audioRtcRtpSenders, setAudioRtcRtpSenders] = useState<RTCRtpSender[]>();
113 const [videoRtcRtpSenders, setVideoRtcRtpSenders] = useState<RTCRtpSender[]>();
simonf353ef42022-11-28 23:14:53 -0500114
simon25bfee82022-11-28 19:41:24 -0500115 // TODO: The ICE candidate queue is used to cache candidates that were received before `setRemoteDescription` was
116 // called. This is currently necessary, because the jami-daemon is unreliable as a WebRTC signaling channel,
117 // because messages can be received with a delay or out of order. This queue is a temporary workaround that
118 // should be replaced if there is a better way to send messages with the daemon.
119 // Relevant links:
120 // - https://github.com/w3c/webrtc-pc/issues/2519#issuecomment-622055440
121 // - https://stackoverflow.com/questions/57256828/how-to-fix-invalidstateerror-cannot-add-ice-candidate-when-there-is-no-remote-s
122 const [isReadyForIceCandidates, setIsReadyForIceCandidates] = useState(false);
123 const [iceCandidateQueue, setIceCandidateQueue] = useState<RTCIceCandidate[]>([]);
124
simonf353ef42022-11-28 23:14:53 -0500125 // TODO: This logic will have to change to support multiple people in a call
126 const contactUri = useMemo(() => conversation.getFirstMember().contact.getUri(), [conversation]);
127
simon492e8402022-11-29 16:48:37 -0500128 const getMediaDevices = useCallback(async (): Promise<MediaDevicesInfo> => {
simon9076a9a2022-11-29 17:13:01 -0500129 try {
130 const devices = await navigator.mediaDevices.enumerateDevices();
simon492e8402022-11-29 16:48:37 -0500131
132 // TODO: On Firefox, some devices can sometime be duplicated (2 devices can share the same deviceId). Using a map
133 // and then converting it to an array makes it so that there is no duplicate. If we find a way to prevent
134 // Firefox from listing 2 devices with the same deviceId, we can remove this logic.
135 const newMediaDevices: Record<MediaDeviceKind, Record<string, MediaDeviceInfo>> = {
136 audioinput: {},
137 audiooutput: {},
138 videoinput: {},
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500139 };
140
simon9076a9a2022-11-29 17:13:01 -0500141 for (const device of devices) {
simon492e8402022-11-29 16:48:37 -0500142 newMediaDevices[device.kind][device.deviceId] = device;
simon9076a9a2022-11-29 17:13:01 -0500143 }
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500144
simon492e8402022-11-29 16:48:37 -0500145 return {
146 audioinput: Object.values(newMediaDevices.audioinput),
147 audiooutput: Object.values(newMediaDevices.audiooutput),
148 videoinput: Object.values(newMediaDevices.videoinput),
149 };
simon9076a9a2022-11-29 17:13:01 -0500150 } catch (e) {
151 throw new Error('Could not get media devices', { cause: e });
152 }
153 }, []);
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500154
simon492e8402022-11-29 16:48:37 -0500155 const updateLocalStream = useCallback(
156 async (mediaDeviceIds?: MediaInputIds) => {
157 const devices = await getMediaDevices();
simon9076a9a2022-11-29 17:13:01 -0500158
simon492e8402022-11-29 16:48:37 -0500159 let audioConstraint: MediaTrackConstraints | boolean = devices.audioinput.length !== 0;
160 let videoConstraint: MediaTrackConstraints | boolean = devices.videoinput.length !== 0;
161
162 if (!audioConstraint && !videoConstraint) {
163 return;
164 }
165
166 if (mediaDeviceIds?.audio !== undefined) {
167 audioConstraint = mediaDeviceIds.audio !== false ? { deviceId: mediaDeviceIds.audio } : false;
168 }
169 if (mediaDeviceIds?.video !== undefined) {
170 videoConstraint = mediaDeviceIds.video !== false ? { deviceId: mediaDeviceIds.video } : false;
171 }
172
simon9076a9a2022-11-29 17:13:01 -0500173 try {
simon492e8402022-11-29 16:48:37 -0500174 const stream = await navigator.mediaDevices.getUserMedia({
175 audio: audioConstraint,
176 video: videoConstraint,
177 });
178
179 for (const track of stream.getTracks()) {
180 track.enabled = false;
181 }
182
183 setLocalStream(stream);
simon9076a9a2022-11-29 17:13:01 -0500184 } catch (e) {
simon492e8402022-11-29 16:48:37 -0500185 throw new Error('Could not get media devices', { cause: e });
simon9076a9a2022-11-29 17:13:01 -0500186 }
simon492e8402022-11-29 16:48:37 -0500187 },
188 [getMediaDevices]
189 );
simon9076a9a2022-11-29 17:13:01 -0500190
simon1e2bf342022-12-02 12:19:40 -0500191 const updateScreenShare = useCallback(
192 async (isOn: boolean) => {
193 if (isOn) {
194 const stream = await navigator.mediaDevices.getDisplayMedia({
195 video: true,
196 audio: false,
197 });
198
199 setScreenShareLocalStream(stream);
200 return stream;
201 } else {
202 if (screenShareLocalStream) {
203 for (const track of screenShareLocalStream.getTracks()) {
204 track.stop();
205 }
206 }
207
208 setScreenShareLocalStream(undefined);
209 }
210 },
211 [screenShareLocalStream]
212 );
213
simon492e8402022-11-29 16:48:37 -0500214 useEffect(() => {
simon1e2bf342022-12-02 12:19:40 -0500215 if ((!localStream && !screenShareLocalStream) || !webRtcConnection) {
simon9076a9a2022-11-29 17:13:01 -0500216 return;
217 }
218
simon1e2bf342022-12-02 12:19:40 -0500219 const updateTracks = async (stream: MediaStream, kind: 'audio' | 'video') => {
simon492e8402022-11-29 16:48:37 -0500220 const senders = kind === 'audio' ? audioRtcRtpSenders : videoRtcRtpSenders;
simon1e2bf342022-12-02 12:19:40 -0500221 const tracks = kind === 'audio' ? stream.getAudioTracks() : stream.getVideoTracks();
simon492e8402022-11-29 16:48:37 -0500222 if (senders) {
223 const promises: Promise<void>[] = [];
224 for (let i = 0; i < senders.length; i++) {
225 // TODO: There is a bug where calling multiple times `addTrack` when changing an input device doesn't work.
226 // Calling `addTrack` doesn't trigger the `track` event listener for the other user.
227 // This workaround makes it possible to replace a track, but it could be improved by figuring out the
228 // proper way of changing a track.
229 promises.push(
230 senders[i].replaceTrack(tracks[i]).catch((e) => {
231 console.error('Error replacing track:', e);
232 })
233 );
234 }
235 return Promise.all(promises);
simon9076a9a2022-11-29 17:13:01 -0500236 }
237
simon492e8402022-11-29 16:48:37 -0500238 // TODO: Currently, we do not support adding new devices. To enable this feature, we would need to implement
239 // the "Perfect negotiation" pattern to renegotiate after `addTrack`.
240 // https://blog.mozilla.org/webrtc/perfect-negotiation-in-webrtc/
simon1e2bf342022-12-02 12:19:40 -0500241 const newSenders = tracks.map((track) => webRtcConnection.addTrack(track, stream));
simon492e8402022-11-29 16:48:37 -0500242 if (kind === 'audio') {
243 setAudioRtcRtpSenders(newSenders);
244 } else {
245 setVideoRtcRtpSenders(newSenders);
246 }
247 };
248
simon1e2bf342022-12-02 12:19:40 -0500249 if (localStream) {
250 updateTracks(localStream, 'audio');
251 updateTracks(localStream, 'video');
252 }
253
254 if (screenShareLocalStream) {
255 updateTracks(screenShareLocalStream, 'video');
256 }
257 }, [localStream, screenShareLocalStream, webRtcConnection, audioRtcRtpSenders, videoRtcRtpSenders]);
simon9076a9a2022-11-29 17:13:01 -0500258
259 const sendWebRtcOffer = useCallback(async () => {
260 const sdp = await webRtcConnection.createOffer({
261 offerToReceiveAudio: true,
262 offerToReceiveVideo: true,
263 });
264
265 const webRtcOffer: WebRtcSdp = {
266 contactId: contactUri,
267 conversationId: conversationId,
268 sdp,
269 };
270
271 await webRtcConnection.setLocalDescription(new RTCSessionDescription(sdp));
272 console.info('Sending WebRtcOffer', webRtcOffer);
273 webSocket.send(WebSocketMessageType.WebRtcOffer, webRtcOffer);
274 }, [webRtcConnection, webSocket, conversationId, contactUri]);
275
276 const sendWebRtcAnswer = useCallback(async () => {
277 const sdp = await webRtcConnection.createAnswer({
278 offerToReceiveAudio: true,
279 offerToReceiveVideo: true,
280 });
281
282 const webRtcAnswer: WebRtcSdp = {
283 contactId: contactUri,
284 conversationId: conversationId,
285 sdp,
286 };
287
288 await webRtcConnection.setLocalDescription(new RTCSessionDescription(sdp));
289 console.info('Sending WebRtcAnswer', webRtcAnswer);
290 webSocket.send(WebSocketMessageType.WebRtcAnswer, webRtcAnswer);
291 }, [contactUri, conversationId, webRtcConnection, webSocket]);
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500292
simonf353ef42022-11-28 23:14:53 -0500293 /* WebSocket Listeners */
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500294
simonf353ef42022-11-28 23:14:53 -0500295 useEffect(() => {
simon25bfee82022-11-28 19:41:24 -0500296 const addQueuedIceCandidates = async () => {
297 console.info('WebRTC remote description has been set. Ready to receive ICE candidates');
298 setIsReadyForIceCandidates(true);
299 if (iceCandidateQueue.length !== 0) {
simon492e8402022-11-29 16:48:37 -0500300 console.warn(
301 'Found queued ICE candidates that were added before `setRemoteDescription` was called. ' +
302 'Adding queued ICE candidates...',
303 iceCandidateQueue
304 );
simon25bfee82022-11-28 19:41:24 -0500305
306 await Promise.all(iceCandidateQueue.map((iceCandidate) => webRtcConnection.addIceCandidate(iceCandidate)));
307 }
308 };
309
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500310 const webRtcOfferListener = async (data: WebRtcSdp) => {
311 console.info('Received event on WebRtcOffer', data);
Charliec18d6402022-11-27 13:01:04 -0500312 if (data.conversationId !== conversationId) {
313 console.warn('Wrong incoming conversationId, ignoring action');
314 return;
315 }
316
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500317 await webRtcConnection.setRemoteDescription(new RTCSessionDescription(data.sdp));
simon9076a9a2022-11-29 17:13:01 -0500318 await sendWebRtcAnswer();
simon25bfee82022-11-28 19:41:24 -0500319 await addQueuedIceCandidates();
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500320 };
321
322 const webRtcAnswerListener = async (data: WebRtcSdp) => {
323 console.info('Received event on WebRtcAnswer', data);
Charliec18d6402022-11-27 13:01:04 -0500324 if (data.conversationId !== conversationId) {
325 console.warn('Wrong incoming conversationId, ignoring action');
326 return;
327 }
328
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500329 await webRtcConnection.setRemoteDescription(new RTCSessionDescription(data.sdp));
simon25bfee82022-11-28 19:41:24 -0500330 await addQueuedIceCandidates();
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500331 };
simon9076a9a2022-11-29 17:13:01 -0500332
simonf353ef42022-11-28 23:14:53 -0500333 webSocket.bind(WebSocketMessageType.WebRtcOffer, webRtcOfferListener);
334 webSocket.bind(WebSocketMessageType.WebRtcAnswer, webRtcAnswerListener);
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500335
simonf353ef42022-11-28 23:14:53 -0500336 return () => {
337 webSocket.unbind(WebSocketMessageType.WebRtcOffer, webRtcOfferListener);
338 webSocket.unbind(WebSocketMessageType.WebRtcAnswer, webRtcAnswerListener);
339 };
simon25bfee82022-11-28 19:41:24 -0500340 }, [webSocket, webRtcConnection, sendWebRtcAnswer, conversationId, iceCandidateQueue]);
simonf353ef42022-11-28 23:14:53 -0500341
342 useEffect(() => {
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500343 const webRtcIceCandidateListener = async (data: WebRtcIceCandidate) => {
Charliec18d6402022-11-27 13:01:04 -0500344 if (data.conversationId !== conversationId) {
345 console.warn('Wrong incoming conversationId, ignoring action');
346 return;
347 }
348
simon25bfee82022-11-28 19:41:24 -0500349 if (!data.candidate) {
350 return;
351 }
352
353 if (isReadyForIceCandidates) {
354 await webRtcConnection.addIceCandidate(data.candidate);
355 } else {
simon25bfee82022-11-28 19:41:24 -0500356 setIceCandidateQueue((v) => {
357 v.push(data.candidate);
358 return v;
359 });
360 }
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500361 };
362
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500363 webSocket.bind(WebSocketMessageType.WebRtcIceCandidate, webRtcIceCandidateListener);
364
365 return () => {
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500366 webSocket.unbind(WebSocketMessageType.WebRtcIceCandidate, webRtcIceCandidateListener);
367 };
simon25bfee82022-11-28 19:41:24 -0500368 }, [webRtcConnection, webSocket, conversationId, isReadyForIceCandidates]);
simonf353ef42022-11-28 23:14:53 -0500369
370 /* WebRTC Listeners */
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500371
372 useEffect(() => {
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500373 const iceCandidateEventListener = (event: RTCPeerConnectionIceEvent) => {
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500374 if (event.candidate) {
375 const webRtcIceCandidate: WebRtcIceCandidate = {
376 contactId: contactUri,
Charliec18d6402022-11-27 13:01:04 -0500377 conversationId: conversationId,
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500378 candidate: event.candidate,
379 };
380
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500381 webSocket.send(WebSocketMessageType.WebRtcIceCandidate, webRtcIceCandidate);
382 }
383 };
simonf353ef42022-11-28 23:14:53 -0500384 webRtcConnection.addEventListener('icecandidate', iceCandidateEventListener);
385
386 return () => {
387 webRtcConnection.removeEventListener('icecandidate', iceCandidateEventListener);
388 };
389 }, [webRtcConnection, webSocket, contactUri, conversationId]);
390
391 useEffect(() => {
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500392 const trackEventListener = (event: RTCTrackEvent) => {
393 console.info('Received WebRTC event on track', event);
394 setRemoteStreams(event.streams);
395 };
396
simon9076a9a2022-11-29 17:13:01 -0500397 const iceConnectionStateChangeEventListener = (event: Event) => {
398 console.info(`Received WebRTC event on iceconnectionstatechange: ${webRtcConnection.iceConnectionState}`, event);
Charlieb837e8f2022-11-28 19:18:46 -0500399 setIceConnectionState(webRtcConnection.iceConnectionState);
simonfeaa1db2022-11-26 20:13:18 -0500400 };
401
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500402 webRtcConnection.addEventListener('track', trackEventListener);
MichelleSS55164202022-11-25 18:36:14 -0500403 webRtcConnection.addEventListener('iceconnectionstatechange', iceConnectionStateChangeEventListener);
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500404
405 return () => {
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500406 webRtcConnection.removeEventListener('track', trackEventListener);
MichelleSS55164202022-11-25 18:36:14 -0500407 webRtcConnection.removeEventListener('iceconnectionstatechange', iceConnectionStateChangeEventListener);
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500408 };
simonf353ef42022-11-28 23:14:53 -0500409 }, [webRtcConnection]);
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500410
simon9076a9a2022-11-29 17:13:01 -0500411 const closeConnection = useCallback(() => {
simon1e2bf342022-12-02 12:19:40 -0500412 const stopStream = (stream: MediaStream) => {
413 const localTracks = stream.getTracks();
414 if (localTracks) {
415 for (const track of localTracks) {
416 track.stop();
417 }
simon9076a9a2022-11-29 17:13:01 -0500418 }
simon1e2bf342022-12-02 12:19:40 -0500419 };
420
421 if (localStream) {
422 stopStream(localStream);
423 }
424 if (screenShareLocalStream) {
425 stopStream(screenShareLocalStream);
simon9076a9a2022-11-29 17:13:01 -0500426 }
427
428 webRtcConnection.close();
simon1e2bf342022-12-02 12:19:40 -0500429 }, [webRtcConnection, localStream, screenShareLocalStream]);
simon9076a9a2022-11-29 17:13:01 -0500430
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500431 return (
432 <WebRtcContext.Provider
433 value={{
Charlieb837e8f2022-11-28 19:18:46 -0500434 iceConnectionState,
simon9076a9a2022-11-29 17:13:01 -0500435 localStream,
simon1e2bf342022-12-02 12:19:40 -0500436 screenShareLocalStream,
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500437 remoteStreams,
simon492e8402022-11-29 16:48:37 -0500438 getMediaDevices,
439 updateLocalStream,
simon1e2bf342022-12-02 12:19:40 -0500440 updateScreenShare,
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500441 sendWebRtcOffer,
simon9076a9a2022-11-29 17:13:01 -0500442 closeConnection,
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500443 }}
444 >
445 {children}
446 </WebRtcContext.Provider>
447 );
448};