From 2e7dfe85e6e2fe0815c3acd4dd1885eb4cdf79fa Mon Sep 17 00:00:00 2001
From: Robert Long <robert@robertlong.me>
Date: Thu, 7 Oct 2021 17:25:40 -0700
Subject: [PATCH 1/2] WIP

---
 src/Room.jsx              | 30 +++++++++++++++--
 src/RoomButton.jsx        | 71 ++++++++++++++++++++++++++++++++++-----
 src/RoomButton.module.css | 36 ++++++++++++++++++--
 3 files changed, 125 insertions(+), 12 deletions(-)

diff --git a/src/Room.jsx b/src/Room.jsx
index 68012a5..b6a124d 100644
--- a/src/Room.jsx
+++ b/src/Room.jsx
@@ -78,7 +78,7 @@ export function Room({ client }) {
 
   return (
     <div className={styles.room}>
-      <GroupCallView groupCall={groupCall} />
+      <GroupCallView client={client} groupCall={groupCall} />
     </div>
   );
 }
@@ -224,6 +224,8 @@ function RoomSetupView({
   );
 }
 
+function useMediaManager() {}
+
 function InRoomView({
   roomName,
   microphoneMuted,
@@ -239,6 +241,15 @@ function InRoomView({
 }) {
   const [layout, toggleLayout] = useVideoGridLayout();
 
+  const {
+    audioDeviceId,
+    audioDevices,
+    setAudioDevice,
+    videoDeviceId,
+    videoDevices,
+    setVideoDevice,
+  } = useMediaManager();
+
   const items = useMemo(() => {
     const participants = [];
 
@@ -284,10 +295,25 @@ function InRoomView({
         <VideoGrid items={items} layout={layout} />
       )}
       <div className={styles.footer}>
-        <MicButton muted={microphoneMuted} onClick={toggleMicrophoneMuted} />
+        <MicButton
+          muted={microphoneMuted}
+          onClick={toggleMicrophoneMuted}
+          value={audioDeviceId}
+          onChange={({ value }) => setAudioDevice(value)}
+          options={audioDevices.map(({ label, deviceId }) => ({
+            label,
+            value: deviceId,
+          }))}
+        />
         <VideoButton
           enabled={localVideoMuted}
           onClick={toggleLocalVideoMuted}
+          value={videoDeviceId}
+          onChange={({ value }) => setVideoDevice(value)}
+          options={videoDevices.map(({ label, deviceId }) => ({
+            label,
+            value: deviceId,
+          }))}
         />
         <ScreenshareButton
           enabled={isScreensharing}
diff --git a/src/RoomButton.jsx b/src/RoomButton.jsx
index 3f49d41..12ff3d1 100644
--- a/src/RoomButton.jsx
+++ b/src/RoomButton.jsx
@@ -10,6 +10,8 @@ import { ReactComponent as SettingsIcon } from "./icons/Settings.svg";
 import { ReactComponent as GridIcon } from "./icons/Grid.svg";
 import { ReactComponent as SpeakerIcon } from "./icons/Speaker.svg";
 import { ReactComponent as ScreenshareIcon } from "./icons/Screenshare.svg";
+import { ReactComponent as ChevronIcon } from "./icons/Chevron.svg";
+import { useEffect } from "react";
 
 export function RoomButton({ on, className, children, ...rest }) {
   return (
@@ -22,19 +24,72 @@ export function RoomButton({ on, className, children, ...rest }) {
   );
 }
 
-export function MicButton({ muted, ...rest }) {
+export function DropdownButton({ onChange, options, children }) {
+  const buttonRef = useRef();
+  const [open, setOpen] = useState(false);
+
+  useEffect(() => {
+    function onClick() {
+      if (open) {
+        setOpen(false);
+      }
+    }
+
+    window.addEventListener("click", onClick);
+
+    return () => {
+      window.removeEventListener("click", onClick);
+    };
+  }, [open]);
+
   return (
-    <RoomButton {...rest} on={muted}>
-      {muted ? <MuteMicIcon /> : <MicIcon />}
-    </RoomButton>
+    <div className={styles.dropDownButtonContainer}>
+      {children}
+      <button
+        ref={buttonRef}
+        className={styles.dropdownButton}
+        onClick={() => setOpen(true)}
+      >
+        <ChevronIcon />
+      </button>
+      {open && (
+        <div className={styles.dropDownContainer}>
+          <ul>
+            {options.map((item) => (
+              <li
+                key={item.value}
+                className={classNames({
+                  [styles.dropDownActiveItem]: item.value === value,
+                })}
+                onClick={() => onChange(item)}
+              >
+                {item.label}
+              </li>
+            ))}
+          </ul>
+        </div>
+      )}
+    </div>
   );
 }
 
-export function VideoButton({ enabled, ...rest }) {
+export function MicButton({ muted, onChange, value, options, ...rest }) {
   return (
-    <RoomButton {...rest} on={enabled}>
-      {enabled ? <DisableVideoIcon /> : <VideoIcon />}
-    </RoomButton>
+    <DropdownButton onChange={onChange} options={options} value={value}>
+      <RoomButton {...rest} on={muted}>
+        {muted ? <MuteMicIcon /> : <MicIcon />}
+      </RoomButton>
+    </DropdownButton>
+  );
+}
+
+export function VideoButton({ enabled, onChange, value, ...rest }) {
+  return (
+    <DropdownButton onChange={onChange} options={options} value={value}>
+      <RoomButton {...rest} on={enabled}>
+        {enabled ? <DisableVideoIcon /> : <VideoIcon />}
+      </RoomButton>
+    </DropdownButton>
   );
 }
 
diff --git a/src/RoomButton.module.css b/src/RoomButton.module.css
index c0f62d1..97fced4 100644
--- a/src/RoomButton.module.css
+++ b/src/RoomButton.module.css
@@ -15,7 +15,8 @@ limitations under the License.
 */
 
 .roomButton,
-.headerButton {
+.headerButton,
+.dropdownButton {
   display: flex;
   justify-content: center;
   align-items: center;
@@ -29,7 +30,7 @@ limitations under the License.
   width: 50px;
   height: 50px;
   border-radius: 50px;
-  background-color: rgba(111, 120, 130, 0.3);
+  background-color: #394049;
 }
 
 .roomButton:hover {
@@ -73,3 +74,34 @@ limitations under the License.
 .screenshareButton.on svg * {
   fill: #0dbd8b;
 }
+
+.dropdownButtonContainer {
+  position: relative;
+}
+
+.dropdownButton {
+  width: 15px;
+  height: 15px;
+  border-radius: 15px;
+  background-color: #394049;
+  position: absolute;
+  bottom: 0;
+  right: 0;
+}
+
+.dropdownButton:hover {
+  background-color: #8d97a5;
+}
+
+.dropdownButton:hover svg * {
+  fill: #8d97a5;
+}
+
+.dropdownContainer {
+  position: absolute;
+  left: 50%;
+  transform: translate(-50%, -100%);
+  top: 0;
+  background-color: #394049;
+  border-radius: 8px;
+}

From 5a714cef8d11ce7aac9aa593f449ef889be78a84 Mon Sep 17 00:00:00 2001
From: Robert Long <robert@robertlong.me>
Date: Tue, 12 Oct 2021 16:52:20 -0700
Subject: [PATCH 2/2] Add mic/webcam switching

---
 src/Room.jsx              | 114 ++++++++++++++++++++++++++++++--------
 src/RoomButton.jsx        |  31 +++++------
 src/RoomButton.module.css |  26 ++++++++-
 3 files changed, 127 insertions(+), 44 deletions(-)

diff --git a/src/Room.jsx b/src/Room.jsx
index b6a124d..60ccd20 100644
--- a/src/Room.jsx
+++ b/src/Room.jsx
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React, { useEffect, useMemo, useState } from "react";
+import React, { useCallback, useEffect, useMemo, useState } from "react";
 import styles from "./Room.module.css";
 import { useLocation, useParams } from "react-router-dom";
 import {
@@ -23,6 +23,7 @@ import {
   VideoButton,
   LayoutToggleButton,
   ScreenshareButton,
+  DropdownButton,
 } from "./RoomButton";
 import { Header, LeftNav, RightNav, CenterNav } from "./Header";
 import { Button } from "./Input";
@@ -83,7 +84,7 @@ export function Room({ client }) {
   );
 }
 
-export function GroupCallView({ groupCall }) {
+export function GroupCallView({ client, groupCall }) {
   const {
     state,
     error,
@@ -108,6 +109,7 @@ export function GroupCallView({ groupCall }) {
   } else if (state === GroupCallState.Entered) {
     return (
       <InRoomView
+        client={client}
         roomName={groupCall.room.name}
         microphoneMuted={microphoneMuted}
         localVideoMuted={localVideoMuted}
@@ -224,9 +226,70 @@ function RoomSetupView({
   );
 }
 
-function useMediaManager() {}
+function useMediaHandler(client) {
+  const [{ audioInput, videoInput, audioInputs, videoInputs }, setState] =
+    useState({
+      audioInput: null,
+      videoInput: null,
+      audioInputs: [],
+      videoInputs: [],
+    });
+
+  useEffect(() => {
+    function updateDevices() {
+      navigator.mediaDevices.enumerateDevices().then((devices) => {
+        const audioInputs = devices.filter(
+          (device) => device.kind === "audioinput"
+        );
+        const videoInputs = devices.filter(
+          (device) => device.kind === "videoinput"
+        );
+
+        setState((prevState) => ({
+          ...prevState,
+          audioInputs,
+          videoInputs,
+        }));
+      });
+    }
+
+    updateDevices();
+
+    navigator.mediaDevices.addEventListener("devicechange", updateDevices);
+
+    return () => {
+      navigator.mediaDevices.removeEventListener("devicechange", updateDevices);
+    };
+  }, []);
+
+  const setAudioInput = useCallback(
+    (deviceId) => {
+      setState((prevState) => ({ ...prevState, audioInput: deviceId }));
+      client.getMediaHandler().setAudioInput(deviceId);
+    },
+    [client]
+  );
+
+  const setVideoInput = useCallback(
+    (deviceId) => {
+      setState((prevState) => ({ ...prevState, videoInput: deviceId }));
+      client.getMediaHandler().setVideoInput(deviceId);
+    },
+    [client]
+  );
+
+  return {
+    audioInput,
+    audioInputs,
+    setAudioInput,
+    videoInput,
+    videoInputs,
+    setVideoInput,
+  };
+}
 
 function InRoomView({
+  client,
   roomName,
   microphoneMuted,
   localVideoMuted,
@@ -242,13 +305,13 @@ function InRoomView({
   const [layout, toggleLayout] = useVideoGridLayout();
 
   const {
-    audioDeviceId,
-    audioDevices,
-    setAudioDevice,
-    videoDeviceId,
-    videoDevices,
-    setVideoDevice,
-  } = useMediaManager();
+    audioInput,
+    audioInputs,
+    setAudioInput,
+    videoInput,
+    videoInputs,
+    setVideoInput,
+  } = useMediaHandler(client);
 
   const items = useMemo(() => {
     const participants = [];
@@ -295,26 +358,29 @@ function InRoomView({
         <VideoGrid items={items} layout={layout} />
       )}
       <div className={styles.footer}>
-        <MicButton
-          muted={microphoneMuted}
-          onClick={toggleMicrophoneMuted}
-          value={audioDeviceId}
-          onChange={({ value }) => setAudioDevice(value)}
-          options={audioDevices.map(({ label, deviceId }) => ({
+        <DropdownButton
+          value={audioInput}
+          onChange={({ value }) => setAudioInput(value)}
+          options={audioInputs.map(({ label, deviceId }) => ({
             label,
             value: deviceId,
           }))}
-        />
-        <VideoButton
-          enabled={localVideoMuted}
-          onClick={toggleLocalVideoMuted}
-          value={videoDeviceId}
-          onChange={({ value }) => setVideoDevice(value)}
-          options={videoDevices.map(({ label, deviceId }) => ({
+        >
+          <MicButton muted={microphoneMuted} onClick={toggleMicrophoneMuted} />
+        </DropdownButton>
+        <DropdownButton
+          value={videoInput}
+          onChange={({ value }) => setVideoInput(value)}
+          options={videoInputs.map(({ label, deviceId }) => ({
             label,
             value: deviceId,
           }))}
-        />
+        >
+          <VideoButton
+            enabled={localVideoMuted}
+            onClick={toggleLocalVideoMuted}
+          />
+        </DropdownButton>
         <ScreenshareButton
           enabled={isScreensharing}
           onClick={toggleScreensharing}
diff --git a/src/RoomButton.jsx b/src/RoomButton.jsx
index 12ff3d1..5c80132 100644
--- a/src/RoomButton.jsx
+++ b/src/RoomButton.jsx
@@ -1,4 +1,4 @@
-import React from "react";
+import React, { useRef, useState, useEffect } from "react";
 import classNames from "classnames";
 import styles from "./RoomButton.module.css";
 import { ReactComponent as MicIcon } from "./icons/Mic.svg";
@@ -11,7 +11,6 @@ import { ReactComponent as GridIcon } from "./icons/Grid.svg";
 import { ReactComponent as SpeakerIcon } from "./icons/Speaker.svg";
 import { ReactComponent as ScreenshareIcon } from "./icons/Screenshare.svg";
 import { ReactComponent as ChevronIcon } from "./icons/Chevron.svg";
-import { useEffect } from "react";
 
 export function RoomButton({ on, className, children, ...rest }) {
   return (
@@ -24,7 +23,7 @@ export function RoomButton({ on, className, children, ...rest }) {
   );
 }
 
-export function DropdownButton({ onChange, options, children }) {
+export function DropdownButton({ onChange, options, value, children }) {
   const buttonRef = useRef();
   const [open, setOpen] = useState(false);
 
@@ -43,7 +42,7 @@ export function DropdownButton({ onChange, options, children }) {
   }, [open]);
 
   return (
-    <div className={styles.dropDownButtonContainer}>
+    <div className={styles.dropdownButtonContainer}>
       {children}
       <button
         ref={buttonRef}
@@ -53,13 +52,13 @@ export function DropdownButton({ onChange, options, children }) {
         <ChevronIcon />
       </button>
       {open && (
-        <div className={styles.dropDownContainer}>
+        <div className={styles.dropdownContainer}>
           <ul>
             {options.map((item) => (
               <li
                 key={item.value}
                 className={classNames({
-                  [styles.dropDownActiveItem]: item.value === value,
+                  [styles.dropdownActiveItem]: item.value === value,
                 })}
                 onClick={() => onChange(item)}
               >
@@ -73,23 +72,19 @@ export function DropdownButton({ onChange, options, children }) {
   );
 }
 
-export function MicButton({ muted, onChange, value, options, ...rest }) {
+export function MicButton({ muted, ...rest }) {
   return (
-    <DropdownButton onChange={onChange} options={options} value={value}>
-      <RoomButton {...rest} on={muted}>
-        {muted ? <MuteMicIcon /> : <MicIcon />}
-      </RoomButton>
-    </DropdownButton>
+    <RoomButton {...rest} on={muted}>
+      {muted ? <MuteMicIcon /> : <MicIcon />}
+    </RoomButton>
   );
 }
 
-export function VideoButton({ enabled, onChange, value, ...rest }) {
+export function VideoButton({ enabled, ...rest }) {
   return (
-    <DropdownButton onChange={onChange} options={options} value={value}>
-      <RoomButton {...rest} on={enabled}>
-        {enabled ? <DisableVideoIcon /> : <VideoIcon />}
-      </RoomButton>
-    </DropdownButton>
+    <RoomButton {...rest} on={enabled}>
+      {enabled ? <DisableVideoIcon /> : <VideoIcon />}
+    </RoomButton>
   );
 }
 
diff --git a/src/RoomButton.module.css b/src/RoomButton.module.css
index 97fced4..ed38c28 100644
--- a/src/RoomButton.module.css
+++ b/src/RoomButton.module.css
@@ -87,6 +87,7 @@ limitations under the License.
   position: absolute;
   bottom: 0;
   right: 0;
+  cursor: pointer;
 }
 
 .dropdownButton:hover {
@@ -100,8 +101,29 @@ limitations under the License.
 .dropdownContainer {
   position: absolute;
   left: 50%;
-  transform: translate(-50%, -100%);
-  top: 0;
+  transform: translate(0, -100%);
+  top: -5px;
   background-color: #394049;
   border-radius: 8px;
+  overflow: hidden;
+}
+
+.dropdownContainer ul {
+  list-style: none;
+  margin: 0;
+  padding: 0;
+}
+
+.dropdownContainer li {
+  padding: 12px;
+  width: 200px;
+  cursor: pointer;
+}
+
+.dropdownContainer li:hover {
+  background-color: #8d97a5;
+}
+
+.dropdownActiveItem {
+  color: #0dbd8b;
 }