Fix "No remoteDescription" error in call

Fix "Cannot add ICE candidate when there is no remote SDP" error in call by adding a iceCandidates queue that is filled when the remote description isn't set. The ice candidates in the queue are all added to the webRtcConnection when `setRemoteDescription` has been called and future ice candidates are added normally.

This is a temporary workaround caused by the fact that the DHT can add some uncertainty. Some messages can be out of order or delayed. The ice candidates cache logic should be removed if there is a better way to send a messages with the Jami-Daemon in a more reliable manner.

GitLab: #188
Change-Id: I44cf4d31cf613fe780a4952120b105815a4a1260
diff --git a/client/src/contexts/WebRtcProvider.tsx b/client/src/contexts/WebRtcProvider.tsx
index 42ac308..9bf7a9a 100644
--- a/client/src/contexts/WebRtcProvider.tsx
+++ b/client/src/contexts/WebRtcProvider.tsx
@@ -107,6 +107,16 @@
     defaultWebRtcContext.mediaDevices
   );
 
+  // TODO: The ICE candidate queue is used to cache candidates that were received before `setRemoteDescription` was
+  //       called. This is currently necessary, because the jami-daemon is unreliable as a WebRTC signaling channel,
+  //       because messages can be received with a delay or out of order. This queue is a temporary workaround that
+  //       should be replaced if there is a better way to send messages with the daemon.
+  //       Relevant links:
+  //       - https://github.com/w3c/webrtc-pc/issues/2519#issuecomment-622055440
+  //       - https://stackoverflow.com/questions/57256828/how-to-fix-invalidstateerror-cannot-add-ice-candidate-when-there-is-no-remote-s
+  const [isReadyForIceCandidates, setIsReadyForIceCandidates] = useState(false);
+  const [iceCandidateQueue, setIceCandidateQueue] = useState<RTCIceCandidate[]>([]);
+
   // TODO: This logic will have to change to support multiple people in a call
   const contactUri = useMemo(() => conversation.getFirstMember().contact.getUri(), [conversation]);
 
@@ -215,6 +225,16 @@
   /* WebSocket Listeners */
 
   useEffect(() => {
+    const addQueuedIceCandidates = async () => {
+      console.info('WebRTC remote description has been set. Ready to receive ICE candidates');
+      setIsReadyForIceCandidates(true);
+      if (iceCandidateQueue.length !== 0) {
+        console.warn('Adding queued ICE candidates...', iceCandidateQueue);
+
+        await Promise.all(iceCandidateQueue.map((iceCandidate) => webRtcConnection.addIceCandidate(iceCandidate)));
+      }
+    };
+
     const webRtcOfferListener = async (data: WebRtcSdp) => {
       console.info('Received event on WebRtcOffer', data);
       if (data.conversationId !== conversationId) {
@@ -224,6 +244,7 @@
 
       await webRtcConnection.setRemoteDescription(new RTCSessionDescription(data.sdp));
       await sendWebRtcAnswer();
+      await addQueuedIceCandidates();
     };
 
     const webRtcAnswerListener = async (data: WebRtcSdp) => {
@@ -234,6 +255,7 @@
       }
 
       await webRtcConnection.setRemoteDescription(new RTCSessionDescription(data.sdp));
+      await addQueuedIceCandidates();
     };
 
     webSocket.bind(WebSocketMessageType.WebRtcOffer, webRtcOfferListener);
@@ -243,7 +265,7 @@
       webSocket.unbind(WebSocketMessageType.WebRtcOffer, webRtcOfferListener);
       webSocket.unbind(WebSocketMessageType.WebRtcAnswer, webRtcAnswerListener);
     };
-  }, [webSocket, webRtcConnection, sendWebRtcAnswer, conversationId]);
+  }, [webSocket, webRtcConnection, sendWebRtcAnswer, conversationId, iceCandidateQueue]);
 
   useEffect(() => {
     const webRtcIceCandidateListener = async (data: WebRtcIceCandidate) => {
@@ -252,7 +274,22 @@
         return;
       }
 
-      await webRtcConnection.addIceCandidate(data.candidate);
+      if (!data.candidate) {
+        return;
+      }
+
+      if (isReadyForIceCandidates) {
+        await webRtcConnection.addIceCandidate(data.candidate);
+      } else {
+        console.warn(
+          "Received event on WebRtcIceCandidate before 'setRemoteDescription' was called. Pushing to ICE candidates queue...",
+          data
+        );
+        setIceCandidateQueue((v) => {
+          v.push(data.candidate);
+          return v;
+        });
+      }
     };
 
     webSocket.bind(WebSocketMessageType.WebRtcIceCandidate, webRtcIceCandidateListener);
@@ -260,7 +297,7 @@
     return () => {
       webSocket.unbind(WebSocketMessageType.WebRtcIceCandidate, webRtcIceCandidateListener);
     };
-  }, [webRtcConnection, webSocket, conversationId]);
+  }, [webRtcConnection, webSocket, conversationId, isReadyForIceCandidates]);
 
   /* WebRTC Listeners */