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  const { state } = await navigator.permissions.query({ name: query });
90  switch (state) {
91    case 'prompt':
92      return {
93        status: PermissionStatus.UNDETERMINED,
94        expires: 'never',
95        canAskAgain: true,
96        granted: false,
97      };
98    case 'granted':
99      return {
100        status: PermissionStatus.GRANTED,
101        expires: 'never',
102        canAskAgain: true,
103        granted: true,
104      };
105    case 'denied':
106      return {
107        status: PermissionStatus.DENIED,
108        expires: 'never',
109        canAskAgain: true,
110        granted: false,
111      };
112  }
113}
114
115export default {
116  get name(): string {
117    return 'ExponentCameraManager';
118  },
119  get Type() {
120    return {
121      back: 'back',
122      front: 'front',
123    };
124  },
125  get FlashMode() {
126    return {
127      on: 'on',
128      off: 'off',
129      auto: 'auto',
130      torch: 'torch',
131    };
132  },
133  get AutoFocus() {
134    return {
135      on: 'on',
136      off: 'off',
137      auto: 'auto',
138      singleShot: 'singleShot',
139    };
140  },
141  get WhiteBalance() {
142    return {
143      auto: 'auto',
144      continuous: 'continuous',
145      manual: 'manual',
146    };
147  },
148  get VideoQuality() {
149    return {};
150  },
151  get VideoStabilization() {
152    return {};
153  },
154  async isAvailableAsync(): Promise<boolean> {
155    return canGetUserMedia();
156  },
157  async takePicture(
158    options: CameraPictureOptions,
159    camera: ExponentCameraRef
160  ): Promise<CameraCapturedPicture> {
161    return await camera.takePicture(options);
162  },
163  async pausePreview(camera: ExponentCameraRef): Promise<void> {
164    await camera.pausePreview();
165  },
166  async resumePreview(camera: ExponentCameraRef): Promise<void> {
167    return await camera.resumePreview();
168  },
169  async getAvailableCameraTypesAsync(): Promise<string[]> {
170    if (!canGetUserMedia() || !navigator.mediaDevices.enumerateDevices) return [];
171
172    const devices = await navigator.mediaDevices.enumerateDevices();
173
174    const types: (string | null)[] = await Promise.all([
175      (await isFrontCameraAvailableAsync(devices)) && CameraType.front,
176      (await isBackCameraAvailableAsync()) && CameraType.back,
177    ]);
178
179    return types.filter(Boolean) as string[];
180  },
181  async getAvailablePictureSizes(ratio: string, camera: ExponentCameraRef): Promise<string[]> {
182    return await camera.getAvailablePictureSizes(ratio);
183  },
184  /* async getSupportedRatios(camera: ExponentCameraRef): Promise<string[]> {
185    // TODO: Support on web
186  },
187  async record(
188    options?: CameraRecordingOptions,
189    camera: ExponentCameraRef
190  ): Promise<{ uri: string }> {
191    // TODO: Support on web
192  },
193  async stopRecording(camera: ExponentCameraRef): Promise<void> {
194    // TODO: Support on web
195  }, */
196  async getPermissionsAsync(): Promise<PermissionResponse> {
197    return handlePermissionsQueryAsync('camera');
198  },
199  async requestPermissionsAsync(): Promise<PermissionResponse> {
200    return handleRequestPermissionsAsync();
201  },
202  async getCameraPermissionsAsync(): Promise<PermissionResponse> {
203    return handlePermissionsQueryAsync('camera');
204  },
205  async requestCameraPermissionsAsync(): Promise<PermissionResponse> {
206    return handleRequestPermissionsAsync();
207  },
208  async getMicrophonePermissionsAsync(): Promise<PermissionResponse> {
209    return handlePermissionsQueryAsync('microphone');
210  },
211  async requestMicrophonePermissionsAsync(): Promise<PermissionResponse> {
212    try {
213      await getUserMedia({
214        audio: true,
215      });
216      return {
217        status: PermissionStatus.GRANTED,
218        expires: 'never',
219        canAskAgain: true,
220        granted: true,
221      };
222    } catch ({ message }) {
223      return handleGetUserMediaError({ message });
224    }
225  },
226};
227