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