This repository has been archived by the owner on Jan 30, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature/mv 44 ability to choose camera and mic (#161)
--------- Co-authored-by: Szymon Kania <[email protected]> Co-authored-by: Paweł Kruczkiewicz <[email protected]> Co-authored-by: Bartosz Błaszków <[email protected]>
- Loading branch information
1 parent
73ca566
commit 14c2d90
Showing
46 changed files
with
2,342 additions
and
744 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
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
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
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
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,26 @@ | ||
import React, { useContext, useState } from "react"; | ||
|
||
export type ModalContextType = { | ||
setOpen: (value: boolean) => void; | ||
isOpen: boolean; | ||
}; | ||
|
||
const ModelContext = React.createContext<ModalContextType | undefined>(undefined); | ||
|
||
type Props = { | ||
children: React.ReactNode; | ||
}; | ||
|
||
export const ModalProvider = ({ children }: Props) => { | ||
const [isOpen, setIsOpen] = useState<boolean>(false); | ||
|
||
return ( | ||
<ModelContext.Provider value={{ setOpen: (value) => setIsOpen(value), isOpen }}>{children}</ModelContext.Provider> | ||
); | ||
}; | ||
|
||
export const useModal = (): ModalContextType => { | ||
const context = useContext(ModelContext); | ||
if (!context) throw new Error("useModal must be used within a ModalProvider"); | ||
return context; | ||
}; |
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,38 @@ | ||
import React, { FC, PropsWithChildren } from "react"; | ||
import useToast from "../shared/hooks/useToast"; | ||
import useEffectOnChange from "../shared/hooks/useEffectOnChange"; | ||
import { useLocalPeer } from "./LocalPeerMediaContext"; | ||
|
||
const prepareErrorMessage = (videoDeviceError: string | null, audioDeviceError: string | null): null | string => { | ||
if (videoDeviceError && audioDeviceError) { | ||
return "Access to camera and microphone is blocked"; | ||
} else if (videoDeviceError) { | ||
return "Access to camera is blocked"; | ||
} else if (audioDeviceError) { | ||
return "Access to microphone is blocked"; | ||
} else return null; | ||
}; | ||
|
||
export const DeviceErrorBoundary: FC<PropsWithChildren> = ({ children }) => { | ||
const { addToast } = useToast(); | ||
const { video, audio } = useLocalPeer(); | ||
|
||
useEffectOnChange( | ||
[video.error, audio.error], | ||
() => { | ||
const message = prepareErrorMessage(video.error, audio.error); | ||
|
||
if (message) { | ||
addToast({ | ||
id: "device-not-allowed-error", | ||
message: message, | ||
timeout: "INFINITY", | ||
type: "error", | ||
}); | ||
} | ||
}, | ||
(next, prev) => prev?.[0] === next[0] && prev?.[1] === next[1] | ||
); | ||
|
||
return <>{children}</>; | ||
}; |
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,30 @@ | ||
import React from "react"; | ||
import { SelectOption } from "../shared/components/Select"; | ||
import Input from "../shared/components/Input"; | ||
|
||
type Props = { | ||
name: string; | ||
devices: MediaDeviceInfo[] | null; | ||
setInput: (value: string | null) => void; | ||
inputValue: string | null; | ||
}; | ||
|
||
export const DeviceSelector = ({ name, devices, setInput, inputValue }: Props) => { | ||
const options: SelectOption[] = (devices || []).map(({ deviceId, label }) => ({ | ||
value: deviceId, | ||
label, | ||
})); | ||
|
||
return ( | ||
<Input | ||
wrapperClassName="mt-14" | ||
label={name} | ||
type="select" | ||
options={options} | ||
onChange={(option) => { | ||
setInput(option.value); | ||
}} | ||
value={options.find(({ value }) => value === inputValue)} | ||
/> | ||
); | ||
}; |
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,145 @@ | ||
import React, { useContext, useMemo, useState } from "react"; | ||
import { AUDIO_TRACK_CONSTRAINTS, VIDEO_TRACK_CONSTRAINTS } from "../../pages/room/consts"; | ||
import { loadObject, saveObject } from "../shared/utils/localStorage"; | ||
import { useMedia } from "./useMedia"; | ||
import { DeviceState, Type, UseUserMediaConfig, UseUserMediaStartConfig } from "./use-user-media/type"; | ||
import { useUserMedia } from "./use-user-media/useUserMedia"; | ||
|
||
export type Device = { | ||
stream: MediaStream | null; | ||
start: () => void; | ||
stop: () => void; | ||
isEnabled: boolean; | ||
disable: () => void; | ||
enable: () => void; | ||
}; | ||
|
||
export type UserMedia = { | ||
id: string | null; | ||
setId: (id: string) => void; | ||
device: Device; | ||
error: string | null; | ||
devices: MediaDeviceInfo[] | null; | ||
}; | ||
|
||
export type DisplayMedia = { | ||
setConfig: (constraints: MediaStreamConstraints | null) => void; | ||
config: MediaStreamConstraints | null; | ||
device: Device; | ||
}; | ||
|
||
export type LocalPeerContextType = { | ||
video: UserMedia; | ||
audio: UserMedia; | ||
screenShare: DisplayMedia; | ||
start: (config: UseUserMediaStartConfig) => void; | ||
}; | ||
|
||
const LocalPeerMediaContext = React.createContext<LocalPeerContextType | undefined>(undefined); | ||
|
||
type Props = { | ||
children: React.ReactNode; | ||
}; | ||
|
||
const LOCAL_STORAGE_VIDEO_DEVICE_KEY = "last-selected-video-device"; | ||
const LOCAL_STORAGE_AUDIO_DEVICE_KEY = "last-selected-audio-device"; | ||
|
||
const useDisplayMedia = (screenSharingConfig: MediaStreamConstraints | null) => | ||
useMedia( | ||
useMemo( | ||
() => (screenSharingConfig ? () => navigator.mediaDevices.getDisplayMedia(screenSharingConfig) : null), | ||
[screenSharingConfig] | ||
) | ||
); | ||
|
||
const USE_USER_MEDIA_CONFIG: UseUserMediaConfig = { | ||
getLastAudioDevice: () => loadObject<MediaDeviceInfo | null>(LOCAL_STORAGE_AUDIO_DEVICE_KEY, null), | ||
saveLastAudioDevice: (info: MediaDeviceInfo) => saveObject<MediaDeviceInfo>(LOCAL_STORAGE_AUDIO_DEVICE_KEY, info), | ||
getLastVideoDevice: () => loadObject<MediaDeviceInfo | null>(LOCAL_STORAGE_VIDEO_DEVICE_KEY, null), | ||
saveLastVideoDevice: (info: MediaDeviceInfo) => saveObject<MediaDeviceInfo>(LOCAL_STORAGE_VIDEO_DEVICE_KEY, info), | ||
videoTrackConstraints: VIDEO_TRACK_CONSTRAINTS, | ||
audioTrackConstraints: AUDIO_TRACK_CONSTRAINTS, | ||
refetchOnMount: true, | ||
}; | ||
|
||
const useMediaData = ( | ||
data: DeviceState | null, | ||
type: Type, | ||
localStorageKey: string, | ||
start: (config: UseUserMediaStartConfig) => void, | ||
stop: (type: Type) => void, | ||
setEnable: (type: Type, value: boolean) => void | ||
) => { | ||
const deviceIdKey: keyof UseUserMediaStartConfig = type === "video" ? "videoDeviceId" : "audioDeviceId"; | ||
|
||
return useMemo( | ||
(): UserMedia => ({ | ||
id: data?.media?.deviceInfo?.deviceId || null, | ||
setId: (value: string) => start({ [deviceIdKey]: value }), | ||
device: { | ||
stream: data?.media?.stream || null, | ||
stop: () => stop(type), | ||
start: () => start({ [deviceIdKey]: loadObject<MediaDeviceInfo | null>(localStorageKey, null)?.deviceId }), | ||
disable: () => setEnable(type, false), | ||
enable: () => setEnable(type, true), | ||
isEnabled: !!data?.media?.enabled, | ||
}, | ||
devices: data?.devices || null, | ||
error: data?.error?.name || null, | ||
}), | ||
[data, stop, start, setEnable, type, localStorageKey, deviceIdKey] | ||
); | ||
}; | ||
|
||
export const LocalPeerMediaProvider = ({ children }: Props) => { | ||
const { data, stop, start, setEnable } = useUserMedia(USE_USER_MEDIA_CONFIG); | ||
|
||
const [screenSharingConfig, setScreenSharingConfig] = useState<MediaStreamConstraints | null>(null); | ||
const screenSharingDevice: Device = useDisplayMedia(screenSharingConfig); | ||
|
||
const video: UserMedia = useMediaData( | ||
data?.video || null, | ||
"video", | ||
LOCAL_STORAGE_VIDEO_DEVICE_KEY, | ||
start, | ||
stop, | ||
setEnable | ||
); | ||
|
||
const audio: UserMedia = useMediaData( | ||
data?.audio || null, | ||
"audio", | ||
LOCAL_STORAGE_AUDIO_DEVICE_KEY, | ||
start, | ||
stop, | ||
setEnable | ||
); | ||
|
||
const screenShare: DisplayMedia = useMemo( | ||
() => ({ | ||
config: screenSharingConfig, | ||
setConfig: setScreenSharingConfig, | ||
device: screenSharingDevice, | ||
}), | ||
[screenSharingConfig, screenSharingDevice] | ||
); | ||
|
||
return ( | ||
<LocalPeerMediaContext.Provider | ||
value={{ | ||
video, | ||
audio, | ||
screenShare, | ||
start, | ||
}} | ||
> | ||
{children} | ||
</LocalPeerMediaContext.Provider> | ||
); | ||
}; | ||
|
||
export const useLocalPeer = (): LocalPeerContextType => { | ||
const context = useContext(LocalPeerMediaContext); | ||
if (!context) throw new Error("useLocalPeer must be used within a LocalPeerMediaContext"); | ||
return context; | ||
}; |
Oops, something went wrong.