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