Merge WebRtcProvider into CallProvider

Change-Id: I2a014d4965147892703624d5e779da8053b06f15
diff --git a/client/src/components/ConversationView.tsx b/client/src/components/ConversationView.tsx
index 9216249..5f76add 100644
--- a/client/src/components/ConversationView.tsx
+++ b/client/src/components/ConversationView.tsx
@@ -21,18 +21,16 @@
 import { CallManagerContext } from '../contexts/CallManagerProvider';
 import { useCallContext } from '../contexts/CallProvider';
 import { useConversationContext } from '../contexts/ConversationProvider';
-import { useWebRtcContext } from '../contexts/WebRtcProvider';
 import CallInterface from '../pages/CallInterface';
 import ChatInterface from '../pages/ChatInterface';
 import { AddParticipantButton, ShowOptionsMenuButton, StartAudioCallButton, StartVideoCallButton } from './Button';
 
 const ConversationView = () => {
   const { conversationId } = useConversationContext();
-  const webRtcContext = useWebRtcContext(true);
   const callContext = useCallContext(true);
   const { callData } = useContext(CallManagerContext);
 
-  if (webRtcContext && callContext && callData?.conversationId === conversationId) {
+  if (callContext && callData?.conversationId === conversationId) {
     return <CallInterface />;
   }
 
diff --git a/client/src/components/VideoOverlay.tsx b/client/src/components/VideoOverlay.tsx
index 11dead5..195f5f9 100644
--- a/client/src/components/VideoOverlay.tsx
+++ b/client/src/components/VideoOverlay.tsx
@@ -21,7 +21,6 @@
 import { useNavigate } from 'react-router-dom';
 
 import { useCallContext } from '../contexts/CallProvider';
-import { useWebRtcContext } from '../contexts/WebRtcProvider';
 import { VideoElementWithSinkId } from '../utils/utils';
 import VideoStream, { VideoStreamProps } from './VideoStream';
 
@@ -75,7 +74,7 @@
 };
 
 export const RemoteVideoOverlay = ({ callConversationId }: { callConversationId: string }) => {
-  const { remoteStreams } = useWebRtcContext();
+  const { remoteStreams } = useCallContext();
   const {
     currentMediaDeviceIds: {
       audiooutput: { id: audioOutDeviceId },
diff --git a/client/src/contexts/CallManagerProvider.tsx b/client/src/contexts/CallManagerProvider.tsx
index b9fbb0b..33b3055 100644
--- a/client/src/contexts/CallManagerProvider.tsx
+++ b/client/src/contexts/CallManagerProvider.tsx
@@ -28,7 +28,6 @@
 import { SetState, WithChildren } from '../utils/utils';
 import { AlertSnackbarContext } from './AlertSnackbarProvider';
 import CallProvider, { CallRole } from './CallProvider';
-import WebRtcProvider from './WebRtcProvider';
 import { WebSocketContext } from './WebSocketProvider';
 
 export type CallData = {
@@ -124,14 +123,12 @@
   return (
     <>
       <CallManagerContext.Provider value={value}>
-        <WebRtcProvider>
-          <CallProvider>
-            {callData && callData.conversationId !== urlParams.conversationId && (
-              <RemoteVideoOverlay callConversationId={callData.conversationId} />
-            )}
-            {children}
-          </CallProvider>
-        </WebRtcProvider>
+        <CallProvider>
+          {callData && callData.conversationId !== urlParams.conversationId && (
+            <RemoteVideoOverlay callConversationId={callData.conversationId} />
+          )}
+          {children}
+        </CallProvider>
       </CallManagerContext.Provider>
     </>
   );
diff --git a/client/src/contexts/CallProvider.tsx b/client/src/contexts/CallProvider.tsx
index 9570ee5..050ad58 100644
--- a/client/src/contexts/CallProvider.tsx
+++ b/client/src/contexts/CallProvider.tsx
@@ -22,9 +22,10 @@
 import { ConversationMember } from '../models/conversation-member';
 import { callTimeoutMs } from '../utils/constants';
 import { AsyncSetState, SetState, WithChildren } from '../utils/utils';
+import { useWebRtcManager } from '../webrtc/WebRtcManager';
+import { useAuthContext } from './AuthProvider';
 import { CallData, CallManagerContext } from './CallManagerProvider';
 import ConditionalContextProvider from './ConditionalContextProvider';
-import { IWebRtcContext, MediaDevicesInfo, MediaInputKind, useWebRtcContext } from './WebRtcProvider';
 import { IWebSocketContext, WebSocketContext } from './WebSocketProvider';
 
 export type CallRole = 'caller' | 'receiver';
@@ -50,7 +51,15 @@
 };
 type CurrentMediaDeviceIds = Record<MediaDeviceKind, MediaDeviceIdState>;
 
+export type MediaDevicesInfo = Record<MediaDeviceKind, MediaDeviceInfo[]>;
+export type MediaInputKind = 'audio' | 'video';
+export type MediaInputIds = Record<MediaInputKind, string | false | undefined>;
+
 export interface ICallContext {
+  localStream: MediaStream | undefined;
+  screenShareLocalStream: MediaStream | undefined;
+  remoteStreams: readonly MediaStream[];
+
   mediaDevices: MediaDevicesInfo;
   currentMediaDeviceIds: CurrentMediaDeviceIds;
 
@@ -76,18 +85,16 @@
 export default ({ children }: WithChildren) => {
   const webSocket = useContext(WebSocketContext);
   const { callMembers, callData, exitCall } = useContext(CallManagerContext);
-  const webRtcContext = useWebRtcContext(true);
 
   const dependencies = useMemo(
     () => ({
       webSocket,
-      webRtcContext,
       callMembers,
       callData,
       exitCall,
       conversationId: callData?.conversationId,
     }),
-    [webSocket, webRtcContext, callMembers, callData, exitCall]
+    [webSocket, callMembers, callData, exitCall]
   );
 
   return (
@@ -103,7 +110,6 @@
 };
 
 const CallProvider = ({
-  webRtcContext,
   callMembers,
   callData,
   exitCall,
@@ -111,21 +117,23 @@
   webSocket,
 }: {
   webSocket: IWebSocketContext;
-  webRtcContext: IWebRtcContext;
   callMembers: ConversationMember[];
   callData: CallData;
   exitCall: () => void;
   conversationId: string;
 }): ICallContext => {
-  const {
-    localStream,
-    updateScreenShare,
-    sendWebRtcOffer,
-    iceConnectionState,
-    closeConnection,
-    getMediaDevices,
-    updateLocalStream,
-  } = webRtcContext;
+  const [localStream, setLocalStream] = useState<MediaStream>();
+  const [screenShareLocalStream, setScreenShareLocalStream] = useState<MediaStream>();
+  const { account } = useAuthContext();
+  const webRtcManager = useWebRtcManager();
+
+  // TODO: This logic will have to change to support multiple people in a call. Could we move this logic to the server?
+  //       The client could make a single request with the conversationId, and the server would be tasked with sending
+  //       all the individual requests to the members of the conversation.
+  const contactUri = callMembers[0]?.contact.uri;
+  const connectionInfos = webRtcManager.connectionsInfos[contactUri];
+  const remoteStreams = connectionInfos?.remoteStreams;
+  const iceConnectionState = connectionInfos?.iceConnectionState;
 
   const [mediaDevices, setMediaDevices] = useState<MediaDevicesInfo>({
     audioinput: [],
@@ -144,10 +152,134 @@
   const [callRole] = useState(callData?.role);
   const [callStartTime, setCallStartTime] = useState<number | undefined>(undefined);
 
-  // TODO: This logic will have to change to support multiple people in a call. Could we move this logic to the server?
-  //       The client could make a single request with the conversationId, and the server would be tasked with sending
-  //       all the individual requests to the members of the conversation.
-  const contactUri = useMemo(() => callMembers[0].contact.uri, [callMembers]);
+  // TODO: Replace this by a callback
+  useEffect(() => {
+    if (callData.role === 'receiver' && contactUri && localStream) {
+      webRtcManager.addConnection(webSocket, account, contactUri, callData, localStream, screenShareLocalStream);
+    }
+  }, [account, callData, contactUri, localStream, screenShareLocalStream, webRtcManager, webSocket]);
+
+  const getMediaDevices = useCallback(async (): Promise<MediaDevicesInfo> => {
+    try {
+      const devices = await navigator.mediaDevices.enumerateDevices();
+
+      // TODO: On Firefox, some devices can sometime be duplicated (2 devices can share the same deviceId). Using a map
+      //       and then converting it to an array makes it so that there is no duplicate. If we find a way to prevent
+      //       Firefox from listing 2 devices with the same deviceId, we can remove this logic.
+      const newMediaDevices: Record<MediaDeviceKind, Record<string, MediaDeviceInfo>> = {
+        audioinput: {},
+        audiooutput: {},
+        videoinput: {},
+      };
+
+      for (const device of devices) {
+        newMediaDevices[device.kind][device.deviceId] = device;
+      }
+
+      return {
+        audioinput: Object.values(newMediaDevices.audioinput),
+        audiooutput: Object.values(newMediaDevices.audiooutput),
+        videoinput: Object.values(newMediaDevices.videoinput),
+      };
+    } catch (e) {
+      throw new Error('Could not get media devices', { cause: e });
+    }
+  }, []);
+
+  const updateLocalStream = useCallback(
+    async (mediaDeviceIds?: MediaInputIds) => {
+      const devices = await getMediaDevices();
+
+      let audioConstraint: MediaTrackConstraints | boolean = devices.audioinput.length !== 0;
+      let videoConstraint: MediaTrackConstraints | boolean = devices.videoinput.length !== 0;
+
+      if (!audioConstraint && !videoConstraint) {
+        return;
+      }
+
+      if (mediaDeviceIds?.audio !== undefined) {
+        audioConstraint = mediaDeviceIds.audio !== false ? { deviceId: mediaDeviceIds.audio } : false;
+      }
+      if (mediaDeviceIds?.video !== undefined) {
+        videoConstraint = mediaDeviceIds.video !== false ? { deviceId: mediaDeviceIds.video } : false;
+      }
+
+      try {
+        const stream = await navigator.mediaDevices.getUserMedia({
+          audio: audioConstraint,
+          video: videoConstraint,
+        });
+
+        for (const track of stream.getTracks()) {
+          track.enabled = false;
+        }
+
+        setLocalStream(stream);
+      } catch (e) {
+        throw new Error('Could not get media devices', { cause: e });
+      }
+    },
+    [getMediaDevices]
+  );
+
+  const updateScreenShare = useCallback(
+    async (isOn: boolean) => {
+      if (isOn) {
+        const stream = await navigator.mediaDevices.getDisplayMedia({
+          video: true,
+          audio: false,
+        });
+
+        setScreenShareLocalStream(stream);
+        return stream;
+      } else {
+        if (screenShareLocalStream) {
+          for (const track of screenShareLocalStream.getTracks()) {
+            track.stop();
+          }
+        }
+
+        setScreenShareLocalStream(undefined);
+      }
+    },
+    [screenShareLocalStream]
+  );
+
+  // TODO: Transform the effect into a callback
+  const updateLocalStreams = webRtcManager.updateLocalStreams;
+  useEffect(() => {
+    if ((!localStream && !screenShareLocalStream) || !updateLocalStreams) {
+      return;
+    }
+
+    updateLocalStreams(localStream, screenShareLocalStream);
+  }, [localStream, screenShareLocalStream, updateLocalStreams]);
+
+  const sendWebRtcOffer = useCallback(async () => {
+    if (contactUri) {
+      webRtcManager.addConnection(webSocket, account, contactUri, callData, localStream, screenShareLocalStream);
+    }
+  }, [account, callData, contactUri, localStream, screenShareLocalStream, webRtcManager, webSocket]);
+
+  const closeConnection = useCallback(() => {
+    const stopStream = (stream: MediaStream) => {
+      const localTracks = stream.getTracks();
+      if (localTracks) {
+        for (const track of localTracks) {
+          track.stop();
+        }
+      }
+    };
+
+    if (localStream) {
+      stopStream(localStream);
+    }
+    if (screenShareLocalStream) {
+      stopStream(screenShareLocalStream);
+    }
+
+    webRtcManager.clean();
+  }, [localStream, screenShareLocalStream, webRtcManager]);
 
   useEffect(() => {
     if (callStatus !== CallStatus.InCall) {
@@ -394,6 +526,9 @@
 
   return useMemo(
     () => ({
+      localStream,
+      screenShareLocalStream,
+      remoteStreams,
       mediaDevices,
       currentMediaDeviceIds,
       isAudioOn,
@@ -411,6 +546,9 @@
       endCall,
     }),
     [
+      localStream,
+      screenShareLocalStream,
+      remoteStreams,
       mediaDevices,
       currentMediaDeviceIds,
       isAudioOn,
diff --git a/client/src/contexts/WebRtcProvider.tsx b/client/src/contexts/WebRtcProvider.tsx
deleted file mode 100644
index f6bc81e..0000000
--- a/client/src/contexts/WebRtcProvider.tsx
+++ /dev/null
@@ -1,250 +0,0 @@
-/*
- * Copyright (C) 2022 Savoir-faire Linux Inc.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation; either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public
- * License along with this program.  If not, see
- * <https://www.gnu.org/licenses/>.
- */
-
-import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
-
-import { createOptionalContext } from '../hooks/createOptionalContext';
-import { ConversationMember } from '../models/conversation-member';
-import { WithChildren } from '../utils/utils';
-import { useWebRtcManager } from '../webrtc/WebRtcManager';
-import { useAuthContext } from './AuthProvider';
-import { CallData, CallManagerContext } from './CallManagerProvider';
-import ConditionalContextProvider from './ConditionalContextProvider';
-import { IWebSocketContext, WebSocketContext } from './WebSocketProvider';
-
-export type MediaDevicesInfo = Record<MediaDeviceKind, MediaDeviceInfo[]>;
-export type MediaInputKind = 'audio' | 'video';
-export type MediaInputIds = Record<MediaInputKind, string | false | undefined>;
-
-export interface IWebRtcContext {
-  iceConnectionState: RTCIceConnectionState | undefined;
-
-  localStream: MediaStream | undefined;
-  screenShareLocalStream: MediaStream | undefined;
-  remoteStreams: readonly MediaStream[];
-  getMediaDevices: () => Promise<MediaDevicesInfo>;
-  updateLocalStream: (mediaDeviceIds?: MediaInputIds) => Promise<void>;
-  updateScreenShare: (active: boolean) => Promise<MediaStream | undefined>;
-
-  sendWebRtcOffer: () => Promise<void>;
-  closeConnection: () => void;
-}
-
-const optionalWebRtcContext = createOptionalContext<IWebRtcContext>('WebRtcContext');
-export const useWebRtcContext = optionalWebRtcContext.useOptionalContext;
-
-export default ({ children }: WithChildren) => {
-  const webSocket = useContext(WebSocketContext);
-  const { callConversationInfos, callMembers, callData } = useContext(CallManagerContext);
-
-  const dependencies = useMemo(
-    () => ({
-      webSocket,
-      conversationInfos: callConversationInfos,
-      members: callMembers,
-      callData: callData,
-    }),
-    [webSocket, callConversationInfos, callMembers, callData]
-  );
-
-  return (
-    <ConditionalContextProvider
-      Context={optionalWebRtcContext.Context}
-      initialValue={undefined}
-      dependencies={dependencies}
-      useProviderValue={useWebRtcContextValue}
-    >
-      {children}
-    </ConditionalContextProvider>
-  );
-};
-
-const useWebRtcContextValue = ({
-  members,
-  callData,
-  webSocket,
-}: {
-  webSocket: IWebSocketContext;
-  members: ConversationMember[];
-  callData: CallData;
-}) => {
-  const [localStream, setLocalStream] = useState<MediaStream>();
-  const [screenShareLocalStream, setScreenShareLocalStream] = useState<MediaStream>();
-  const { account } = useAuthContext();
-  const webRtcManager = useWebRtcManager();
-
-  // TODO: This logic will have to change to support multiple people in a call
-  const contactUri = members[0]?.contact.uri;
-  const connectionInfos = webRtcManager.connectionsInfos[contactUri];
-  const remoteStreams = connectionInfos?.remoteStreams;
-  const iceConnectionState = connectionInfos?.iceConnectionState;
-
-  // TODO: Replace this by a callback
-  useEffect(() => {
-    if (callData.role === 'receiver' && contactUri && localStream) {
-      webRtcManager.addConnection(webSocket, account, contactUri, callData, localStream, screenShareLocalStream);
-    }
-  }, [account, callData, contactUri, localStream, screenShareLocalStream, webRtcManager, webSocket]);
-
-  const getMediaDevices = useCallback(async (): Promise<MediaDevicesInfo> => {
-    try {
-      const devices = await navigator.mediaDevices.enumerateDevices();
-
-      // TODO: On Firefox, some devices can sometime be duplicated (2 devices can share the same deviceId). Using a map
-      //       and then converting it to an array makes it so that there is no duplicate. If we find a way to prevent
-      //       Firefox from listing 2 devices with the same deviceId, we can remove this logic.
-      const newMediaDevices: Record<MediaDeviceKind, Record<string, MediaDeviceInfo>> = {
-        audioinput: {},
-        audiooutput: {},
-        videoinput: {},
-      };
-
-      for (const device of devices) {
-        newMediaDevices[device.kind][device.deviceId] = device;
-      }
-
-      return {
-        audioinput: Object.values(newMediaDevices.audioinput),
-        audiooutput: Object.values(newMediaDevices.audiooutput),
-        videoinput: Object.values(newMediaDevices.videoinput),
-      };
-    } catch (e) {
-      throw new Error('Could not get media devices', { cause: e });
-    }
-  }, []);
-
-  const updateLocalStream = useCallback(
-    async (mediaDeviceIds?: MediaInputIds) => {
-      const devices = await getMediaDevices();
-
-      let audioConstraint: MediaTrackConstraints | boolean = devices.audioinput.length !== 0;
-      let videoConstraint: MediaTrackConstraints | boolean = devices.videoinput.length !== 0;
-
-      if (!audioConstraint && !videoConstraint) {
-        return;
-      }
-
-      if (mediaDeviceIds?.audio !== undefined) {
-        audioConstraint = mediaDeviceIds.audio !== false ? { deviceId: mediaDeviceIds.audio } : false;
-      }
-      if (mediaDeviceIds?.video !== undefined) {
-        videoConstraint = mediaDeviceIds.video !== false ? { deviceId: mediaDeviceIds.video } : false;
-      }
-
-      try {
-        const stream = await navigator.mediaDevices.getUserMedia({
-          audio: audioConstraint,
-          video: videoConstraint,
-        });
-
-        for (const track of stream.getTracks()) {
-          track.enabled = false;
-        }
-
-        setLocalStream(stream);
-      } catch (e) {
-        throw new Error('Could not get media devices', { cause: e });
-      }
-    },
-    [getMediaDevices]
-  );
-
-  const updateScreenShare = useCallback(
-    async (isOn: boolean) => {
-      if (isOn) {
-        const stream = await navigator.mediaDevices.getDisplayMedia({
-          video: true,
-          audio: false,
-        });
-
-        setScreenShareLocalStream(stream);
-        return stream;
-      } else {
-        if (screenShareLocalStream) {
-          for (const track of screenShareLocalStream.getTracks()) {
-            track.stop();
-          }
-        }
-
-        setScreenShareLocalStream(undefined);
-      }
-    },
-    [screenShareLocalStream]
-  );
-
-  // TODO: Transform the effect into a callback
-  const updateLocalStreams = webRtcManager.updateLocalStreams;
-  useEffect(() => {
-    if ((!localStream && !screenShareLocalStream) || !updateLocalStreams) {
-      return;
-    }
-
-    updateLocalStreams(localStream, screenShareLocalStream);
-  }, [localStream, screenShareLocalStream, updateLocalStreams]);
-
-  const sendWebRtcOffer = useCallback(async () => {
-    if (contactUri) {
-      webRtcManager.addConnection(webSocket, account, contactUri, callData, localStream, screenShareLocalStream);
-    }
-  }, [account, callData, contactUri, localStream, screenShareLocalStream, webRtcManager, webSocket]);
-
-  const closeConnection = useCallback(() => {
-    const stopStream = (stream: MediaStream) => {
-      const localTracks = stream.getTracks();
-      if (localTracks) {
-        for (const track of localTracks) {
-          track.stop();
-        }
-      }
-    };
-
-    if (localStream) {
-      stopStream(localStream);
-    }
-    if (screenShareLocalStream) {
-      stopStream(screenShareLocalStream);
-    }
-
-    webRtcManager.clean();
-  }, [localStream, screenShareLocalStream, webRtcManager]);
-
-  return useMemo(
-    () => ({
-      iceConnectionState,
-      localStream,
-      screenShareLocalStream,
-      remoteStreams,
-      getMediaDevices,
-      updateLocalStream,
-      updateScreenShare,
-      sendWebRtcOffer,
-      closeConnection,
-    }),
-    [
-      iceConnectionState,
-      localStream,
-      screenShareLocalStream,
-      remoteStreams,
-      getMediaDevices,
-      updateLocalStream,
-      updateScreenShare,
-      sendWebRtcOffer,
-      closeConnection,
-    ]
-  );
-};
diff --git a/client/src/pages/CallInterface.tsx b/client/src/pages/CallInterface.tsx
index 2425b52..1cfd1e5 100644
--- a/client/src/pages/CallInterface.tsx
+++ b/client/src/pages/CallInterface.tsx
@@ -50,7 +50,6 @@
 import VideoStream from '../components/VideoStream';
 import { CallStatus, useCallContext, VideoStatus } from '../contexts/CallProvider';
 import { useConversationContext } from '../contexts/ConversationProvider';
-import { useWebRtcContext } from '../contexts/WebRtcProvider';
 import { formatCallDuration } from '../utils/dates&times';
 import { VideoElementWithSinkId } from '../utils/utils';
 import { CallPending } from './CallPending';
@@ -92,7 +91,7 @@
 }
 
 const CallInterface = () => {
-  const { localStream, screenShareLocalStream, remoteStreams } = useWebRtcContext();
+  const { localStream, screenShareLocalStream, remoteStreams } = useCallContext();
   const {
     currentMediaDeviceIds: {
       audiooutput: { id: audioOutDeviceId },
diff --git a/client/src/pages/CallPending.tsx b/client/src/pages/CallPending.tsx
index a7566db..7ed5aad 100644
--- a/client/src/pages/CallPending.tsx
+++ b/client/src/pages/CallPending.tsx
@@ -30,13 +30,11 @@
 import ConversationAvatar from '../components/ConversationAvatar';
 import { CallStatus, useCallContext } from '../contexts/CallProvider';
 import { useConversationContext } from '../contexts/ConversationProvider';
-import { useWebRtcContext } from '../contexts/WebRtcProvider';
 import { VideoElementWithSinkId } from '../utils/utils';
 
 export const CallPending = () => {
-  const { localStream } = useWebRtcContext();
   const { conversationDisplayName } = useConversationContext();
-  const { callRole } = useCallContext();
+  const { callRole, localStream } = useCallContext();
   const localVideoRef = useRef<VideoElementWithSinkId | null>(null);
 
   useEffect(() => {