1 // Copyright 2022-present 650 Industries. All rights reserved. 2 3 import AVFoundation 4 import ExpoModulesCore 5 6 public final class CameraViewModule: Module { 7 public func definition() -> ModuleDefinition { 8 Name("ExponentCamera") 9 10 OnCreate { 11 let permissionsManager = self.appContext?.permissions 12 13 EXPermissionsMethodsDelegate.register( 14 [EXCameraPermissionRequester(), EXCameraCameraPermissionRequester(), EXCameraMicrophonePermissionRequester()], 15 withPermissionsManager: permissionsManager 16 ) 17 } 18 19 Constants([ 20 "Type": [ 21 "front": EXCameraType.front, 22 "back": EXCameraType.back 23 ], 24 "FlashMode": [ 25 "off": EXCameraFlashMode.off, 26 "on": EXCameraFlashMode.on, 27 "auto": EXCameraFlashMode.auto, 28 "torch": EXCameraFlashMode.torch 29 ], 30 "AutoFocus": [ 31 "on": EXCameraAutoFocus.on, 32 "off": EXCameraAutoFocus.off 33 ], 34 "WhiteBalance": [ 35 "auto": EXCameraWhiteBalance.auto, 36 "sunny": EXCameraWhiteBalance.sunny, 37 "cloudy": EXCameraWhiteBalance.cloudy, 38 "shadow": EXCameraWhiteBalance.shadow, 39 "incandescent": EXCameraWhiteBalance.incandescent, 40 "fluorescent": EXCameraWhiteBalance.fluorescent 41 ], 42 "VideoQuality": [ 43 "2160p": EXCameraVideoResolution.video2160p, 44 "1080p": EXCameraVideoResolution.video1080p, 45 "720p": EXCameraVideoResolution.video720p, 46 "480p": EXCameraVideoResolution.video4x3, 47 "4:3": EXCameraVideoResolution.video4x3 48 ], 49 "VideoStabilization": [ 50 "off": EXCameraVideoStabilizationMode.videoStabilizationModeOff, 51 "standard": EXCameraVideoStabilizationMode.videoStabilizationModeStandard, 52 "cinematic": EXCameraVideoStabilizationMode.videoStabilizationModeCinematic, 53 "auto": EXCameraVideoStabilizationMode.avCaptureVideoStabilizationModeAuto 54 ], 55 "VideoCodec": [ 56 "H264": EXCameraVideoCodec.H264, 57 "HEVC": EXCameraVideoCodec.HEVC, 58 "JPEG": EXCameraVideoCodec.JPEG, 59 "AppleProRes422": EXCameraVideoCodec.appleProRes422, 60 "AppleProRes4444": EXCameraVideoCodec.appleProRes4444 61 ] 62 ]) 63 64 ViewManager { 65 // TODO: For some unknown reason, the below line doesn't compile when used within the `View` component. 66 // That's probably fine as a workaround, since we plan to get rid of custom view factories anyway. 67 let legacyModuleRegistry = self.appContext?.legacyModuleRegistry 68 69 View { 70 return EXCamera(moduleRegistry: legacyModuleRegistry) 71 } 72 73 Events( 74 "onCameraReady", 75 "onMountError", 76 "onPictureSaved", 77 "onBarCodeScanned", 78 "onFacesDetected" 79 ) 80 81 Prop("type") { (view: EXCamera, type: Int) in 82 if view.presetCamera != type { 83 view.presetCamera = type 84 view.updateType() 85 } 86 } 87 88 Prop("flashMode") { (view: EXCamera, flashMode: Int) in 89 if let flashMode = EXCameraFlashMode(rawValue: flashMode), view.flashMode != flashMode { 90 view.flashMode = flashMode 91 view.updateFlashMode() 92 } 93 } 94 95 Prop("faceDetectorSettings") { (view: EXCamera, settings: [String: Any]) in 96 view.updateFaceDetectorSettings(settings) 97 } 98 99 Prop("barCodeScannerSettings") { (view: EXCamera, settings: [String: Any]) in 100 view.setBarCodeScannerSettings(settings) 101 } 102 103 Prop("autoFocus") { (view: EXCamera, autoFocus: Int) in 104 if view.autoFocus != autoFocus { 105 view.autoFocus = autoFocus 106 view.updateFocusMode() 107 } 108 } 109 110 Prop("focusDepth") { (view: EXCamera, focusDepth: Float) in 111 if fabsf(view.focusDepth - focusDepth) > Float.ulpOfOne { 112 view.focusDepth = focusDepth 113 view.updateFocusDepth() 114 } 115 } 116 117 Prop("zoom") { (view: EXCamera, zoom: Double) in 118 if fabs(view.zoom - zoom) > Double.ulpOfOne { 119 view.zoom = zoom 120 view.updateZoom() 121 } 122 } 123 124 Prop("whiteBalance") { (view: EXCamera, whiteBalance: Int) in 125 if view.whiteBalance != whiteBalance { 126 view.whiteBalance = whiteBalance 127 view.updateWhiteBalance() 128 } 129 } 130 131 Prop("pictureSize") { (view: EXCamera, pictureSize: String) in 132 view.pictureSize = pictureSizesDict[pictureSize]?.rawValue as NSString? 133 view.updatePictureSize() 134 } 135 136 Prop("faceDetectorEnabled") { (view: EXCamera, detectFaces: Bool) in 137 if view.isDetectingFaces != detectFaces { 138 view.isDetectingFaces = detectFaces 139 } 140 } 141 142 Prop("barCodeScannerEnabled") { (view: EXCamera, scanBarCodes: Bool) in 143 if view.isScanningBarCodes != scanBarCodes { 144 view.isScanningBarCodes = scanBarCodes 145 } 146 } 147 } 148 149 AsyncFunction("takePicture") { (options: TakePictureOptions, viewTag: Int, promise: Promise) in 150 guard let view = self.appContext?.findView(withTag: viewTag, ofType: EXCamera.self) else { 151 throw Exceptions.ViewNotFound((tag: viewTag, type: EXCamera.self)) 152 } 153 #if targetEnvironment(simulator) 154 try takePictureForSimulator(self.appContext, view, options, promise) 155 #else // simulator 156 view.takePicture(options.toDictionary(), resolve: promise.resolver, reject: promise.legacyRejecter) 157 #endif // not simulator 158 } 159 .runOnQueue(.main) 160 161 AsyncFunction("record") { (options: [String: Any], viewTag: Int, promise: Promise) in 162 #if targetEnvironment(simulator) 163 throw Exceptions.SimulatorNotSupported() 164 #endif 165 guard let view = self.appContext?.findView(withTag: viewTag, ofType: EXCamera.self) else { 166 throw Exceptions.ViewNotFound((tag: viewTag, type: EXCamera.self)) 167 } 168 view.record(options, resolve: promise.resolver, reject: promise.legacyRejecter) 169 } 170 .runOnQueue(.main) 171 172 AsyncFunction("stopRecording") { (viewTag: Int) in 173 #if targetEnvironment(simulator) 174 throw Exceptions.SimulatorNotSupported() 175 #endif 176 guard let view = self.appContext?.findView(withTag: viewTag, ofType: EXCamera.self) else { 177 throw Exceptions.ViewNotFound((tag: viewTag, type: EXCamera.self)) 178 } 179 view.stopRecording() 180 } 181 .runOnQueue(.main) 182 183 AsyncFunction("resumePreview") { (viewTag: Int) in 184 #if targetEnvironment(simulator) 185 throw Exceptions.SimulatorNotSupported() 186 #endif 187 guard let view = self.appContext?.findView(withTag: viewTag, ofType: EXCamera.self) else { 188 throw Exceptions.ViewNotFound((tag: viewTag, type: EXCamera.self)) 189 } 190 view.resumePreview() 191 } 192 .runOnQueue(.main) 193 194 AsyncFunction("pausePreview") { (viewTag: Int) in 195 #if targetEnvironment(simulator) 196 throw Exceptions.SimulatorNotSupported() 197 #endif 198 guard let view = self.appContext?.findView(withTag: viewTag, ofType: EXCamera.self) else { 199 throw Exceptions.ViewNotFound((tag: viewTag, type: EXCamera.self)) 200 } 201 view.pausePreview() 202 } 203 .runOnQueue(.main) 204 205 AsyncFunction("getAvailablePictureSizes") { (_: String?, _: Int) in 206 // Argument types must be compatible with Android which receives the ratio and view tag. 207 return pictureSizesDict.keys 208 } 209 210 AsyncFunction("getAvailableVideoCodecsAsync") { () -> [String] in 211 return getAvailableVideoCodecs() 212 } 213 214 AsyncFunction("getPermissionsAsync") { (promise: Promise) in 215 EXPermissionsMethodsDelegate.getPermissionWithPermissionsManager( 216 self.appContext?.permissions, 217 withRequester: EXCameraPermissionRequester.self, 218 resolve: promise.resolver, 219 reject: promise.legacyRejecter 220 ) 221 } 222 223 AsyncFunction("requestPermissionsAsync") { (promise: Promise) in 224 EXPermissionsMethodsDelegate.askForPermission( 225 withPermissionsManager: self.appContext?.permissions, 226 withRequester: EXCameraPermissionRequester.self, 227 resolve: promise.resolver, 228 reject: promise.legacyRejecter 229 ) 230 } 231 232 AsyncFunction("getCameraPermissionsAsync") { (promise: Promise) in 233 EXPermissionsMethodsDelegate.getPermissionWithPermissionsManager( 234 self.appContext?.permissions, 235 withRequester: EXCameraCameraPermissionRequester.self, 236 resolve: promise.resolver, 237 reject: promise.legacyRejecter 238 ) 239 } 240 241 AsyncFunction("requestCameraPermissionsAsync") { (promise: Promise) in 242 EXPermissionsMethodsDelegate.askForPermission( 243 withPermissionsManager: self.appContext?.permissions, 244 withRequester: EXCameraCameraPermissionRequester.self, 245 resolve: promise.resolver, 246 reject: promise.legacyRejecter 247 ) 248 } 249 250 AsyncFunction("getMicrophonePermissionsAsync") { (promise: Promise) in 251 EXPermissionsMethodsDelegate.getPermissionWithPermissionsManager( 252 self.appContext?.permissions, 253 withRequester: EXCameraMicrophonePermissionRequester.self, 254 resolve: promise.resolver, 255 reject: promise.legacyRejecter 256 ) 257 } 258 259 AsyncFunction("requestMicrophonePermissionsAsync") { (promise: Promise) in 260 EXPermissionsMethodsDelegate.askForPermission( 261 withPermissionsManager: self.appContext?.permissions, 262 withRequester: EXCameraMicrophonePermissionRequester.self, 263 resolve: promise.resolver, 264 reject: promise.legacyRejecter 265 ) 266 } 267 } 268 } 269 270 private func takePictureForSimulator(_ appContext: AppContext?, _ view: EXCamera, _ options: TakePictureOptions, _ promise: Promise) throws { 271 if options.fastMode { 272 promise.resolve() 273 } 274 let result = try generatePictureForSimulator(appContext: appContext, options: options) 275 276 if options.fastMode { 277 view.onPictureSaved([ 278 "data": result, 279 "id": options.id 280 ]) 281 } else { 282 promise.resolve(result) 283 } 284 } 285 286 private func generatePictureForSimulator(appContext: AppContext?, options: TakePictureOptions) throws -> [String: Any?] { 287 guard let fs = appContext?.fileSystem else { 288 throw Exceptions.FileSystemModuleNotFound() 289 } 290 let path = fs.generatePath(inDirectory: fs.cachesDirectory.appending("Camera"), withExtension: ".jpg") 291 let generatedPhoto = EXCameraUtils.generatePhoto(of: CGSize(width: 200, height: 200)) 292 let photoData = generatedPhoto.jpegData(compressionQuality: options.quality) 293 294 return [ 295 "uri": EXCameraUtils.writeImage(photoData, toPath: path), 296 "width": generatedPhoto.size.width, 297 "height": generatedPhoto.size.height, 298 "base64": options.base64 ? photoData?.base64EncodedString() : nil 299 ] 300 } 301 302 private func getAvailableVideoCodecs() -> [String] { 303 let session = AVCaptureSession() 304 305 session.beginConfiguration() 306 307 guard let captureDevice = EXCameraUtils.device(withMediaType: AVMediaType.video.rawValue, preferring: AVCaptureDevice.Position.front) else { 308 return [] 309 } 310 guard let deviceInput = try? AVCaptureDeviceInput(device: captureDevice) else { 311 return [] 312 } 313 if session.canAddInput(deviceInput) { 314 session.addInput(deviceInput) 315 } 316 317 session.commitConfiguration() 318 319 let movieFileOutput = AVCaptureMovieFileOutput() 320 321 if session.canAddOutput(movieFileOutput) { 322 session.addOutput(movieFileOutput) 323 } 324 return movieFileOutput.availableVideoCodecTypes.map { $0.rawValue } 325 } 326 327 private let pictureSizesDict = [ 328 "3840x2160": AVCaptureSession.Preset.hd4K3840x2160, 329 "1920x1080": AVCaptureSession.Preset.hd1920x1080, 330 "1280x720": AVCaptureSession.Preset.hd1280x720, 331 "640x480": AVCaptureSession.Preset.vga640x480, 332 "352x288": AVCaptureSession.Preset.cif352x288, 333 "Photo": AVCaptureSession.Preset.photo, 334 "High": AVCaptureSession.Preset.high, 335 "Medium": AVCaptureSession.Preset.medium, 336 "Low": AVCaptureSession.Preset.low 337 ] 338