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; }