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