1 // Copyright 2022-present 650 Industries. All rights reserved.
2 
3 import AVFoundation
4 import ExpoModulesCore
5 
6 public final class CameraViewModule: Module {
definitionnull7   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 
takePictureForSimulatornull274 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 
generatePictureForSimulatornull290 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 
getAvailableVideoCodecsnull306 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