Merge pull request #77 from daily-demos/use-devices-hook
useDevices hook - Daily react hooks
This commit is contained in:
commit
a162de9065
|
|
@ -38,9 +38,6 @@ This demo puts to work the following [shared libraries](../shared):
|
|||
**[MediaDeviceProvider.js](../shared/contexts/MediaDeviceProvider.js)**
|
||||
Convenience context that provides an interface to media devices throughout app
|
||||
|
||||
**[useDevices.js](../shared/contexts/useDevices.js)**
|
||||
Hook for managing the enumeration and status of client media devices)
|
||||
|
||||
**[CallProvider.js](../shared/contexts/CallProvider.js)**
|
||||
Primary call context that manages Daily call state, participant state and call object interaction
|
||||
|
||||
|
|
|
|||
|
|
@ -38,9 +38,6 @@ This demo puts to work the following [shared libraries](../shared):
|
|||
**[MediaDeviceProvider.js](../shared/contexts/MediaDeviceProvider.js)**
|
||||
Convenience context that provides an interface to media devices throughout app
|
||||
|
||||
**[useDevices.js](../shared/contexts/useDevices.js)**
|
||||
Hook for managing the enumeration and status of client media devices)
|
||||
|
||||
**[CallProvider.js](../shared/contexts/CallProvider.js)**
|
||||
Primary call context that manages Daily call state, participant state and call object interaction
|
||||
|
||||
|
|
|
|||
|
|
@ -7,17 +7,15 @@ import MuteButton from '@custom/shared/components/MuteButton';
|
|||
import Tile from '@custom/shared/components/Tile';
|
||||
import { ACCESS_STATE_LOBBY } from '@custom/shared/constants';
|
||||
import { useCallState } from '@custom/shared/contexts/CallProvider';
|
||||
import { useMediaDevices } from '@custom/shared/contexts/MediaDeviceProvider';
|
||||
import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider';
|
||||
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
|
||||
import {
|
||||
DEVICE_STATE_BLOCKED,
|
||||
DEVICE_STATE_NOT_FOUND,
|
||||
DEVICE_STATE_IN_USE,
|
||||
DEVICE_STATE_PENDING,
|
||||
DEVICE_STATE_LOADING,
|
||||
DEVICE_STATE_GRANTED,
|
||||
} from '@custom/shared/contexts/useDevices';
|
||||
useMediaDevices,
|
||||
} from '@custom/shared/contexts/MediaDeviceProvider';
|
||||
import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider';
|
||||
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
|
||||
import IconSettings from '@custom/shared/icons/settings-sm.svg';
|
||||
|
||||
import { useDeepCompareMemo } from 'use-deep-compare';
|
||||
|
|
@ -33,7 +31,8 @@ export const HairCheck = () => {
|
|||
const { callObject } = useCallState();
|
||||
const { localParticipant } = useParticipants();
|
||||
const {
|
||||
deviceState,
|
||||
camState,
|
||||
micState,
|
||||
camError,
|
||||
micError,
|
||||
isCamMuted,
|
||||
|
|
@ -100,21 +99,14 @@ export const HairCheck = () => {
|
|||
[localParticipant]
|
||||
);
|
||||
|
||||
const isLoading = useMemo(() => deviceState === DEVICE_STATE_LOADING, [
|
||||
deviceState,
|
||||
const isLoading = useMemo(() => camState === DEVICE_STATE_PENDING || micState === DEVICE_STATE_PENDING, [
|
||||
camState, micState,
|
||||
]);
|
||||
|
||||
const hasError = useMemo(() => {
|
||||
return !(!deviceState ||
|
||||
[
|
||||
DEVICE_STATE_LOADING,
|
||||
DEVICE_STATE_PENDING,
|
||||
DEVICE_STATE_GRANTED,
|
||||
].includes(deviceState));
|
||||
}, [deviceState]);
|
||||
const hasError = useMemo(() => camError || micError, [camError, micError]);
|
||||
|
||||
const camErrorVerbose = useMemo(() => {
|
||||
switch (camError) {
|
||||
switch (camState) {
|
||||
case DEVICE_STATE_BLOCKED:
|
||||
return 'Camera blocked by user';
|
||||
case DEVICE_STATE_NOT_FOUND:
|
||||
|
|
@ -124,7 +116,20 @@ export const HairCheck = () => {
|
|||
default:
|
||||
return 'unknown';
|
||||
}
|
||||
}, [camError]);
|
||||
}, [camState]);
|
||||
|
||||
const micErrorVerbose = useMemo(() => {
|
||||
switch (micState) {
|
||||
case DEVICE_STATE_BLOCKED:
|
||||
return 'Microphone blocked by user';
|
||||
case DEVICE_STATE_NOT_FOUND:
|
||||
return 'Microphone not found';
|
||||
case DEVICE_STATE_IN_USE:
|
||||
return 'Microphone in use';
|
||||
default:
|
||||
return 'unknown';
|
||||
}
|
||||
}, [micState]);
|
||||
|
||||
const showWaitingMessage = useMemo(() => {
|
||||
return (
|
||||
|
|
@ -206,14 +211,14 @@ export const HairCheck = () => {
|
|||
<div className="overlay-message">{camErrorVerbose}</div>
|
||||
)}
|
||||
{micError && (
|
||||
<div className="overlay-message">{micError}</div>
|
||||
<div className="overlay-message">{micErrorVerbose}</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="mute-buttons">
|
||||
<MuteButton isMuted={isCamMuted} disabled={!!camError} />
|
||||
<MuteButton mic isMuted={isMicMuted} disabled={!!micError} />
|
||||
<MuteButton isMuted={isCamMuted} disabled={camError} />
|
||||
<MuteButton mic isMuted={isMicMuted} disabled={micError} />
|
||||
</div>
|
||||
{tileMemo}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { PEOPLE_ASIDE } from '@custom/shared/components/Aside/PeopleAside';
|
|||
import Button from '@custom/shared/components/Button';
|
||||
import { DEVICE_MODAL } from '@custom/shared/components/DeviceSelectModal';
|
||||
import { useCallState } from '@custom/shared/contexts/CallProvider';
|
||||
import { useMediaDevices } from '@custom/shared/contexts/MediaDeviceProvider';
|
||||
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
|
||||
import { useResponsive } from '@custom/shared/hooks/useResponsive';
|
||||
import { ReactComponent as IconCameraOff } from '@custom/shared/icons/camera-off-md.svg';
|
||||
|
|
@ -16,6 +15,7 @@ import { ReactComponent as IconMore } from '@custom/shared/icons/more-md.svg';
|
|||
import { ReactComponent as IconNetwork } from '@custom/shared/icons/network-md.svg';
|
||||
import { ReactComponent as IconPeople } from '@custom/shared/icons/people-md.svg';
|
||||
import { ReactComponent as IconSettings } from '@custom/shared/icons/settings-md.svg';
|
||||
import { useMediaDevices } from '../../contexts/MediaDeviceProvider';
|
||||
import { Tray, TrayButton } from './Tray';
|
||||
|
||||
export const BasicTray = () => {
|
||||
|
|
@ -24,7 +24,7 @@ export const BasicTray = () => {
|
|||
const [showMore, setShowMore] = useState(false);
|
||||
const { callObject, leave } = useCallState();
|
||||
const { customTrayComponent, openModal, toggleAside } = useUIState();
|
||||
const { isCamMuted, isMicMuted } = useMediaDevices();
|
||||
const { isMicMuted, isCamMuted } = useMediaDevices();
|
||||
|
||||
const toggleCamera = (newState) => {
|
||||
if (!callObject) return false;
|
||||
|
|
|
|||
|
|
@ -1,82 +1,64 @@
|
|||
import React, { createContext, useContext, useCallback } from 'react';
|
||||
import React, { createContext, useContext, useMemo } from 'react';
|
||||
import { useDaily, useDevices } from '@daily-co/daily-react-hooks';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useCallState } from './CallProvider';
|
||||
import { useParticipants } from './ParticipantsProvider';
|
||||
import { useDevices } from './useDevices';
|
||||
export const DEVICE_STATE_LOADING = 'loading';
|
||||
export const DEVICE_STATE_PENDING = 'pending';
|
||||
export const DEVICE_STATE_ERROR = 'error';
|
||||
export const DEVICE_STATE_GRANTED = 'granted';
|
||||
export const DEVICE_STATE_NOT_FOUND = 'not-found';
|
||||
export const DEVICE_STATE_NOT_SUPPORTED = 'not-supported';
|
||||
export const DEVICE_STATE_BLOCKED = 'blocked';
|
||||
export const DEVICE_STATE_IN_USE = 'in-use';
|
||||
export const DEVICE_STATE_OFF = 'off';
|
||||
export const DEVICE_STATE_PLAYABLE = 'playable';
|
||||
export const DEVICE_STATE_SENDABLE = 'sendable';
|
||||
|
||||
export const MediaDeviceContext = createContext();
|
||||
|
||||
export const MediaDeviceProvider = ({ children }) => {
|
||||
const { callObject } = useCallState();
|
||||
const { localParticipant } = useParticipants();
|
||||
|
||||
const {
|
||||
camError,
|
||||
cams,
|
||||
currentCam,
|
||||
currentMic,
|
||||
currentSpeaker,
|
||||
deviceState,
|
||||
micError,
|
||||
mics,
|
||||
refreshDevices,
|
||||
setCurrentCam,
|
||||
setCurrentMic,
|
||||
setCurrentSpeaker,
|
||||
hasCamError,
|
||||
cameras,
|
||||
camState,
|
||||
setCamera,
|
||||
hasMicError,
|
||||
microphones,
|
||||
micState,
|
||||
setMicrophone,
|
||||
speakers,
|
||||
} = useDevices(callObject);
|
||||
setSpeaker,
|
||||
refreshDevices,
|
||||
} = useDevices();
|
||||
|
||||
const selectCamera = useCallback(
|
||||
async (newCam) => {
|
||||
if (!callObject || newCam.deviceId === currentCam?.deviceId) return;
|
||||
const { camera } = await callObject.setInputDevicesAsync({
|
||||
videoDeviceId: newCam.deviceId,
|
||||
});
|
||||
setCurrentCam(camera);
|
||||
},
|
||||
[callObject, currentCam, setCurrentCam]
|
||||
);
|
||||
const daily = useDaily();
|
||||
const localParticipant = daily?.participants().local;
|
||||
|
||||
const selectMic = useCallback(
|
||||
async (newMic) => {
|
||||
if (!callObject || newMic.deviceId === currentMic?.deviceId) return;
|
||||
const { mic } = await callObject.setInputDevicesAsync({
|
||||
audioDeviceId: newMic.deviceId,
|
||||
});
|
||||
setCurrentMic(mic);
|
||||
},
|
||||
[callObject, currentMic, setCurrentMic]
|
||||
);
|
||||
const isCamMuted = useMemo(() => {
|
||||
const videoState = localParticipant?.tracks?.video?.state;
|
||||
return videoState === DEVICE_STATE_OFF || videoState === DEVICE_STATE_BLOCKED || hasCamError;
|
||||
}, [hasCamError, localParticipant?.tracks?.video?.state]);
|
||||
|
||||
const selectSpeaker = useCallback(
|
||||
(newSpeaker) => {
|
||||
if (!callObject || newSpeaker.deviceId === currentSpeaker?.deviceId) return;
|
||||
callObject.setOutputDevice({
|
||||
outputDeviceId: newSpeaker.deviceId,
|
||||
});
|
||||
setCurrentSpeaker(newSpeaker);
|
||||
},
|
||||
[callObject, currentSpeaker, setCurrentSpeaker]
|
||||
);
|
||||
const isMicMuted = useMemo(() => {
|
||||
const audioState = localParticipant?.tracks?.audio?.state;
|
||||
return audioState === DEVICE_STATE_OFF || audioState === DEVICE_STATE_BLOCKED || hasMicError;
|
||||
}, [hasMicError, localParticipant?.tracks?.audio?.state]);
|
||||
|
||||
return (
|
||||
<MediaDeviceContext.Provider
|
||||
value={{
|
||||
camError,
|
||||
cams,
|
||||
currentCam,
|
||||
currentMic,
|
||||
currentSpeaker,
|
||||
deviceState,
|
||||
isCamMuted: localParticipant.isCamMuted,
|
||||
isMicMuted: localParticipant.isMicMuted,
|
||||
micError,
|
||||
mics,
|
||||
isCamMuted,
|
||||
isMicMuted,
|
||||
camError: hasCamError,
|
||||
cams: cameras,
|
||||
camState,
|
||||
micError: hasMicError,
|
||||
mics: microphones,
|
||||
micState,
|
||||
refreshDevices,
|
||||
setCurrentCam: selectCamera,
|
||||
setCurrentMic: selectMic,
|
||||
setCurrentSpeaker: selectSpeaker,
|
||||
setCurrentCam: setCamera,
|
||||
setCurrentMic: setMicrophone,
|
||||
setCurrentSpeaker: setSpeaker,
|
||||
speakers,
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,262 +0,0 @@
|
|||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { sortByKey } from '../lib/sortByKey';
|
||||
|
||||
export const DEVICE_STATE_LOADING = 'loading';
|
||||
export const DEVICE_STATE_PENDING = 'pending';
|
||||
export const DEVICE_STATE_ERROR = 'error';
|
||||
export const DEVICE_STATE_GRANTED = 'granted';
|
||||
export const DEVICE_STATE_NOT_FOUND = 'not-found';
|
||||
export const DEVICE_STATE_NOT_SUPPORTED = 'not-supported';
|
||||
export const DEVICE_STATE_BLOCKED = 'blocked';
|
||||
export const DEVICE_STATE_IN_USE = 'in-use';
|
||||
export const DEVICE_STATE_OFF = 'off';
|
||||
export const DEVICE_STATE_PLAYABLE = 'playable';
|
||||
export const DEVICE_STATE_SENDABLE = 'sendable';
|
||||
|
||||
export const useDevices = (callObject) => {
|
||||
const [deviceState, setDeviceState] = useState(DEVICE_STATE_LOADING);
|
||||
const [currentCam, setCurrentCam] = useState(null);
|
||||
const [currentMic, setCurrentMic] = useState(null);
|
||||
const [currentSpeaker, setCurrentSpeaker] = useState(null);
|
||||
|
||||
const [cams, setCams] = useState([]);
|
||||
const [mics, setMics] = useState([]);
|
||||
const [speakers, setSpeakers] = useState([]);
|
||||
|
||||
const [camError, setCamError] = useState(null);
|
||||
const [micError, setMicError] = useState(null);
|
||||
|
||||
const updateDeviceState = useCallback(async () => {
|
||||
if (
|
||||
typeof navigator?.mediaDevices?.getUserMedia === 'undefined' ||
|
||||
typeof navigator?.mediaDevices?.enumerateDevices === 'undefined'
|
||||
) {
|
||||
setDeviceState(DEVICE_STATE_NOT_SUPPORTED);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { devices } = await callObject.enumerateDevices();
|
||||
|
||||
const { camera, mic, speaker } = await callObject.getInputDevices();
|
||||
|
||||
setCurrentCam(camera ?? null);
|
||||
setCurrentMic(mic ?? null);
|
||||
setCurrentSpeaker(speaker ?? null);
|
||||
|
||||
const [defaultCam, ...videoDevices] = devices.filter(
|
||||
(d) => d.kind === 'videoinput' && d.deviceId !== ''
|
||||
);
|
||||
setCams(
|
||||
[
|
||||
defaultCam,
|
||||
...videoDevices.sort((a, b) => sortByKey(a, b, 'label', false)),
|
||||
].filter(Boolean)
|
||||
);
|
||||
const [defaultMic, ...micDevices] = devices.filter(
|
||||
(d) => d.kind === 'audioinput' && d.deviceId !== ''
|
||||
);
|
||||
setMics(
|
||||
[
|
||||
defaultMic,
|
||||
...micDevices.sort((a, b) => sortByKey(a, b, 'label', false)),
|
||||
].filter(Boolean)
|
||||
);
|
||||
const [defaultSpeaker, ...speakerDevices] = devices.filter(
|
||||
(d) => d.kind === 'audiooutput' && d.deviceId !== ''
|
||||
);
|
||||
setSpeakers(
|
||||
[
|
||||
defaultSpeaker,
|
||||
...speakerDevices.sort((a, b) => sortByKey(a, b, 'label', false)),
|
||||
].filter(Boolean)
|
||||
);
|
||||
|
||||
console.log(`Current cam: ${camera.label}`);
|
||||
console.log(`Current mic: ${mic.label}`);
|
||||
console.log(`Current speakers: ${speaker.label}`);
|
||||
} catch (e) {
|
||||
setDeviceState(DEVICE_STATE_NOT_SUPPORTED);
|
||||
}
|
||||
}, [callObject]);
|
||||
|
||||
const updateDeviceErrors = useCallback(() => {
|
||||
if (!callObject) return;
|
||||
const { tracks } = callObject.participants().local;
|
||||
|
||||
if (tracks.video?.blocked?.byPermissions) {
|
||||
setCamError(DEVICE_STATE_BLOCKED);
|
||||
} else if (tracks.video?.blocked?.byDeviceMissing) {
|
||||
setCamError(DEVICE_STATE_NOT_FOUND);
|
||||
} else if (tracks.video?.blocked?.byDeviceInUse) {
|
||||
setCamError(DEVICE_STATE_IN_USE);
|
||||
}
|
||||
|
||||
if (
|
||||
[
|
||||
DEVICE_STATE_LOADING,
|
||||
DEVICE_STATE_OFF,
|
||||
DEVICE_STATE_PLAYABLE,
|
||||
DEVICE_STATE_SENDABLE,
|
||||
].includes(tracks.video.state)
|
||||
) {
|
||||
setCamError(null);
|
||||
}
|
||||
|
||||
if (tracks.audio?.blocked?.byPermissions) {
|
||||
setMicError(DEVICE_STATE_BLOCKED);
|
||||
} else if (tracks.audio?.blocked?.byDeviceMissing) {
|
||||
setMicError(DEVICE_STATE_NOT_FOUND);
|
||||
} else if (tracks.audio?.blocked?.byDeviceInUse) {
|
||||
setMicError(DEVICE_STATE_IN_USE);
|
||||
}
|
||||
|
||||
if (
|
||||
[
|
||||
DEVICE_STATE_LOADING,
|
||||
DEVICE_STATE_OFF,
|
||||
DEVICE_STATE_PLAYABLE,
|
||||
DEVICE_STATE_SENDABLE,
|
||||
].includes(tracks.audio.state)
|
||||
) {
|
||||
setMicError(null);
|
||||
}
|
||||
}, [callObject]);
|
||||
|
||||
const handleParticipantUpdated = useCallback(
|
||||
({ participant }) => {
|
||||
if (!callObject || deviceState === 'not-supported' || !participant.local) return;
|
||||
|
||||
switch (participant?.tracks.video.state) {
|
||||
case DEVICE_STATE_BLOCKED:
|
||||
setDeviceState(DEVICE_STATE_ERROR);
|
||||
break;
|
||||
case DEVICE_STATE_OFF:
|
||||
case DEVICE_STATE_PLAYABLE:
|
||||
updateDeviceState();
|
||||
setDeviceState(DEVICE_STATE_GRANTED);
|
||||
break;
|
||||
}
|
||||
|
||||
updateDeviceErrors();
|
||||
},
|
||||
[callObject, deviceState, updateDeviceErrors, updateDeviceState]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!callObject) return;
|
||||
|
||||
/**
|
||||
If the user is slow to allow access, we'll update the device state
|
||||
so our app can show a prompt requesting access
|
||||
*/
|
||||
let pendingAccessTimeout;
|
||||
|
||||
const handleJoiningMeeting = () => {
|
||||
pendingAccessTimeout = setTimeout(() => {
|
||||
setDeviceState(DEVICE_STATE_PENDING);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
const handleJoinedMeeting = () => {
|
||||
clearTimeout(pendingAccessTimeout);
|
||||
// Note: setOutputDevice() is not honored before join() so we must enumerate again
|
||||
updateDeviceState();
|
||||
};
|
||||
|
||||
updateDeviceState();
|
||||
callObject.on('joining-meeting', handleJoiningMeeting);
|
||||
callObject.on('joined-meeting', handleJoinedMeeting);
|
||||
callObject.on('participant-updated', handleParticipantUpdated);
|
||||
return () => {
|
||||
clearTimeout(pendingAccessTimeout);
|
||||
callObject.off('joining-meeting', handleJoiningMeeting);
|
||||
callObject.off('joined-meeting', handleJoinedMeeting);
|
||||
callObject.off('participant-updated', handleParticipantUpdated);
|
||||
};
|
||||
}, [callObject, handleParticipantUpdated, updateDeviceState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!callObject) return;
|
||||
|
||||
console.log('💻 Device provider events bound');
|
||||
|
||||
const handleCameraError = ({
|
||||
errorMsg: { errorMsg, audioOk, videoOk },
|
||||
error,
|
||||
}) => {
|
||||
switch (error?.type) {
|
||||
case 'cam-in-use':
|
||||
setDeviceState(DEVICE_STATE_ERROR);
|
||||
setCamError(DEVICE_STATE_IN_USE);
|
||||
break;
|
||||
case 'mic-in-use':
|
||||
setDeviceState(DEVICE_STATE_ERROR);
|
||||
setMicError(DEVICE_STATE_IN_USE);
|
||||
break;
|
||||
case 'cam-mic-in-use':
|
||||
setDeviceState(DEVICE_STATE_ERROR);
|
||||
setCamError(DEVICE_STATE_IN_USE);
|
||||
setMicError(DEVICE_STATE_IN_USE);
|
||||
break;
|
||||
default:
|
||||
switch (errorMsg) {
|
||||
case 'devices error':
|
||||
setDeviceState(DEVICE_STATE_ERROR);
|
||||
setCamError(videoOk ? null : DEVICE_STATE_NOT_FOUND);
|
||||
setMicError(audioOk ? null : DEVICE_STATE_NOT_FOUND);
|
||||
break;
|
||||
case 'not allowed':
|
||||
setDeviceState(DEVICE_STATE_ERROR);
|
||||
updateDeviceErrors();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleError = ({ errorMsg }) => {
|
||||
switch (errorMsg) {
|
||||
case 'not allowed':
|
||||
setDeviceState(DEVICE_STATE_ERROR);
|
||||
updateDeviceErrors();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartedCamera = () => {
|
||||
updateDeviceErrors();
|
||||
};
|
||||
|
||||
callObject.on('camera-error', handleCameraError);
|
||||
callObject.on('error', handleError);
|
||||
callObject.on('started-camera', handleStartedCamera);
|
||||
return () => {
|
||||
callObject.off('camera-error', handleCameraError);
|
||||
callObject.off('error', handleError);
|
||||
callObject.off('started-camera', handleStartedCamera);
|
||||
};
|
||||
}, [callObject, updateDeviceErrors]);
|
||||
|
||||
return {
|
||||
camError,
|
||||
cams,
|
||||
currentCam,
|
||||
currentMic,
|
||||
currentSpeaker,
|
||||
deviceState,
|
||||
micError,
|
||||
mics,
|
||||
refreshDevices: updateDeviceState,
|
||||
setCurrentCam,
|
||||
setCurrentMic,
|
||||
setCurrentSpeaker,
|
||||
speakers,
|
||||
};
|
||||
};
|
||||
|
||||
export default useDevices;
|
||||
Loading…
Reference in New Issue