Add audio/video device options in call interface

Detect audio and video devices and show the options in the expand menus of the calling buttons in the call interface.
The options are not clickable for now, this will be implemented in https://git.jami.net/savoirfairelinux/jami-web/-/issues/146

GitLab: #98
Change-Id: I51ebf9ec90820db37e0d36b4ac9f57f119119da3
diff --git a/client/src/components/Button.tsx b/client/src/components/Button.tsx
index 268cab6..379dc8f 100644
--- a/client/src/components/Button.tsx
+++ b/client/src/components/Button.tsx
@@ -106,14 +106,14 @@
 };
 
 export type ExpandableButtonProps = IconButtonProps & {
-  hidden?: boolean;
+  isVertical?: boolean;
   Icon?: ComponentType<SvgIconProps>;
   expandMenuOptions?: (ExpandMenuOption | ExpandMenuRadioOption)[];
   IconButtonComp?: ComponentType<IconButtonProps>;
 };
 
 export const ExpandableButton = ({
-  hidden,
+  isVertical,
   Icon,
   expandMenuOptions = undefined,
   IconButtonComp = IconButton,
@@ -125,19 +125,19 @@
   };
 
   return (
-    <Box>
+    <>
       {expandMenuOptions && (
         <Menu
           anchorEl={anchorEl}
           open={!!anchorEl}
           onClose={handleClose}
           anchorOrigin={{
-            vertical: !hidden ? 'top' : 'center',
-            horizontal: !hidden ? 'center' : 'left',
+            vertical: !isVertical ? 'top' : 'center',
+            horizontal: !isVertical ? 'center' : 'left',
           }}
           transformOrigin={{
-            vertical: !hidden ? 'bottom' : 'center',
-            horizontal: !hidden ? 'center' : 'right',
+            vertical: !isVertical ? 'bottom' : 'center',
+            horizontal: !isVertical ? 'center' : 'right',
           }}
         >
           {expandMenuOptions?.map((option, id) => {
@@ -168,28 +168,22 @@
           })}
         </Menu>
       )}
-      <Box
-        position="relative"
-        display="flex"
-        justifyContent="center"
-        alignItems="center"
-        onClick={(e) => setAnchorEl(e.currentTarget)}
-      >
+      <Box position="relative" display="flex" justifyContent="center" alignItems="center">
         {expandMenuOptions && (
           <IconButton
-            {...props}
             aria-label="expand options"
-            size="small"
+            onClick={(e) => setAnchorEl(e.currentTarget)}
             sx={{
-              transform: !hidden ? 'scale(0.5)' : 'scale(0.5) rotate(-90deg)',
+              rotate: !isVertical ? '' : '-90deg',
               position: 'absolute',
-              top: !hidden ? '-50%' : 0,
-              left: hidden ? '-50%' : 0,
-              width: '100%',
-              height: '100%',
+              top: !isVertical ? '-55%' : 'auto',
+              left: !isVertical ? 'auto' : '-55%',
+              zIndex: 1,
             }}
+            className={props.className}
           >
             <ExpandLessIcon
+              fontSize="small"
               sx={{
                 backgroundColor: '#444444',
                 borderRadius: '5px',
@@ -199,7 +193,7 @@
         )}
         <IconButtonComp {...props}>{Icon && <Icon />}</IconButtonComp>
       </Box>
-    </Box>
+    </>
   );
 };
 
diff --git a/client/src/components/CallButtons.tsx b/client/src/components/CallButtons.tsx
index 79d5485..dbaef70 100644
--- a/client/src/components/CallButtons.tsx
+++ b/client/src/components/CallButtons.tsx
@@ -17,7 +17,7 @@
  */
 
 import { styled } from '@mui/material/styles';
-import React, { useContext } from 'react';
+import React, { useContext, useMemo } from 'react';
 import { Trans } from 'react-i18next';
 
 import { WebRTCContext } from '../contexts/WebRTCProvider';
@@ -129,19 +129,29 @@
   );
 };
 
+const useMediaDeviceExpandMenuOptions = (kind: MediaDeviceKind) => {
+  const { mediaDevices } = useContext(WebRTCContext);
+
+  return useMemo(
+    () =>
+      mediaDevices[kind].map((device) => ({
+        key: device.deviceId,
+        description: device.label,
+      })),
+    [mediaDevices, kind]
+  );
+};
+
 export const CallingVolumeButton = (props: ExpandableButtonProps) => {
+  const options = useMediaDeviceExpandMenuOptions('audiooutput');
+
   return (
     <CallButton
       aria-label="volume options"
       Icon={VolumeIcon}
       expandMenuOptions={[
         {
-          options: [
-            {
-              key: '0',
-              description: <Trans i18nKey="dummy_option_string" />,
-            },
-          ],
+          options,
         },
       ]}
       {...props}
@@ -151,10 +161,16 @@
 
 export const CallingMicButton = (props: ExpandableButtonProps) => {
   const { isAudioOn, setAudioStatus } = useContext(WebRTCContext);
+  const options = useMediaDeviceExpandMenuOptions('audioinput');
 
   return (
     <CallButton
       aria-label="microphone options"
+      expandMenuOptions={[
+        {
+          options,
+        },
+      ]}
       IconButtonComp={(props) => (
         <ToggleIconButton
           IconOn={MicroIcon}
@@ -171,9 +187,16 @@
 
 export const CallingVideoCameraButton = (props: ExpandableButtonProps) => {
   const { isVideoOn, setVideoStatus } = useContext(WebRTCContext);
+  const options = useMediaDeviceExpandMenuOptions('videoinput');
+
   return (
     <CallButton
       aria-label="camera options"
+      expandMenuOptions={[
+        {
+          options,
+        },
+      ]}
       IconButtonComp={(props) => (
         <ToggleIconButton
           IconOn={VideoCameraIcon}
diff --git a/client/src/contexts/WebRTCProvider.tsx b/client/src/contexts/WebRTCProvider.tsx
index eb52e6b..7dc1611 100644
--- a/client/src/contexts/WebRTCProvider.tsx
+++ b/client/src/contexts/WebRTCProvider.tsx
@@ -27,6 +27,8 @@
   localVideoRef: React.RefObject<HTMLVideoElement> | null;
   remoteVideoRef: React.RefObject<HTMLVideoElement> | null;
 
+  mediaDevices: Record<MediaDeviceKind, MediaDeviceInfo[]>;
+
   contactId: string;
 
   isAudioOn: boolean;
@@ -39,6 +41,11 @@
 const defaultWebRTCContext: IWebRTCContext = {
   localVideoRef: null,
   remoteVideoRef: null,
+  mediaDevices: {
+    audioinput: [],
+    audiooutput: [],
+    videoinput: [],
+  },
 
   contactId: '',
 
@@ -74,6 +81,25 @@
   const [webRTCConnection, setWebRTCConnection] = useState<RTCPeerConnection | undefined>();
   const localStreamRef = useRef<MediaStream>();
   const webSocket = useContext(WebSocketContext);
+  const [mediaDevices, setMediaDevices] = useState<Record<MediaDeviceKind, MediaDeviceInfo[]>>(
+    defaultWebRTCContext.mediaDevices
+  );
+
+  useEffect(() => {
+    navigator.mediaDevices.enumerateDevices().then((devices) => {
+      const newMediaDevices: Record<MediaDeviceKind, MediaDeviceInfo[]> = {
+        audioinput: [],
+        audiooutput: [],
+        videoinput: [],
+      };
+
+      for (const device of devices) {
+        newMediaDevices[device.kind].push(device);
+      }
+
+      setMediaDevices(newMediaDevices);
+    });
+  }, []);
 
   useEffect(() => {
     if (!webRTCConnection) {
@@ -217,6 +243,7 @@
       value={{
         localVideoRef,
         remoteVideoRef,
+        mediaDevices,
         contactId,
         isAudioOn,
         setAudioStatus,
diff --git a/client/src/locale/en/translation.json b/client/src/locale/en/translation.json
index e797090..9210cdd 100644
--- a/client/src/locale/en/translation.json
+++ b/client/src/locale/en/translation.json
@@ -4,7 +4,6 @@
   "share_window": "Share window",
   "share_screen_area": "Share an area of your screen",
   "share_file": "Share a file",
-  "dummy_option_string": "Test option",
   "conversation_message": "Message",
   "conversation_start_audiocall": "Start audio call",
   "conversation_start_videocall": "Start video call",
diff --git a/client/src/locale/fr/translation.json b/client/src/locale/fr/translation.json
index 90818e5..b519e9d 100644
--- a/client/src/locale/fr/translation.json
+++ b/client/src/locale/fr/translation.json
@@ -4,7 +4,6 @@
   "share_window": "Partager la fenêtre",
   "share_screen_area": "Partager une partie de l'écran",
   "share_file": "Partager le fichier",
-  "dummy_option_string": "Option test",
   "conversation_message": "Envoyer un message",
   "conversation_start_audiocall": "Démarrer appel audio",
   "conversation_start_videocall": "Démarrer appel vidéo",
diff --git a/client/src/pages/CallInterface.tsx b/client/src/pages/CallInterface.tsx
index 5759721..3484858 100644
--- a/client/src/pages/CallInterface.tsx
+++ b/client/src/pages/CallInterface.tsx
@@ -154,7 +154,7 @@
           Call
         </Button>
         <CallingMicButton />
-        <CallingEndButton hidden={false} />
+        <CallingEndButton />
         <CallingVideoCameraButton />
       </Stack>
     </Card>
@@ -229,11 +229,11 @@
         {initialMeasurementDone &&
           displayedButtons.map((SecondaryButton, i) => (
             <Fragment key={i}>
-              <SecondaryButton hidden={false} />
+              <SecondaryButton />
             </Fragment>
           ))}
         {(!!hiddenButtons.length || !initialMeasurementDone) && (
-          <CallingMoreVerticalButton hidden={true} onClick={() => setHiddenMenuVisible(!hiddenMenuVisible)} />
+          <CallingMoreVerticalButton isVertical onClick={() => setHiddenMenuVisible(!hiddenMenuVisible)} />
         )}
       </Stack>
 
@@ -248,7 +248,7 @@
             >
               {hiddenButtons.map((SecondaryButton, i) => (
                 <Fragment key={i}>
-                  <SecondaryButton hidden={true} />
+                  <SecondaryButton vertical />
                 </Fragment>
               ))}
             </Stack>