Enable screen sharing

For now, only the video is transmitted.
The camera is turned off when screensharing, just like on the desktop
app.

In CallProvider, replace `isVideoOn` state with `videoStatus` to toggle
between off, on or screenshare.
Add icon for when screenshare is active.
Remove screenshare button menu options, as they are unavailable on the
web.

Change-Id: I95dd1c18efd91ab8b9063f0e7f817839e4f24aba
diff --git a/client/src/contexts/CallProvider.tsx b/client/src/contexts/CallProvider.tsx
index 39aad07..5464228 100644
--- a/client/src/contexts/CallProvider.tsx
+++ b/client/src/contexts/CallProvider.tsx
@@ -24,7 +24,7 @@
 import CallPermissionDenied from '../pages/CallPermissionDenied';
 import { CallRouteParams } from '../router';
 import { callTimeoutMs } from '../utils/constants';
-import { SetState, WithChildren } from '../utils/utils';
+import { AsyncSetState, SetState, WithChildren } from '../utils/utils';
 import { useConversationContext } from './ConversationProvider';
 import { MediaDevicesInfo, MediaInputKind, WebRtcContext } from './WebRtcProvider';
 import { IWebSocketContext, WebSocketContext } from './WebSocketProvider';
@@ -40,6 +40,12 @@
   PermissionsDenied,
 }
 
+export enum VideoStatus {
+  Off,
+  Camera,
+  ScreenShare,
+}
+
 type MediaDeviceIdState = {
   id: string | undefined;
   setId: (id: string | undefined) => void | Promise<void>;
@@ -52,8 +58,8 @@
 
   isAudioOn: boolean;
   setIsAudioOn: SetState<boolean>;
-  isVideoOn: boolean;
-  setIsVideoOn: SetState<boolean>;
+  videoStatus: VideoStatus;
+  updateVideoStatus: AsyncSetState<VideoStatus>;
   isChatShown: boolean;
   setIsChatShown: SetState<boolean>;
   isFullscreen: boolean;
@@ -89,8 +95,8 @@
 
   isAudioOn: false,
   setIsAudioOn: () => {},
-  isVideoOn: false,
-  setIsVideoOn: () => {},
+  videoStatus: VideoStatus.Off,
+  updateVideoStatus: () => Promise.reject(),
   isChatShown: false,
   setIsChatShown: () => {},
   isFullscreen: false,
@@ -122,8 +128,15 @@
   webSocket: IWebSocketContext;
 }) => {
   const { state: routeState } = useUrlParams<CallRouteParams>();
-  const { localStream, sendWebRtcOffer, iceConnectionState, closeConnection, getMediaDevices, updateLocalStream } =
-    useContext(WebRtcContext);
+  const {
+    localStream,
+    updateScreenShare,
+    sendWebRtcOffer,
+    iceConnectionState,
+    closeConnection,
+    getMediaDevices,
+    updateLocalStream,
+  } = useContext(WebRtcContext);
   const { conversationId, conversation } = useConversationContext();
   const navigate = useNavigate();
 
@@ -133,7 +146,7 @@
   const [videoDeviceId, setVideoDeviceId] = useState<string>();
 
   const [isAudioOn, setIsAudioOn] = useState(false);
-  const [isVideoOn, setIsVideoOn] = useState(false);
+  const [videoStatus, setVideoStatus] = useState(VideoStatus.Off);
   const [isChatShown, setIsChatShown] = useState(false);
   const [isFullscreen, setIsFullscreen] = useState(false);
   const [callStatus, setCallStatus] = useState(routeState?.callStatus);
@@ -187,14 +200,35 @@
   useEffect(() => {
     if (localStream) {
       for (const track of localStream.getVideoTracks()) {
-        track.enabled = isVideoOn;
+        track.enabled = videoStatus === VideoStatus.Camera;
         const deviceId = track.getSettings().deviceId;
         if (deviceId) {
           setVideoDeviceId(deviceId);
         }
       }
     }
-  }, [isVideoOn, localStream]);
+  }, [videoStatus, localStream]);
+
+  const updateVideoStatus = useCallback(
+    async (newStatus: ((prevState: VideoStatus) => VideoStatus) | VideoStatus) => {
+      if (typeof newStatus === 'function') {
+        newStatus = newStatus(videoStatus);
+      }
+
+      const stream = await updateScreenShare(newStatus === VideoStatus.ScreenShare);
+      if (stream) {
+        for (const track of stream.getTracks()) {
+          track.addEventListener('ended', () => {
+            console.warn('Browser ended screen sharing');
+            updateVideoStatus(VideoStatus.Off);
+          });
+        }
+      }
+
+      setVideoStatus(newStatus);
+    },
+    [videoStatus, updateScreenShare]
+  );
 
   useEffect(() => {
     const onFullscreenChange = () => {
@@ -220,7 +254,7 @@
           };
 
           setCallStatus(CallStatus.Ringing);
-          setIsVideoOn(withVideoOn);
+          setVideoStatus(withVideoOn ? VideoStatus.Camera : VideoStatus.Off);
           console.info('Sending CallBegin', callBegin);
           webSocket.send(WebSocketMessageType.CallBegin, callBegin);
         })
@@ -241,7 +275,7 @@
             conversationId,
           };
 
-          setIsVideoOn(withVideoOn);
+          setVideoStatus(withVideoOn ? VideoStatus.Camera : VideoStatus.Off);
           setCallStatus(CallStatus.Connecting);
           console.info('Sending CallAccept', callAccept);
           webSocket.send(WebSocketMessageType.CallAccept, callAccept);
@@ -324,7 +358,7 @@
       console.info('ICE connection disconnected or failed, ending call');
       endCall();
     }
-  }, [iceConnectionState, callStatus, isVideoOn, endCall]);
+  }, [iceConnectionState, callStatus, videoStatus, endCall]);
 
   useEffect(() => {
     const checkStatusTimeout = () => {
@@ -386,8 +420,8 @@
         currentMediaDeviceIds,
         isAudioOn,
         setIsAudioOn,
-        isVideoOn,
-        setIsVideoOn,
+        videoStatus,
+        updateVideoStatus,
         isChatShown,
         setIsChatShown,
         isFullscreen,