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

330 lines
9.4 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 [currentDevices, setCurrentDevices] = 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();
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)
);
setCurrentDevices({
camera,
mic,
speaker,
});
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 || !participant.local) return;
setDeviceState((prevState) => {
if (prevState === DEVICE_STATE_NOT_SUPPORTED) return prevState;
switch (participant?.tracks.video.state) {
case DEVICE_STATE_BLOCKED:
updateDeviceErrors();
return DEVICE_STATE_ERROR;
case DEVICE_STATE_OFF:
case DEVICE_STATE_PLAYABLE:
if (prevState === DEVICE_STATE_GRANTED) {
return prevState;
}
updateDeviceState();
return DEVICE_STATE_GRANTED;
default:
return prevState;
}
});
},
[callObject, updateDeviceState, updateDeviceErrors]
);
useEffect(() => {
if (!callObject) return false;
/**
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();
};
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]);
const setCamDevice = useCallback(
async (newCam, useLocalStorage = true) => {
if (!callObject || newCam.deviceId === currentDevices?.camera?.deviceId) {
return;
}
console.log(`🔛 Changing camera device to: ${newCam.label}`);
if (useLocalStorage) {
localStorage.setItem('defaultCamId', newCam.deviceId);
}
await callObject.setInputDevicesAsync({
videoDeviceId: newCam.deviceId,
});
setCurrentDevices((prev) => ({ ...prev, camera: newCam }));
},
[callObject, currentDevices]
);
const setMicDevice = useCallback(
async (newMic, useLocalStorage = true) => {
if (!callObject || newMic.deviceId === currentDevices?.mic?.deviceId) {
return;
}
console.log(`🔛 Changing mic device to: ${newMic.label}`);
if (useLocalStorage) {
localStorage.setItem('defaultMicId', newMic.deviceId);
}
await callObject.setInputDevicesAsync({
audioDeviceId: newMic.deviceId,
});
setCurrentDevices((prev) => ({ ...prev, mic: newMic }));
},
[callObject, currentDevices]
);
const setSpeakersDevice = useCallback(
async (newSpeakers, useLocalStorage = true) => {
if (
!callObject ||
newSpeakers.deviceId === currentDevices?.speaker?.deviceId
) {
return;
}
console.log(`Changing speakers device to: ${newSpeakers.label}`);
if (useLocalStorage) {
localStorage.setItem('defaultSpeakersId', newSpeakers.deviceId);
}
callObject.setOutputDevice({
outputDeviceId: newSpeakers.deviceId,
});
setCurrentDevices((prev) => ({ ...prev, speaker: newSpeakers }));
},
[callObject, currentDevices]
);
useEffect(() => {
if (!callObject) return false;
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 {
cams,
mics,
speakers,
camError,
micError,
currentDevices,
deviceState,
setCamDevice,
setMicDevice,
setSpeakersDevice,
};
};
export default useDevices;