Merge pull request #77 from daily-demos/use-devices-hook

useDevices hook - Daily react hooks
This commit is contained in:
Harshith Pabbati 2022-04-07 16:49:51 +05:30 committed by GitHub
commit a162de9065
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 74 additions and 355 deletions

View File

@ -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

View File

@ -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

View File

@ -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>

View File

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

View File

@ -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,
}}
>

View File

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