@capgo/camera-preview 7.4.0-beta.2 → 7.4.0-beta.20
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 +212 -35
- 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 +1 -4
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java +731 -83
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraXView.java +2813 -805
- package/android/src/main/java/com/ahm/capacitor/camera/preview/GridOverlayView.java +112 -0
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraDevice.java +55 -46
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraLens.java +61 -52
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraSessionConfiguration.java +161 -59
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/LensInfo.java +29 -23
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/ZoomFactors.java +24 -23
- package/dist/docs.json +292 -29
- package/dist/esm/definitions.d.ts +148 -13
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +52 -3
- package/dist/esm/web.js +555 -97
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +553 -97
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +553 -97
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapgoCameraPreview/CameraController.swift +888 -214
- package/ios/Sources/CapgoCameraPreview/GridOverlayView.swift +65 -0
- package/ios/Sources/CapgoCameraPreview/Plugin.swift +967 -250
- package/package.json +2 -2
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import Foundation
|
|
2
|
-
import Capacitor
|
|
3
2
|
import AVFoundation
|
|
4
3
|
import Photos
|
|
4
|
+
import Capacitor
|
|
5
5
|
import CoreImage
|
|
6
|
+
import CoreLocation
|
|
7
|
+
import MobileCoreServices
|
|
6
8
|
|
|
7
9
|
extension UIWindow {
|
|
8
10
|
static var isLandscape: Bool {
|
|
@@ -34,7 +36,7 @@ extension UIWindow {
|
|
|
34
36
|
* here: https://capacitor.ionicframework.com/docs/plugins/ios
|
|
35
37
|
*/
|
|
36
38
|
@objc(CameraPreview)
|
|
37
|
-
public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
39
|
+
public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelegate {
|
|
38
40
|
public let identifier = "CameraPreviewPlugin"
|
|
39
41
|
public let jsName = "CameraPreview"
|
|
40
42
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
@@ -56,11 +58,19 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
56
58
|
CAPPluginMethod(name: "setZoom", returnType: CAPPluginReturnPromise),
|
|
57
59
|
CAPPluginMethod(name: "getFlashMode", returnType: CAPPluginReturnPromise),
|
|
58
60
|
CAPPluginMethod(name: "setDeviceId", returnType: CAPPluginReturnPromise),
|
|
59
|
-
CAPPluginMethod(name: "getDeviceId", 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
|
+
CAPPluginMethod(name: "setFocus", returnType: CAPPluginReturnPromise)
|
|
60
69
|
]
|
|
61
70
|
// Camera state tracking
|
|
62
71
|
private var isInitializing: Bool = false
|
|
63
72
|
private var isInitialized: Bool = false
|
|
73
|
+
private var backgroundSession: AVCaptureSession?
|
|
64
74
|
|
|
65
75
|
var previewView: UIView!
|
|
66
76
|
var cameraPosition = String()
|
|
@@ -74,37 +84,42 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
74
84
|
var toBack: Bool?
|
|
75
85
|
var storeToFile: Bool?
|
|
76
86
|
var enableZoom: Bool?
|
|
77
|
-
var highResolutionOutput: Bool = false
|
|
78
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
|
+
private var permissionCallID: String?
|
|
93
|
+
private var waitingForLocation: Bool = false
|
|
79
94
|
|
|
80
95
|
// MARK: - Transparency Methods
|
|
81
|
-
|
|
96
|
+
|
|
82
97
|
private func makeWebViewTransparent() {
|
|
83
98
|
guard let webView = self.webView else { return }
|
|
84
|
-
|
|
85
|
-
// Define a recursive function to traverse the view hierarchy
|
|
86
|
-
func makeSubviewsTransparent(_ view: UIView) {
|
|
87
|
-
// Set the background color to clear
|
|
88
|
-
view.backgroundColor = .clear
|
|
89
|
-
|
|
90
|
-
// Recurse for all subviews
|
|
91
|
-
for subview in view.subviews {
|
|
92
|
-
makeSubviewsTransparent(subview)
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Set the main webView to be transparent
|
|
97
|
-
webView.isOpaque = false
|
|
98
|
-
webView.backgroundColor = .clear
|
|
99
|
-
|
|
100
|
-
// Recursively make all subviews transparent
|
|
101
|
-
makeSubviewsTransparent(webView)
|
|
102
|
-
|
|
103
|
-
// Also ensure the webview's container is transparent
|
|
104
|
-
webView.superview?.backgroundColor = .clear
|
|
105
|
-
|
|
106
|
-
// Force a layout pass to apply changes
|
|
99
|
+
|
|
107
100
|
DispatchQueue.main.async {
|
|
101
|
+
// Define a recursive function to traverse the view hierarchy
|
|
102
|
+
func makeSubviewsTransparent(_ view: UIView) {
|
|
103
|
+
// Set the background color to clear
|
|
104
|
+
view.backgroundColor = .clear
|
|
105
|
+
|
|
106
|
+
// Recurse for all subviews
|
|
107
|
+
for subview in view.subviews {
|
|
108
|
+
makeSubviewsTransparent(subview)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Set the main webView to be transparent
|
|
113
|
+
webView.isOpaque = false
|
|
114
|
+
webView.backgroundColor = .clear
|
|
115
|
+
|
|
116
|
+
// Recursively make all subviews transparent
|
|
117
|
+
makeSubviewsTransparent(webView)
|
|
118
|
+
|
|
119
|
+
// Also ensure the webview's container is transparent
|
|
120
|
+
webView.superview?.backgroundColor = .clear
|
|
121
|
+
|
|
122
|
+
// Force a layout pass to apply changes
|
|
108
123
|
webView.setNeedsLayout()
|
|
109
124
|
webView.layoutIfNeeded()
|
|
110
125
|
}
|
|
@@ -121,15 +136,9 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
121
136
|
let paddingBottom = self.paddingBottom ?? 0
|
|
122
137
|
let height = heightValue - paddingBottom
|
|
123
138
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if UIWindow.isPortrait {
|
|
130
|
-
previewView.frame = CGRect(x: posX, y: posY, width: min(height, width), height: max(height, width))
|
|
131
|
-
self.cameraController.previewLayer?.frame = previewView.frame
|
|
132
|
-
}
|
|
139
|
+
// Handle auto-centering during rotation
|
|
140
|
+
// Always use the factorized method for consistent positioning
|
|
141
|
+
self.updateCameraFrame()
|
|
133
142
|
|
|
134
143
|
if let connection = self.cameraController.fileVideoOutput?.connection(with: .video) {
|
|
135
144
|
switch UIDevice.current.orientation {
|
|
@@ -147,13 +156,135 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
147
156
|
}
|
|
148
157
|
|
|
149
158
|
cameraController.updateVideoOrientation()
|
|
150
|
-
|
|
159
|
+
|
|
160
|
+
cameraController.updateVideoOrientation()
|
|
161
|
+
|
|
162
|
+
// Update grid overlay frame if it exists - no animation
|
|
163
|
+
if let gridOverlay = self.cameraController.gridOverlayView {
|
|
164
|
+
CATransaction.begin()
|
|
165
|
+
CATransaction.setDisableActions(true)
|
|
166
|
+
gridOverlay.frame = previewView.bounds
|
|
167
|
+
CATransaction.commit()
|
|
168
|
+
}
|
|
169
|
+
|
|
151
170
|
// Ensure webview remains transparent after rotation
|
|
152
171
|
if self.isInitialized {
|
|
153
172
|
self.makeWebViewTransparent()
|
|
154
173
|
}
|
|
155
174
|
}
|
|
156
|
-
|
|
175
|
+
|
|
176
|
+
@objc func setAspectRatio(_ call: CAPPluginCall) {
|
|
177
|
+
guard self.isInitialized else {
|
|
178
|
+
call.reject("camera not started")
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
guard let newAspectRatio = call.getString("aspectRatio") else {
|
|
183
|
+
call.reject("aspectRatio parameter is required")
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
self.aspectRatio = newAspectRatio
|
|
188
|
+
|
|
189
|
+
DispatchQueue.main.async {
|
|
190
|
+
// When aspect ratio changes, always auto-center the view
|
|
191
|
+
// This ensures consistent behavior where changing aspect ratio recenters the view
|
|
192
|
+
self.posX = -1
|
|
193
|
+
self.posY = -1
|
|
194
|
+
|
|
195
|
+
// Calculate maximum size based on aspect ratio
|
|
196
|
+
let webViewWidth = self.webView?.frame.width ?? UIScreen.main.bounds.width
|
|
197
|
+
let webViewHeight = self.webView?.frame.height ?? UIScreen.main.bounds.height
|
|
198
|
+
let paddingBottom = self.paddingBottom ?? 0
|
|
199
|
+
|
|
200
|
+
// Calculate available space
|
|
201
|
+
let availableWidth: CGFloat
|
|
202
|
+
let availableHeight: CGFloat
|
|
203
|
+
|
|
204
|
+
if self.posX == -1 || self.posY == -1 {
|
|
205
|
+
// Auto-centering mode - use full dimensions
|
|
206
|
+
availableWidth = webViewWidth
|
|
207
|
+
availableHeight = webViewHeight - paddingBottom
|
|
208
|
+
} else {
|
|
209
|
+
// Manual positioning - calculate remaining space
|
|
210
|
+
availableWidth = webViewWidth - self.posX!
|
|
211
|
+
availableHeight = webViewHeight - self.posY! - paddingBottom
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Parse aspect ratio - convert to portrait orientation for camera use
|
|
215
|
+
let ratioParts = newAspectRatio.split(separator: ":").map { Double($0) ?? 1.0 }
|
|
216
|
+
// For camera, we want portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
|
|
217
|
+
let ratio = ratioParts[1] / ratioParts[0]
|
|
218
|
+
|
|
219
|
+
// Calculate maximum size that fits the aspect ratio in available space
|
|
220
|
+
let maxWidthByHeight = availableHeight * CGFloat(ratio)
|
|
221
|
+
let maxHeightByWidth = availableWidth / CGFloat(ratio)
|
|
222
|
+
|
|
223
|
+
if maxWidthByHeight <= availableWidth {
|
|
224
|
+
// Height is the limiting factor
|
|
225
|
+
self.width = maxWidthByHeight
|
|
226
|
+
self.height = availableHeight
|
|
227
|
+
} else {
|
|
228
|
+
// Width is the limiting factor
|
|
229
|
+
self.width = availableWidth
|
|
230
|
+
self.height = maxHeightByWidth
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
print("[CameraPreview] Aspect ratio changed to \(newAspectRatio), new size: \(self.width!)x\(self.height!)")
|
|
234
|
+
|
|
235
|
+
self.updateCameraFrame()
|
|
236
|
+
|
|
237
|
+
// Return the actual preview bounds
|
|
238
|
+
var result = JSObject()
|
|
239
|
+
result["x"] = Double(self.previewView.frame.origin.x)
|
|
240
|
+
result["y"] = Double(self.previewView.frame.origin.y)
|
|
241
|
+
result["width"] = Double(self.previewView.frame.width)
|
|
242
|
+
result["height"] = Double(self.previewView.frame.height)
|
|
243
|
+
call.resolve(result)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
@objc func getAspectRatio(_ call: CAPPluginCall) {
|
|
248
|
+
guard self.isInitialized else {
|
|
249
|
+
call.reject("camera not started")
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
call.resolve(["aspectRatio": self.aspectRatio ?? "4:3"])
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
@objc func setGridMode(_ call: CAPPluginCall) {
|
|
256
|
+
guard self.isInitialized else {
|
|
257
|
+
call.reject("camera not started")
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
guard let gridMode = call.getString("gridMode") else {
|
|
262
|
+
call.reject("gridMode parameter is required")
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
self.gridMode = gridMode
|
|
267
|
+
|
|
268
|
+
// Update grid overlay
|
|
269
|
+
DispatchQueue.main.async {
|
|
270
|
+
if gridMode == "none" {
|
|
271
|
+
self.cameraController.removeGridOverlay()
|
|
272
|
+
} else {
|
|
273
|
+
self.cameraController.addGridOverlay(to: self.previewView, gridMode: gridMode)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
call.resolve()
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
@objc func getGridMode(_ call: CAPPluginCall) {
|
|
281
|
+
guard self.isInitialized else {
|
|
282
|
+
call.reject("camera not started")
|
|
283
|
+
return
|
|
284
|
+
}
|
|
285
|
+
call.resolve(["gridMode": self.gridMode])
|
|
286
|
+
}
|
|
287
|
+
|
|
157
288
|
@objc func appDidBecomeActive() {
|
|
158
289
|
if self.isInitialized {
|
|
159
290
|
DispatchQueue.main.async {
|
|
@@ -161,7 +292,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
161
292
|
}
|
|
162
293
|
}
|
|
163
294
|
}
|
|
164
|
-
|
|
295
|
+
|
|
165
296
|
@objc func appWillEnterForeground() {
|
|
166
297
|
if self.isInitialized {
|
|
167
298
|
DispatchQueue.main.async {
|
|
@@ -265,21 +396,34 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
265
396
|
self.cameraPosition = call.getString("position") ?? "rear"
|
|
266
397
|
let deviceId = call.getString("deviceId")
|
|
267
398
|
let cameraMode = call.getBool("cameraMode") ?? false
|
|
268
|
-
self.highResolutionOutput = call.getBool("enableHighResolution") ?? false
|
|
269
|
-
self.cameraController.highResolutionOutput = self.highResolutionOutput
|
|
270
399
|
|
|
271
|
-
|
|
272
|
-
|
|
400
|
+
// Set width - use screen width if not provided or if 0
|
|
401
|
+
if let width = call.getInt("width"), width > 0 {
|
|
402
|
+
self.width = CGFloat(width)
|
|
273
403
|
} else {
|
|
274
404
|
self.width = UIScreen.main.bounds.size.width
|
|
275
405
|
}
|
|
276
|
-
|
|
277
|
-
|
|
406
|
+
|
|
407
|
+
// Set height - use screen height if not provided or if 0
|
|
408
|
+
if let height = call.getInt("height"), height > 0 {
|
|
409
|
+
self.height = CGFloat(height)
|
|
278
410
|
} else {
|
|
279
411
|
self.height = UIScreen.main.bounds.size.height
|
|
280
412
|
}
|
|
281
|
-
|
|
282
|
-
|
|
413
|
+
|
|
414
|
+
// Set x position - use exact CSS pixel value from web view, or mark for centering
|
|
415
|
+
if let x = call.getInt("x") {
|
|
416
|
+
self.posX = CGFloat(x)
|
|
417
|
+
} else {
|
|
418
|
+
self.posX = -1 // Use -1 to indicate auto-centering
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Set y position - use exact CSS pixel value from web view, or mark for centering
|
|
422
|
+
if let y = call.getInt("y") {
|
|
423
|
+
self.posY = CGFloat(y)
|
|
424
|
+
} else {
|
|
425
|
+
self.posY = -1 // Use -1 to indicate auto-centering
|
|
426
|
+
}
|
|
283
427
|
if call.getInt("paddingBottom") != nil {
|
|
284
428
|
self.paddingBottom = CGFloat(call.getInt("paddingBottom")!)
|
|
285
429
|
}
|
|
@@ -288,7 +432,18 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
288
432
|
self.toBack = call.getBool("toBack") ?? true
|
|
289
433
|
self.storeToFile = call.getBool("storeToFile") ?? false
|
|
290
434
|
self.enableZoom = call.getBool("enableZoom") ?? false
|
|
291
|
-
self.disableAudio = call.getBool("disableAudio") ??
|
|
435
|
+
self.disableAudio = call.getBool("disableAudio") ?? true
|
|
436
|
+
self.aspectRatio = call.getString("aspectRatio")
|
|
437
|
+
self.gridMode = call.getString("gridMode") ?? "none"
|
|
438
|
+
let initialZoomLevel = call.getFloat("initialZoomLevel") ?? 1.0
|
|
439
|
+
if self.aspectRatio != nil && (call.getInt("width") != nil || call.getInt("height") != nil) {
|
|
440
|
+
call.reject("Cannot set both aspectRatio and size (width/height). Use setPreviewSize after start.")
|
|
441
|
+
return
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
print("[CameraPreview] Camera start parameters - aspectRatio: \(String(describing: self.aspectRatio)), gridMode: \(self.gridMode)")
|
|
445
|
+
print("[CameraPreview] Screen dimensions: \(UIScreen.main.bounds.size)")
|
|
446
|
+
print("[CameraPreview] Final frame dimensions - width: \(String(describing: self.width)), height: \(String(describing: self.height)), x: \(String(describing: self.posX)), y: \(String(describing: self.posY))")
|
|
292
447
|
|
|
293
448
|
AVCaptureDevice.requestAccess(for: .video, completionHandler: { (granted: Bool) in
|
|
294
449
|
guard granted else {
|
|
@@ -296,48 +451,80 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
296
451
|
return
|
|
297
452
|
}
|
|
298
453
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
self.previewView = UIView(frame: CGRect(x: self.posX ?? 0, y: self.posY ?? 0, width: self.width!, height: height))
|
|
311
|
-
|
|
312
|
-
// Make webview transparent - comprehensive approach
|
|
313
|
-
self.makeWebViewTransparent()
|
|
314
|
-
|
|
315
|
-
self.webView?.superview?.addSubview(self.previewView)
|
|
316
|
-
if self.toBack! {
|
|
317
|
-
self.webView?.superview?.bringSubviewToFront(self.webView!)
|
|
318
|
-
}
|
|
319
|
-
try? self.cameraController.displayPreview(on: self.previewView)
|
|
320
|
-
|
|
321
|
-
let frontView = self.toBack! ? self.webView : self.previewView
|
|
322
|
-
self.cameraController.setupGestures(target: frontView ?? self.previewView, enableZoom: self.enableZoom!)
|
|
323
|
-
|
|
324
|
-
if self.rotateWhenOrientationChanged == true {
|
|
325
|
-
NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.rotated), name: UIDevice.orientationDidChangeNotification, object: nil)
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Add observers for app state changes to maintain transparency
|
|
329
|
-
NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
|
|
330
|
-
NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.appWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
|
|
331
|
-
|
|
332
|
-
self.isInitializing = false
|
|
333
|
-
self.isInitialized = true
|
|
334
|
-
call.resolve()
|
|
335
|
-
|
|
454
|
+
if self.cameraController.captureSession?.isRunning ?? false {
|
|
455
|
+
call.reject("camera already started")
|
|
456
|
+
} else {
|
|
457
|
+
self.cameraController.prepare(cameraPosition: self.cameraPosition, deviceId: deviceId, disableAudio: self.disableAudio, cameraMode: cameraMode, aspectRatio: self.aspectRatio, initialZoomLevel: Float(initialZoomLevel)) {error in
|
|
458
|
+
if let error = error {
|
|
459
|
+
print(error)
|
|
460
|
+
call.reject(error.localizedDescription)
|
|
461
|
+
return
|
|
462
|
+
}
|
|
463
|
+
DispatchQueue.main.async {
|
|
464
|
+
self.completeStartCamera(call: call)
|
|
336
465
|
}
|
|
337
466
|
}
|
|
338
467
|
}
|
|
339
468
|
})
|
|
469
|
+
}
|
|
340
470
|
|
|
471
|
+
private func completeStartCamera(call: CAPPluginCall) {
|
|
472
|
+
// Create and configure the preview view first
|
|
473
|
+
self.updateCameraFrame()
|
|
474
|
+
|
|
475
|
+
// Make webview transparent - comprehensive approach
|
|
476
|
+
self.makeWebViewTransparent()
|
|
477
|
+
|
|
478
|
+
// Add the preview view to the webview itself to use same coordinate system
|
|
479
|
+
self.webView?.addSubview(self.previewView)
|
|
480
|
+
if self.toBack! {
|
|
481
|
+
self.webView?.sendSubviewToBack(self.previewView)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Display the camera preview on the configured view
|
|
485
|
+
try? self.cameraController.displayPreview(on: self.previewView)
|
|
486
|
+
|
|
487
|
+
self.cameraController.setupGestures(target: self.previewView, enableZoom: self.enableZoom!)
|
|
488
|
+
|
|
489
|
+
// Add grid overlay if enabled
|
|
490
|
+
if self.gridMode != "none" {
|
|
491
|
+
self.cameraController.addGridOverlay(to: self.previewView, gridMode: self.gridMode)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if self.rotateWhenOrientationChanged == true {
|
|
495
|
+
NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.rotated), name: UIDevice.orientationDidChangeNotification, object: nil)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Add observers for app state changes to maintain transparency
|
|
499
|
+
NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
|
|
500
|
+
NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.appWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
|
|
501
|
+
|
|
502
|
+
self.isInitializing = false
|
|
503
|
+
self.isInitialized = true
|
|
504
|
+
|
|
505
|
+
// Set up callback to wait for first frame before resolving
|
|
506
|
+
self.cameraController.firstFrameReadyCallback = { [weak self] in
|
|
507
|
+
guard let self = self else { return }
|
|
508
|
+
|
|
509
|
+
DispatchQueue.main.async {
|
|
510
|
+
var returnedObject = JSObject()
|
|
511
|
+
returnedObject["width"] = self.previewView.frame.width as any JSValue
|
|
512
|
+
returnedObject["height"] = self.previewView.frame.height as any JSValue
|
|
513
|
+
returnedObject["x"] = self.previewView.frame.origin.x as any JSValue
|
|
514
|
+
returnedObject["y"] = self.previewView.frame.origin.y as any JSValue
|
|
515
|
+
call.resolve(returnedObject)
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// If already received first frame (unlikely but possible), resolve immediately
|
|
520
|
+
if self.cameraController.hasReceivedFirstFrame {
|
|
521
|
+
var returnedObject = JSObject()
|
|
522
|
+
returnedObject["width"] = self.previewView.frame.width as any JSValue
|
|
523
|
+
returnedObject["height"] = self.previewView.frame.height as any JSValue
|
|
524
|
+
returnedObject["x"] = self.previewView.frame.origin.x as any JSValue
|
|
525
|
+
returnedObject["y"] = self.previewView.frame.origin.y as any JSValue
|
|
526
|
+
call.resolve(returnedObject)
|
|
527
|
+
}
|
|
341
528
|
}
|
|
342
529
|
|
|
343
530
|
@objc func flip(_ call: CAPPluginCall) {
|
|
@@ -346,68 +533,46 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
346
533
|
return
|
|
347
534
|
}
|
|
348
535
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
call.reject("Camera controller deallocated")
|
|
352
|
-
return
|
|
353
|
-
}
|
|
536
|
+
// Disable user interaction during flip
|
|
537
|
+
self.previewView.isUserInteractionEnabled = false
|
|
354
538
|
|
|
355
|
-
|
|
356
|
-
self.
|
|
539
|
+
do {
|
|
540
|
+
try self.cameraController.switchCameras()
|
|
357
541
|
|
|
358
|
-
//
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
542
|
+
// Update preview layer frame without animation
|
|
543
|
+
CATransaction.begin()
|
|
544
|
+
CATransaction.setDisableActions(true)
|
|
545
|
+
self.cameraController.previewLayer?.frame = self.previewView.bounds
|
|
546
|
+
self.cameraController.previewLayer?.videoGravity = .resizeAspectFill
|
|
547
|
+
CATransaction.commit()
|
|
362
548
|
|
|
363
|
-
|
|
364
|
-
do {
|
|
365
|
-
try self.cameraController.switchCameras()
|
|
549
|
+
self.previewView.isUserInteractionEnabled = true
|
|
366
550
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
self.cameraController.previewLayer?.videoGravity = .resizeAspectFill
|
|
370
|
-
self.previewView.isUserInteractionEnabled = true
|
|
371
|
-
|
|
372
|
-
// Ensure webview remains transparent after flip
|
|
373
|
-
self.makeWebViewTransparent()
|
|
374
|
-
|
|
375
|
-
call.resolve()
|
|
376
|
-
}
|
|
377
|
-
} catch {
|
|
378
|
-
retryCount += 1
|
|
379
|
-
|
|
380
|
-
if retryCount < maxRetries {
|
|
381
|
-
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 0.5) {
|
|
382
|
-
attemptFlip()
|
|
383
|
-
}
|
|
384
|
-
} else {
|
|
385
|
-
DispatchQueue.main.async {
|
|
386
|
-
self.previewView.isUserInteractionEnabled = true
|
|
387
|
-
print("Failed to flip camera after \(maxRetries) attempts: \(error.localizedDescription)")
|
|
388
|
-
call.reject("Failed to flip camera: \(error.localizedDescription)")
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
}
|
|
551
|
+
// Ensure webview remains transparent after flip
|
|
552
|
+
self.makeWebViewTransparent()
|
|
393
553
|
|
|
394
|
-
|
|
395
|
-
|
|
554
|
+
call.resolve()
|
|
555
|
+
} catch {
|
|
556
|
+
self.previewView.isUserInteractionEnabled = true
|
|
557
|
+
print("Failed to flip camera: \(error.localizedDescription)")
|
|
558
|
+
call.reject("Failed to flip camera: \(error.localizedDescription)")
|
|
396
559
|
}
|
|
397
560
|
}
|
|
398
561
|
|
|
399
562
|
@objc func stop(_ call: CAPPluginCall) {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
}
|
|
563
|
+
if self.isInitializing {
|
|
564
|
+
call.reject("cannot stop camera while initialization is in progress")
|
|
565
|
+
return
|
|
566
|
+
}
|
|
567
|
+
if !self.isInitialized {
|
|
568
|
+
call.reject("camera not initialized")
|
|
569
|
+
return
|
|
570
|
+
}
|
|
409
571
|
|
|
572
|
+
// UI operations must be on main thread
|
|
573
|
+
DispatchQueue.main.async {
|
|
410
574
|
// Always attempt to stop and clean up, regardless of captureSession state
|
|
575
|
+
self.cameraController.removeGridOverlay()
|
|
411
576
|
if let previewView = self.previewView {
|
|
412
577
|
previewView.removeFromSuperview()
|
|
413
578
|
self.previewView = nil
|
|
@@ -417,7 +582,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
417
582
|
self.isInitialized = false
|
|
418
583
|
self.isInitializing = false
|
|
419
584
|
self.cameraController.cleanup()
|
|
420
|
-
|
|
585
|
+
|
|
421
586
|
// Remove notification observers
|
|
422
587
|
NotificationCenter.default.removeObserver(self)
|
|
423
588
|
|
|
@@ -436,39 +601,159 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
436
601
|
}
|
|
437
602
|
|
|
438
603
|
@objc func capture(_ call: CAPPluginCall) {
|
|
439
|
-
|
|
604
|
+
let withExifLocation = call.getBool("withExifLocation", false)
|
|
605
|
+
print("[CameraPreview] capture called, withExifLocation: \(withExifLocation)")
|
|
606
|
+
|
|
607
|
+
if withExifLocation {
|
|
608
|
+
print("[CameraPreview] Location required for capture")
|
|
609
|
+
|
|
610
|
+
// Check location services before main thread dispatch
|
|
611
|
+
guard CLLocationManager.locationServicesEnabled() else {
|
|
612
|
+
print("[CameraPreview] Location services are disabled")
|
|
613
|
+
call.reject("Location services are disabled")
|
|
614
|
+
return
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Check if Info.plist has the required key
|
|
618
|
+
guard Bundle.main.object(forInfoDictionaryKey: "NSLocationWhenInUseUsageDescription") != nil else {
|
|
619
|
+
print("[CameraPreview] ERROR: NSLocationWhenInUseUsageDescription key missing from Info.plist")
|
|
620
|
+
call.reject("NSLocationWhenInUseUsageDescription key missing from Info.plist. Add this key with a description of how your app uses location.")
|
|
621
|
+
return
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Ensure location manager setup happens on main thread
|
|
625
|
+
DispatchQueue.main.async {
|
|
626
|
+
if self.locationManager == nil {
|
|
627
|
+
print("[CameraPreview] Creating location manager on main thread")
|
|
628
|
+
self.locationManager = CLLocationManager()
|
|
629
|
+
self.locationManager?.delegate = self
|
|
630
|
+
self.locationManager?.desiredAccuracy = kCLLocationAccuracyBest
|
|
631
|
+
print("[CameraPreview] Location manager created, delegate set to: \(self)")
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Check current authorization status
|
|
635
|
+
let currentStatus = self.locationManager?.authorizationStatus ?? .notDetermined
|
|
636
|
+
print("[CameraPreview] Current authorization status: \(currentStatus.rawValue)")
|
|
637
|
+
|
|
638
|
+
switch currentStatus {
|
|
639
|
+
case .authorizedWhenInUse, .authorizedAlways:
|
|
640
|
+
// Already authorized, get location and capture
|
|
641
|
+
print("[CameraPreview] Already authorized, getting location immediately")
|
|
642
|
+
self.getCurrentLocation { _ in
|
|
643
|
+
self.performCapture(call: call)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
case .denied, .restricted:
|
|
647
|
+
// Permission denied
|
|
648
|
+
print("[CameraPreview] Location permission denied")
|
|
649
|
+
call.reject("Location permission denied")
|
|
650
|
+
|
|
651
|
+
case .notDetermined:
|
|
652
|
+
// Need to request permission
|
|
653
|
+
print("[CameraPreview] Location permission not determined, requesting...")
|
|
654
|
+
// Save the call for the delegate callback
|
|
655
|
+
print("[CameraPreview] Saving call for location authorization flow")
|
|
656
|
+
self.bridge?.saveCall(call)
|
|
657
|
+
self.permissionCallID = call.callbackId
|
|
658
|
+
self.waitingForLocation = true
|
|
659
|
+
|
|
660
|
+
// Request authorization - this will trigger locationManagerDidChangeAuthorization
|
|
661
|
+
print("[CameraPreview] Requesting location authorization...")
|
|
662
|
+
self.locationManager?.requestWhenInUseAuthorization()
|
|
663
|
+
// The delegate will handle the rest
|
|
664
|
+
|
|
665
|
+
@unknown default:
|
|
666
|
+
print("[CameraPreview] Unknown authorization status")
|
|
667
|
+
call.reject("Unknown location permission status")
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
} else {
|
|
671
|
+
print("[CameraPreview] No location required, performing capture directly")
|
|
672
|
+
self.performCapture(call: call)
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
private func performCapture(call: CAPPluginCall) {
|
|
677
|
+
print("[CameraPreview] performCapture called")
|
|
678
|
+
let quality = call.getFloat("quality", 85)
|
|
679
|
+
let saveToGallery = call.getBool("saveToGallery", false)
|
|
680
|
+
let withExifLocation = call.getBool("withExifLocation", false)
|
|
681
|
+
let width = call.getInt("width")
|
|
682
|
+
let height = call.getInt("height")
|
|
683
|
+
let aspectRatio = call.getString("aspectRatio")
|
|
684
|
+
|
|
685
|
+
// Check for conflicting parameters
|
|
686
|
+
if aspectRatio != nil && (width != nil || height != nil) {
|
|
687
|
+
print("[CameraPreview] Error: Cannot set both aspectRatio and size (width/height)")
|
|
688
|
+
call.reject("Cannot set both aspectRatio and size (width/height). Use setPreviewSize after start.")
|
|
689
|
+
return
|
|
690
|
+
}
|
|
440
691
|
|
|
441
|
-
|
|
442
|
-
|
|
692
|
+
// Use the stored aspectRatio if none is provided and no width/height is specified
|
|
693
|
+
// If no aspectRatio was set at all, use "4:3" as default (matching getAspectRatio behavior)
|
|
694
|
+
let captureAspectRatio: String? = if width == nil && height == nil && aspectRatio == nil {
|
|
695
|
+
self.aspectRatio ?? "4:3"
|
|
696
|
+
} else {
|
|
697
|
+
aspectRatio
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
print("[CameraPreview] Capture params - quality: \(quality), saveToGallery: \(saveToGallery), withExifLocation: \(withExifLocation), width: \(width ?? -1), height: \(height ?? -1), aspectRatio: \(aspectRatio ?? "nil"), using aspectRatio: \(captureAspectRatio ?? "nil")")
|
|
701
|
+
print("[CameraPreview] Current location: \(self.currentLocation?.description ?? "nil")")
|
|
443
702
|
|
|
444
|
-
|
|
703
|
+
self.cameraController.captureImage(width: width, height: height, aspectRatio: captureAspectRatio, quality: quality, gpsLocation: self.currentLocation) { (image, error) in
|
|
704
|
+
print("[CameraPreview] captureImage callback received")
|
|
705
|
+
DispatchQueue.main.async {
|
|
706
|
+
print("[CameraPreview] Processing capture on main thread")
|
|
445
707
|
if let error = error {
|
|
708
|
+
print("[CameraPreview] Capture error: \(error.localizedDescription)")
|
|
446
709
|
call.reject(error.localizedDescription)
|
|
447
710
|
return
|
|
448
711
|
}
|
|
449
712
|
|
|
713
|
+
guard let image = image,
|
|
714
|
+
let imageDataWithExif = self.createImageDataWithExif(
|
|
715
|
+
from: image,
|
|
716
|
+
quality: Int(quality),
|
|
717
|
+
location: withExifLocation ? self.currentLocation : nil
|
|
718
|
+
)
|
|
719
|
+
else {
|
|
720
|
+
print("[CameraPreview] Failed to create image data with EXIF")
|
|
721
|
+
call.reject("Failed to create image data with EXIF")
|
|
722
|
+
return
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
print("[CameraPreview] Image data created, size: \(imageDataWithExif.count) bytes")
|
|
726
|
+
|
|
450
727
|
if saveToGallery {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
728
|
+
print("[CameraPreview] Saving to gallery...")
|
|
729
|
+
self.saveImageDataToGallery(imageData: imageDataWithExif) { success, error in
|
|
730
|
+
print("[CameraPreview] Save to gallery completed, success: \(success), error: \(error?.localizedDescription ?? "none")")
|
|
731
|
+
let exifData = self.getExifData(from: imageDataWithExif)
|
|
732
|
+
let base64Image = imageDataWithExif.base64EncodedString()
|
|
733
|
+
|
|
734
|
+
var result = JSObject()
|
|
735
|
+
result["value"] = base64Image
|
|
736
|
+
result["exif"] = exifData
|
|
737
|
+
result["gallerySaved"] = success
|
|
738
|
+
if !success, let error = error {
|
|
739
|
+
result["galleryError"] = error.localizedDescription
|
|
456
740
|
}
|
|
457
|
-
})
|
|
458
|
-
}
|
|
459
741
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
742
|
+
print("[CameraPreview] Resolving capture call with gallery save")
|
|
743
|
+
call.resolve(result)
|
|
744
|
+
}
|
|
745
|
+
} else {
|
|
746
|
+
print("[CameraPreview] Not saving to gallery, returning image data")
|
|
747
|
+
let exifData = self.getExifData(from: imageDataWithExif)
|
|
748
|
+
let base64Image = imageDataWithExif.base64EncodedString()
|
|
749
|
+
|
|
750
|
+
var result = JSObject()
|
|
751
|
+
result["value"] = base64Image
|
|
752
|
+
result["exif"] = exifData
|
|
753
|
+
|
|
754
|
+
print("[CameraPreview] Resolving capture call")
|
|
755
|
+
call.resolve(result)
|
|
463
756
|
}
|
|
464
|
-
|
|
465
|
-
let exifData = self.getExifData(from: imageData)
|
|
466
|
-
let base64Image = imageData.base64EncodedString()
|
|
467
|
-
|
|
468
|
-
var result = JSObject()
|
|
469
|
-
result["value"] = base64Image
|
|
470
|
-
result["exif"] = exifData
|
|
471
|
-
call.resolve(result)
|
|
472
757
|
}
|
|
473
758
|
}
|
|
474
759
|
}
|
|
@@ -479,45 +764,109 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
479
764
|
let exifDict = imageProperties[kCGImagePropertyExifDictionary as String] as? [String: Any] else {
|
|
480
765
|
return [:]
|
|
481
766
|
}
|
|
482
|
-
|
|
767
|
+
|
|
483
768
|
var exifData = JSObject()
|
|
484
769
|
for (key, value) in exifDict {
|
|
485
|
-
|
|
770
|
+
// Convert value to JSValue-compatible type
|
|
771
|
+
if let stringValue = value as? String {
|
|
772
|
+
exifData[key] = stringValue
|
|
773
|
+
} else if let numberValue = value as? NSNumber {
|
|
774
|
+
exifData[key] = numberValue
|
|
775
|
+
} else if let boolValue = value as? Bool {
|
|
776
|
+
exifData[key] = boolValue
|
|
777
|
+
} else if let arrayValue = value as? [Any] {
|
|
778
|
+
exifData[key] = arrayValue
|
|
779
|
+
} else if let dictValue = value as? [String: Any] {
|
|
780
|
+
exifData[key] = JSObject(_immutableCocoaDictionary: NSMutableDictionary(dictionary: dictValue))
|
|
781
|
+
} else {
|
|
782
|
+
// Convert other types to string as fallback
|
|
783
|
+
exifData[key] = String(describing: value)
|
|
784
|
+
}
|
|
486
785
|
}
|
|
487
|
-
|
|
786
|
+
|
|
488
787
|
return exifData
|
|
489
788
|
}
|
|
490
789
|
|
|
790
|
+
private func createImageDataWithExif(from image: UIImage, quality: Int, location: CLLocation?) -> Data? {
|
|
791
|
+
guard let originalImageData = image.jpegData(compressionQuality: CGFloat(Double(quality) / 100.0)) else {
|
|
792
|
+
return nil
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
guard let imageSource = CGImageSourceCreateWithData(originalImageData as CFData, nil),
|
|
796
|
+
let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [String: Any],
|
|
797
|
+
let cgImage = image.cgImage else {
|
|
798
|
+
return originalImageData
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
let mutableData = NSMutableData()
|
|
802
|
+
guard let destination = CGImageDestinationCreateWithData(mutableData, kUTTypeJPEG, 1, nil) else {
|
|
803
|
+
return originalImageData
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
var finalProperties = imageProperties
|
|
807
|
+
|
|
808
|
+
// Add GPS location if available
|
|
809
|
+
if let location = location {
|
|
810
|
+
let formatter = DateFormatter()
|
|
811
|
+
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
|
|
812
|
+
formatter.timeZone = TimeZone(abbreviation: "UTC")
|
|
813
|
+
|
|
814
|
+
let gpsDict: [String: Any] = [
|
|
815
|
+
kCGImagePropertyGPSLatitude as String: abs(location.coordinate.latitude),
|
|
816
|
+
kCGImagePropertyGPSLatitudeRef as String: location.coordinate.latitude >= 0 ? "N" : "S",
|
|
817
|
+
kCGImagePropertyGPSLongitude as String: abs(location.coordinate.longitude),
|
|
818
|
+
kCGImagePropertyGPSLongitudeRef as String: location.coordinate.longitude >= 0 ? "E" : "W",
|
|
819
|
+
kCGImagePropertyGPSTimeStamp as String: formatter.string(from: location.timestamp),
|
|
820
|
+
kCGImagePropertyGPSAltitude as String: location.altitude,
|
|
821
|
+
kCGImagePropertyGPSAltitudeRef as String: location.altitude >= 0 ? 0 : 1
|
|
822
|
+
]
|
|
823
|
+
|
|
824
|
+
finalProperties[kCGImagePropertyGPSDictionary as String] = gpsDict
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Create or update TIFF dictionary for device info
|
|
828
|
+
var tiffDict = finalProperties[kCGImagePropertyTIFFDictionary as String] as? [String: Any] ?? [:]
|
|
829
|
+
tiffDict[kCGImagePropertyTIFFMake as String] = "Apple"
|
|
830
|
+
tiffDict[kCGImagePropertyTIFFModel as String] = UIDevice.current.model
|
|
831
|
+
finalProperties[kCGImagePropertyTIFFDictionary as String] = tiffDict
|
|
832
|
+
|
|
833
|
+
CGImageDestinationAddImage(destination, cgImage, finalProperties as CFDictionary)
|
|
834
|
+
|
|
835
|
+
if CGImageDestinationFinalize(destination) {
|
|
836
|
+
return mutableData as Data
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
return originalImageData
|
|
840
|
+
}
|
|
841
|
+
|
|
491
842
|
@objc func captureSample(_ call: CAPPluginCall) {
|
|
492
|
-
|
|
493
|
-
let quality: Int? = call.getInt("quality", 85)
|
|
843
|
+
let quality: Int? = call.getInt("quality", 85)
|
|
494
844
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
845
|
+
self.cameraController.captureSample { image, error in
|
|
846
|
+
guard let image = image else {
|
|
847
|
+
print("Image capture error: \(String(describing: error))")
|
|
848
|
+
call.reject("Image capture error: \(String(describing: error))")
|
|
849
|
+
return
|
|
850
|
+
}
|
|
501
851
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
852
|
+
let imageData: Data?
|
|
853
|
+
if self.cameraPosition == "front" {
|
|
854
|
+
let flippedImage = image.withHorizontallyFlippedOrientation()
|
|
855
|
+
imageData = flippedImage.jpegData(compressionQuality: CGFloat(quality!/100))
|
|
856
|
+
} else {
|
|
857
|
+
imageData = image.jpegData(compressionQuality: CGFloat(quality!/100))
|
|
858
|
+
}
|
|
509
859
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
}
|
|
860
|
+
if self.storeToFile == false {
|
|
861
|
+
let imageBase64 = imageData?.base64EncodedString()
|
|
862
|
+
call.resolve(["value": imageBase64!])
|
|
863
|
+
} else {
|
|
864
|
+
do {
|
|
865
|
+
let fileUrl = self.getTempFilePath()
|
|
866
|
+
try imageData?.write(to: fileUrl)
|
|
867
|
+
call.resolve(["value": fileUrl.absoluteString])
|
|
868
|
+
} catch {
|
|
869
|
+
call.reject("Error writing image to file")
|
|
521
870
|
}
|
|
522
871
|
}
|
|
523
872
|
}
|
|
@@ -572,31 +921,27 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
572
921
|
}
|
|
573
922
|
|
|
574
923
|
@objc func startRecordVideo(_ call: CAPPluginCall) {
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
call.reject(error.localizedDescription)
|
|
581
|
-
}
|
|
924
|
+
do {
|
|
925
|
+
try self.cameraController.captureVideo()
|
|
926
|
+
call.resolve()
|
|
927
|
+
} catch {
|
|
928
|
+
call.reject(error.localizedDescription)
|
|
582
929
|
}
|
|
583
930
|
}
|
|
584
931
|
|
|
585
932
|
@objc func stopRecordVideo(_ call: CAPPluginCall) {
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
call.reject("Video capture error")
|
|
592
|
-
return
|
|
593
|
-
}
|
|
594
|
-
call.reject(error.localizedDescription)
|
|
933
|
+
self.cameraController.stopRecording { (fileURL, error) in
|
|
934
|
+
guard let fileURL = fileURL else {
|
|
935
|
+
print(error ?? "Video capture error")
|
|
936
|
+
guard let error = error else {
|
|
937
|
+
call.reject("Video capture error")
|
|
595
938
|
return
|
|
596
939
|
}
|
|
597
|
-
|
|
598
|
-
|
|
940
|
+
call.reject(error.localizedDescription)
|
|
941
|
+
return
|
|
599
942
|
}
|
|
943
|
+
|
|
944
|
+
call.resolve(["videoFilePath": fileURL.absoluteString])
|
|
600
945
|
}
|
|
601
946
|
}
|
|
602
947
|
|
|
@@ -627,20 +972,20 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
627
972
|
// Collect all devices by position
|
|
628
973
|
for device in session.devices {
|
|
629
974
|
var lenses: [[String: Any]] = []
|
|
630
|
-
|
|
975
|
+
|
|
631
976
|
let constituentDevices = device.isVirtualDevice ? device.constituentDevices : [device]
|
|
632
|
-
|
|
977
|
+
|
|
633
978
|
for lensDevice in constituentDevices {
|
|
634
979
|
var deviceType: String
|
|
635
980
|
switch lensDevice.deviceType {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
981
|
+
case .builtInWideAngleCamera: deviceType = "wideAngle"
|
|
982
|
+
case .builtInUltraWideCamera: deviceType = "ultraWide"
|
|
983
|
+
case .builtInTelephotoCamera: deviceType = "telephoto"
|
|
984
|
+
case .builtInDualCamera: deviceType = "dual"
|
|
985
|
+
case .builtInDualWideCamera: deviceType = "dualWide"
|
|
986
|
+
case .builtInTripleCamera: deviceType = "triple"
|
|
987
|
+
case .builtInTrueDepthCamera: deviceType = "trueDepth"
|
|
988
|
+
default: deviceType = "unknown"
|
|
644
989
|
}
|
|
645
990
|
|
|
646
991
|
var baseZoomRatio: Float = 1.0
|
|
@@ -649,7 +994,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
649
994
|
} else if lensDevice.deviceType == .builtInTelephotoCamera {
|
|
650
995
|
baseZoomRatio = 2.0 // A common value for telephoto lenses
|
|
651
996
|
}
|
|
652
|
-
|
|
997
|
+
|
|
653
998
|
let lensInfo: [String: Any] = [
|
|
654
999
|
"label": lensDevice.localizedName,
|
|
655
1000
|
"deviceType": deviceType,
|
|
@@ -660,7 +1005,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
660
1005
|
]
|
|
661
1006
|
lenses.append(lensInfo)
|
|
662
1007
|
}
|
|
663
|
-
|
|
1008
|
+
|
|
664
1009
|
let deviceData: [String: Any] = [
|
|
665
1010
|
"deviceId": device.uniqueID,
|
|
666
1011
|
"label": device.localizedName,
|
|
@@ -670,7 +1015,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
670
1015
|
"maxZoom": Float(device.maxAvailableVideoZoomFactor),
|
|
671
1016
|
"isLogical": device.isVirtualDevice
|
|
672
1017
|
]
|
|
673
|
-
|
|
1018
|
+
|
|
674
1019
|
devices.append(deviceData)
|
|
675
1020
|
}
|
|
676
1021
|
|
|
@@ -686,7 +1031,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
686
1031
|
do {
|
|
687
1032
|
let zoomInfo = try self.cameraController.getZoom()
|
|
688
1033
|
let lensInfo = try self.cameraController.getCurrentLensInfo()
|
|
689
|
-
|
|
1034
|
+
|
|
690
1035
|
var minZoom = zoomInfo.min
|
|
691
1036
|
var maxZoom = zoomInfo.max
|
|
692
1037
|
var currentZoom = zoomInfo.current
|
|
@@ -731,9 +1076,10 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
731
1076
|
}
|
|
732
1077
|
|
|
733
1078
|
let ramp = call.getBool("ramp") ?? true
|
|
1079
|
+
let autoFocus = call.getBool("autoFocus") ?? true
|
|
734
1080
|
|
|
735
1081
|
do {
|
|
736
|
-
try self.cameraController.setZoom(level: CGFloat(level), ramp: ramp)
|
|
1082
|
+
try self.cameraController.setZoom(level: CGFloat(level), ramp: ramp, autoFocus: autoFocus)
|
|
737
1083
|
call.resolve()
|
|
738
1084
|
} catch {
|
|
739
1085
|
call.reject("Failed to set zoom: \(error.localizedDescription)")
|
|
@@ -765,40 +1111,32 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
765
1111
|
return
|
|
766
1112
|
}
|
|
767
1113
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
call.reject("Camera controller deallocated")
|
|
771
|
-
return
|
|
772
|
-
}
|
|
1114
|
+
// Disable user interaction during device swap
|
|
1115
|
+
self.previewView.isUserInteractionEnabled = false
|
|
773
1116
|
|
|
774
|
-
|
|
775
|
-
self.
|
|
1117
|
+
do {
|
|
1118
|
+
try self.cameraController.swapToDevice(deviceId: deviceId)
|
|
776
1119
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
1120
|
+
// Update preview layer frame without animation
|
|
1121
|
+
CATransaction.begin()
|
|
1122
|
+
CATransaction.setDisableActions(true)
|
|
1123
|
+
self.cameraController.previewLayer?.frame = self.previewView.bounds
|
|
1124
|
+
self.cameraController.previewLayer?.videoGravity = .resizeAspectFill
|
|
1125
|
+
CATransaction.commit()
|
|
780
1126
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
}
|
|
791
|
-
} catch {
|
|
792
|
-
DispatchQueue.main.async {
|
|
793
|
-
self.previewView.isUserInteractionEnabled = true
|
|
794
|
-
call.reject("Failed to swap to device \(deviceId): \(error.localizedDescription)")
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
}
|
|
1127
|
+
self.previewView.isUserInteractionEnabled = true
|
|
1128
|
+
|
|
1129
|
+
// Ensure webview remains transparent after device switch
|
|
1130
|
+
self.makeWebViewTransparent()
|
|
1131
|
+
|
|
1132
|
+
call.resolve()
|
|
1133
|
+
} catch {
|
|
1134
|
+
self.previewView.isUserInteractionEnabled = true
|
|
1135
|
+
call.reject("Failed to swap to device \(deviceId): \(error.localizedDescription)")
|
|
798
1136
|
}
|
|
799
1137
|
}
|
|
800
1138
|
|
|
801
|
-
|
|
1139
|
+
@objc func getDeviceId(_ call: CAPPluginCall) {
|
|
802
1140
|
guard isInitialized else {
|
|
803
1141
|
call.reject("Camera not initialized")
|
|
804
1142
|
return
|
|
@@ -812,6 +1150,385 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
812
1150
|
}
|
|
813
1151
|
}
|
|
814
1152
|
|
|
1153
|
+
// MARK: - Capacitor Permissions
|
|
1154
|
+
|
|
1155
|
+
private func requestLocationPermission(completion: @escaping (Bool) -> Void) {
|
|
1156
|
+
print("[CameraPreview] requestLocationPermission called")
|
|
1157
|
+
if self.locationManager == nil {
|
|
1158
|
+
print("[CameraPreview] Creating location manager")
|
|
1159
|
+
self.locationManager = CLLocationManager()
|
|
1160
|
+
self.locationManager?.delegate = self
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
let authStatus = self.locationManager?.authorizationStatus
|
|
1164
|
+
print("[CameraPreview] Current authorization status: \(String(describing: authStatus))")
|
|
1165
|
+
|
|
1166
|
+
switch authStatus {
|
|
1167
|
+
case .authorizedWhenInUse, .authorizedAlways:
|
|
1168
|
+
print("[CameraPreview] Location already authorized")
|
|
1169
|
+
completion(true)
|
|
1170
|
+
case .notDetermined:
|
|
1171
|
+
print("[CameraPreview] Location not determined, requesting authorization...")
|
|
1172
|
+
self.permissionCompletion = completion
|
|
1173
|
+
self.locationManager?.requestWhenInUseAuthorization()
|
|
1174
|
+
case .denied, .restricted:
|
|
1175
|
+
print("[CameraPreview] Location denied or restricted")
|
|
1176
|
+
completion(false)
|
|
1177
|
+
case .none:
|
|
1178
|
+
print("[CameraPreview] Location manager authorization status is nil")
|
|
1179
|
+
completion(false)
|
|
1180
|
+
@unknown default:
|
|
1181
|
+
print("[CameraPreview] Unknown authorization status")
|
|
1182
|
+
completion(false)
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
private var permissionCompletion: ((Bool) -> Void)?
|
|
1187
|
+
|
|
1188
|
+
public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
|
1189
|
+
let status = manager.authorizationStatus
|
|
1190
|
+
print("[CameraPreview] locationManagerDidChangeAuthorization called, status: \(status.rawValue), thread: \(Thread.current)")
|
|
1191
|
+
|
|
1192
|
+
// Handle pending capture call if we have one
|
|
1193
|
+
if let callID = self.permissionCallID, self.waitingForLocation {
|
|
1194
|
+
print("[CameraPreview] Found pending capture call ID: \(callID)")
|
|
1195
|
+
|
|
1196
|
+
let handleAuthorization = {
|
|
1197
|
+
print("[CameraPreview] Getting saved call on thread: \(Thread.current)")
|
|
1198
|
+
guard let call = self.bridge?.savedCall(withID: callID) else {
|
|
1199
|
+
print("[CameraPreview] ERROR: Could not retrieve saved call")
|
|
1200
|
+
self.permissionCallID = nil
|
|
1201
|
+
self.waitingForLocation = false
|
|
1202
|
+
return
|
|
1203
|
+
}
|
|
1204
|
+
print("[CameraPreview] Successfully retrieved saved call")
|
|
1205
|
+
|
|
1206
|
+
switch status {
|
|
1207
|
+
case .authorizedWhenInUse, .authorizedAlways:
|
|
1208
|
+
print("[CameraPreview] Location authorized, getting location for capture")
|
|
1209
|
+
self.getCurrentLocation { _ in
|
|
1210
|
+
self.performCapture(call: call)
|
|
1211
|
+
self.bridge?.releaseCall(call)
|
|
1212
|
+
self.permissionCallID = nil
|
|
1213
|
+
self.waitingForLocation = false
|
|
1214
|
+
}
|
|
1215
|
+
case .denied, .restricted:
|
|
1216
|
+
print("[CameraPreview] Location denied, rejecting capture")
|
|
1217
|
+
call.reject("Location permission denied")
|
|
1218
|
+
self.bridge?.releaseCall(call)
|
|
1219
|
+
self.permissionCallID = nil
|
|
1220
|
+
self.waitingForLocation = false
|
|
1221
|
+
case .notDetermined:
|
|
1222
|
+
print("[CameraPreview] Authorization not determined yet")
|
|
1223
|
+
// Don't do anything, wait for user response
|
|
1224
|
+
@unknown default:
|
|
1225
|
+
print("[CameraPreview] Unknown status, rejecting capture")
|
|
1226
|
+
call.reject("Unknown location permission status")
|
|
1227
|
+
self.bridge?.releaseCall(call)
|
|
1228
|
+
self.permissionCallID = nil
|
|
1229
|
+
self.waitingForLocation = false
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// Check if we're already on main thread
|
|
1234
|
+
if Thread.isMainThread {
|
|
1235
|
+
print("[CameraPreview] Already on main thread")
|
|
1236
|
+
handleAuthorization()
|
|
1237
|
+
} else {
|
|
1238
|
+
print("[CameraPreview] Not on main thread, dispatching")
|
|
1239
|
+
DispatchQueue.main.async(execute: handleAuthorization)
|
|
1240
|
+
}
|
|
1241
|
+
} else {
|
|
1242
|
+
print("[CameraPreview] No pending capture call")
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
|
1247
|
+
print("[CameraPreview] locationManager didFailWithError: \(error.localizedDescription)")
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
private func getCurrentLocation(completion: @escaping (CLLocation?) -> Void) {
|
|
1251
|
+
print("[CameraPreview] getCurrentLocation called")
|
|
1252
|
+
self.locationCompletion = completion
|
|
1253
|
+
self.locationManager?.startUpdatingLocation()
|
|
1254
|
+
print("[CameraPreview] Started updating location")
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
private var locationCompletion: ((CLLocation?) -> Void)?
|
|
1258
|
+
|
|
1259
|
+
public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
|
1260
|
+
print("[CameraPreview] locationManager didUpdateLocations called, locations count: \(locations.count)")
|
|
1261
|
+
self.currentLocation = locations.last
|
|
1262
|
+
if let completion = locationCompletion {
|
|
1263
|
+
print("[CameraPreview] Calling location completion with location: \(self.currentLocation?.description ?? "nil")")
|
|
1264
|
+
self.locationManager?.stopUpdatingLocation()
|
|
1265
|
+
completion(self.currentLocation)
|
|
1266
|
+
locationCompletion = nil
|
|
1267
|
+
} else {
|
|
1268
|
+
print("[CameraPreview] No location completion handler found")
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
private func saveImageDataToGallery(imageData: Data, completion: @escaping (Bool, Error?) -> Void) {
|
|
1273
|
+
// Check if NSPhotoLibraryUsageDescription is present in Info.plist
|
|
1274
|
+
guard Bundle.main.object(forInfoDictionaryKey: "NSPhotoLibraryUsageDescription") != nil else {
|
|
1275
|
+
let error = NSError(domain: "CameraPreview", code: 2, userInfo: [
|
|
1276
|
+
NSLocalizedDescriptionKey: "NSPhotoLibraryUsageDescription key missing from Info.plist. Add this key with a description of how your app uses photo library access."
|
|
1277
|
+
])
|
|
1278
|
+
completion(false, error)
|
|
1279
|
+
return
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
let status = PHPhotoLibrary.authorizationStatus()
|
|
1283
|
+
|
|
1284
|
+
switch status {
|
|
1285
|
+
case .authorized:
|
|
1286
|
+
performSaveDataToGallery(imageData: imageData, completion: completion)
|
|
1287
|
+
case .notDetermined:
|
|
1288
|
+
PHPhotoLibrary.requestAuthorization { newStatus in
|
|
1289
|
+
if newStatus == .authorized {
|
|
1290
|
+
self.performSaveDataToGallery(imageData: imageData, completion: completion)
|
|
1291
|
+
} else {
|
|
1292
|
+
completion(false, NSError(domain: "CameraPreview", code: 1, userInfo: [NSLocalizedDescriptionKey: "Photo library access denied"]))
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
case .denied, .restricted:
|
|
1296
|
+
completion(false, NSError(domain: "CameraPreview", code: 1, userInfo: [NSLocalizedDescriptionKey: "Photo library access denied"]))
|
|
1297
|
+
case .limited:
|
|
1298
|
+
performSaveDataToGallery(imageData: imageData, completion: completion)
|
|
1299
|
+
@unknown default:
|
|
1300
|
+
completion(false, NSError(domain: "CameraPreview", code: 1, userInfo: [NSLocalizedDescriptionKey: "Unknown photo library authorization status"]))
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
private func performSaveDataToGallery(imageData: Data, completion: @escaping (Bool, Error?) -> Void) {
|
|
1305
|
+
// Create a temporary file to write the JPEG data with EXIF
|
|
1306
|
+
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".jpg")
|
|
1307
|
+
|
|
1308
|
+
do {
|
|
1309
|
+
try imageData.write(to: tempURL)
|
|
1310
|
+
|
|
1311
|
+
PHPhotoLibrary.shared().performChanges({
|
|
1312
|
+
PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: tempURL)
|
|
1313
|
+
}, completionHandler: { success, error in
|
|
1314
|
+
// Clean up temporary file
|
|
1315
|
+
try? FileManager.default.removeItem(at: tempURL)
|
|
1316
|
+
|
|
1317
|
+
completion(success, error)
|
|
1318
|
+
})
|
|
1319
|
+
} catch {
|
|
1320
|
+
completion(false, error)
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
private func calculateCameraFrame(x: CGFloat? = nil, y: CGFloat? = nil, width: CGFloat? = nil, height: CGFloat? = nil, aspectRatio: String? = nil) -> CGRect {
|
|
1325
|
+
// Use provided values or existing ones
|
|
1326
|
+
let currentWidth = width ?? self.width ?? UIScreen.main.bounds.size.width
|
|
1327
|
+
let currentHeight = height ?? self.height ?? UIScreen.main.bounds.size.height
|
|
1328
|
+
let currentX = x ?? self.posX ?? -1
|
|
1329
|
+
let currentY = y ?? self.posY ?? -1
|
|
1330
|
+
let currentAspectRatio = aspectRatio ?? self.aspectRatio
|
|
1331
|
+
|
|
1332
|
+
let paddingBottom = self.paddingBottom ?? 0
|
|
1333
|
+
let adjustedHeight = currentHeight - CGFloat(paddingBottom)
|
|
1334
|
+
|
|
1335
|
+
// Cache webView dimensions for performance
|
|
1336
|
+
let webViewWidth = self.webView?.frame.width ?? UIScreen.main.bounds.width
|
|
1337
|
+
let webViewHeight = self.webView?.frame.height ?? UIScreen.main.bounds.height
|
|
1338
|
+
|
|
1339
|
+
var finalX = currentX
|
|
1340
|
+
var finalY = currentY
|
|
1341
|
+
var finalWidth = currentWidth
|
|
1342
|
+
var finalHeight = adjustedHeight
|
|
1343
|
+
|
|
1344
|
+
// Handle auto-centering when position is -1
|
|
1345
|
+
if currentX == -1 || currentY == -1 {
|
|
1346
|
+
// Only override dimensions if aspect ratio is provided and no explicit dimensions given
|
|
1347
|
+
if let ratio = currentAspectRatio,
|
|
1348
|
+
currentWidth == UIScreen.main.bounds.size.width &&
|
|
1349
|
+
currentHeight == UIScreen.main.bounds.size.height {
|
|
1350
|
+
finalWidth = webViewWidth
|
|
1351
|
+
|
|
1352
|
+
// Calculate height based on aspect ratio
|
|
1353
|
+
let ratioParts = ratio.split(separator: ":").compactMap { Double($0) }
|
|
1354
|
+
if ratioParts.count == 2 {
|
|
1355
|
+
// For camera, use portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
|
|
1356
|
+
let ratioValue = ratioParts[1] / ratioParts[0]
|
|
1357
|
+
finalHeight = finalWidth / CGFloat(ratioValue)
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// Center horizontally if x is -1
|
|
1362
|
+
if currentX == -1 {
|
|
1363
|
+
finalX = (webViewWidth - finalWidth) / 2
|
|
1364
|
+
} else {
|
|
1365
|
+
finalX = currentX
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// Center vertically if y is -1
|
|
1369
|
+
if currentY == -1 {
|
|
1370
|
+
// Use full screen height for centering
|
|
1371
|
+
let screenHeight = UIScreen.main.bounds.size.height
|
|
1372
|
+
finalY = (screenHeight - finalHeight) / 2
|
|
1373
|
+
print("[CameraPreview] Centering vertically: screenHeight=\(screenHeight), finalHeight=\(finalHeight), finalY=\(finalY)")
|
|
1374
|
+
} else {
|
|
1375
|
+
finalY = currentY
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
return CGRect(x: finalX, y: finalY, width: finalWidth, height: finalHeight)
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
private func updateCameraFrame() {
|
|
1383
|
+
guard let width = self.width, let height = self.height, let posX = self.posX, let posY = self.posY else {
|
|
1384
|
+
return
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// Ensure UI operations happen on main thread
|
|
1388
|
+
guard Thread.isMainThread else {
|
|
1389
|
+
DispatchQueue.main.async {
|
|
1390
|
+
self.updateCameraFrame()
|
|
1391
|
+
}
|
|
1392
|
+
return
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// Calculate the base frame using the factorized method
|
|
1396
|
+
var frame = calculateCameraFrame()
|
|
1397
|
+
|
|
1398
|
+
// Apply aspect ratio adjustments only if not auto-centering
|
|
1399
|
+
if posX != -1 && posY != -1, let aspectRatio = self.aspectRatio {
|
|
1400
|
+
let ratioParts = aspectRatio.split(separator: ":").compactMap { Double($0) }
|
|
1401
|
+
if ratioParts.count == 2 {
|
|
1402
|
+
// For camera, use portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
|
|
1403
|
+
let ratio = ratioParts[1] / ratioParts[0]
|
|
1404
|
+
let currentRatio = Double(frame.width) / Double(frame.height)
|
|
1405
|
+
|
|
1406
|
+
if currentRatio > ratio {
|
|
1407
|
+
let newWidth = Double(frame.height) * ratio
|
|
1408
|
+
frame.origin.x = frame.origin.x + (frame.width - CGFloat(newWidth)) / 2
|
|
1409
|
+
frame.size.width = CGFloat(newWidth)
|
|
1410
|
+
} else {
|
|
1411
|
+
let newHeight = Double(frame.width) / ratio
|
|
1412
|
+
frame.origin.y = frame.origin.y + (frame.height - CGFloat(newHeight)) / 2
|
|
1413
|
+
frame.size.height = CGFloat(newHeight)
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// Disable ALL animations for frame updates - we want instant positioning
|
|
1419
|
+
CATransaction.begin()
|
|
1420
|
+
CATransaction.setDisableActions(true)
|
|
1421
|
+
|
|
1422
|
+
// Batch UI updates for better performance
|
|
1423
|
+
if self.previewView == nil {
|
|
1424
|
+
self.previewView = UIView(frame: frame)
|
|
1425
|
+
self.previewView.backgroundColor = UIColor.clear
|
|
1426
|
+
} else {
|
|
1427
|
+
self.previewView.frame = frame
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// Update preview layer frame efficiently
|
|
1431
|
+
if let previewLayer = self.cameraController.previewLayer {
|
|
1432
|
+
previewLayer.frame = self.previewView.bounds
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// Update grid overlay frame if it exists
|
|
1436
|
+
if let gridOverlay = self.cameraController.gridOverlayView {
|
|
1437
|
+
gridOverlay.frame = self.previewView.bounds
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
CATransaction.commit()
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
@objc func getPreviewSize(_ call: CAPPluginCall) {
|
|
1444
|
+
guard self.isInitialized else {
|
|
1445
|
+
call.reject("camera not started")
|
|
1446
|
+
return
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
DispatchQueue.main.async {
|
|
1450
|
+
var result = JSObject()
|
|
1451
|
+
result["x"] = Double(self.previewView.frame.origin.x)
|
|
1452
|
+
result["y"] = Double(self.previewView.frame.origin.y)
|
|
1453
|
+
result["width"] = Double(self.previewView.frame.width)
|
|
1454
|
+
result["height"] = Double(self.previewView.frame.height)
|
|
1455
|
+
call.resolve(result)
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
@objc func setPreviewSize(_ call: CAPPluginCall) {
|
|
1460
|
+
guard self.isInitialized else {
|
|
1461
|
+
call.reject("camera not started")
|
|
1462
|
+
return
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
// Always set to -1 for auto-centering if not explicitly provided
|
|
1466
|
+
if let x = call.getInt("x") {
|
|
1467
|
+
self.posX = CGFloat(x)
|
|
1468
|
+
} else {
|
|
1469
|
+
self.posX = -1 // Auto-center if X not provided
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
if let y = call.getInt("y") {
|
|
1473
|
+
self.posY = CGFloat(y)
|
|
1474
|
+
} else {
|
|
1475
|
+
self.posY = -1 // Auto-center if Y not provided
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
if let width = call.getInt("width") { self.width = CGFloat(width) }
|
|
1479
|
+
if let height = call.getInt("height") { self.height = CGFloat(height) }
|
|
1480
|
+
|
|
1481
|
+
DispatchQueue.main.async {
|
|
1482
|
+
// Direct update without animation for better performance
|
|
1483
|
+
self.updateCameraFrame()
|
|
1484
|
+
self.makeWebViewTransparent()
|
|
1485
|
+
|
|
1486
|
+
// Return the actual preview bounds
|
|
1487
|
+
var result = JSObject()
|
|
1488
|
+
result["x"] = Double(self.previewView.frame.origin.x)
|
|
1489
|
+
result["y"] = Double(self.previewView.frame.origin.y)
|
|
1490
|
+
result["width"] = Double(self.previewView.frame.width)
|
|
1491
|
+
result["height"] = Double(self.previewView.frame.height)
|
|
1492
|
+
call.resolve(result)
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
@objc func setFocus(_ call: CAPPluginCall) {
|
|
1497
|
+
guard isInitialized else {
|
|
1498
|
+
call.reject("Camera not initialized")
|
|
1499
|
+
return
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
guard let x = call.getFloat("x"), let y = call.getFloat("y") else {
|
|
1503
|
+
call.reject("x and y parameters are required")
|
|
1504
|
+
return
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
// Reject if values are outside 0-1 range
|
|
1508
|
+
if x < 0 || x > 1 || y < 0 || y > 1 {
|
|
1509
|
+
call.reject("Focus coordinates must be between 0 and 1")
|
|
1510
|
+
return
|
|
1511
|
+
}
|
|
815
1512
|
|
|
1513
|
+
DispatchQueue.main.async {
|
|
1514
|
+
do {
|
|
1515
|
+
// Convert normalized coordinates to view coordinates
|
|
1516
|
+
let viewX = CGFloat(x) * self.previewView.bounds.width
|
|
1517
|
+
let viewY = CGFloat(y) * self.previewView.bounds.height
|
|
1518
|
+
let focusPoint = CGPoint(x: viewX, y: viewY)
|
|
1519
|
+
|
|
1520
|
+
// Convert view coordinates to device coordinates
|
|
1521
|
+
guard let previewLayer = self.cameraController.previewLayer else {
|
|
1522
|
+
call.reject("Preview layer not available")
|
|
1523
|
+
return
|
|
1524
|
+
}
|
|
1525
|
+
let devicePoint = previewLayer.captureDevicePointConverted(fromLayerPoint: focusPoint)
|
|
816
1526
|
|
|
1527
|
+
try self.cameraController.setFocus(at: devicePoint, showIndicator: true, in: self.previewView)
|
|
1528
|
+
call.resolve()
|
|
1529
|
+
} catch {
|
|
1530
|
+
call.reject("Failed to set focus: \(error.localizedDescription)")
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
817
1534
|
}
|