Move WebRtc logic to WebRtcManager

- This is intended to reduce the quantity of useEffect, which causes "cascading" updates. The end goal is to reintroduce StrictMode (soon), and hopefully to make the code easier to understand.
- Some logic to support calls with more than one peer has been added. This is intended to test the new architecture will be scalable with React hooks.

Change-Id: Id76c4061b06a759f55957a48a1283d46afc3f73a
diff --git a/client/src/webrtc/WebRtcManager.ts b/client/src/webrtc/WebRtcManager.ts
new file mode 100644
index 0000000..afae8a0
--- /dev/null
+++ b/client/src/webrtc/WebRtcManager.ts
@@ -0,0 +1,111 @@
+/*
+ * 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 { useMemo, useRef, useSyncExternalStore } from 'react';
+
+import { CallData } from '../contexts/CallManagerProvider';
+import { IWebSocketContext } from '../contexts/WebSocketProvider';
+import { Account } from '../models/account';
+import { Listener } from '../utils/utils';
+import { RTCPeerConnectionHandler, RTCPeerConnectionInfos } from './RtcPeerConnectionHandler';
+
+export const useWebRtcManager = () => {
+  const webRtcManagerRef = useRef(new WebRtcManager());
+  const connectionsInfos = useSyncExternalStore(
+    webRtcManagerRef.current.subscribe.bind(webRtcManagerRef.current),
+    webRtcManagerRef.current.getSnapshot.bind(webRtcManagerRef.current)
+  );
+
+  return useMemo(
+    () => ({
+      addConnection: webRtcManagerRef.current.addConnection.bind(webRtcManagerRef.current),
+      removeConnection: webRtcManagerRef.current.removeConnection.bind(webRtcManagerRef.current),
+      updateLocalStreams: webRtcManagerRef.current.updateLocalStreams.bind(webRtcManagerRef.current),
+      clean: webRtcManagerRef.current.clean.bind(webRtcManagerRef.current),
+      connectionsInfos: connectionsInfos,
+    }),
+    [connectionsInfos]
+  );
+};
+
+class WebRtcManager {
+  private connections: Record<string, RTCPeerConnectionHandler> = {}; // key is contactUri
+
+  private listeners: Listener[] = [];
+  private snapshot: Record<string, RTCPeerConnectionInfos> = {}; // key is contactUri
+
+  addConnection(
+    webSocket: IWebSocketContext,
+    account: Account,
+    contactUri: string,
+    callData: CallData,
+    localStream: MediaStream | undefined,
+    screenShareLocalStream: MediaStream | undefined
+  ) {
+    if (this.connections[contactUri]) {
+      console.debug('Attempted to establish an WebRTC connection with the same peer more than once');
+      return;
+    }
+
+    const connection = new RTCPeerConnectionHandler(
+      webSocket,
+      account,
+      contactUri,
+      callData,
+      localStream,
+      screenShareLocalStream,
+      this.emitChange.bind(this)
+    );
+    this.connections[contactUri] = connection;
+  }
+
+  removeConnection(contactUri: string) {
+    const connection = this.connections[contactUri];
+    connection.disconnect();
+    delete this.connections[contactUri];
+  }
+
+  updateLocalStreams(localStream: MediaStream | undefined, screenShareLocalStream: MediaStream | undefined) {
+    Object.values(this.connections).forEach((connection) =>
+      connection.updateLocalStreams(localStream, screenShareLocalStream)
+    );
+  }
+
+  subscribe(listener: Listener) {
+    this.listeners.push(listener);
+    return () => {
+      this.listeners.filter((otherListener) => otherListener !== listener);
+    };
+  }
+
+  getSnapshot(): Record<string, RTCPeerConnectionInfos> {
+    return this.snapshot;
+  }
+
+  emitChange() {
+    this.snapshot = Object.entries(this.connections).reduce((acc, [contactUri, connection]) => {
+      acc[contactUri] = connection.getInfos();
+      return acc;
+    }, {} as Record<string, RTCPeerConnectionInfos>);
+
+    this.listeners.forEach((listener) => listener());
+  }
+
+  clean() {
+    Object.values(this.connections).forEach((connection) => connection.disconnect());
+  }
+}