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