daily-examples/custom/shared/contexts/useDevices.js

263 lines
7.7 KiB
JavaScript

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;