-
Notifications
You must be signed in to change notification settings - Fork 205
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
e5f3cae
commit 76a142e
Showing
13 changed files
with
689 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
176 changes: 176 additions & 0 deletions
176
packages/telemed-intake/app/src/components/CallSettingsDialog/CallSettings.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
import { FC, useCallback, useEffect, useRef, useState } from 'react'; | ||
import { Box, Button, Typography } from '@mui/material'; | ||
import { ConsoleLogger, DefaultDeviceController } from 'amazon-chime-sdk-js'; | ||
import { CustomDialog } from 'ottehr-components'; | ||
import { SoundSettings } from './SoundSettings'; | ||
import { MicrophoneSettings } from './MicrophoneSettings'; | ||
import { CameraSettings } from './CameraSettings'; | ||
import { useIntakeCommonStore } from 'src/features/common'; | ||
import { useCallSettingsStore } from 'src/features/video-call/call-settings.store'; | ||
import { getSelectors } from 'ottehr-utils'; | ||
|
||
interface CallSettingsProps { | ||
onClose: () => void; | ||
} | ||
|
||
const useSelectedDevices = (): { | ||
selectedVideoDevice: string; | ||
selectedAudioDevice: string; | ||
selectedOutputDevice: string; | ||
setSelectedVideoDevice: (value: string) => void; | ||
setSelectedAudioDevice: (value: string) => void; | ||
setSelectedOutputDevice: (value: string) => void; | ||
} => { | ||
const { videoInput, audioInput, audioOutput } = getSelectors(useCallSettingsStore, [ | ||
'videoInput', | ||
'audioInput', | ||
'audioOutput', | ||
]); | ||
|
||
const createSetFunction = (name: 'videoInput' | 'audioInput' | 'audioOutput'): ((value: string) => void) => { | ||
return (value) => useCallSettingsStore.setState({ [name]: value }); | ||
}; | ||
|
||
return { | ||
selectedVideoDevice: videoInput, | ||
selectedAudioDevice: audioInput, | ||
selectedOutputDevice: audioOutput, | ||
setSelectedVideoDevice: createSetFunction('videoInput'), | ||
setSelectedAudioDevice: createSetFunction('audioInput'), | ||
setSelectedOutputDevice: createSetFunction('audioOutput'), | ||
}; | ||
}; | ||
|
||
export const CallSettings: FC<CallSettingsProps> = ({ onClose }) => { | ||
const [videoDevices, setVideoDevices] = useState<MediaDeviceInfo[]>([]); | ||
const [audioDevices, setAudioDevices] = useState<MediaDeviceInfo[]>([]); | ||
const [outputDevices, setOutputDevices] = useState<MediaDeviceInfo[]>([]); | ||
const { | ||
selectedVideoDevice, | ||
setSelectedVideoDevice, | ||
selectedAudioDevice, | ||
setSelectedAudioDevice, | ||
selectedOutputDevice, | ||
setSelectedOutputDevice, | ||
} = useSelectedDevices(); | ||
const videoPreviewRef = useRef<HTMLVideoElement>(null); | ||
const audioPreviewRef = useRef<HTMLAudioElement & { setSinkId: (value: string) => Promise<undefined> }>(null); | ||
const [deviceController, setDeviceController] = useState<DefaultDeviceController | null>(null); | ||
const [isCameraOpen, setIsCameraOpen] = useState(false); | ||
|
||
useEffect(() => { | ||
const logger = new ConsoleLogger('preview'); | ||
const deviceController = new DefaultDeviceController(logger); | ||
setDeviceController(deviceController); | ||
|
||
const fetchDevices = async (): Promise<void> => { | ||
const videoInputs = await deviceController.listVideoInputDevices(); | ||
const audioInputs = await deviceController.listAudioInputDevices(); | ||
const audioOutputs = await deviceController.listAudioOutputDevices(); | ||
setVideoDevices(videoInputs); | ||
setAudioDevices(audioInputs); | ||
setOutputDevices(audioOutputs); | ||
if (videoInputs.length > 0 && !selectedVideoDevice) setSelectedVideoDevice(videoInputs[0].deviceId); | ||
if (audioInputs.length > 0 && !selectedAudioDevice) setSelectedAudioDevice(audioInputs[0].deviceId); | ||
if (audioOutputs.length > 0 && !selectedOutputDevice) setSelectedOutputDevice(audioOutputs[0].deviceId); | ||
}; | ||
|
||
void fetchDevices(); | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, []); | ||
|
||
const startVideoPreview = useCallback(async (): Promise<void> => { | ||
if (deviceController && selectedVideoDevice && videoPreviewRef.current) { | ||
await deviceController.startVideoInput(selectedVideoDevice); | ||
deviceController.startVideoPreviewForVideoInput(videoPreviewRef.current); | ||
} | ||
}, [deviceController, selectedVideoDevice]); | ||
|
||
const stopVideoPreview = useCallback(async (): Promise<void> => { | ||
if (deviceController) { | ||
await deviceController.stopVideoInput(); | ||
await deviceController.stopAudioInput(); | ||
if (videoPreviewRef.current) { | ||
deviceController.stopVideoPreviewForVideoInput(videoPreviewRef.current); | ||
} | ||
} | ||
}, [deviceController]); | ||
|
||
const setAudioOutput = useCallback( | ||
async (deviceId: string): Promise<void> => { | ||
if (deviceController && audioPreviewRef.current) { | ||
await deviceController.chooseAudioOutput(deviceId); | ||
await audioPreviewRef.current.setSinkId(deviceId); | ||
} | ||
}, | ||
[deviceController], | ||
); | ||
|
||
useEffect(() => { | ||
if (isCameraOpen) { | ||
void startVideoPreview(); | ||
} else { | ||
void stopVideoPreview(); | ||
} | ||
return () => { | ||
void stopVideoPreview(); | ||
}; | ||
}, [isCameraOpen, selectedVideoDevice, startVideoPreview, stopVideoPreview]); | ||
|
||
useEffect(() => { | ||
if (selectedOutputDevice) { | ||
void setAudioOutput(selectedOutputDevice); | ||
} | ||
}, [selectedOutputDevice, setAudioOutput]); | ||
|
||
return ( | ||
<CustomDialog open onClose={onClose} maxWidth="xs" PaperProps={{ sx: { borderRadius: 2 } }}> | ||
<Box | ||
sx={{ | ||
display: 'flex', | ||
flexDirection: 'column', | ||
gap: 2, | ||
}} | ||
> | ||
<Typography variant="h2" color="primary.main"> | ||
Settings and testing | ||
</Typography> | ||
|
||
<SoundSettings | ||
selectedOutputDevice={selectedOutputDevice} | ||
setSelectedOutputDevice={setSelectedOutputDevice} | ||
outputDevices={outputDevices} | ||
audioPreviewRef={audioPreviewRef} | ||
/> | ||
|
||
<MicrophoneSettings | ||
selectedAudioDevice={selectedAudioDevice} | ||
setSelectedAudioDevice={setSelectedAudioDevice} | ||
audioDevices={audioDevices} | ||
/> | ||
|
||
<CameraSettings | ||
selectedVideoDevice={selectedVideoDevice} | ||
setSelectedVideoDevice={setSelectedVideoDevice} | ||
videoDevices={videoDevices} | ||
videoPreviewRef={videoPreviewRef} | ||
isCameraOpen={isCameraOpen} | ||
setIsCameraOpen={setIsCameraOpen} | ||
/> | ||
|
||
<Typography> | ||
Functional microphone, sound and camera are required to proceed with the visit. If something is not working | ||
for you, please contact out support team. | ||
</Typography> | ||
|
||
<Button | ||
sx={{ alignSelf: 'start' }} | ||
variant="outlined" | ||
onClick={() => useIntakeCommonStore.setState({ supportDialogOpen: true })} | ||
> | ||
Contact support | ||
</Button> | ||
</Box> | ||
</CustomDialog> | ||
); | ||
}; |
98 changes: 98 additions & 0 deletions
98
packages/telemed-intake/app/src/components/CallSettingsDialog/CameraSettings.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import { FC, RefObject } from 'react'; | ||
import { Box, Button, Card, FormControl, IconButton, InputLabel, MenuItem, Select, Typography } from '@mui/material'; | ||
import { otherColors } from '../../IntakeThemeProvider'; | ||
import VideocamOutlinedIcon from '@mui/icons-material/VideocamOutlined'; | ||
import VideocamIcon from '@mui/icons-material/Videocam'; | ||
import VideocamOffIcon from '@mui/icons-material/VideocamOff'; | ||
|
||
type CameraSettingsProps = { | ||
selectedVideoDevice: string; | ||
setSelectedVideoDevice: (value: string) => void; | ||
videoDevices: MediaDeviceInfo[]; | ||
videoPreviewRef: RefObject<HTMLVideoElement>; | ||
isCameraOpen: boolean; | ||
setIsCameraOpen: (value: boolean) => void; | ||
}; | ||
|
||
export const CameraSettings: FC<CameraSettingsProps> = (props) => { | ||
const { selectedVideoDevice, setSelectedVideoDevice, videoDevices, videoPreviewRef, isCameraOpen, setIsCameraOpen } = | ||
props; | ||
|
||
const toggleCamera = (): void => { | ||
setIsCameraOpen(!isCameraOpen); | ||
}; | ||
|
||
return ( | ||
<Box | ||
sx={{ | ||
display: 'flex', | ||
flexDirection: 'column', | ||
gap: 1, | ||
}} | ||
> | ||
<FormControl fullWidth margin="normal"> | ||
<InputLabel>Camera</InputLabel> | ||
<Select | ||
value={videoDevices.map(({ deviceId }) => deviceId).includes(selectedVideoDevice) ? selectedVideoDevice : ''} | ||
onChange={(e) => setSelectedVideoDevice(e.target.value)} | ||
label="Camera" | ||
size="small" | ||
> | ||
{videoDevices.map((device) => ( | ||
<MenuItem key={device.deviceId} value={device.deviceId}> | ||
{device.label} | ||
</MenuItem> | ||
))} | ||
</Select> | ||
</FormControl> | ||
|
||
<Card | ||
sx={{ | ||
backgroundColor: otherColors.coachingVisit, | ||
py: 1, | ||
px: 2, | ||
display: 'flex', | ||
alignItems: 'center', | ||
gap: 1, | ||
}} | ||
elevation={0} | ||
> | ||
<VideocamOutlinedIcon color="primary" /> | ||
<Typography variant="body2" fontWeight={700} color="primary.main" sx={{ flexGrow: 1 }}> | ||
Test your video | ||
</Typography> | ||
<IconButton | ||
sx={{ | ||
backgroundColor: 'primary.main', | ||
color: 'white', | ||
'&:hover': { backgroundColor: 'primary.main' }, | ||
}} | ||
size="small" | ||
onClick={toggleCamera} | ||
> | ||
{isCameraOpen ? <VideocamOffIcon fontSize="small" /> : <VideocamIcon fontSize="small" />} | ||
</IconButton> | ||
</Card> | ||
|
||
{isCameraOpen && ( | ||
<> | ||
<video | ||
ref={videoPreviewRef} | ||
autoPlay | ||
muted | ||
playsInline | ||
style={{ | ||
height: '100%', | ||
width: '100%', | ||
borderRadius: '8px', | ||
}} | ||
/> | ||
|
||
<Button color="error" sx={{ alignSelf: 'start' }} onClick={toggleCamera}> | ||
Hide video | ||
</Button> | ||
</> | ||
)} | ||
</Box> | ||
); | ||
}; |
Oops, something went wrong.