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