Skip to content

Commit

Permalink
Feat/mn 584/specfic error message camera (#817)
Browse files Browse the repository at this point in the history
* ✨ different error message for webpage and browser not allowed

* 🐛 refactor to not use async inside

* 🐛 query camera permission should get after media stream completed

* 👌change naming

* 👌 remove unwanted comment

* 👌 improve comments
  • Loading branch information
arunachalam-monk authored Sep 4, 2024
1 parent 87174eb commit f0976a7
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 27 deletions.
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

0 comments on commit f0976a7

Please sign in to comment.