Bind call buttons and set ringing timeout

- Accept call with or without video
- Ringing / connection timeout
- Close camera when call ends
- Display incoming call type (audio vs video)

GitLab: #154
GitLab: #165
GitLab: #168
Change-Id: I93ba7148941656b5bebd3ca38898bce0d4db41ca
diff --git a/client/src/components/CallButtons.tsx b/client/src/components/CallButtons.tsx
index 6e77b39..f6f7275 100644
--- a/client/src/components/CallButtons.tsx
+++ b/client/src/components/CallButtons.tsx
@@ -284,7 +284,7 @@
     <ColoredCallButton
       aria-label="answer call audio"
       onClick={() => {
-        acceptCall();
+        acceptCall(false);
       }}
       Icon={PlaceAudioCallIcon}
       paletteColor={(theme) => theme.palette.success}
@@ -300,7 +300,7 @@
     <ColoredCallButton
       aria-label="answer call video"
       onClick={() => {
-        acceptCall();
+        acceptCall(true);
       }}
       paletteColor={(theme) => theme.palette.success}
       Icon={VideoCameraIcon}
diff --git a/client/src/contexts/CallProvider.tsx b/client/src/contexts/CallProvider.tsx
index a4a9ead..6b8d4ee 100644
--- a/client/src/contexts/CallProvider.tsx
+++ b/client/src/contexts/CallProvider.tsx
@@ -15,12 +15,13 @@
  * License along with this program.  If not, see
  * <https://www.gnu.org/licenses/>.
  */
-import { CallAction, WebSocketMessageType } from 'jami-web-common';
+import { CallAction, CallBegin, WebSocketMessageType } from 'jami-web-common';
 import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
 import { Navigate, useNavigate } from 'react-router-dom';
 
 import { useUrlParams } from '../hooks/useUrlParams';
 import { CallRouteParams } from '../router';
+import { callTimeoutMs } from '../utils/constants';
 import { SetState, WithChildren } from '../utils/utils';
 import { ConversationContext } from './ConversationProvider';
 import { WebRtcContext } from './WebRtcProvider';
@@ -53,7 +54,7 @@
   callStatus: CallStatus;
   callStartTime: Date | undefined;
 
-  acceptCall: () => void;
+  acceptCall: (withVideoOn: boolean) => void;
   endCall: () => void;
 }
 
@@ -79,7 +80,7 @@
   callStatus: CallStatus.Default,
   callStartTime: undefined,
 
-  acceptCall: () => {},
+  acceptCall: (_: boolean) => {},
   endCall: () => {},
 };
 
@@ -163,22 +164,54 @@
     }
   }, [localStream, webRtcConnection]);
 
+  const setAudioStatus = useCallback(
+    (isOn: boolean) => {
+      if (!localStream) {
+        return;
+      }
+
+      for (const track of localStream.getAudioTracks()) {
+        track.enabled = isOn;
+      }
+
+      setIsAudioOn(isOn);
+    },
+    [localStream]
+  );
+
+  const setVideoStatus = useCallback(
+    (isOn: boolean) => {
+      if (!localStream) {
+        return;
+      }
+
+      for (const track of localStream.getVideoTracks()) {
+        track.enabled = isOn;
+      }
+
+      setIsVideoOn(isOn);
+    },
+    [localStream]
+  );
+
   useEffect(() => {
     if (!webSocket) {
       return;
     }
 
     if (callRole === 'caller' && callStatus === CallStatus.Default) {
-      const callBegin: CallAction = {
+      const callBegin: CallBegin = {
         contactId: contactUri,
         conversationId,
+        withVideoOn: routeState?.isVideoOn ?? false,
       };
 
       console.info('Sending CallBegin', callBegin);
       webSocket.send(WebSocketMessageType.CallBegin, callBegin);
       setCallStatus(CallStatus.Ringing);
+      setIsVideoOn(routeState?.isVideoOn ?? false);
     }
-  }, [webSocket, callRole, callStatus, contactUri, conversationId]);
+  }, [webSocket, callRole, callStatus, contactUri, conversationId, routeState]);
 
   useEffect(() => {
     const onFullscreenChange = () => {
@@ -224,9 +257,16 @@
       throw new Error('Could not quit call: webRtcConnection is not defined');
     }
 
+    const localTracks = localStream?.getTracks();
+    if (localTracks) {
+      for (const track of localTracks) {
+        track.stop();
+      }
+    }
+
     webRtcConnection.close();
     navigate(`/conversation/${conversationId}`);
-  }, [webRtcConnection, navigate, conversationId]);
+  }, [webRtcConnection, localStream, navigate, conversationId]);
 
   useEffect(() => {
     if (!webSocket) {
@@ -249,24 +289,29 @@
     if (callStatus === CallStatus.Connecting && isConnected) {
       console.info('Changing call status to InCall');
       setCallStatus(CallStatus.InCall);
+      setVideoStatus(isVideoOn);
       setCallStartTime(new Date());
     }
-  }, [isConnected, callStatus]);
+  }, [isConnected, callStatus, setVideoStatus, isVideoOn]);
 
-  const acceptCall = useCallback(() => {
-    if (!webSocket) {
-      throw new Error('Could not accept call');
-    }
+  const acceptCall = useCallback(
+    (withVideoOn: boolean) => {
+      if (!webSocket) {
+        throw new Error('Could not accept call');
+      }
 
-    const callAccept: CallAction = {
-      contactId: contactUri,
-      conversationId,
-    };
+      const callAccept: CallAction = {
+        contactId: contactUri,
+        conversationId,
+      };
 
-    console.info('Sending CallAccept', callAccept);
-    webSocket.send(WebSocketMessageType.CallAccept, callAccept);
-    setCallStatus(CallStatus.Connecting);
-  }, [webSocket, contactUri, conversationId]);
+      console.info('Sending CallAccept', callAccept);
+      webSocket.send(WebSocketMessageType.CallAccept, callAccept);
+      setIsVideoOn(withVideoOn);
+      setCallStatus(CallStatus.Connecting);
+    },
+    [webSocket, contactUri, conversationId]
+  );
 
   const endCall = useCallback(() => {
     if (!webSocket) {
@@ -284,35 +329,18 @@
     // TODO: write in chat that the call ended
   }, [webSocket, contactUri, conversationId, quitCall]);
 
-  const setAudioStatus = useCallback(
-    (isOn: boolean) => {
-      if (!localStream) {
-        return;
+  useEffect(() => {
+    const checkStatusTimeout = () => {
+      if (callStatus !== CallStatus.InCall) {
+        endCall();
       }
+    };
+    const timeoutId = setTimeout(checkStatusTimeout, callTimeoutMs);
 
-      for (const track of localStream.getAudioTracks()) {
-        track.enabled = isOn;
-      }
-
-      setIsAudioOn(isOn);
-    },
-    [localStream]
-  );
-
-  const setVideoStatus = useCallback(
-    (isOn: boolean) => {
-      if (!localStream) {
-        return;
-      }
-
-      for (const track of localStream.getVideoTracks()) {
-        track.enabled = isOn;
-      }
-
-      setIsVideoOn(isOn);
-    },
-    [localStream]
-  );
+    return () => {
+      clearTimeout(timeoutId);
+    };
+  }, [callStatus, endCall]);
 
   if (!callRole || callStatus === undefined) {
     console.error('Invalid route. Redirecting...');
diff --git a/client/src/contexts/WebRtcProvider.tsx b/client/src/contexts/WebRtcProvider.tsx
index f87e1ff..f1c9add 100644
--- a/client/src/contexts/WebRtcProvider.tsx
+++ b/client/src/contexts/WebRtcProvider.tsx
@@ -175,20 +175,20 @@
       setRemoteStreams(event.streams);
     };
 
-    const connectionStateChangeEventListener = () => {
+    const iceConnectionStateChangeEventListener = () => {
       setIsConnected(
-        webRtcConnection.iceConnectionState === 'completed' || webRtcConnection.iceConnectionState === 'connected'
+        webRtcConnection.iceConnectionState === 'connected' || webRtcConnection.iceConnectionState === 'completed'
       );
     };
 
     webRtcConnection.addEventListener('icecandidate', iceCandidateEventListener);
     webRtcConnection.addEventListener('track', trackEventListener);
-    webRtcConnection.addEventListener('iceconnectionstatechange', connectionStateChangeEventListener);
+    webRtcConnection.addEventListener('iceconnectionstatechange', iceConnectionStateChangeEventListener);
 
     return () => {
       webRtcConnection.removeEventListener('icecandidate', iceCandidateEventListener);
       webRtcConnection.removeEventListener('track', trackEventListener);
-      webRtcConnection.removeEventListener('iceconnectionstatechange', connectionStateChangeEventListener);
+      webRtcConnection.removeEventListener('iceconnectionstatechange', iceConnectionStateChangeEventListener);
     };
   }, [webRtcConnection, webSocket, contactUri]);
 
diff --git a/client/src/managers/NotificationManager.tsx b/client/src/managers/NotificationManager.tsx
index dacc3c1..5362f54 100644
--- a/client/src/managers/NotificationManager.tsx
+++ b/client/src/managers/NotificationManager.tsx
@@ -15,7 +15,7 @@
  * License along with this program.  If not, see
  * <https://www.gnu.org/licenses/>.
  */
-import { CallAction, WebSocketMessageType } from 'jami-web-common';
+import { CallBegin, WebSocketMessageType } from 'jami-web-common';
 import { useContext, useEffect } from 'react';
 import { useNavigate } from 'react-router-dom';
 
@@ -37,11 +37,12 @@
       return;
     }
 
-    const callBeginListener = (data: CallAction) => {
+    const callBeginListener = (data: CallBegin) => {
       console.info('Received event on CallBegin', data);
       navigate(`/conversation/${data.conversationId}/call?role=receiver`, {
         state: {
           callStatus: CallStatus.Ringing,
+          isVideoOn: data.withVideoOn,
         },
       });
     };
diff --git a/client/src/pages/CallInterface.tsx b/client/src/pages/CallInterface.tsx
index c0ab334..11a1df5 100644
--- a/client/src/pages/CallInterface.tsx
+++ b/client/src/pages/CallInterface.tsx
@@ -29,6 +29,7 @@
   useState,
 } from 'react';
 import Draggable from 'react-draggable';
+import { useLocation } from 'react-router-dom';
 
 import { ExpandableButtonProps } from '../components/Button';
 import {
@@ -51,6 +52,7 @@
 export default () => {
   const { callRole, callStatus, isChatShown, isFullscreen } = useContext(CallContext);
   const callInterfaceRef = useRef<HTMLDivElement>();
+  const { state } = useLocation();
 
   useEffect(() => {
     if (!callInterfaceRef.current) {
@@ -69,7 +71,7 @@
       <CallPending
         pending={callRole}
         caller={callStatus === CallStatus.Connecting ? 'connecting' : 'calling'}
-        medium="audio"
+        medium={state?.isVideoOn ? 'video' : 'audio'}
       />
     );
   }
diff --git a/client/src/utils/constants.ts b/client/src/utils/constants.ts
index 56c738a..d81a145 100644
--- a/client/src/utils/constants.ts
+++ b/client/src/utils/constants.ts
@@ -21,6 +21,8 @@
 
 export const jamiLogoDefaultSize = '512px';
 
+export const callTimeoutMs = 60_000;
+
 const apiUrl: string = import.meta.env.VITE_API_URL;
 
 if (!apiUrl) {