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