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    navigator.getUserMedia ||
29    (navigator as any).webkitGetUserMedia ||
30    (navigator as any).mozGetUserMedia ||
31    function () {
32      const error: any = new Error('Permission unimplemented');
33      error.code = 0;
34      error.name = 'NotAllowedError';
35      throw error;
36    };
37
38  return new Promise((resolve, reject) => {
39    getUserMedia.call(navigator, constraints, resolve, reject);
40  });
41}
42
43function handleGetUserMediaError({ message }: { message: string }): PermissionResponse {
44  // name: NotAllowedError
45  // code: 0
46  if (message === 'Permission dismissed') {
47    return {
48      status: PermissionStatus.UNDETERMINED,
49      expires: 'never',
50      canAskAgain: true,
51      granted: false,
52    };
53  } else {
54    // TODO: Bacon: [OSX] The system could deny access to chrome.
55    // TODO: Bacon: add: { status: 'unimplemented' }
56    return {
57      status: PermissionStatus.DENIED,
58      expires: 'never',
59      canAskAgain: true,
60      granted: false,
61    };
62  }
63}
64
65async function handleRequestPermissionsAsync(): Promise<PermissionResponse> {
66  try {
67    await getUserMedia({
68      video: true,
69    });
70    return {
71      status: PermissionStatus.GRANTED,
72      expires: 'never',
73      canAskAgain: true,
74      granted: true,
75    };
76  } catch ({ message }) {
77    return handleGetUserMediaError({ message });
78  }
79}
80
81async function handlePermissionsQueryAsync(
82  query: 'camera' | 'microphone'
83): Promise<PermissionResponse> {
84  if (!navigator?.permissions?.query) {
85    throw new UnavailabilityError('expo-camera', 'navigator.permissions API is not available');
86  }
87
88  const { state } = await navigator.permissions.query({ name: query });
89  switch (state) {
90    case 'prompt':
91      return {
92        status: PermissionStatus.UNDETERMINED,
93        expires: 'never',
94        canAskAgain: true,
95        granted: false,
96      };
97    case 'granted':
98      return {
99        status: PermissionStatus.GRANTED,
100        expires: 'never',
101        canAskAgain: true,
102        granted: true,
103      };
104    case 'denied':
105      return {
106        status: PermissionStatus.DENIED,
107        expires: 'never',
108        canAskAgain: true,
109        granted: false,
110      };
111  }
112}
113
114export default {
115  get name(): string {
116    return 'ExponentCameraManager';
117  },
118  get Type() {
119    return {
120      back: 'back',
121      front: 'front',
122    };
123  },
124  get FlashMode() {
125    return {
126      on: 'on',
127      off: 'off',
128      auto: 'auto',
129      torch: 'torch',
130    };
131  },
132  get AutoFocus() {
133    return {
134      on: 'on',
135      off: 'off',
136      auto: 'auto',
137      singleShot: 'singleShot',
138    };
139  },
140  get WhiteBalance() {
141    return {
142      auto: 'auto',
143      continuous: 'continuous',
144      manual: 'manual',
145    };
146  },
147  get VideoQuality() {
148    return {};
149  },
150  get VideoStabilization() {
151    return {};
152  },
153  async isAvailableAsync(): Promise<boolean> {
154    return canGetUserMedia();
155  },
156  async takePicture(
157    options: CameraPictureOptions,
158    camera: ExponentCameraRef
159  ): Promise<CameraCapturedPicture> {
160    return await camera.takePicture(options);
161  },
162  async pausePreview(camera: ExponentCameraRef): Promise<void> {
163    await camera.pausePreview();
164  },
165  async resumePreview(camera: ExponentCameraRef): Promise<void> {
166    return await camera.resumePreview();
167  },
168  async getAvailableCameraTypesAsync(): Promise<string[]> {
169    if (!canGetUserMedia() || !navigator.mediaDevices.enumerateDevices) return [];
170
171    const devices = await navigator.mediaDevices.enumerateDevices();
172
173    const types: (string | null)[] = await Promise.all([
174      (await isFrontCameraAvailableAsync(devices)) && CameraType.front,
175      (await isBackCameraAvailableAsync()) && CameraType.back,
176    ]);
177
178    return types.filter(Boolean) as string[];
179  },
180  async getAvailablePictureSizes(ratio: string, camera: ExponentCameraRef): Promise<string[]> {
181    return await camera.getAvailablePictureSizes(ratio);
182  },
183  /* async getSupportedRatios(camera: ExponentCameraRef): Promise<string[]> {
184    // TODO: Support on web
185  },
186  async record(
187    options?: CameraRecordingOptions,
188    camera: ExponentCameraRef
189  ): Promise<{ uri: string }> {
190    // TODO: Support on web
191  },
192  async stopRecording(camera: ExponentCameraRef): Promise<void> {
193    // TODO: Support on web
194  }, */
195  async getPermissionsAsync(): Promise<PermissionResponse> {
196    return handlePermissionsQueryAsync('camera');
197  },
198  async requestPermissionsAsync(): Promise<PermissionResponse> {
199    return handleRequestPermissionsAsync();
200  },
201  async getCameraPermissionsAsync(): Promise<PermissionResponse> {
202    return handlePermissionsQueryAsync('camera');
203  },
204  async requestCameraPermissionsAsync(): Promise<PermissionResponse> {
205    return handleRequestPermissionsAsync();
206  },
207  async getMicrophonePermissionsAsync(): Promise<PermissionResponse> {
208    return handlePermissionsQueryAsync('microphone');
209  },
210  async requestMicrophonePermissionsAsync(): Promise<PermissionResponse> {
211    try {
212      await getUserMedia({
213        audio: true,
214      });
215      return {
216        status: PermissionStatus.GRANTED,
217        expires: 'never',
218        canAskAgain: true,
219        granted: true,
220      };
221    } catch ({ message }) {
222      return handleGetUserMediaError({ message });
223    }
224  },
225};
226