@capgo/camera-preview 7.4.0-beta.1 → 7.4.0-beta.10
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.
- package/README.md +195 -31
- package/android/.gradle/8.14.2/checksums/checksums.lock +0 -0
- package/android/.gradle/8.14.2/checksums/md5-checksums.bin +0 -0
- package/android/.gradle/8.14.2/checksums/sha1-checksums.bin +0 -0
- package/android/.gradle/8.14.2/executionHistory/executionHistory.bin +0 -0
- package/android/.gradle/8.14.2/executionHistory/executionHistory.lock +0 -0
- package/android/.gradle/8.14.2/fileHashes/fileHashes.bin +0 -0
- package/android/.gradle/8.14.2/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.14.2/fileHashes/resourceHashesCache.bin +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/file-system.probe +0 -0
- package/android/build.gradle +3 -1
- package/android/src/main/AndroidManifest.xml +5 -3
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java +282 -45
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraXView.java +902 -102
- package/android/src/main/java/com/ahm/capacitor/camera/preview/GridOverlayView.java +82 -0
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraSessionConfiguration.java +19 -5
- package/dist/docs.json +235 -6
- package/dist/esm/definitions.d.ts +119 -3
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +47 -3
- package/dist/esm/web.js +262 -78
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +258 -78
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +258 -78
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapgoCameraPreview/CameraController.swift +245 -28
- package/ios/Sources/CapgoCameraPreview/GridOverlayView.swift +65 -0
- package/ios/Sources/CapgoCameraPreview/Plugin.swift +657 -90
- package/package.json +1 -1
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
import Capacitor
|
|
3
3
|
import AVFoundation
|
|
4
|
+
import Photos
|
|
5
|
+
import CoreImage
|
|
6
|
+
import CoreLocation
|
|
7
|
+
import MobileCoreServices
|
|
8
|
+
|
|
4
9
|
|
|
5
10
|
extension UIWindow {
|
|
6
11
|
static var isLandscape: Bool {
|
|
@@ -32,7 +37,7 @@ extension UIWindow {
|
|
|
32
37
|
* here: https://capacitor.ionicframework.com/docs/plugins/ios
|
|
33
38
|
*/
|
|
34
39
|
@objc(CameraPreview)
|
|
35
|
-
public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
40
|
+
public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelegate {
|
|
36
41
|
public let identifier = "CameraPreviewPlugin"
|
|
37
42
|
public let jsName = "CameraPreview"
|
|
38
43
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
@@ -54,11 +59,18 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
54
59
|
CAPPluginMethod(name: "setZoom", returnType: CAPPluginReturnPromise),
|
|
55
60
|
CAPPluginMethod(name: "getFlashMode", returnType: CAPPluginReturnPromise),
|
|
56
61
|
CAPPluginMethod(name: "setDeviceId", returnType: CAPPluginReturnPromise),
|
|
57
|
-
CAPPluginMethod(name: "getDeviceId", returnType: CAPPluginReturnPromise)
|
|
62
|
+
CAPPluginMethod(name: "getDeviceId", returnType: CAPPluginReturnPromise),
|
|
63
|
+
CAPPluginMethod(name: "setAspectRatio", returnType: CAPPluginReturnPromise),
|
|
64
|
+
CAPPluginMethod(name: "getAspectRatio", returnType: CAPPluginReturnPromise),
|
|
65
|
+
CAPPluginMethod(name: "setGridMode", returnType: CAPPluginReturnPromise),
|
|
66
|
+
CAPPluginMethod(name: "getGridMode", returnType: CAPPluginReturnPromise),
|
|
67
|
+
CAPPluginMethod(name: "getPreviewSize", returnType: CAPPluginReturnPromise),
|
|
68
|
+
CAPPluginMethod(name: "setPreviewSize", returnType: CAPPluginReturnPromise)
|
|
58
69
|
]
|
|
59
70
|
// Camera state tracking
|
|
60
71
|
private var isInitializing: Bool = false
|
|
61
72
|
private var isInitialized: Bool = false
|
|
73
|
+
private var backgroundSession: AVCaptureSession?
|
|
62
74
|
|
|
63
75
|
var previewView: UIView!
|
|
64
76
|
var cameraPosition = String()
|
|
@@ -74,33 +86,44 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
74
86
|
var enableZoom: Bool?
|
|
75
87
|
var highResolutionOutput: Bool = false
|
|
76
88
|
var disableAudio: Bool = false
|
|
89
|
+
var locationManager: CLLocationManager?
|
|
90
|
+
var currentLocation: CLLocation?
|
|
91
|
+
private var aspectRatio: String?
|
|
92
|
+
private var gridMode: String = "none"
|
|
77
93
|
|
|
78
94
|
// MARK: - Transparency Methods
|
|
79
|
-
|
|
95
|
+
|
|
96
|
+
|
|
80
97
|
private func makeWebViewTransparent() {
|
|
81
98
|
guard let webView = self.webView else { return }
|
|
82
|
-
|
|
99
|
+
|
|
100
|
+
|
|
83
101
|
// Define a recursive function to traverse the view hierarchy
|
|
84
102
|
func makeSubviewsTransparent(_ view: UIView) {
|
|
85
103
|
// Set the background color to clear
|
|
86
104
|
view.backgroundColor = .clear
|
|
87
|
-
|
|
105
|
+
|
|
106
|
+
|
|
88
107
|
// Recurse for all subviews
|
|
89
108
|
for subview in view.subviews {
|
|
90
109
|
makeSubviewsTransparent(subview)
|
|
91
110
|
}
|
|
92
111
|
}
|
|
93
|
-
|
|
112
|
+
|
|
113
|
+
|
|
94
114
|
// Set the main webView to be transparent
|
|
95
115
|
webView.isOpaque = false
|
|
96
116
|
webView.backgroundColor = .clear
|
|
97
|
-
|
|
117
|
+
|
|
118
|
+
|
|
98
119
|
// Recursively make all subviews transparent
|
|
99
120
|
makeSubviewsTransparent(webView)
|
|
100
|
-
|
|
121
|
+
|
|
122
|
+
|
|
101
123
|
// Also ensure the webview's container is transparent
|
|
102
124
|
webView.superview?.backgroundColor = .clear
|
|
103
|
-
|
|
125
|
+
|
|
126
|
+
|
|
104
127
|
// Force a layout pass to apply changes
|
|
105
128
|
DispatchQueue.main.async {
|
|
106
129
|
webView.setNeedsLayout()
|
|
@@ -119,14 +142,26 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
119
142
|
let paddingBottom = self.paddingBottom ?? 0
|
|
120
143
|
let height = heightValue - paddingBottom
|
|
121
144
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
145
|
+
// Handle auto-centering during rotation
|
|
146
|
+
if posX == -1 || posY == -1 {
|
|
147
|
+
// Trigger full recalculation for auto-centered views
|
|
148
|
+
self.updateCameraFrame()
|
|
149
|
+
} else {
|
|
150
|
+
// Manual positioning - use original rotation logic with no animation
|
|
151
|
+
CATransaction.begin()
|
|
152
|
+
CATransaction.setDisableActions(true)
|
|
153
|
+
|
|
154
|
+
if UIWindow.isLandscape {
|
|
155
|
+
previewView.frame = CGRect(x: posY, y: posX, width: max(height, width), height: min(height, width))
|
|
156
|
+
self.cameraController.previewLayer?.frame = previewView.bounds
|
|
157
|
+
}
|
|
126
158
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
159
|
+
if UIWindow.isPortrait {
|
|
160
|
+
previewView.frame = CGRect(x: posX, y: posY, width: min(height, width), height: max(height, width))
|
|
161
|
+
self.cameraController.previewLayer?.frame = previewView.bounds
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
CATransaction.commit()
|
|
130
165
|
}
|
|
131
166
|
|
|
132
167
|
if let connection = self.cameraController.fileVideoOutput?.connection(with: .video) {
|
|
@@ -145,13 +180,130 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
145
180
|
}
|
|
146
181
|
|
|
147
182
|
cameraController.updateVideoOrientation()
|
|
148
|
-
|
|
183
|
+
|
|
184
|
+
cameraController.updateVideoOrientation()
|
|
185
|
+
|
|
186
|
+
// Update grid overlay frame if it exists - no animation
|
|
187
|
+
if let gridOverlay = self.cameraController.gridOverlayView {
|
|
188
|
+
CATransaction.begin()
|
|
189
|
+
CATransaction.setDisableActions(true)
|
|
190
|
+
gridOverlay.frame = previewView.bounds
|
|
191
|
+
CATransaction.commit()
|
|
192
|
+
}
|
|
193
|
+
|
|
149
194
|
// Ensure webview remains transparent after rotation
|
|
150
195
|
if self.isInitialized {
|
|
151
196
|
self.makeWebViewTransparent()
|
|
152
197
|
}
|
|
153
198
|
}
|
|
154
|
-
|
|
199
|
+
|
|
200
|
+
@objc func setAspectRatio(_ call: CAPPluginCall) {
|
|
201
|
+
guard self.isInitialized else {
|
|
202
|
+
call.reject("camera not started")
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
guard let newAspectRatio = call.getString("aspectRatio") else {
|
|
207
|
+
call.reject("aspectRatio parameter is required")
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
self.aspectRatio = newAspectRatio
|
|
212
|
+
|
|
213
|
+
// When aspect ratio changes, calculate maximum size possible from current position
|
|
214
|
+
if let posX = self.posX, let posY = self.posY {
|
|
215
|
+
let webViewWidth = self.webView?.frame.width ?? UIScreen.main.bounds.width
|
|
216
|
+
let webViewHeight = self.webView?.frame.height ?? UIScreen.main.bounds.height
|
|
217
|
+
let paddingBottom = self.paddingBottom ?? 0
|
|
218
|
+
|
|
219
|
+
// Calculate available space from current position
|
|
220
|
+
let availableWidth: CGFloat
|
|
221
|
+
let availableHeight: CGFloat
|
|
222
|
+
|
|
223
|
+
if posX == -1 || posY == -1 {
|
|
224
|
+
// Auto-centering mode - use full dimensions
|
|
225
|
+
availableWidth = webViewWidth
|
|
226
|
+
availableHeight = webViewHeight - paddingBottom
|
|
227
|
+
} else {
|
|
228
|
+
// Manual positioning - calculate remaining space
|
|
229
|
+
availableWidth = webViewWidth - posX
|
|
230
|
+
availableHeight = webViewHeight - posY - paddingBottom
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Parse aspect ratio - convert to portrait orientation for camera use
|
|
234
|
+
let ratioParts = newAspectRatio.split(separator: ":").map { Double($0) ?? 1.0 }
|
|
235
|
+
// For camera, we want portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
|
|
236
|
+
let ratio = ratioParts[1] / ratioParts[0]
|
|
237
|
+
|
|
238
|
+
// Calculate maximum size that fits the aspect ratio in available space
|
|
239
|
+
let maxWidthByHeight = availableHeight * CGFloat(ratio)
|
|
240
|
+
let maxHeightByWidth = availableWidth / CGFloat(ratio)
|
|
241
|
+
|
|
242
|
+
if maxWidthByHeight <= availableWidth {
|
|
243
|
+
// Height is the limiting factor
|
|
244
|
+
self.width = maxWidthByHeight
|
|
245
|
+
self.height = availableHeight
|
|
246
|
+
} else {
|
|
247
|
+
// Width is the limiting factor
|
|
248
|
+
self.width = availableWidth
|
|
249
|
+
self.height = maxHeightByWidth
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
print("[CameraPreview] Aspect ratio changed to \(newAspectRatio), new size: \(self.width!)x\(self.height!)")
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
self.updateCameraFrame()
|
|
256
|
+
|
|
257
|
+
// Return the actual preview bounds
|
|
258
|
+
var result = JSObject()
|
|
259
|
+
result["x"] = Double(self.previewView.frame.origin.x)
|
|
260
|
+
result["y"] = Double(self.previewView.frame.origin.y)
|
|
261
|
+
result["width"] = Double(self.previewView.frame.width)
|
|
262
|
+
result["height"] = Double(self.previewView.frame.height)
|
|
263
|
+
call.resolve(result)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
@objc func getAspectRatio(_ call: CAPPluginCall) {
|
|
267
|
+
guard self.isInitialized else {
|
|
268
|
+
call.reject("camera not started")
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
call.resolve(["aspectRatio": self.aspectRatio ?? "4:3"])
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
@objc func setGridMode(_ call: CAPPluginCall) {
|
|
275
|
+
guard self.isInitialized else {
|
|
276
|
+
call.reject("camera not started")
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
guard let gridMode = call.getString("gridMode") else {
|
|
281
|
+
call.reject("gridMode parameter is required")
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
self.gridMode = gridMode
|
|
286
|
+
|
|
287
|
+
// Update grid overlay
|
|
288
|
+
DispatchQueue.main.async {
|
|
289
|
+
if gridMode == "none" {
|
|
290
|
+
self.cameraController.removeGridOverlay()
|
|
291
|
+
} else {
|
|
292
|
+
self.cameraController.addGridOverlay(to: self.previewView, gridMode: gridMode)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
call.resolve()
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
@objc func getGridMode(_ call: CAPPluginCall) {
|
|
300
|
+
guard self.isInitialized else {
|
|
301
|
+
call.reject("camera not started")
|
|
302
|
+
return
|
|
303
|
+
}
|
|
304
|
+
call.resolve(["gridMode": self.gridMode])
|
|
305
|
+
}
|
|
306
|
+
|
|
155
307
|
@objc func appDidBecomeActive() {
|
|
156
308
|
if self.isInitialized {
|
|
157
309
|
DispatchQueue.main.async {
|
|
@@ -159,7 +311,8 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
159
311
|
}
|
|
160
312
|
}
|
|
161
313
|
}
|
|
162
|
-
|
|
314
|
+
|
|
315
|
+
|
|
163
316
|
@objc func appWillEnterForeground() {
|
|
164
317
|
if self.isInitialized {
|
|
165
318
|
DispatchQueue.main.async {
|
|
@@ -266,18 +419,33 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
266
419
|
self.highResolutionOutput = call.getBool("enableHighResolution") ?? false
|
|
267
420
|
self.cameraController.highResolutionOutput = self.highResolutionOutput
|
|
268
421
|
|
|
269
|
-
|
|
270
|
-
|
|
422
|
+
// Set width - use screen width if not provided or if 0
|
|
423
|
+
if let width = call.getInt("width"), width > 0 {
|
|
424
|
+
self.width = CGFloat(width)
|
|
271
425
|
} else {
|
|
272
426
|
self.width = UIScreen.main.bounds.size.width
|
|
273
427
|
}
|
|
274
|
-
|
|
275
|
-
|
|
428
|
+
|
|
429
|
+
// Set height - use screen height if not provided or if 0
|
|
430
|
+
if let height = call.getInt("height"), height > 0 {
|
|
431
|
+
self.height = CGFloat(height)
|
|
276
432
|
} else {
|
|
277
433
|
self.height = UIScreen.main.bounds.size.height
|
|
278
434
|
}
|
|
279
|
-
|
|
280
|
-
|
|
435
|
+
|
|
436
|
+
// Set x position - use exact CSS pixel value from web view, or mark for centering
|
|
437
|
+
if let x = call.getInt("x") {
|
|
438
|
+
self.posX = CGFloat(x)
|
|
439
|
+
} else {
|
|
440
|
+
self.posX = -1 // Use -1 to indicate auto-centering
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Set y position - use exact CSS pixel value from web view, or mark for centering
|
|
444
|
+
if let y = call.getInt("y") {
|
|
445
|
+
self.posY = CGFloat(y)
|
|
446
|
+
} else {
|
|
447
|
+
self.posY = -1 // Use -1 to indicate auto-centering
|
|
448
|
+
}
|
|
281
449
|
if call.getInt("paddingBottom") != nil {
|
|
282
450
|
self.paddingBottom = CGFloat(call.getInt("paddingBottom")!)
|
|
283
451
|
}
|
|
@@ -286,7 +454,17 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
286
454
|
self.toBack = call.getBool("toBack") ?? true
|
|
287
455
|
self.storeToFile = call.getBool("storeToFile") ?? false
|
|
288
456
|
self.enableZoom = call.getBool("enableZoom") ?? false
|
|
289
|
-
self.disableAudio = call.getBool("disableAudio") ??
|
|
457
|
+
self.disableAudio = call.getBool("disableAudio") ?? true
|
|
458
|
+
self.aspectRatio = call.getString("aspectRatio")
|
|
459
|
+
self.gridMode = call.getString("gridMode") ?? "none"
|
|
460
|
+
if self.aspectRatio != nil && (call.getInt("width") != nil || call.getInt("height") != nil) {
|
|
461
|
+
call.reject("Cannot set both aspectRatio and size (width/height). Use setPreviewSize after start.")
|
|
462
|
+
return
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
print("[CameraPreview] Camera start parameters - aspectRatio: \(String(describing: self.aspectRatio)), gridMode: \(self.gridMode)")
|
|
466
|
+
print("[CameraPreview] Screen dimensions: \(UIScreen.main.bounds.size)")
|
|
467
|
+
print("[CameraPreview] Final frame dimensions - width: \(self.width), height: \(self.height), x: \(self.posX), y: \(self.posY)")
|
|
290
468
|
|
|
291
469
|
AVCaptureDevice.requestAccess(for: .video, completionHandler: { (granted: Bool) in
|
|
292
470
|
guard granted else {
|
|
@@ -298,44 +476,79 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
298
476
|
if self.cameraController.captureSession?.isRunning ?? false {
|
|
299
477
|
call.reject("camera already started")
|
|
300
478
|
} else {
|
|
301
|
-
self.cameraController.prepare(cameraPosition: self.cameraPosition, deviceId: deviceId, disableAudio: self.disableAudio, cameraMode: cameraMode) {error in
|
|
479
|
+
self.cameraController.prepare(cameraPosition: self.cameraPosition, deviceId: deviceId, disableAudio: self.disableAudio, cameraMode: cameraMode, aspectRatio: self.aspectRatio) {error in
|
|
302
480
|
if let error = error {
|
|
303
481
|
print(error)
|
|
304
482
|
call.reject(error.localizedDescription)
|
|
305
483
|
return
|
|
306
484
|
}
|
|
307
|
-
|
|
308
|
-
self.previewView = UIView(frame: CGRect(x: self.posX ?? 0, y: self.posY ?? 0, width: self.width!, height: height))
|
|
309
|
-
|
|
310
|
-
// Make webview transparent - comprehensive approach
|
|
311
|
-
self.makeWebViewTransparent()
|
|
312
|
-
|
|
313
|
-
self.webView?.superview?.addSubview(self.previewView)
|
|
314
|
-
if self.toBack! {
|
|
315
|
-
self.webView?.superview?.bringSubviewToFront(self.webView!)
|
|
316
|
-
}
|
|
317
|
-
try? self.cameraController.displayPreview(on: self.previewView)
|
|
318
|
-
|
|
319
|
-
let frontView = self.toBack! ? self.webView : self.previewView
|
|
320
|
-
self.cameraController.setupGestures(target: frontView ?? self.previewView, enableZoom: self.enableZoom!)
|
|
321
|
-
|
|
322
|
-
if self.rotateWhenOrientationChanged == true {
|
|
323
|
-
NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.rotated), name: UIDevice.orientationDidChangeNotification, object: nil)
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Add observers for app state changes to maintain transparency
|
|
327
|
-
NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
|
|
328
|
-
NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.appWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
|
|
329
|
-
|
|
330
|
-
self.isInitializing = false
|
|
331
|
-
self.isInitialized = true
|
|
332
|
-
call.resolve()
|
|
333
|
-
|
|
485
|
+
self.completeStartCamera(call: call)
|
|
334
486
|
}
|
|
335
487
|
}
|
|
336
488
|
}
|
|
337
489
|
})
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
override public func load() {
|
|
493
|
+
super.load()
|
|
494
|
+
// Initialize camera session in background for faster startup
|
|
495
|
+
prepareBackgroundCamera()
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private func prepareBackgroundCamera() {
|
|
499
|
+
DispatchQueue.global(qos: .background).async {
|
|
500
|
+
AVCaptureDevice.requestAccess(for: .video) { granted in
|
|
501
|
+
guard granted else { return }
|
|
502
|
+
|
|
503
|
+
// Pre-initialize camera controller for faster startup
|
|
504
|
+
DispatchQueue.main.async {
|
|
505
|
+
self.cameraController.prepareBasicSession()
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private func completeStartCamera(call: CAPPluginCall) {
|
|
512
|
+
// Create and configure the preview view first
|
|
513
|
+
self.updateCameraFrame()
|
|
514
|
+
|
|
515
|
+
// Make webview transparent - comprehensive approach
|
|
516
|
+
self.makeWebViewTransparent()
|
|
517
|
+
|
|
518
|
+
// Add the preview view to the webview itself to use same coordinate system
|
|
519
|
+
self.webView?.addSubview(self.previewView)
|
|
520
|
+
if self.toBack! {
|
|
521
|
+
self.webView?.sendSubviewToBack(self.previewView)
|
|
522
|
+
}
|
|
338
523
|
|
|
524
|
+
// Display the camera preview on the configured view
|
|
525
|
+
try? self.cameraController.displayPreview(on: self.previewView)
|
|
526
|
+
|
|
527
|
+
let frontView = self.toBack! ? self.webView : self.previewView
|
|
528
|
+
self.cameraController.setupGestures(target: frontView ?? self.previewView, enableZoom: self.enableZoom!)
|
|
529
|
+
|
|
530
|
+
// Add grid overlay if enabled
|
|
531
|
+
if self.gridMode != "none" {
|
|
532
|
+
self.cameraController.addGridOverlay(to: self.previewView, gridMode: self.gridMode)
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if self.rotateWhenOrientationChanged == true {
|
|
536
|
+
NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.rotated), name: UIDevice.orientationDidChangeNotification, object: nil)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Add observers for app state changes to maintain transparency
|
|
540
|
+
NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
|
|
541
|
+
NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.appWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
|
|
542
|
+
|
|
543
|
+
self.isInitializing = false
|
|
544
|
+
self.isInitialized = true
|
|
545
|
+
|
|
546
|
+
var returnedObject = JSObject()
|
|
547
|
+
returnedObject["width"] = self.previewView.frame.width as any JSValue
|
|
548
|
+
returnedObject["height"] = self.previewView.frame.height as any JSValue
|
|
549
|
+
returnedObject["x"] = self.previewView.frame.origin.x as any JSValue
|
|
550
|
+
returnedObject["y"] = self.previewView.frame.origin.y as any JSValue
|
|
551
|
+
call.resolve(returnedObject)
|
|
339
552
|
}
|
|
340
553
|
|
|
341
554
|
@objc func flip(_ call: CAPPluginCall) {
|
|
@@ -363,13 +576,20 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
363
576
|
try self.cameraController.switchCameras()
|
|
364
577
|
|
|
365
578
|
DispatchQueue.main.async {
|
|
579
|
+
// Update preview layer frame without animation
|
|
580
|
+
CATransaction.begin()
|
|
581
|
+
CATransaction.setDisableActions(true)
|
|
366
582
|
self.cameraController.previewLayer?.frame = self.previewView.bounds
|
|
367
583
|
self.cameraController.previewLayer?.videoGravity = .resizeAspectFill
|
|
368
|
-
|
|
584
|
+
CATransaction.commit()
|
|
369
585
|
|
|
586
|
+
self.previewView.isUserInteractionEnabled = true
|
|
587
|
+
|
|
588
|
+
|
|
370
589
|
// Ensure webview remains transparent after flip
|
|
371
590
|
self.makeWebViewTransparent()
|
|
372
|
-
|
|
591
|
+
|
|
592
|
+
|
|
373
593
|
call.resolve()
|
|
374
594
|
}
|
|
375
595
|
} catch {
|
|
@@ -406,6 +626,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
406
626
|
}
|
|
407
627
|
|
|
408
628
|
// Always attempt to stop and clean up, regardless of captureSession state
|
|
629
|
+
self.cameraController.removeGridOverlay()
|
|
409
630
|
if let previewView = self.previewView {
|
|
410
631
|
previewView.removeFromSuperview()
|
|
411
632
|
self.previewView = nil
|
|
@@ -415,7 +636,8 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
415
636
|
self.isInitialized = false
|
|
416
637
|
self.isInitializing = false
|
|
417
638
|
self.cameraController.cleanup()
|
|
418
|
-
|
|
639
|
+
|
|
640
|
+
|
|
419
641
|
// Remove notification observers
|
|
420
642
|
NotificationCenter.default.removeObserver(self)
|
|
421
643
|
|
|
@@ -436,41 +658,181 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
436
658
|
@objc func capture(_ call: CAPPluginCall) {
|
|
437
659
|
DispatchQueue.main.async {
|
|
438
660
|
|
|
439
|
-
let quality
|
|
440
|
-
|
|
441
|
-
|
|
661
|
+
let quality = call.getFloat("quality", 85)
|
|
662
|
+
let saveToGallery = call.getBool("saveToGallery", false)
|
|
663
|
+
let withExifLocation = call.getBool("withExifLocation", false)
|
|
664
|
+
let width = call.getInt("width")
|
|
665
|
+
let height = call.getInt("height")
|
|
666
|
+
|
|
667
|
+
if withExifLocation {
|
|
668
|
+
self.locationManager = CLLocationManager()
|
|
669
|
+
self.locationManager?.delegate = self
|
|
670
|
+
self.locationManager?.requestWhenInUseAuthorization()
|
|
671
|
+
self.locationManager?.startUpdatingLocation()
|
|
672
|
+
}
|
|
442
673
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
guard let error = error else {
|
|
446
|
-
call.reject("Image capture error")
|
|
447
|
-
return
|
|
448
|
-
}
|
|
674
|
+
self.cameraController.captureImage(width: width, height: height, quality: quality, gpsLocation: self.currentLocation) { (image, error) in
|
|
675
|
+
if let error = error {
|
|
449
676
|
call.reject(error.localizedDescription)
|
|
450
677
|
return
|
|
451
678
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
679
|
+
|
|
680
|
+
var gallerySuccess = true
|
|
681
|
+
var galleryError: String?
|
|
682
|
+
|
|
683
|
+
let group = DispatchGroup()
|
|
684
|
+
|
|
685
|
+
group.notify(queue: .main) {
|
|
686
|
+
guard let imageDataWithExif = self.createImageDataWithExif(from: image!, quality: Int(quality), location: withExifLocation ? self.currentLocation : nil) else {
|
|
687
|
+
call.reject("Failed to create image data with EXIF")
|
|
688
|
+
return
|
|
458
689
|
}
|
|
459
690
|
|
|
460
|
-
if
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
call.reject("error writing image to file")
|
|
691
|
+
if saveToGallery {
|
|
692
|
+
group.enter()
|
|
693
|
+
self.saveImageDataToGallery(imageData: imageDataWithExif) { success, error in
|
|
694
|
+
gallerySuccess = success
|
|
695
|
+
if !success {
|
|
696
|
+
galleryError = error?.localizedDescription ?? "Unknown error"
|
|
697
|
+
print("CameraPreview: Error saving image to gallery: \(galleryError!)")
|
|
698
|
+
}
|
|
699
|
+
group.leave()
|
|
470
700
|
}
|
|
701
|
+
|
|
702
|
+
group.notify(queue: .main) {
|
|
703
|
+
let exifData = self.getExifData(from: imageDataWithExif)
|
|
704
|
+
let base64Image = imageDataWithExif.base64EncodedString()
|
|
705
|
+
|
|
706
|
+
var result = JSObject()
|
|
707
|
+
result["value"] = base64Image
|
|
708
|
+
result["exif"] = exifData
|
|
709
|
+
result["gallerySaved"] = gallerySuccess
|
|
710
|
+
if !gallerySuccess, let error = galleryError {
|
|
711
|
+
result["galleryError"] = error
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
call.resolve(result)
|
|
715
|
+
}
|
|
716
|
+
} else {
|
|
717
|
+
let exifData = self.getExifData(from: imageDataWithExif)
|
|
718
|
+
let base64Image = imageDataWithExif.base64EncodedString()
|
|
719
|
+
|
|
720
|
+
var result = JSObject()
|
|
721
|
+
result["value"] = base64Image
|
|
722
|
+
result["exif"] = exifData
|
|
723
|
+
|
|
724
|
+
call.resolve(result)
|
|
471
725
|
}
|
|
472
726
|
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
private func getExifData(from imageData: Data) -> JSObject {
|
|
732
|
+
guard let imageSource = CGImageSourceCreateWithData(imageData as CFData, nil),
|
|
733
|
+
let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [String: Any],
|
|
734
|
+
let exifDict = imageProperties[kCGImagePropertyExifDictionary as String] as? [String: Any] else {
|
|
735
|
+
return [:]
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
var exifData = JSObject()
|
|
740
|
+
for (key, value) in exifDict {
|
|
741
|
+
// Convert value to JSValue-compatible type
|
|
742
|
+
if let stringValue = value as? String {
|
|
743
|
+
exifData[key] = stringValue
|
|
744
|
+
} else if let numberValue = value as? NSNumber {
|
|
745
|
+
exifData[key] = numberValue
|
|
746
|
+
} else if let boolValue = value as? Bool {
|
|
747
|
+
exifData[key] = boolValue
|
|
748
|
+
} else if let arrayValue = value as? [Any] {
|
|
749
|
+
exifData[key] = arrayValue
|
|
750
|
+
} else if let dictValue = value as? [String: Any] {
|
|
751
|
+
exifData[key] = JSObject(_immutableCocoaDictionary: NSMutableDictionary(dictionary: dictValue))
|
|
752
|
+
} else {
|
|
753
|
+
// Convert other types to string as fallback
|
|
754
|
+
exifData[key] = String(describing: value)
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
return exifData
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
private func createImageDataWithExif(from image: UIImage, quality: Int, location: CLLocation?) -> Data? {
|
|
763
|
+
guard let originalImageData = image.jpegData(compressionQuality: CGFloat(Double(quality) / 100.0)) else {
|
|
764
|
+
return nil
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
guard let imageSource = CGImageSourceCreateWithData(originalImageData as CFData, nil),
|
|
768
|
+
let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [String: Any],
|
|
769
|
+
let cgImage = image.cgImage else {
|
|
770
|
+
return originalImageData
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
let mutableData = NSMutableData()
|
|
774
|
+
guard let destination = CGImageDestinationCreateWithData(mutableData, kUTTypeJPEG, 1, nil) else {
|
|
775
|
+
return originalImageData
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
var finalProperties = imageProperties
|
|
779
|
+
|
|
780
|
+
// Add GPS location if available
|
|
781
|
+
if let location = location {
|
|
782
|
+
let formatter = DateFormatter()
|
|
783
|
+
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
|
|
784
|
+
formatter.timeZone = TimeZone(abbreviation: "UTC")
|
|
785
|
+
|
|
786
|
+
let gpsDict: [String: Any] = [
|
|
787
|
+
kCGImagePropertyGPSLatitude as String: abs(location.coordinate.latitude),
|
|
788
|
+
kCGImagePropertyGPSLatitudeRef as String: location.coordinate.latitude >= 0 ? "N" : "S",
|
|
789
|
+
kCGImagePropertyGPSLongitude as String: abs(location.coordinate.longitude),
|
|
790
|
+
kCGImagePropertyGPSLongitudeRef as String: location.coordinate.longitude >= 0 ? "E" : "W",
|
|
791
|
+
kCGImagePropertyGPSTimeStamp as String: formatter.string(from: location.timestamp),
|
|
792
|
+
kCGImagePropertyGPSAltitude as String: location.altitude,
|
|
793
|
+
kCGImagePropertyGPSAltitudeRef as String: location.altitude >= 0 ? 0 : 1
|
|
794
|
+
]
|
|
795
|
+
|
|
796
|
+
finalProperties[kCGImagePropertyGPSDictionary as String] = gpsDict
|
|
473
797
|
}
|
|
798
|
+
|
|
799
|
+
// Add lens information
|
|
800
|
+
do {
|
|
801
|
+
let currentZoom = try self.cameraController.getZoom()
|
|
802
|
+
let lensInfo = try self.cameraController.getCurrentLensInfo()
|
|
803
|
+
|
|
804
|
+
// Create or update EXIF dictionary
|
|
805
|
+
var exifDict = finalProperties[kCGImagePropertyExifDictionary as String] as? [String: Any] ?? [:]
|
|
806
|
+
|
|
807
|
+
// Add focal length (in mm)
|
|
808
|
+
exifDict[kCGImagePropertyExifFocalLength as String] = lensInfo.focalLength
|
|
809
|
+
|
|
810
|
+
// Add digital zoom ratio
|
|
811
|
+
let digitalZoom = Float(currentZoom.current) / lensInfo.baseZoomRatio
|
|
812
|
+
exifDict[kCGImagePropertyExifDigitalZoomRatio as String] = digitalZoom
|
|
813
|
+
|
|
814
|
+
// Add lens model info
|
|
815
|
+
exifDict[kCGImagePropertyExifLensModel as String] = lensInfo.deviceType
|
|
816
|
+
|
|
817
|
+
finalProperties[kCGImagePropertyExifDictionary as String] = exifDict
|
|
818
|
+
|
|
819
|
+
// Create or update TIFF dictionary for device info
|
|
820
|
+
var tiffDict = finalProperties[kCGImagePropertyTIFFDictionary as String] as? [String: Any] ?? [:]
|
|
821
|
+
tiffDict[kCGImagePropertyTIFFMake as String] = "Apple"
|
|
822
|
+
tiffDict[kCGImagePropertyTIFFModel as String] = UIDevice.current.model
|
|
823
|
+
finalProperties[kCGImagePropertyTIFFDictionary as String] = tiffDict
|
|
824
|
+
|
|
825
|
+
} catch {
|
|
826
|
+
print("CameraPreview: Failed to get lens information: \(error)")
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
CGImageDestinationAddImage(destination, cgImage, finalProperties as CFDictionary)
|
|
830
|
+
|
|
831
|
+
if CGImageDestinationFinalize(destination) {
|
|
832
|
+
return mutableData as Data
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
return originalImageData
|
|
474
836
|
}
|
|
475
837
|
|
|
476
838
|
@objc func captureSample(_ call: CAPPluginCall) {
|
|
@@ -612,9 +974,11 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
612
974
|
// Collect all devices by position
|
|
613
975
|
for device in session.devices {
|
|
614
976
|
var lenses: [[String: Any]] = []
|
|
615
|
-
|
|
977
|
+
|
|
978
|
+
|
|
616
979
|
let constituentDevices = device.isVirtualDevice ? device.constituentDevices : [device]
|
|
617
|
-
|
|
980
|
+
|
|
981
|
+
|
|
618
982
|
for lensDevice in constituentDevices {
|
|
619
983
|
var deviceType: String
|
|
620
984
|
switch lensDevice.deviceType {
|
|
@@ -634,7 +998,8 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
634
998
|
} else if lensDevice.deviceType == .builtInTelephotoCamera {
|
|
635
999
|
baseZoomRatio = 2.0 // A common value for telephoto lenses
|
|
636
1000
|
}
|
|
637
|
-
|
|
1001
|
+
|
|
1002
|
+
|
|
638
1003
|
let lensInfo: [String: Any] = [
|
|
639
1004
|
"label": lensDevice.localizedName,
|
|
640
1005
|
"deviceType": deviceType,
|
|
@@ -645,7 +1010,8 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
645
1010
|
]
|
|
646
1011
|
lenses.append(lensInfo)
|
|
647
1012
|
}
|
|
648
|
-
|
|
1013
|
+
|
|
1014
|
+
|
|
649
1015
|
let deviceData: [String: Any] = [
|
|
650
1016
|
"deviceId": device.uniqueID,
|
|
651
1017
|
"label": device.localizedName,
|
|
@@ -655,7 +1021,8 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
655
1021
|
"maxZoom": Float(device.maxAvailableVideoZoomFactor),
|
|
656
1022
|
"isLogical": device.isVirtualDevice
|
|
657
1023
|
]
|
|
658
|
-
|
|
1024
|
+
|
|
1025
|
+
|
|
659
1026
|
devices.append(deviceData)
|
|
660
1027
|
}
|
|
661
1028
|
|
|
@@ -671,7 +1038,8 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
671
1038
|
do {
|
|
672
1039
|
let zoomInfo = try self.cameraController.getZoom()
|
|
673
1040
|
let lensInfo = try self.cameraController.getCurrentLensInfo()
|
|
674
|
-
|
|
1041
|
+
|
|
1042
|
+
|
|
675
1043
|
var minZoom = zoomInfo.min
|
|
676
1044
|
var maxZoom = zoomInfo.max
|
|
677
1045
|
var currentZoom = zoomInfo.current
|
|
@@ -764,13 +1132,20 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
764
1132
|
try self.cameraController.swapToDevice(deviceId: deviceId)
|
|
765
1133
|
|
|
766
1134
|
DispatchQueue.main.async {
|
|
1135
|
+
// Update preview layer frame without animation
|
|
1136
|
+
CATransaction.begin()
|
|
1137
|
+
CATransaction.setDisableActions(true)
|
|
767
1138
|
self.cameraController.previewLayer?.frame = self.previewView.bounds
|
|
768
1139
|
self.cameraController.previewLayer?.videoGravity = .resizeAspectFill
|
|
769
|
-
|
|
1140
|
+
CATransaction.commit()
|
|
770
1141
|
|
|
1142
|
+
self.previewView.isUserInteractionEnabled = true
|
|
1143
|
+
|
|
1144
|
+
|
|
771
1145
|
// Ensure webview remains transparent after device switch
|
|
772
1146
|
self.makeWebViewTransparent()
|
|
773
|
-
|
|
1147
|
+
|
|
1148
|
+
|
|
774
1149
|
call.resolve()
|
|
775
1150
|
}
|
|
776
1151
|
} catch {
|
|
@@ -797,6 +1172,198 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
797
1172
|
}
|
|
798
1173
|
}
|
|
799
1174
|
|
|
1175
|
+
public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
|
1176
|
+
self.currentLocation = locations.last
|
|
1177
|
+
self.locationManager?.stopUpdatingLocation()
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
|
1181
|
+
print("CameraPreview: Failed to get location: \(error.localizedDescription)")
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
private func saveImageDataToGallery(imageData: Data, completion: @escaping (Bool, Error?) -> Void) {
|
|
1185
|
+
// Check if NSPhotoLibraryUsageDescription is present in Info.plist
|
|
1186
|
+
guard Bundle.main.object(forInfoDictionaryKey: "NSPhotoLibraryUsageDescription") != nil else {
|
|
1187
|
+
let error = NSError(domain: "CameraPreview", code: 2, userInfo: [
|
|
1188
|
+
NSLocalizedDescriptionKey: "NSPhotoLibraryUsageDescription key missing from Info.plist. Add this key with a description of how your app uses photo library access."
|
|
1189
|
+
])
|
|
1190
|
+
completion(false, error)
|
|
1191
|
+
return
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
let status = PHPhotoLibrary.authorizationStatus()
|
|
1195
|
+
|
|
1196
|
+
switch status {
|
|
1197
|
+
case .authorized:
|
|
1198
|
+
performSaveDataToGallery(imageData: imageData, completion: completion)
|
|
1199
|
+
case .notDetermined:
|
|
1200
|
+
PHPhotoLibrary.requestAuthorization { newStatus in
|
|
1201
|
+
DispatchQueue.main.async {
|
|
1202
|
+
if newStatus == .authorized {
|
|
1203
|
+
self.performSaveDataToGallery(imageData: imageData, completion: completion)
|
|
1204
|
+
} else {
|
|
1205
|
+
completion(false, NSError(domain: "CameraPreview", code: 1, userInfo: [NSLocalizedDescriptionKey: "Photo library access denied"]))
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
case .denied, .restricted:
|
|
1210
|
+
completion(false, NSError(domain: "CameraPreview", code: 1, userInfo: [NSLocalizedDescriptionKey: "Photo library access denied"]))
|
|
1211
|
+
case .limited:
|
|
1212
|
+
performSaveDataToGallery(imageData: imageData, completion: completion)
|
|
1213
|
+
@unknown default:
|
|
1214
|
+
completion(false, NSError(domain: "CameraPreview", code: 1, userInfo: [NSLocalizedDescriptionKey: "Unknown photo library authorization status"]))
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
private func performSaveDataToGallery(imageData: Data, completion: @escaping (Bool, Error?) -> Void) {
|
|
1219
|
+
// Create a temporary file to write the JPEG data with EXIF
|
|
1220
|
+
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".jpg")
|
|
1221
|
+
|
|
1222
|
+
do {
|
|
1223
|
+
try imageData.write(to: tempURL)
|
|
1224
|
+
|
|
1225
|
+
PHPhotoLibrary.shared().performChanges({
|
|
1226
|
+
PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: tempURL)
|
|
1227
|
+
}, completionHandler: { success, error in
|
|
1228
|
+
// Clean up temporary file
|
|
1229
|
+
try? FileManager.default.removeItem(at: tempURL)
|
|
1230
|
+
|
|
1231
|
+
DispatchQueue.main.async {
|
|
1232
|
+
completion(success, error)
|
|
1233
|
+
}
|
|
1234
|
+
})
|
|
1235
|
+
} catch {
|
|
1236
|
+
DispatchQueue.main.async {
|
|
1237
|
+
completion(false, error)
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
private func updateCameraFrame() {
|
|
1243
|
+
guard let width = self.width, var height = self.height, let posX = self.posX, let posY = self.posY else {
|
|
1244
|
+
return
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
let paddingBottom = self.paddingBottom ?? 0
|
|
1248
|
+
height -= paddingBottom
|
|
1249
|
+
|
|
1250
|
+
// Cache webView dimensions for performance
|
|
1251
|
+
let webViewWidth = self.webView?.frame.width ?? UIScreen.main.bounds.width
|
|
1252
|
+
let webViewHeight = self.webView?.frame.height ?? UIScreen.main.bounds.height
|
|
800
1253
|
|
|
1254
|
+
var finalX = posX
|
|
1255
|
+
var finalY = posY
|
|
1256
|
+
var finalWidth = width
|
|
1257
|
+
var finalHeight = height
|
|
801
1258
|
|
|
1259
|
+
// Handle auto-centering when position is -1
|
|
1260
|
+
if posX == -1 || posY == -1 {
|
|
1261
|
+
finalWidth = webViewWidth
|
|
1262
|
+
|
|
1263
|
+
// Calculate height based on aspect ratio or use provided height
|
|
1264
|
+
if let aspectRatio = self.aspectRatio {
|
|
1265
|
+
let ratioParts = aspectRatio.split(separator: ":").compactMap { Double($0) }
|
|
1266
|
+
if ratioParts.count == 2 {
|
|
1267
|
+
// For camera, use portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
|
|
1268
|
+
let ratio = ratioParts[1] / ratioParts[0]
|
|
1269
|
+
finalHeight = finalWidth / CGFloat(ratio)
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
finalX = posX == -1 ? 0 : posX
|
|
1274
|
+
|
|
1275
|
+
if posY == -1 {
|
|
1276
|
+
let availableHeight = webViewHeight - paddingBottom
|
|
1277
|
+
finalY = finalHeight < availableHeight ? (availableHeight - finalHeight) / 2 : 0
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
var frame = CGRect(x: finalX, y: finalY, width: finalWidth, height: finalHeight)
|
|
1282
|
+
|
|
1283
|
+
// Apply aspect ratio adjustments only if not auto-centering
|
|
1284
|
+
if posX != -1 && posY != -1, let aspectRatio = self.aspectRatio {
|
|
1285
|
+
let ratioParts = aspectRatio.split(separator: ":").compactMap { Double($0) }
|
|
1286
|
+
if ratioParts.count == 2 {
|
|
1287
|
+
// For camera, use portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
|
|
1288
|
+
let ratio = ratioParts[1] / ratioParts[0]
|
|
1289
|
+
let currentRatio = Double(finalWidth) / Double(finalHeight)
|
|
1290
|
+
|
|
1291
|
+
if currentRatio > ratio {
|
|
1292
|
+
let newWidth = Double(finalHeight) * ratio
|
|
1293
|
+
frame.origin.x = finalX + (Double(finalWidth) - newWidth) / 2
|
|
1294
|
+
frame.size.width = CGFloat(newWidth)
|
|
1295
|
+
} else {
|
|
1296
|
+
let newHeight = Double(finalWidth) / ratio
|
|
1297
|
+
frame.origin.y = finalY + (Double(finalHeight) - newHeight) / 2
|
|
1298
|
+
frame.size.height = CGFloat(newHeight)
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// Disable ALL animations for frame updates - we want instant positioning
|
|
1304
|
+
CATransaction.begin()
|
|
1305
|
+
CATransaction.setDisableActions(true)
|
|
1306
|
+
|
|
1307
|
+
// Batch UI updates for better performance
|
|
1308
|
+
if self.previewView == nil {
|
|
1309
|
+
self.previewView = UIView(frame: frame)
|
|
1310
|
+
self.previewView.backgroundColor = UIColor.clear
|
|
1311
|
+
} else {
|
|
1312
|
+
self.previewView.frame = frame
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// Update preview layer frame efficiently
|
|
1316
|
+
if let previewLayer = self.cameraController.previewLayer {
|
|
1317
|
+
previewLayer.frame = self.previewView.bounds
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// Update grid overlay frame if it exists
|
|
1321
|
+
if let gridOverlay = self.cameraController.gridOverlayView {
|
|
1322
|
+
gridOverlay.frame = self.previewView.bounds
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
CATransaction.commit()
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
@objc func getPreviewSize(_ call: CAPPluginCall) {
|
|
1329
|
+
guard self.isInitialized else {
|
|
1330
|
+
call.reject("camera not started")
|
|
1331
|
+
return
|
|
1332
|
+
}
|
|
1333
|
+
var result = JSObject()
|
|
1334
|
+
result["x"] = Double(self.previewView.frame.origin.x)
|
|
1335
|
+
result["y"] = Double(self.previewView.frame.origin.y)
|
|
1336
|
+
result["width"] = Double(self.previewView.frame.width)
|
|
1337
|
+
result["height"] = Double(self.previewView.frame.height)
|
|
1338
|
+
call.resolve(result)
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
@objc func setPreviewSize(_ call: CAPPluginCall) {
|
|
1342
|
+
guard self.isInitialized else {
|
|
1343
|
+
call.reject("camera not started")
|
|
1344
|
+
return
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// Only update position if explicitly provided, otherwise keep auto-centering
|
|
1348
|
+
if let x = call.getInt("x") {
|
|
1349
|
+
self.posX = CGFloat(x)
|
|
1350
|
+
}
|
|
1351
|
+
if let y = call.getInt("y") {
|
|
1352
|
+
self.posY = CGFloat(y)
|
|
1353
|
+
}
|
|
1354
|
+
if let width = call.getInt("width") { self.width = CGFloat(width) }
|
|
1355
|
+
if let height = call.getInt("height") { self.height = CGFloat(height) }
|
|
1356
|
+
|
|
1357
|
+
// Direct update without animation for better performance
|
|
1358
|
+
self.updateCameraFrame()
|
|
1359
|
+
self.makeWebViewTransparent()
|
|
1360
|
+
|
|
1361
|
+
// Return the actual preview bounds
|
|
1362
|
+
var result = JSObject()
|
|
1363
|
+
result["x"] = Double(self.previewView.frame.origin.x)
|
|
1364
|
+
result["y"] = Double(self.previewView.frame.origin.y)
|
|
1365
|
+
result["width"] = Double(self.previewView.frame.width)
|
|
1366
|
+
result["height"] = Double(self.previewView.frame.height)
|
|
1367
|
+
call.resolve(result)
|
|
1368
|
+
}
|
|
802
1369
|
}
|