1import { UnavailabilityError } from 'expo-modules-core';
2
3import {
4  CameraCapturedPicture,
5  CameraPictureOptions,
6  CameraType,
7  PermissionResponse,
8  PermissionStatus,
9} from './Camera.types';
10import { ExponentCameraRef } from './ExponentCamera.web';
11import {
12  canGetUserMedia,
13  isBackCameraAvailableAsync,
14  isFrontCameraAvailableAsync,
15} from './WebUserMediaManager';
16
17function getUserMedia(constraints: MediaStreamConstraints): Promise<MediaStream> {
18  if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
19    return navigator.mediaDevices.getUserMedia(constraints);
20  }
21
22  // Some browsers partially implement mediaDevices. We can't just assign an object
23  // with getUserMedia as it would overwrite existing properties.
24  // Here, we will just add the getUserMedia property if it's missing.
25
26  // First get ahold of the legacy getUserMedia, if present
27  const getUserMedia =
28    // TODO: this method is deprecated, migrate to https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
29    navigator.getUserMedia ||
30    navigator.webkitGetUserMedia ||
31    navigator.mozGetUserMedia ||
32    function () {
33      const error: any = new Error('Permission unimplemented');
34      error.code = 0;
35      error.name = 'NotAllowedError';
36      throw error;
37    };
38
39  return new Promise((resolve, reject) => {
40    getUserMedia.call(navigator, constraints, resolve, reject);
41  });
42}
43
44function handleGetUserMediaError({ message }: { message: string }): PermissionResponse {
45  // name: NotAllowedError
46  // code: 0
47  if (message === 'Permission dismissed') {
48    return {
49      status: PermissionStatus.UNDETERMINED,
50      expires: 'never',
51      canAskAgain: true,
52      granted: false,
53    };
54  } else {
55    // TODO: Bacon: [OSX] The system could deny access to chrome.
56    // TODO: Bacon: add: { status: 'unimplemented' }
57    return {
58      status: PermissionStatus.DENIED,
59      expires: 'never',
60      canAskAgain: true,
61      granted: false,
62    };
63  }
64}
65
66async function handleRequestPermissionsAsync(): Promise<PermissionResponse> {
67  try {
68    await getUserMedia({
69      video: true,
70    });
71    return {
72      status: PermissionStatus.GRANTED,
73      expires: 'never',
74      canAskAgain: true,
75      granted: true,
76    };
77  } catch ({ message }) {
78    return handleGetUserMediaError({ message });
79  }
80}
81
82async function handlePermissionsQueryAsync(
83  query: 'camera' | 'microphone'
84): Promise<PermissionResponse> {
85  if (!navigator?.permissions?.query) {
86    throw new UnavailabilityError('expo-camera', 'navigator.permissions API is not available');
87  }
88
89  try {
90    const { state } = await navigator.permissions.query({ name: query });
91    switch (state) {
92      case 'prompt':
93        return {
94          status: PermissionStatus.UNDETERMINED,
95          expires: 'never',
96          canAskAgain: true,
97          granted: false,
98        };
99      case 'granted':
100        return {
101          status: PermissionStatus.GRANTED,
102          expires: 'never',
103          canAskAgain: true,
104          granted: true,
105        };
106      case 'denied':
107        return {
108          status: PermissionStatus.DENIED,
109          expires: 'never',
110          canAskAgain: true,
111          granted: false,
112        };
113    }
114  } catch (e) {
115    // Firefox doesn't support querying for the camera permission, so return undetermined status
116    if (e instanceof TypeError) {
117      return {
118        status: PermissionStatus.UNDETERMINED,
119        expires: 'never',
120        canAskAgain: true,
121        granted: false,
122      };
123    }
124    throw e;
125  }
126}
127
128export default {
129  get name(): string {
130    return 'ExponentCameraManager';
131  },
132  get Type() {
133    return {
134      back: 'back',
135      front: 'front',
136    };
137  },
138  get FlashMode() {
139    return {
140      on: 'on',
141      off: 'off',
142      auto: 'auto',
143      torch: 'torch',
144    };
145  },
146  get AutoFocus() {
147    return {
148      on: 'on',
149      off: 'off',
150      auto: 'auto',
151      singleShot: 'singleShot',
152    };
153  },
154  get WhiteBalance() {
155    return {
156      auto: 'auto',
157      continuous: 'continuous',
158      manual: 'manual',
159    };
160  },
161  get VideoQuality() {
162    return {};
163  },
164  get VideoStabilization() {
165    return {};
166  },
167  async isAvailableAsync(): Promise<boolean> {
168    return canGetUserMedia();
169  },
170  async takePicture(
171    options: CameraPictureOptions,
172    camera: ExponentCameraRef
173  ): Promise<CameraCapturedPicture> {
174    return await camera.takePicture(options);
175  },
176  async pausePreview(camera: ExponentCameraRef): Promise<void> {
177    await camera.pausePreview();
178  },
179  async resumePreview(camera: ExponentCameraRef): Promise<void> {
180    return await camera.resumePreview();
181  },
182  async getAvailableCameraTypesAsync(): Promise<string[]> {
183    if (!canGetUserMedia() || !navigator.mediaDevices.enumerateDevices) return [];
184
185    const devices = await navigator.mediaDevices.enumerateDevices();
186
187    const types: (string | null)[] = await Promise.all([
188      (await isFrontCameraAvailableAsync(devices)) && CameraType.front,
189      (await isBackCameraAvailableAsync()) && CameraType.back,
190    ]);
191
192    return types.filter(Boolean) as string[];
193  },
194  async getAvailablePictureSizes(ratio: string, camera: ExponentCameraRef): Promise<string[]> {
195    return await camera.getAvailablePictureSizes(ratio);
196  },
197  /* async getSupportedRatios(camera: ExponentCameraRef): Promise<string[]> {
198    // TODO: Support on web
199  },
200  async record(
201    options?: CameraRecordingOptions,
202    camera: ExponentCameraRef
203  ): Promise<{ uri: string }> {
204    // TODO: Support on web
205  },
206  async stopRecording(camera: ExponentCameraRef): Promise<void> {
207    // TODO: Support on web
208  }, */
209  async getPermissionsAsync(): Promise<PermissionResponse> {
210    return handlePermissionsQueryAsync('camera');
211  },
212  async requestPermissionsAsync(): Promise<PermissionResponse> {
213    return handleRequestPermissionsAsync();
214  },
215  async getCameraPermissionsAsync(): Promise<PermissionResponse> {
216    return handlePermissionsQueryAsync('camera');
217  },
218  async requestCameraPermissionsAsync(): Promise<PermissionResponse> {
219    return handleRequestPermissionsAsync();
220  },
221  async getMicrophonePermissionsAsync(): Promise<PermissionResponse> {
222    return handlePermissionsQueryAsync('microphone');
223  },
224  async requestMicrophonePermissionsAsync(): Promise<PermissionResponse> {
225    try {
226      await getUserMedia({
227        audio: true,
228      });
229      return {
230        status: PermissionStatus.GRANTED,
231        expires: 'never',
232        canAskAgain: true,
233        granted: true,
234      };
235    } catch ({ message }) {
236      return handleGetUserMediaError({ message });
237    }
238  },
239};
240