Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/mn 584/specfic error message camera #817

Merged
merged 6 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 67 additions & 27 deletions packages/camera-web/src/Camera/hooks/useUserMedia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@ export enum UserMediaErrorType {
/**
* The camera stream couldn't be fetched because the web page does not have the permissions to access the camera.
*/
WEBPAGE_NOT_ALLOWED = 'webpage_not_allowed',
/**
* The camera stream couldn't be fetched because the camera permissions are not granted to the browser in the device
* settings.
*/
BROWSER_NOT_ALLOWED = 'browser_not_allowed',
/**
* The camera stream couldn't be fetched, but the app is unable to know if it is because of the website or
* the browser not being allowed to have camera permission access. This error is usually returned on Firefox and
* other similar browsers where `navigator.permissions.query` is not supported for videoinput devices.
*/
NOT_ALLOWED = 'not_allowed',
/**
* The camera stream was successfully fetched, but it could be processed. This error can happen for the following
Expand Down Expand Up @@ -197,6 +208,7 @@ export function useUserMedia(
const { handleError } = useMonitoring();
const isActive = useRef(true);

let cameraPermissionState: PermissionState | null = null;
useEffect(() => {
return () => {
isActive.current = false;
Expand All @@ -206,7 +218,16 @@ export function useUserMedia(
const handleGetUserMediaError = (err: unknown) => {
let type = UserMediaErrorType.OTHER;
if (err instanceof Error && err.name === 'NotAllowedError') {
type = UserMediaErrorType.NOT_ALLOWED;
switch (cameraPermissionState) {
case 'denied':
type = UserMediaErrorType.WEBPAGE_NOT_ALLOWED;
break;
case 'granted':
type = UserMediaErrorType.BROWSER_NOT_ALLOWED;
break;
default:
type = UserMediaErrorType.NOT_ALLOWED;
}
} else if (
err instanceof Error &&
Object.values(InvalidStreamErrorName).includes(err.name as InvalidStreamErrorName)
Expand Down Expand Up @@ -242,37 +263,56 @@ export function useUserMedia(
setLastConstraintsApplied(constraints);

const getUserMedia = async () => {
setIsLoading(true);
if (stream) {
stream.removeEventListener('inactive', onStreamInactive);
stream.getTracks().forEach((track) => track.stop());
}
const deviceDetails = await analyzeCameraDevices(constraints);
const updatedConstraints = {
...constraints,
video: {
...(constraints ? (constraints.video as MediaTrackConstraints) : {}),
deviceId: { exact: deviceDetails.validDeviceIds },
},
};
const str = await navigator.mediaDevices.getUserMedia(updatedConstraints);
str?.addEventListener('inactive', onStreamInactive);
if (isActive.current) {
setStream(str);
setDimensions(getStreamDimensions(str, true));
setIsLoading(false);
setAvailableCameraDevices(deviceDetails.availableDevices);
setSelectedCameraDeviceId(getStreamDeviceId(str));
}
};
const getCameraPermissionState = async () => {
try {
setIsLoading(true);
if (stream) {
stream.removeEventListener('inactive', onStreamInactive);
stream.getTracks().forEach((track) => track.stop());
}
const deviceDetails = await analyzeCameraDevices(constraints);
const updatedConstraints = {
...constraints,
video: {
...(constraints ? (constraints.video as MediaTrackConstraints) : {}),
deviceId: { exact: deviceDetails.validDeviceIds },
},
};
const str = await navigator.mediaDevices.getUserMedia(updatedConstraints);
str?.addEventListener('inactive', onStreamInactive);
if (isActive.current) {
setStream(str);
setDimensions(getStreamDimensions(str, true));
setIsLoading(false);
setAvailableCameraDevices(deviceDetails.availableDevices);
setSelectedCameraDeviceId(getStreamDeviceId(str));
}
return await navigator.permissions.query({
name: 'camera' as PermissionName,
});
} catch (err) {
if (isActive.current) {
return null;
}
};
getUserMedia()
.catch((err) => {
return Promise.all([err, getCameraPermissionState()]);
})
.then((result) => {
if (!result) {
return Promise.all([null, getCameraPermissionState()]);
}
return result;
})
.then(([err, cameraPermission]) => {
cameraPermissionState = cameraPermission?.state ?? null;
if (err && isActive.current) {
handleGetUserMediaError(err);
throw err;
}
}
};
getUserMedia().catch(handleError);
})
.catch(handleError);
}, [constraints, stream, error, isLoading, lastConstraintsApplied]);

useEffect(() => {
Expand Down
14 changes: 14 additions & 0 deletions packages/camera-web/src/utils/errors.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@ export function getCameraErrorLabel(error?: UserMediaErrorType): TranslationObje
de: 'Die Kameravorschau ist nicht verfügbar, da für die Seite kein Kamerazugriff gewährt wurde.',
nl: 'De cameravoorbeeld is niet beschikbaar omdat er geen toegang tot de camera is verleend aan de pagina.',
};
case UserMediaErrorType.WEBPAGE_NOT_ALLOWED:
return {
en: 'Unable to get camera access. Make sure to press “Allow” when asked to grant camera permission for this web page.',
fr: "Impossible d'accéder à la caméra. Veuillez vous assurer d'appuyer sur “Autoriser” lorsqu'on vous propose d'autoriser l'accès à la caméra pour cette page web.",
de: 'Die Kamera kann nicht zugelassen werden. Stellen Sie sicher, dass Sie auf „Zulassen“ drücken, wenn Sie aufgefordert werden, die Kamera für diese Webseite zuzulassen.',
nl: 'Kan geen toestemming krijgen voor de camera. Zorg ervoor dat u op “Toestaan” drukt wanneer u wordt gevraagd om toestemming te geven voor het gebruik van de camera op deze webpagina.',
};
case UserMediaErrorType.BROWSER_NOT_ALLOWED:
return {
en: "Unable to get camera access. Make sure to grant camera access to your current internet browser in your device's settings.",
fr: "Impossible d'accéder à la caméra. Veuillez vous assurer d'autoriser l'accès à la caméra pour ce navigateur internet dans les paramètres de votre téléphone.",
de: 'Der Zugriff auf die Kamera ist nicht möglich. Stellen Sie sicher, dass Sie in den Einstellungen Ihres Geräts den Kamerazugriff für Ihren aktuellen Internetbrowser zulassen.',
nl: 'Kan geen cameratoegang krijgen. Zorg ervoor dat u de camera toegang verleent tot uw huidige internet browser in de instellingen van uw apparaat.',
};
case UserMediaErrorType.STREAM_INACTIVE:
return {
en: 'The camera video stream was closed unexpectedly.',
Expand Down
57 changes: 57 additions & 0 deletions packages/camera-web/test/Camera/hooks/useUserMedia.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ describe('useUserMedia hook', () => {

beforeEach(() => {
gumMock = mockGetUserMedia();
Object.defineProperty(global.navigator, 'permissions', {
value: {
query: jest.fn(() => Promise.reject()),
},
configurable: true,
writable: true,
});
});

afterEach(() => {
Expand Down Expand Up @@ -125,6 +132,56 @@ describe('useUserMedia hook', () => {
unmount();
});

it('should return a NotAllowed for webpage error in case of camera permission error', async () => {
const videoRef = { current: {} } as RefObject<HTMLVideoElement>;
const nativeError = new Error();
nativeError.name = 'NotAllowedError';
mockGetUserMedia({ createMock: () => jest.fn(() => Promise.reject(nativeError)) });
navigator.permissions.query = jest.fn(() =>
Promise.resolve({ state: 'granted' } as PermissionStatus),
);
const { result } = renderUseUserMedia({ constraints: {}, videoRef });
await waitFor(() => {
expect(result.current).toEqual({
stream: null,
dimensions: null,
error: {
type: UserMediaErrorType.BROWSER_NOT_ALLOWED,
nativeError,
},
isLoading: false,
retry: expect.any(Function),
availableCameraDevices: [],
selectedCameraDeviceId: null,
});
});
});

it('should return a NotAllowed for browser error in case of camera permission error', async () => {
const videoRef = { current: {} } as RefObject<HTMLVideoElement>;
const nativeError = new Error();
nativeError.name = 'NotAllowedError';
mockGetUserMedia({ createMock: () => jest.fn(() => Promise.reject(nativeError)) });
navigator.permissions.query = jest.fn(() =>
Promise.resolve({ state: 'denied' } as PermissionStatus),
);
const { result } = renderUseUserMedia({ constraints: {}, videoRef });
await waitFor(() => {
expect(result.current).toEqual({
stream: null,
dimensions: null,
error: {
type: UserMediaErrorType.WEBPAGE_NOT_ALLOWED,
nativeError,
},
isLoading: false,
retry: expect.any(Function),
availableCameraDevices: [],
selectedCameraDeviceId: null,
});
});
});

it('should return an InvalidStream error if the stream has no tracks', async () => {
const videoRef = { current: {} } as RefObject<HTMLVideoElement>;
mockGetUserMedia({ tracks: [] });
Expand Down