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 "onResponsiveOrientationChanged" 72 ) 73 74 Prop("type") { (view, type: Int) in 75 if view.presetCamera != type { 76 view.presetCamera = type 77 view.updateType() 78 } 79 } 80 81 Prop("flashMode") { (view, flashMode: Int) in 82 if let flashMode = EXCameraFlashMode(rawValue: flashMode), view.flashMode != flashMode { 83 view.flashMode = flashMode 84 view.updateFlashMode() 85 } 86 } 87 88 Prop("faceDetectorSettings") { (view, settings: [String: Any]) in 89 view.updateFaceDetectorSettings(settings) 90 } 91 92 Prop("barCodeScannerSettings") { (view, settings: [String: Any]) in 93 view.setBarCodeScannerSettings(settings) 94 } 95 96 Prop("autoFocus") { (view, autoFocus: Int) in 97 if view.autoFocus != autoFocus { 98 view.autoFocus = autoFocus 99 view.updateFocusMode() 100 } 101 } 102 103 Prop("focusDepth") { (view, focusDepth: Float) in 104 if fabsf(view.focusDepth - focusDepth) > Float.ulpOfOne { 105 view.focusDepth = focusDepth 106 view.updateFocusDepth() 107 } 108 } 109 110 Prop("zoom") { (view, zoom: Double) in 111 if fabs(view.zoom - zoom) > Double.ulpOfOne { 112 view.zoom = zoom 113 view.updateZoom() 114 } 115 } 116 117 Prop("whiteBalance") { (view, whiteBalance: Int) in 118 if view.whiteBalance != whiteBalance { 119 view.whiteBalance = whiteBalance 120 view.updateWhiteBalance() 121 } 122 } 123 124 Prop("pictureSize") { (view, pictureSize: String) in 125 view.pictureSize = pictureSizesDict[pictureSize]?.rawValue as NSString? 126 view.updatePictureSize() 127 } 128 129 Prop("faceDetectorEnabled") { (view, detectFaces: Bool?) in 130 if view.isDetectingFaces != detectFaces { 131 view.isDetectingFaces = detectFaces ?? false 132 } 133 } 134 135 Prop("barCodeScannerEnabled") { (view, scanBarCodes: Bool?) in 136 if view.isScanningBarCodes != scanBarCodes { 137 view.isScanningBarCodes = scanBarCodes ?? false 138 } 139 } 140 141 Prop("responsiveOrientationWhenOrientationLocked") { (view, responsiveOrientation: Bool) in 142 if view.responsiveOrientationWhenOrientationLocked != responsiveOrientation { 143 view.responsiveOrientationWhenOrientationLocked = responsiveOrientation 144 view.updateResponsiveOrientationWhenOrientationLocked() 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 #else 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 #endif 170 } 171 .runOnQueue(.main) 172 173 AsyncFunction("stopRecording") { (viewTag: Int) in 174 #if targetEnvironment(simulator) 175 throw Exceptions.SimulatorNotSupported() 176 #else 177 guard let view = self.appContext?.findView(withTag: viewTag, ofType: EXCamera.self) else { 178 throw Exceptions.ViewNotFound((tag: viewTag, type: EXCamera.self)) 179 } 180 view.stopRecording() 181 #endif 182 } 183 .runOnQueue(.main) 184 185 AsyncFunction("resumePreview") { (viewTag: Int) in 186 #if targetEnvironment(simulator) 187 throw Exceptions.SimulatorNotSupported() 188 #else 189 guard let view = self.appContext?.findView(withTag: viewTag, ofType: EXCamera.self) else { 190 throw Exceptions.ViewNotFound((tag: viewTag, type: EXCamera.self)) 191 } 192 view.resumePreview() 193 #endif 194 } 195 .runOnQueue(.main) 196 197 AsyncFunction("pausePreview") { (viewTag: Int) in 198 #if targetEnvironment(simulator) 199 throw Exceptions.SimulatorNotSupported() 200 #else 201 guard let view = self.appContext?.findView(withTag: viewTag, ofType: EXCamera.self) else { 202 throw Exceptions.ViewNotFound((tag: viewTag, type: EXCamera.self)) 203 } 204 view.pausePreview() 205 #endif 206 } 207 .runOnQueue(.main) 208 209 AsyncFunction("getAvailablePictureSizes") { (_: String?, _: Int) in 210 // Argument types must be compatible with Android which receives the ratio and view tag. 211 return pictureSizesDict.keys 212 } 213 214 AsyncFunction("getAvailableVideoCodecsAsync") { () -> [String] in 215 return getAvailableVideoCodecs() 216 } 217 218 AsyncFunction("getPermissionsAsync") { (promise: Promise) in 219 EXPermissionsMethodsDelegate.getPermissionWithPermissionsManager( 220 self.appContext?.permissions, 221 withRequester: EXCameraPermissionRequester.self, 222 resolve: promise.resolver, 223 reject: promise.legacyRejecter 224 ) 225 } 226 227 AsyncFunction("requestPermissionsAsync") { (promise: Promise) in 228 EXPermissionsMethodsDelegate.askForPermission( 229 withPermissionsManager: self.appContext?.permissions, 230 withRequester: EXCameraPermissionRequester.self, 231 resolve: promise.resolver, 232 reject: promise.legacyRejecter 233 ) 234 } 235 236 AsyncFunction("getCameraPermissionsAsync") { (promise: Promise) in 237 EXPermissionsMethodsDelegate.getPermissionWithPermissionsManager( 238 self.appContext?.permissions, 239 withRequester: EXCameraCameraPermissionRequester.self, 240 resolve: promise.resolver, 241 reject: promise.legacyRejecter 242 ) 243 } 244 245 AsyncFunction("requestCameraPermissionsAsync") { (promise: Promise) in 246 EXPermissionsMethodsDelegate.askForPermission( 247 withPermissionsManager: self.appContext?.permissions, 248 withRequester: EXCameraCameraPermissionRequester.self, 249 resolve: promise.resolver, 250 reject: promise.legacyRejecter 251 ) 252 } 253 254 AsyncFunction("getMicrophonePermissionsAsync") { (promise: Promise) in 255 EXPermissionsMethodsDelegate.getPermissionWithPermissionsManager( 256 self.appContext?.permissions, 257 withRequester: EXCameraMicrophonePermissionRequester.self, 258 resolve: promise.resolver, 259 reject: promise.legacyRejecter 260 ) 261 } 262 263 AsyncFunction("requestMicrophonePermissionsAsync") { (promise: Promise) in 264 EXPermissionsMethodsDelegate.askForPermission( 265 withPermissionsManager: self.appContext?.permissions, 266 withRequester: EXCameraMicrophonePermissionRequester.self, 267 resolve: promise.resolver, 268 reject: promise.legacyRejecter 269 ) 270 } 271 } 272 } 273 274 private func takePictureForSimulator(_ appContext: AppContext?, _ view: EXCamera, _ options: TakePictureOptions, _ promise: Promise) throws { 275 if options.fastMode { 276 promise.resolve() 277 } 278 let result = try generatePictureForSimulator(appContext: appContext, options: options) 279 280 if options.fastMode { 281 view.onPictureSaved([ 282 "data": result, 283 "id": options.id 284 ]) 285 } else { 286 promise.resolve(result) 287 } 288 } 289 290 private func generatePictureForSimulator(appContext: AppContext?, options: TakePictureOptions) throws -> [String: Any?] { 291 guard let fs = appContext?.fileSystem else { 292 throw Exceptions.FileSystemModuleNotFound() 293 } 294 let path = fs.generatePath(inDirectory: fs.cachesDirectory.appending("/Camera"), withExtension: ".jpg") 295 let generatedPhoto = EXCameraUtils.generatePhoto(of: CGSize(width: 200, height: 200)) 296 let photoData = generatedPhoto.jpegData(compressionQuality: options.quality) 297 298 return [ 299 "uri": EXCameraUtils.writeImage(photoData, toPath: path), 300 "width": generatedPhoto.size.width, 301 "height": generatedPhoto.size.height, 302 "base64": options.base64 ? photoData?.base64EncodedString() : nil 303 ] 304 } 305 306 private func getAvailableVideoCodecs() -> [String] { 307 let session = AVCaptureSession() 308 309 session.beginConfiguration() 310 311 guard let captureDevice = EXCameraUtils.device(withMediaType: AVMediaType.video.rawValue, preferring: AVCaptureDevice.Position.front) else { 312 return [] 313 } 314 guard let deviceInput = try? AVCaptureDeviceInput(device: captureDevice) else { 315 return [] 316 } 317 if session.canAddInput(deviceInput) { 318 session.addInput(deviceInput) 319 } 320 321 session.commitConfiguration() 322 323 let movieFileOutput = AVCaptureMovieFileOutput() 324 325 if session.canAddOutput(movieFileOutput) { 326 session.addOutput(movieFileOutput) 327 } 328 return movieFileOutput.availableVideoCodecTypes.map { $0.rawValue } 329 } 330 331 private let pictureSizesDict = [ 332 "3840x2160": AVCaptureSession.Preset.hd4K3840x2160, 333 "1920x1080": AVCaptureSession.Preset.hd1920x1080, 334 "1280x720": AVCaptureSession.Preset.hd1280x720, 335 "640x480": AVCaptureSession.Preset.vga640x480, 336 "352x288": AVCaptureSession.Preset.cif352x288, 337 "Photo": AVCaptureSession.Preset.photo, 338 "High": AVCaptureSession.Preset.high, 339 "Medium": AVCaptureSession.Preset.medium, 340 "Low": AVCaptureSession.Preset.low 341 ] 342