@capgo/camera-preview 7.4.0-beta.8 → 7.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +243 -51
  2. package/android/gradle/wrapper/gradle-wrapper.properties +1 -1
  3. package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java +1249 -143
  4. package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraXView.java +3400 -1382
  5. package/android/src/main/java/com/ahm/capacitor/camera/preview/GridOverlayView.java +95 -58
  6. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraDevice.java +55 -46
  7. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraLens.java +61 -52
  8. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraSessionConfiguration.java +160 -72
  9. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/LensInfo.java +29 -23
  10. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/ZoomFactors.java +24 -23
  11. package/dist/docs.json +441 -40
  12. package/dist/esm/definitions.d.ts +167 -25
  13. package/dist/esm/definitions.js.map +1 -1
  14. package/dist/esm/index.d.ts +2 -0
  15. package/dist/esm/index.js +24 -1
  16. package/dist/esm/index.js.map +1 -1
  17. package/dist/esm/web.d.ts +23 -3
  18. package/dist/esm/web.js +463 -65
  19. package/dist/esm/web.js.map +1 -1
  20. package/dist/plugin.cjs.js +485 -64
  21. package/dist/plugin.cjs.js.map +1 -1
  22. package/dist/plugin.js +485 -64
  23. package/dist/plugin.js.map +1 -1
  24. package/ios/Sources/{CapgoCameraPreview → CapgoCameraPreviewPlugin}/CameraController.swift +731 -315
  25. package/ios/Sources/CapgoCameraPreviewPlugin/Plugin.swift +1902 -0
  26. package/package.json +11 -3
  27. package/android/.gradle/8.14.2/checksums/checksums.lock +0 -0
  28. package/android/.gradle/8.14.2/checksums/md5-checksums.bin +0 -0
  29. package/android/.gradle/8.14.2/checksums/sha1-checksums.bin +0 -0
  30. package/android/.gradle/8.14.2/executionHistory/executionHistory.bin +0 -0
  31. package/android/.gradle/8.14.2/executionHistory/executionHistory.lock +0 -0
  32. package/android/.gradle/8.14.2/fileChanges/last-build.bin +0 -0
  33. package/android/.gradle/8.14.2/fileHashes/fileHashes.bin +0 -0
  34. package/android/.gradle/8.14.2/fileHashes/fileHashes.lock +0 -0
  35. package/android/.gradle/8.14.2/fileHashes/resourceHashesCache.bin +0 -0
  36. package/android/.gradle/8.14.2/gc.properties +0 -0
  37. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  38. package/android/.gradle/buildOutputCleanup/cache.properties +0 -2
  39. package/android/.gradle/buildOutputCleanup/outputFiles.bin +0 -0
  40. package/android/.gradle/file-system.probe +0 -0
  41. package/android/.gradle/vcs-1/gc.properties +0 -0
  42. package/ios/Sources/CapgoCameraPreview/Plugin.swift +0 -1211
  43. /package/ios/Sources/{CapgoCameraPreview → CapgoCameraPreviewPlugin}/GridOverlayView.swift +0 -0
@@ -1,1211 +0,0 @@
1
- import Foundation
2
- import Capacitor
3
- import AVFoundation
4
- import Photos
5
- import CoreImage
6
- import CoreLocation
7
-
8
-
9
- extension UIWindow {
10
- static var isLandscape: Bool {
11
- if #available(iOS 13.0, *) {
12
- return UIApplication.shared.windows
13
- .first?
14
- .windowScene?
15
- .interfaceOrientation
16
- .isLandscape ?? false
17
- } else {
18
- return UIApplication.shared.statusBarOrientation.isLandscape
19
- }
20
- }
21
- static var isPortrait: Bool {
22
- if #available(iOS 13.0, *) {
23
- return UIApplication.shared.windows
24
- .first?
25
- .windowScene?
26
- .interfaceOrientation
27
- .isPortrait ?? false
28
- } else {
29
- return UIApplication.shared.statusBarOrientation.isPortrait
30
- }
31
- }
32
- }
33
-
34
- /**
35
- * Please read the Capacitor iOS Plugin Development Guide
36
- * here: https://capacitor.ionicframework.com/docs/plugins/ios
37
- */
38
- @objc(CameraPreview)
39
- public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelegate {
40
- public let identifier = "CameraPreviewPlugin"
41
- public let jsName = "CameraPreview"
42
- public let pluginMethods: [CAPPluginMethod] = [
43
- CAPPluginMethod(name: "start", returnType: CAPPluginReturnPromise),
44
- CAPPluginMethod(name: "flip", returnType: CAPPluginReturnPromise),
45
- CAPPluginMethod(name: "stop", returnType: CAPPluginReturnPromise),
46
- CAPPluginMethod(name: "capture", returnType: CAPPluginReturnPromise),
47
- CAPPluginMethod(name: "captureSample", returnType: CAPPluginReturnPromise),
48
- CAPPluginMethod(name: "getSupportedFlashModes", returnType: CAPPluginReturnPromise),
49
- CAPPluginMethod(name: "getHorizontalFov", returnType: CAPPluginReturnPromise),
50
- CAPPluginMethod(name: "setFlashMode", returnType: CAPPluginReturnPromise),
51
- CAPPluginMethod(name: "startRecordVideo", returnType: CAPPluginReturnPromise),
52
- CAPPluginMethod(name: "stopRecordVideo", returnType: CAPPluginReturnPromise),
53
- CAPPluginMethod(name: "getTempFilePath", returnType: CAPPluginReturnPromise),
54
- CAPPluginMethod(name: "getSupportedPictureSizes", returnType: CAPPluginReturnPromise),
55
- CAPPluginMethod(name: "isRunning", returnType: CAPPluginReturnPromise),
56
- CAPPluginMethod(name: "getAvailableDevices", returnType: CAPPluginReturnPromise),
57
- CAPPluginMethod(name: "getZoom", returnType: CAPPluginReturnPromise),
58
- CAPPluginMethod(name: "setZoom", returnType: CAPPluginReturnPromise),
59
- CAPPluginMethod(name: "getFlashMode", returnType: CAPPluginReturnPromise),
60
- CAPPluginMethod(name: "setDeviceId", returnType: CAPPluginReturnPromise),
61
- CAPPluginMethod(name: "getDeviceId", returnType: CAPPluginReturnPromise),
62
- CAPPluginMethod(name: "setAspectRatio", returnType: CAPPluginReturnPromise),
63
- CAPPluginMethod(name: "getAspectRatio", returnType: CAPPluginReturnPromise),
64
- CAPPluginMethod(name: "setGridMode", returnType: CAPPluginReturnPromise),
65
- CAPPluginMethod(name: "getGridMode", returnType: CAPPluginReturnPromise),
66
- CAPPluginMethod(name: "getPreviewSize", returnType: CAPPluginReturnPromise),
67
- CAPPluginMethod(name: "setPreviewSize", returnType: CAPPluginReturnPromise)
68
- ]
69
- // Camera state tracking
70
- private var isInitializing: Bool = false
71
- private var isInitialized: Bool = false
72
- private var backgroundSession: AVCaptureSession?
73
-
74
- var previewView: UIView!
75
- var cameraPosition = String()
76
- let cameraController = CameraController()
77
- var posX: CGFloat?
78
- var posY: CGFloat?
79
- var width: CGFloat?
80
- var height: CGFloat?
81
- var paddingBottom: CGFloat?
82
- var rotateWhenOrientationChanged: Bool?
83
- var toBack: Bool?
84
- var storeToFile: Bool?
85
- var enableZoom: Bool?
86
- var highResolutionOutput: Bool = false
87
- var disableAudio: Bool = false
88
- var locationManager: CLLocationManager?
89
- var currentLocation: CLLocation?
90
- private var aspectRatio: String?
91
- private var gridMode: String = "none"
92
-
93
- // MARK: - Transparency Methods
94
-
95
-
96
- private func makeWebViewTransparent() {
97
- guard let webView = self.webView else { return }
98
-
99
-
100
- // Define a recursive function to traverse the view hierarchy
101
- func makeSubviewsTransparent(_ view: UIView) {
102
- // Set the background color to clear
103
- view.backgroundColor = .clear
104
-
105
-
106
- // Recurse for all subviews
107
- for subview in view.subviews {
108
- makeSubviewsTransparent(subview)
109
- }
110
- }
111
-
112
-
113
- // Set the main webView to be transparent
114
- webView.isOpaque = false
115
- webView.backgroundColor = .clear
116
-
117
-
118
- // Recursively make all subviews transparent
119
- makeSubviewsTransparent(webView)
120
-
121
-
122
- // Also ensure the webview's container is transparent
123
- webView.superview?.backgroundColor = .clear
124
-
125
-
126
- // Force a layout pass to apply changes
127
- DispatchQueue.main.async {
128
- webView.setNeedsLayout()
129
- webView.layoutIfNeeded()
130
- }
131
- }
132
-
133
- @objc func rotated() {
134
- guard let previewView = self.previewView,
135
- let posX = self.posX,
136
- let posY = self.posY,
137
- let width = self.width,
138
- let heightValue = self.height else {
139
- return
140
- }
141
- let paddingBottom = self.paddingBottom ?? 0
142
- let height = heightValue - paddingBottom
143
-
144
- // Handle auto-centering during rotation
145
- if posX == -1 || posY == -1 {
146
- // Trigger full recalculation for auto-centered views
147
- self.updateCameraFrame()
148
- } else {
149
- // Manual positioning - use original rotation logic with no animation
150
- CATransaction.begin()
151
- CATransaction.setDisableActions(true)
152
-
153
- if UIWindow.isLandscape {
154
- previewView.frame = CGRect(x: posY, y: posX, width: max(height, width), height: min(height, width))
155
- self.cameraController.previewLayer?.frame = previewView.bounds
156
- }
157
-
158
- if UIWindow.isPortrait {
159
- previewView.frame = CGRect(x: posX, y: posY, width: min(height, width), height: max(height, width))
160
- self.cameraController.previewLayer?.frame = previewView.bounds
161
- }
162
-
163
- CATransaction.commit()
164
- }
165
-
166
- if let connection = self.cameraController.fileVideoOutput?.connection(with: .video) {
167
- switch UIDevice.current.orientation {
168
- case .landscapeRight:
169
- connection.videoOrientation = .landscapeLeft
170
- case .landscapeLeft:
171
- connection.videoOrientation = .landscapeRight
172
- case .portrait:
173
- connection.videoOrientation = .portrait
174
- case .portraitUpsideDown:
175
- connection.videoOrientation = .portraitUpsideDown
176
- default:
177
- connection.videoOrientation = .portrait
178
- }
179
- }
180
-
181
- cameraController.updateVideoOrientation()
182
-
183
- cameraController.updateVideoOrientation()
184
-
185
- // Update grid overlay frame if it exists - no animation
186
- if let gridOverlay = self.cameraController.gridOverlayView {
187
- CATransaction.begin()
188
- CATransaction.setDisableActions(true)
189
- gridOverlay.frame = previewView.bounds
190
- CATransaction.commit()
191
- }
192
-
193
- // Ensure webview remains transparent after rotation
194
- if self.isInitialized {
195
- self.makeWebViewTransparent()
196
- }
197
- }
198
-
199
- @objc func setAspectRatio(_ call: CAPPluginCall) {
200
- guard self.isInitialized else {
201
- call.reject("camera not started")
202
- return
203
- }
204
-
205
- guard let newAspectRatio = call.getString("aspectRatio") else {
206
- call.reject("aspectRatio parameter is required")
207
- return
208
- }
209
-
210
- self.aspectRatio = newAspectRatio
211
-
212
- // When aspect ratio changes, calculate maximum size possible from current position
213
- if let posX = self.posX, let posY = self.posY {
214
- let webViewWidth = self.webView?.frame.width ?? UIScreen.main.bounds.width
215
- let webViewHeight = self.webView?.frame.height ?? UIScreen.main.bounds.height
216
- let paddingBottom = self.paddingBottom ?? 0
217
-
218
- // Calculate available space from current position
219
- let availableWidth: CGFloat
220
- let availableHeight: CGFloat
221
-
222
- if posX == -1 || posY == -1 {
223
- // Auto-centering mode - use full dimensions
224
- availableWidth = webViewWidth
225
- availableHeight = webViewHeight - paddingBottom
226
- } else {
227
- // Manual positioning - calculate remaining space
228
- availableWidth = webViewWidth - posX
229
- availableHeight = webViewHeight - posY - paddingBottom
230
- }
231
-
232
- // Parse aspect ratio - convert to portrait orientation for camera use
233
- let ratioParts = newAspectRatio.split(separator: ":").map { Double($0) ?? 1.0 }
234
- // For camera, we want portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
235
- let ratio = ratioParts[1] / ratioParts[0]
236
-
237
- // Calculate maximum size that fits the aspect ratio in available space
238
- let maxWidthByHeight = availableHeight * CGFloat(ratio)
239
- let maxHeightByWidth = availableWidth / CGFloat(ratio)
240
-
241
- if maxWidthByHeight <= availableWidth {
242
- // Height is the limiting factor
243
- self.width = maxWidthByHeight
244
- self.height = availableHeight
245
- } else {
246
- // Width is the limiting factor
247
- self.width = availableWidth
248
- self.height = maxHeightByWidth
249
- }
250
-
251
- print("[CameraPreview] Aspect ratio changed to \(newAspectRatio), new size: \(self.width!)x\(self.height!)")
252
- }
253
-
254
- self.updateCameraFrame()
255
-
256
- // Return the actual preview bounds
257
- var result = JSObject()
258
- result["x"] = Double(self.previewView.frame.origin.x)
259
- result["y"] = Double(self.previewView.frame.origin.y)
260
- result["width"] = Double(self.previewView.frame.width)
261
- result["height"] = Double(self.previewView.frame.height)
262
- call.resolve(result)
263
- }
264
-
265
- @objc func getAspectRatio(_ call: CAPPluginCall) {
266
- guard self.isInitialized else {
267
- call.reject("camera not started")
268
- return
269
- }
270
- call.resolve(["aspectRatio": self.aspectRatio ?? "4:3"])
271
- }
272
-
273
- @objc func setGridMode(_ call: CAPPluginCall) {
274
- guard self.isInitialized else {
275
- call.reject("camera not started")
276
- return
277
- }
278
-
279
- guard let gridMode = call.getString("gridMode") else {
280
- call.reject("gridMode parameter is required")
281
- return
282
- }
283
-
284
- self.gridMode = gridMode
285
-
286
- // Update grid overlay
287
- DispatchQueue.main.async {
288
- if gridMode == "none" {
289
- self.cameraController.removeGridOverlay()
290
- } else {
291
- self.cameraController.addGridOverlay(to: self.previewView, gridMode: gridMode)
292
- }
293
- }
294
-
295
- call.resolve()
296
- }
297
-
298
- @objc func getGridMode(_ call: CAPPluginCall) {
299
- guard self.isInitialized else {
300
- call.reject("camera not started")
301
- return
302
- }
303
- call.resolve(["gridMode": self.gridMode])
304
- }
305
-
306
- @objc func appDidBecomeActive() {
307
- if self.isInitialized {
308
- DispatchQueue.main.async {
309
- self.makeWebViewTransparent()
310
- }
311
- }
312
- }
313
-
314
-
315
- @objc func appWillEnterForeground() {
316
- if self.isInitialized {
317
- DispatchQueue.main.async {
318
- self.makeWebViewTransparent()
319
- }
320
- }
321
- }
322
-
323
- struct CameraInfo {
324
- let deviceID: String
325
- let position: String
326
- let pictureSizes: [CGSize]
327
- }
328
-
329
- func getSupportedPictureSizes() -> [CameraInfo] {
330
- var cameraInfos = [CameraInfo]()
331
-
332
- // Discover all available cameras
333
- let deviceTypes: [AVCaptureDevice.DeviceType] = [
334
- .builtInWideAngleCamera,
335
- .builtInUltraWideCamera,
336
- .builtInTelephotoCamera,
337
- .builtInDualCamera,
338
- .builtInDualWideCamera,
339
- .builtInTripleCamera,
340
- .builtInTrueDepthCamera
341
- ]
342
-
343
- let session = AVCaptureDevice.DiscoverySession(
344
- deviceTypes: deviceTypes,
345
- mediaType: .video,
346
- position: .unspecified
347
- )
348
-
349
- let devices = session.devices
350
-
351
- for device in devices {
352
- // Determine the position of the camera
353
- var position = "Unknown"
354
- switch device.position {
355
- case .front:
356
- position = "Front"
357
- case .back:
358
- position = "Back"
359
- case .unspecified:
360
- position = "Unspecified"
361
- @unknown default:
362
- position = "Unknown"
363
- }
364
-
365
- var pictureSizes = [CGSize]()
366
-
367
- // Get supported formats
368
- for format in device.formats {
369
- let description = format.formatDescription
370
- let dimensions = CMVideoFormatDescriptionGetDimensions(description)
371
- let size = CGSize(width: CGFloat(dimensions.width), height: CGFloat(dimensions.height))
372
- if !pictureSizes.contains(size) {
373
- pictureSizes.append(size)
374
- }
375
- }
376
-
377
- // Sort sizes in descending order (largest to smallest)
378
- pictureSizes.sort { $0.width * $0.height > $1.width * $1.height }
379
-
380
- let cameraInfo = CameraInfo(deviceID: device.uniqueID, position: position, pictureSizes: pictureSizes)
381
- cameraInfos.append(cameraInfo)
382
- }
383
-
384
- return cameraInfos
385
- }
386
-
387
- @objc func getSupportedPictureSizes(_ call: CAPPluginCall) {
388
- let cameraInfos = getSupportedPictureSizes()
389
- call.resolve([
390
- "supportedPictureSizes": cameraInfos.map {
391
- return [
392
- "facing": $0.position,
393
- "supportedPictureSizes": $0.pictureSizes.map { size in
394
- return [
395
- "width": String(describing: size.width),
396
- "height": String(describing: size.height)
397
- ]
398
- }
399
- ]
400
- }
401
- ])
402
- }
403
-
404
- @objc func start(_ call: CAPPluginCall) {
405
- if self.isInitializing {
406
- call.reject("camera initialization in progress")
407
- return
408
- }
409
- if self.isInitialized {
410
- call.reject("camera already started")
411
- return
412
- }
413
- self.isInitializing = true
414
-
415
- self.cameraPosition = call.getString("position") ?? "rear"
416
- let deviceId = call.getString("deviceId")
417
- let cameraMode = call.getBool("cameraMode") ?? false
418
- self.highResolutionOutput = call.getBool("enableHighResolution") ?? false
419
- self.cameraController.highResolutionOutput = self.highResolutionOutput
420
-
421
- // Set width - use screen width if not provided or if 0
422
- if let width = call.getInt("width"), width > 0 {
423
- self.width = CGFloat(width)
424
- } else {
425
- self.width = UIScreen.main.bounds.size.width
426
- }
427
-
428
- // Set height - use screen height if not provided or if 0
429
- if let height = call.getInt("height"), height > 0 {
430
- self.height = CGFloat(height)
431
- } else {
432
- self.height = UIScreen.main.bounds.size.height
433
- }
434
-
435
- // Set x position - use exact CSS pixel value from web view, or mark for centering
436
- if let x = call.getInt("x") {
437
- self.posX = CGFloat(x)
438
- } else {
439
- self.posX = -1 // Use -1 to indicate auto-centering
440
- }
441
-
442
- // Set y position - use exact CSS pixel value from web view, or mark for centering
443
- if let y = call.getInt("y") {
444
- self.posY = CGFloat(y)
445
- } else {
446
- self.posY = -1 // Use -1 to indicate auto-centering
447
- }
448
- if call.getInt("paddingBottom") != nil {
449
- self.paddingBottom = CGFloat(call.getInt("paddingBottom")!)
450
- }
451
-
452
- self.rotateWhenOrientationChanged = call.getBool("rotateWhenOrientationChanged") ?? true
453
- self.toBack = call.getBool("toBack") ?? true
454
- self.storeToFile = call.getBool("storeToFile") ?? false
455
- self.enableZoom = call.getBool("enableZoom") ?? false
456
- self.disableAudio = call.getBool("disableAudio") ?? true
457
- self.aspectRatio = call.getString("aspectRatio")
458
- self.gridMode = call.getString("gridMode") ?? "none"
459
- if self.aspectRatio != nil && (call.getInt("width") != nil || call.getInt("height") != nil) {
460
- call.reject("Cannot set both aspectRatio and size (width/height). Use setPreviewSize after start.")
461
- return
462
- }
463
-
464
- print("[CameraPreview] Camera start parameters - aspectRatio: \(String(describing: self.aspectRatio)), gridMode: \(self.gridMode)")
465
- print("[CameraPreview] Screen dimensions: \(UIScreen.main.bounds.size)")
466
- print("[CameraPreview] Final frame dimensions - width: \(self.width), height: \(self.height), x: \(self.posX), y: \(self.posY)")
467
-
468
- AVCaptureDevice.requestAccess(for: .video, completionHandler: { (granted: Bool) in
469
- guard granted else {
470
- call.reject("permission failed")
471
- return
472
- }
473
-
474
- DispatchQueue.main.async {
475
- if self.cameraController.captureSession?.isRunning ?? false {
476
- call.reject("camera already started")
477
- } else {
478
- self.cameraController.prepare(cameraPosition: self.cameraPosition, deviceId: deviceId, disableAudio: self.disableAudio, cameraMode: cameraMode, aspectRatio: self.aspectRatio) {error in
479
- if let error = error {
480
- print(error)
481
- call.reject(error.localizedDescription)
482
- return
483
- }
484
- self.completeStartCamera(call: call)
485
- }
486
- }
487
- }
488
- })
489
- }
490
-
491
- override public func load() {
492
- super.load()
493
- // Initialize camera session in background for faster startup
494
- prepareBackgroundCamera()
495
- }
496
-
497
- private func prepareBackgroundCamera() {
498
- DispatchQueue.global(qos: .background).async {
499
- AVCaptureDevice.requestAccess(for: .video) { granted in
500
- guard granted else { return }
501
-
502
- // Pre-initialize camera controller for faster startup
503
- DispatchQueue.main.async {
504
- self.cameraController.prepareBasicSession()
505
- }
506
- }
507
- }
508
- }
509
-
510
- private func completeStartCamera(call: CAPPluginCall) {
511
- // Create and configure the preview view first
512
- self.updateCameraFrame()
513
-
514
- // Make webview transparent - comprehensive approach
515
- self.makeWebViewTransparent()
516
-
517
- // Add the preview view to the webview itself to use same coordinate system
518
- self.webView?.addSubview(self.previewView)
519
- if self.toBack! {
520
- self.webView?.sendSubviewToBack(self.previewView)
521
- }
522
-
523
- // Display the camera preview on the configured view
524
- try? self.cameraController.displayPreview(on: self.previewView)
525
-
526
- let frontView = self.toBack! ? self.webView : self.previewView
527
- self.cameraController.setupGestures(target: frontView ?? self.previewView, enableZoom: self.enableZoom!)
528
-
529
- // Add grid overlay if enabled
530
- if self.gridMode != "none" {
531
- self.cameraController.addGridOverlay(to: self.previewView, gridMode: self.gridMode)
532
- }
533
-
534
- if self.rotateWhenOrientationChanged == true {
535
- NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.rotated), name: UIDevice.orientationDidChangeNotification, object: nil)
536
- }
537
-
538
- // Add observers for app state changes to maintain transparency
539
- NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
540
- NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.appWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
541
-
542
- self.isInitializing = false
543
- self.isInitialized = true
544
-
545
- var returnedObject = JSObject()
546
- returnedObject["width"] = self.previewView.frame.width as any JSValue
547
- returnedObject["height"] = self.previewView.frame.height as any JSValue
548
- returnedObject["x"] = self.previewView.frame.origin.x as any JSValue
549
- returnedObject["y"] = self.previewView.frame.origin.y as any JSValue
550
- call.resolve(returnedObject)
551
- }
552
-
553
- @objc func flip(_ call: CAPPluginCall) {
554
- guard isInitialized else {
555
- call.reject("Camera not initialized")
556
- return
557
- }
558
-
559
- DispatchQueue.main.async { [weak self] in
560
- guard let self = self else {
561
- call.reject("Camera controller deallocated")
562
- return
563
- }
564
-
565
- // Disable user interaction during flip
566
- self.previewView.isUserInteractionEnabled = false
567
-
568
- // Perform camera switch on background thread
569
- DispatchQueue.global(qos: .userInitiated).async {
570
- var retryCount = 0
571
- let maxRetries = 3
572
-
573
- func attemptFlip() {
574
- do {
575
- try self.cameraController.switchCameras()
576
-
577
- DispatchQueue.main.async {
578
- // Update preview layer frame without animation
579
- CATransaction.begin()
580
- CATransaction.setDisableActions(true)
581
- self.cameraController.previewLayer?.frame = self.previewView.bounds
582
- self.cameraController.previewLayer?.videoGravity = .resizeAspectFill
583
- CATransaction.commit()
584
-
585
- self.previewView.isUserInteractionEnabled = true
586
-
587
-
588
- // Ensure webview remains transparent after flip
589
- self.makeWebViewTransparent()
590
-
591
-
592
- call.resolve()
593
- }
594
- } catch {
595
- retryCount += 1
596
-
597
- if retryCount < maxRetries {
598
- DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 0.5) {
599
- attemptFlip()
600
- }
601
- } else {
602
- DispatchQueue.main.async {
603
- self.previewView.isUserInteractionEnabled = true
604
- print("Failed to flip camera after \(maxRetries) attempts: \(error.localizedDescription)")
605
- call.reject("Failed to flip camera: \(error.localizedDescription)")
606
- }
607
- }
608
- }
609
- }
610
-
611
- attemptFlip()
612
- }
613
- }
614
- }
615
-
616
- @objc func stop(_ call: CAPPluginCall) {
617
- DispatchQueue.main.async {
618
- if self.isInitializing {
619
- call.reject("cannot stop camera while initialization is in progress")
620
- return
621
- }
622
- if !self.isInitialized {
623
- call.reject("camera not initialized")
624
- return
625
- }
626
-
627
- // Always attempt to stop and clean up, regardless of captureSession state
628
- self.cameraController.removeGridOverlay()
629
- if let previewView = self.previewView {
630
- previewView.removeFromSuperview()
631
- self.previewView = nil
632
- }
633
-
634
- self.webView?.isOpaque = true
635
- self.isInitialized = false
636
- self.isInitializing = false
637
- self.cameraController.cleanup()
638
-
639
-
640
- // Remove notification observers
641
- NotificationCenter.default.removeObserver(self)
642
-
643
- call.resolve()
644
- }
645
- }
646
- // Get user's cache directory path
647
- @objc func getTempFilePath() -> URL {
648
- let path = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
649
- let identifier = UUID()
650
- let randomIdentifier = identifier.uuidString.replacingOccurrences(of: "-", with: "")
651
- let finalIdentifier = String(randomIdentifier.prefix(8))
652
- let fileName="cpcp_capture_"+finalIdentifier+".jpg"
653
- let fileUrl=path.appendingPathComponent(fileName)
654
- return fileUrl
655
- }
656
-
657
- @objc func capture(_ call: CAPPluginCall) {
658
- DispatchQueue.main.async {
659
-
660
- let quality = call.getFloat("quality", 85)
661
- let saveToGallery = call.getBool("saveToGallery", false)
662
- let withExifLocation = call.getBool("withExifLocation", false)
663
- let width = call.getInt("width")
664
- let height = call.getInt("height")
665
-
666
- if withExifLocation {
667
- self.locationManager = CLLocationManager()
668
- self.locationManager?.delegate = self
669
- self.locationManager?.requestWhenInUseAuthorization()
670
- self.locationManager?.startUpdatingLocation()
671
- }
672
-
673
- self.cameraController.captureImage(width: width, height: height, quality: quality, gpsLocation: self.currentLocation) { (image, error) in
674
- if let error = error {
675
- call.reject(error.localizedDescription)
676
- return
677
- }
678
-
679
- if saveToGallery {
680
- PHPhotoLibrary.shared().performChanges({
681
- PHAssetChangeRequest.creationRequestForAsset(from: image!)
682
- }, completionHandler: { (success, error) in
683
- if !success {
684
- print("CameraPreview: Error saving image to gallery: \(error?.localizedDescription ?? "Unknown error")")
685
- }
686
- })
687
- }
688
-
689
- guard let imageData = image?.jpegData(compressionQuality: CGFloat(quality / 100.0)) else {
690
- call.reject("Failed to get JPEG data from image")
691
- return
692
- }
693
-
694
-
695
- let exifData = self.getExifData(from: imageData)
696
- let base64Image = imageData.base64EncodedString()
697
-
698
-
699
- var result = JSObject()
700
- result["value"] = base64Image
701
- result["exif"] = exifData
702
- call.resolve(result)
703
- }
704
- }
705
- }
706
-
707
- private func getExifData(from imageData: Data) -> JSObject {
708
- guard let imageSource = CGImageSourceCreateWithData(imageData as CFData, nil),
709
- let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [String: Any],
710
- let exifDict = imageProperties[kCGImagePropertyExifDictionary as String] as? [String: Any] else {
711
- return [:]
712
- }
713
-
714
-
715
- var exifData = JSObject()
716
- for (key, value) in exifDict {
717
- // Convert value to JSValue-compatible type
718
- if let stringValue = value as? String {
719
- exifData[key] = stringValue
720
- } else if let numberValue = value as? NSNumber {
721
- exifData[key] = numberValue
722
- } else if let boolValue = value as? Bool {
723
- exifData[key] = boolValue
724
- } else if let arrayValue = value as? [Any] {
725
- exifData[key] = arrayValue
726
- } else if let dictValue = value as? [String: Any] {
727
- exifData[key] = JSObject(_immutableCocoaDictionary: NSMutableDictionary(dictionary: dictValue))
728
- } else {
729
- // Convert other types to string as fallback
730
- exifData[key] = String(describing: value)
731
- }
732
- }
733
-
734
-
735
- return exifData
736
- }
737
-
738
- @objc func captureSample(_ call: CAPPluginCall) {
739
- DispatchQueue.main.async {
740
- let quality: Int? = call.getInt("quality", 85)
741
-
742
- self.cameraController.captureSample { image, error in
743
- guard let image = image else {
744
- print("Image capture error: \(String(describing: error))")
745
- call.reject("Image capture error: \(String(describing: error))")
746
- return
747
- }
748
-
749
- let imageData: Data?
750
- if self.cameraPosition == "front" {
751
- let flippedImage = image.withHorizontallyFlippedOrientation()
752
- imageData = flippedImage.jpegData(compressionQuality: CGFloat(quality!/100))
753
- } else {
754
- imageData = image.jpegData(compressionQuality: CGFloat(quality!/100))
755
- }
756
-
757
- if self.storeToFile == false {
758
- let imageBase64 = imageData?.base64EncodedString()
759
- call.resolve(["value": imageBase64!])
760
- } else {
761
- do {
762
- let fileUrl = self.getTempFilePath()
763
- try imageData?.write(to: fileUrl)
764
- call.resolve(["value": fileUrl.absoluteString])
765
- } catch {
766
- call.reject("Error writing image to file")
767
- }
768
- }
769
- }
770
- }
771
- }
772
-
773
- @objc func getSupportedFlashModes(_ call: CAPPluginCall) {
774
- do {
775
- let supportedFlashModes = try self.cameraController.getSupportedFlashModes()
776
- call.resolve(["result": supportedFlashModes])
777
- } catch {
778
- call.reject("failed to get supported flash modes")
779
- }
780
- }
781
-
782
- @objc func getHorizontalFov(_ call: CAPPluginCall) {
783
- do {
784
- let horizontalFov = try self.cameraController.getHorizontalFov()
785
- call.resolve(["result": horizontalFov])
786
- } catch {
787
- call.reject("failed to get FOV")
788
- }
789
- }
790
-
791
- @objc func setFlashMode(_ call: CAPPluginCall) {
792
- guard let flashMode = call.getString("flashMode") else {
793
- call.reject("failed to set flash mode. required parameter flashMode is missing")
794
- return
795
- }
796
- do {
797
- var flashModeAsEnum: AVCaptureDevice.FlashMode?
798
- switch flashMode {
799
- case "off":
800
- flashModeAsEnum = AVCaptureDevice.FlashMode.off
801
- case "on":
802
- flashModeAsEnum = AVCaptureDevice.FlashMode.on
803
- case "auto":
804
- flashModeAsEnum = AVCaptureDevice.FlashMode.auto
805
- default: break
806
- }
807
- if flashModeAsEnum != nil {
808
- try self.cameraController.setFlashMode(flashMode: flashModeAsEnum!)
809
- } else if flashMode == "torch" {
810
- try self.cameraController.setTorchMode()
811
- } else {
812
- call.reject("Flash Mode not supported")
813
- return
814
- }
815
- call.resolve()
816
- } catch {
817
- call.reject("failed to set flash mode")
818
- }
819
- }
820
-
821
- @objc func startRecordVideo(_ call: CAPPluginCall) {
822
- DispatchQueue.main.async {
823
- do {
824
- try self.cameraController.captureVideo()
825
- call.resolve()
826
- } catch {
827
- call.reject(error.localizedDescription)
828
- }
829
- }
830
- }
831
-
832
- @objc func stopRecordVideo(_ call: CAPPluginCall) {
833
- DispatchQueue.main.async {
834
- self.cameraController.stopRecording { (fileURL, error) in
835
- guard let fileURL = fileURL else {
836
- print(error ?? "Video capture error")
837
- guard let error = error else {
838
- call.reject("Video capture error")
839
- return
840
- }
841
- call.reject(error.localizedDescription)
842
- return
843
- }
844
-
845
- call.resolve(["videoFilePath": fileURL.absoluteString])
846
- }
847
- }
848
- }
849
-
850
- @objc func isRunning(_ call: CAPPluginCall) {
851
- let isRunning = self.isInitialized && (self.cameraController.captureSession?.isRunning ?? false)
852
- call.resolve(["isRunning": isRunning])
853
- }
854
-
855
- @objc func getAvailableDevices(_ call: CAPPluginCall) {
856
- let deviceTypes: [AVCaptureDevice.DeviceType] = [
857
- .builtInWideAngleCamera,
858
- .builtInUltraWideCamera,
859
- .builtInTelephotoCamera,
860
- .builtInDualCamera,
861
- .builtInDualWideCamera,
862
- .builtInTripleCamera,
863
- .builtInTrueDepthCamera
864
- ]
865
-
866
- let session = AVCaptureDevice.DiscoverySession(
867
- deviceTypes: deviceTypes,
868
- mediaType: .video,
869
- position: .unspecified
870
- )
871
-
872
- var devices: [[String: Any]] = []
873
-
874
- // Collect all devices by position
875
- for device in session.devices {
876
- var lenses: [[String: Any]] = []
877
-
878
-
879
- let constituentDevices = device.isVirtualDevice ? device.constituentDevices : [device]
880
-
881
-
882
- for lensDevice in constituentDevices {
883
- var deviceType: String
884
- switch lensDevice.deviceType {
885
- case .builtInWideAngleCamera: deviceType = "wideAngle"
886
- case .builtInUltraWideCamera: deviceType = "ultraWide"
887
- case .builtInTelephotoCamera: deviceType = "telephoto"
888
- case .builtInDualCamera: deviceType = "dual"
889
- case .builtInDualWideCamera: deviceType = "dualWide"
890
- case .builtInTripleCamera: deviceType = "triple"
891
- case .builtInTrueDepthCamera: deviceType = "trueDepth"
892
- default: deviceType = "unknown"
893
- }
894
-
895
- var baseZoomRatio: Float = 1.0
896
- if lensDevice.deviceType == .builtInUltraWideCamera {
897
- baseZoomRatio = 0.5
898
- } else if lensDevice.deviceType == .builtInTelephotoCamera {
899
- baseZoomRatio = 2.0 // A common value for telephoto lenses
900
- }
901
-
902
-
903
- let lensInfo: [String: Any] = [
904
- "label": lensDevice.localizedName,
905
- "deviceType": deviceType,
906
- "focalLength": 4.25, // Placeholder
907
- "baseZoomRatio": baseZoomRatio,
908
- "minZoom": Float(lensDevice.minAvailableVideoZoomFactor),
909
- "maxZoom": Float(lensDevice.maxAvailableVideoZoomFactor)
910
- ]
911
- lenses.append(lensInfo)
912
- }
913
-
914
-
915
- let deviceData: [String: Any] = [
916
- "deviceId": device.uniqueID,
917
- "label": device.localizedName,
918
- "position": device.position == .front ? "front" : "rear",
919
- "lenses": lenses,
920
- "minZoom": Float(device.minAvailableVideoZoomFactor),
921
- "maxZoom": Float(device.maxAvailableVideoZoomFactor),
922
- "isLogical": device.isVirtualDevice
923
- ]
924
-
925
-
926
- devices.append(deviceData)
927
- }
928
-
929
- call.resolve(["devices": devices])
930
- }
931
-
932
- @objc func getZoom(_ call: CAPPluginCall) {
933
- guard isInitialized else {
934
- call.reject("Camera not initialized")
935
- return
936
- }
937
-
938
- do {
939
- let zoomInfo = try self.cameraController.getZoom()
940
- let lensInfo = try self.cameraController.getCurrentLensInfo()
941
-
942
-
943
- var minZoom = zoomInfo.min
944
- var maxZoom = zoomInfo.max
945
- var currentZoom = zoomInfo.current
946
-
947
- // If using the multi-lens camera, translate the native zoom values for JS
948
- if self.cameraController.isUsingMultiLensVirtualCamera {
949
- minZoom -= 0.5
950
- maxZoom -= 0.5
951
- currentZoom -= 0.5
952
- }
953
-
954
- call.resolve([
955
- "min": minZoom,
956
- "max": maxZoom,
957
- "current": currentZoom,
958
- "lens": [
959
- "focalLength": lensInfo.focalLength,
960
- "deviceType": lensInfo.deviceType,
961
- "baseZoomRatio": lensInfo.baseZoomRatio,
962
- "digitalZoom": Float(currentZoom) / lensInfo.baseZoomRatio
963
- ]
964
- ])
965
- } catch {
966
- call.reject("Failed to get zoom: \(error.localizedDescription)")
967
- }
968
- }
969
-
970
- @objc func setZoom(_ call: CAPPluginCall) {
971
- guard isInitialized else {
972
- call.reject("Camera not initialized")
973
- return
974
- }
975
-
976
- guard var level = call.getFloat("level") else {
977
- call.reject("level parameter is required")
978
- return
979
- }
980
-
981
- // If using the multi-lens camera, translate the JS zoom value for the native layer
982
- if self.cameraController.isUsingMultiLensVirtualCamera {
983
- level += 0.5
984
- }
985
-
986
- let ramp = call.getBool("ramp") ?? true
987
-
988
- do {
989
- try self.cameraController.setZoom(level: CGFloat(level), ramp: ramp)
990
- call.resolve()
991
- } catch {
992
- call.reject("Failed to set zoom: \(error.localizedDescription)")
993
- }
994
- }
995
-
996
- @objc func getFlashMode(_ call: CAPPluginCall) {
997
- guard isInitialized else {
998
- call.reject("Camera not initialized")
999
- return
1000
- }
1001
-
1002
- do {
1003
- let flashMode = try self.cameraController.getFlashMode()
1004
- call.resolve(["flashMode": flashMode])
1005
- } catch {
1006
- call.reject("Failed to get flash mode: \(error.localizedDescription)")
1007
- }
1008
- }
1009
-
1010
- @objc func setDeviceId(_ call: CAPPluginCall) {
1011
- guard isInitialized else {
1012
- call.reject("Camera not initialized")
1013
- return
1014
- }
1015
-
1016
- guard let deviceId = call.getString("deviceId") else {
1017
- call.reject("deviceId parameter is required")
1018
- return
1019
- }
1020
-
1021
- DispatchQueue.main.async { [weak self] in
1022
- guard let self = self else {
1023
- call.reject("Camera controller deallocated")
1024
- return
1025
- }
1026
-
1027
- // Disable user interaction during device swap
1028
- self.previewView.isUserInteractionEnabled = false
1029
-
1030
- DispatchQueue.global(qos: .userInitiated).async {
1031
- do {
1032
- try self.cameraController.swapToDevice(deviceId: deviceId)
1033
-
1034
- DispatchQueue.main.async {
1035
- // Update preview layer frame without animation
1036
- CATransaction.begin()
1037
- CATransaction.setDisableActions(true)
1038
- self.cameraController.previewLayer?.frame = self.previewView.bounds
1039
- self.cameraController.previewLayer?.videoGravity = .resizeAspectFill
1040
- CATransaction.commit()
1041
-
1042
- self.previewView.isUserInteractionEnabled = true
1043
-
1044
-
1045
- // Ensure webview remains transparent after device switch
1046
- self.makeWebViewTransparent()
1047
-
1048
-
1049
- call.resolve()
1050
- }
1051
- } catch {
1052
- DispatchQueue.main.async {
1053
- self.previewView.isUserInteractionEnabled = true
1054
- call.reject("Failed to swap to device \(deviceId): \(error.localizedDescription)")
1055
- }
1056
- }
1057
- }
1058
- }
1059
- }
1060
-
1061
- @objc func getDeviceId(_ call: CAPPluginCall) {
1062
- guard isInitialized else {
1063
- call.reject("Camera not initialized")
1064
- return
1065
- }
1066
-
1067
- do {
1068
- let deviceId = try self.cameraController.getCurrentDeviceId()
1069
- call.resolve(["deviceId": deviceId])
1070
- } catch {
1071
- call.reject("Failed to get device ID: \(error.localizedDescription)")
1072
- }
1073
- }
1074
-
1075
- public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
1076
- self.currentLocation = locations.last
1077
- self.locationManager?.stopUpdatingLocation()
1078
- }
1079
-
1080
- public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
1081
- print("CameraPreview: Failed to get location: \(error.localizedDescription)")
1082
- }
1083
-
1084
- private func updateCameraFrame() {
1085
- guard let width = self.width, var height = self.height, let posX = self.posX, let posY = self.posY else {
1086
- return
1087
- }
1088
-
1089
- let paddingBottom = self.paddingBottom ?? 0
1090
- height -= paddingBottom
1091
-
1092
- // Cache webView dimensions for performance
1093
- let webViewWidth = self.webView?.frame.width ?? UIScreen.main.bounds.width
1094
- let webViewHeight = self.webView?.frame.height ?? UIScreen.main.bounds.height
1095
-
1096
- var finalX = posX
1097
- var finalY = posY
1098
- var finalWidth = width
1099
- var finalHeight = height
1100
-
1101
- // Handle auto-centering when position is -1
1102
- if posX == -1 || posY == -1 {
1103
- finalWidth = webViewWidth
1104
-
1105
- // Calculate height based on aspect ratio or use provided height
1106
- if let aspectRatio = self.aspectRatio {
1107
- let ratioParts = aspectRatio.split(separator: ":").compactMap { Double($0) }
1108
- if ratioParts.count == 2 {
1109
- // For camera, use portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
1110
- let ratio = ratioParts[1] / ratioParts[0]
1111
- finalHeight = finalWidth / CGFloat(ratio)
1112
- }
1113
- }
1114
-
1115
- finalX = posX == -1 ? 0 : posX
1116
-
1117
- if posY == -1 {
1118
- let availableHeight = webViewHeight - paddingBottom
1119
- finalY = finalHeight < availableHeight ? (availableHeight - finalHeight) / 2 : 0
1120
- }
1121
- }
1122
-
1123
- var frame = CGRect(x: finalX, y: finalY, width: finalWidth, height: finalHeight)
1124
-
1125
- // Apply aspect ratio adjustments only if not auto-centering
1126
- if posX != -1 && posY != -1, let aspectRatio = self.aspectRatio {
1127
- let ratioParts = aspectRatio.split(separator: ":").compactMap { Double($0) }
1128
- if ratioParts.count == 2 {
1129
- // For camera, use portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
1130
- let ratio = ratioParts[1] / ratioParts[0]
1131
- let currentRatio = Double(finalWidth) / Double(finalHeight)
1132
-
1133
- if currentRatio > ratio {
1134
- let newWidth = Double(finalHeight) * ratio
1135
- frame.origin.x = finalX + (Double(finalWidth) - newWidth) / 2
1136
- frame.size.width = CGFloat(newWidth)
1137
- } else {
1138
- let newHeight = Double(finalWidth) / ratio
1139
- frame.origin.y = finalY + (Double(finalHeight) - newHeight) / 2
1140
- frame.size.height = CGFloat(newHeight)
1141
- }
1142
- }
1143
- }
1144
-
1145
- // Disable ALL animations for frame updates - we want instant positioning
1146
- CATransaction.begin()
1147
- CATransaction.setDisableActions(true)
1148
-
1149
- // Batch UI updates for better performance
1150
- if self.previewView == nil {
1151
- self.previewView = UIView(frame: frame)
1152
- self.previewView.backgroundColor = UIColor.clear
1153
- } else {
1154
- self.previewView.frame = frame
1155
- }
1156
-
1157
- // Update preview layer frame efficiently
1158
- if let previewLayer = self.cameraController.previewLayer {
1159
- previewLayer.frame = self.previewView.bounds
1160
- }
1161
-
1162
- // Update grid overlay frame if it exists
1163
- if let gridOverlay = self.cameraController.gridOverlayView {
1164
- gridOverlay.frame = self.previewView.bounds
1165
- }
1166
-
1167
- CATransaction.commit()
1168
- }
1169
-
1170
- @objc func getPreviewSize(_ call: CAPPluginCall) {
1171
- guard self.isInitialized else {
1172
- call.reject("camera not started")
1173
- return
1174
- }
1175
- var result = JSObject()
1176
- result["x"] = Double(self.previewView.frame.origin.x)
1177
- result["y"] = Double(self.previewView.frame.origin.y)
1178
- result["width"] = Double(self.previewView.frame.width)
1179
- result["height"] = Double(self.previewView.frame.height)
1180
- call.resolve(result)
1181
- }
1182
-
1183
- @objc func setPreviewSize(_ call: CAPPluginCall) {
1184
- guard self.isInitialized else {
1185
- call.reject("camera not started")
1186
- return
1187
- }
1188
-
1189
- // Only update position if explicitly provided, otherwise keep auto-centering
1190
- if let x = call.getInt("x") {
1191
- self.posX = CGFloat(x)
1192
- }
1193
- if let y = call.getInt("y") {
1194
- self.posY = CGFloat(y)
1195
- }
1196
- if let width = call.getInt("width") { self.width = CGFloat(width) }
1197
- if let height = call.getInt("height") { self.height = CGFloat(height) }
1198
-
1199
- // Direct update without animation for better performance
1200
- self.updateCameraFrame()
1201
- self.makeWebViewTransparent()
1202
-
1203
- // Return the actual preview bounds
1204
- var result = JSObject()
1205
- result["x"] = Double(self.previewView.frame.origin.x)
1206
- result["y"] = Double(self.previewView.frame.origin.y)
1207
- result["width"] = Double(self.previewView.frame.width)
1208
- result["height"] = Double(self.previewView.frame.height)
1209
- call.resolve(result)
1210
- }
1211
- }