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