331 lines
9.4 KiB
JavaScript
331 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 () => {
|
|
console.log('UNMOUNT');
|
|
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?.cam?.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;
|