@capgo/camera-preview 7.4.0-beta.8 → 7.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +243 -51
- package/android/gradle/wrapper/gradle-wrapper.properties +1 -1
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java +1249 -143
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraXView.java +3400 -1382
- package/android/src/main/java/com/ahm/capacitor/camera/preview/GridOverlayView.java +95 -58
- 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 +160 -72
- 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 +441 -40
- package/dist/esm/definitions.d.ts +167 -25
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +24 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +23 -3
- package/dist/esm/web.js +463 -65
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +485 -64
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +485 -64
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/{CapgoCameraPreview → CapgoCameraPreviewPlugin}/CameraController.swift +731 -315
- package/ios/Sources/CapgoCameraPreviewPlugin/Plugin.swift +1902 -0
- package/package.json +11 -3
- 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/fileChanges/last-build.bin +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/8.14.2/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +0 -2
- package/android/.gradle/buildOutputCleanup/outputFiles.bin +0 -0
- package/android/.gradle/file-system.probe +0 -0
- package/android/.gradle/vcs-1/gc.properties +0 -0
- package/ios/Sources/CapgoCameraPreview/Plugin.swift +0 -1211
- /package/ios/Sources/{CapgoCameraPreview → CapgoCameraPreviewPlugin}/GridOverlayView.swift +0 -0
|
@@ -0,0 +1,1902 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import AVFoundation
|
|
3
|
+
import Photos
|
|
4
|
+
import Capacitor
|
|
5
|
+
import CoreImage
|
|
6
|
+
import CoreLocation
|
|
7
|
+
import MobileCoreServices
|
|
8
|
+
|
|
9
|
+
extension UIWindow {
|
|
10
|
+
static var isLandscape: Bool {
|
|
11
|
+
if #available(iOS 13.0, *) {
|
|
12
|
+
return UIApplication.shared.windows
|
|
13
|
+
.first?
|
|
14
|
+
.windowScene?
|
|
15
|
+
.interfaceOrientation
|
|
16
|
+
.isLandscape ?? false
|
|
17
|
+
} else {
|
|
18
|
+
return UIApplication.shared.statusBarOrientation.isLandscape
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
static var isPortrait: Bool {
|
|
22
|
+
if #available(iOS 13.0, *) {
|
|
23
|
+
return UIApplication.shared.windows
|
|
24
|
+
.first?
|
|
25
|
+
.windowScene?
|
|
26
|
+
.interfaceOrientation
|
|
27
|
+
.isPortrait ?? false
|
|
28
|
+
} else {
|
|
29
|
+
return UIApplication.shared.statusBarOrientation.isPortrait
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Please read the Capacitor iOS Plugin Development Guide
|
|
36
|
+
* here: https://capacitor.ionicframework.com/docs/plugins/ios
|
|
37
|
+
*/
|
|
38
|
+
@objc(CameraPreview)
|
|
39
|
+
public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelegate {
|
|
40
|
+
public let identifier = "CameraPreviewPlugin"
|
|
41
|
+
public let jsName = "CameraPreview"
|
|
42
|
+
public let pluginMethods: [CAPPluginMethod] = [
|
|
43
|
+
CAPPluginMethod(name: "start", returnType: CAPPluginReturnPromise),
|
|
44
|
+
CAPPluginMethod(name: "flip", returnType: CAPPluginReturnPromise),
|
|
45
|
+
CAPPluginMethod(name: "stop", returnType: CAPPluginReturnPromise),
|
|
46
|
+
CAPPluginMethod(name: "capture", returnType: CAPPluginReturnPromise),
|
|
47
|
+
CAPPluginMethod(name: "captureSample", returnType: CAPPluginReturnPromise),
|
|
48
|
+
CAPPluginMethod(name: "getSupportedFlashModes", returnType: CAPPluginReturnPromise),
|
|
49
|
+
CAPPluginMethod(name: "getHorizontalFov", returnType: CAPPluginReturnPromise),
|
|
50
|
+
CAPPluginMethod(name: "setFlashMode", returnType: CAPPluginReturnPromise),
|
|
51
|
+
CAPPluginMethod(name: "startRecordVideo", returnType: CAPPluginReturnPromise),
|
|
52
|
+
CAPPluginMethod(name: "stopRecordVideo", returnType: CAPPluginReturnPromise),
|
|
53
|
+
CAPPluginMethod(name: "getTempFilePath", returnType: CAPPluginReturnPromise),
|
|
54
|
+
CAPPluginMethod(name: "getSupportedPictureSizes", returnType: CAPPluginReturnPromise),
|
|
55
|
+
CAPPluginMethod(name: "isRunning", returnType: CAPPluginReturnPromise),
|
|
56
|
+
CAPPluginMethod(name: "getAvailableDevices", returnType: CAPPluginReturnPromise),
|
|
57
|
+
CAPPluginMethod(name: "getZoom", returnType: CAPPluginReturnPromise),
|
|
58
|
+
CAPPluginMethod(name: "getZoomButtonValues", returnType: CAPPluginReturnPromise),
|
|
59
|
+
CAPPluginMethod(name: "setZoom", returnType: CAPPluginReturnPromise),
|
|
60
|
+
CAPPluginMethod(name: "getFlashMode", returnType: CAPPluginReturnPromise),
|
|
61
|
+
CAPPluginMethod(name: "setDeviceId", returnType: CAPPluginReturnPromise),
|
|
62
|
+
CAPPluginMethod(name: "getDeviceId", returnType: CAPPluginReturnPromise),
|
|
63
|
+
CAPPluginMethod(name: "setAspectRatio", returnType: CAPPluginReturnPromise),
|
|
64
|
+
CAPPluginMethod(name: "getAspectRatio", returnType: CAPPluginReturnPromise),
|
|
65
|
+
CAPPluginMethod(name: "setGridMode", returnType: CAPPluginReturnPromise),
|
|
66
|
+
CAPPluginMethod(name: "getGridMode", returnType: CAPPluginReturnPromise),
|
|
67
|
+
CAPPluginMethod(name: "getPreviewSize", returnType: CAPPluginReturnPromise),
|
|
68
|
+
CAPPluginMethod(name: "setPreviewSize", returnType: CAPPluginReturnPromise),
|
|
69
|
+
CAPPluginMethod(name: "setFocus", returnType: CAPPluginReturnPromise),
|
|
70
|
+
CAPPluginMethod(name: "deleteFile", returnType: CAPPluginReturnPromise),
|
|
71
|
+
CAPPluginMethod(name: "getOrientation", returnType: CAPPluginReturnPromise),
|
|
72
|
+
CAPPluginMethod(name: "getSafeAreaInsets", returnType: CAPPluginReturnPromise)
|
|
73
|
+
|
|
74
|
+
]
|
|
75
|
+
// Camera state tracking
|
|
76
|
+
private var isInitializing: Bool = false
|
|
77
|
+
private var isInitialized: Bool = false
|
|
78
|
+
private var backgroundSession: AVCaptureSession?
|
|
79
|
+
|
|
80
|
+
var previewView: UIView!
|
|
81
|
+
var cameraPosition = String()
|
|
82
|
+
let cameraController = CameraController()
|
|
83
|
+
var posX: CGFloat?
|
|
84
|
+
var posY: CGFloat?
|
|
85
|
+
var width: CGFloat?
|
|
86
|
+
var height: CGFloat?
|
|
87
|
+
var paddingBottom: CGFloat?
|
|
88
|
+
var rotateWhenOrientationChanged: Bool?
|
|
89
|
+
var toBack: Bool?
|
|
90
|
+
var storeToFile: Bool?
|
|
91
|
+
var enableZoom: Bool?
|
|
92
|
+
var disableAudio: Bool = false
|
|
93
|
+
var locationManager: CLLocationManager?
|
|
94
|
+
var currentLocation: CLLocation?
|
|
95
|
+
private var aspectRatio: String?
|
|
96
|
+
private var gridMode: String = "none"
|
|
97
|
+
private var positioning: String = "center"
|
|
98
|
+
private var permissionCallID: String?
|
|
99
|
+
private var waitingForLocation: Bool = false
|
|
100
|
+
|
|
101
|
+
// MARK: - Helper Methods for Aspect Ratio
|
|
102
|
+
|
|
103
|
+
/// Validates that aspectRatio and size (width/height) are not both set
|
|
104
|
+
private func validateAspectRatioParameters(aspectRatio: String?, width: Int?, height: Int?) -> String? {
|
|
105
|
+
if aspectRatio != nil && (width != nil || height != nil) {
|
|
106
|
+
return "Cannot set both aspectRatio and size (width/height). Use setPreviewSize after start."
|
|
107
|
+
}
|
|
108
|
+
return nil
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// Parses aspect ratio string and returns the appropriate ratio for the current orientation
|
|
112
|
+
private func parseAspectRatio(_ ratio: String, isPortrait: Bool) -> CGFloat {
|
|
113
|
+
let parts = ratio.split(separator: ":").compactMap { Double($0) }
|
|
114
|
+
guard parts.count == 2 else { return 1.0 }
|
|
115
|
+
|
|
116
|
+
// For camera (portrait), we want portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
|
|
117
|
+
return isPortrait ?
|
|
118
|
+
CGFloat(parts[1] / parts[0]) :
|
|
119
|
+
CGFloat(parts[0] / parts[1])
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/// Calculates dimensions based on aspect ratio and available space
|
|
123
|
+
private func calculateDimensionsForAspectRatio(_ aspectRatio: String, availableWidth: CGFloat, availableHeight: CGFloat, isPortrait: Bool) -> (width: CGFloat, height: CGFloat) {
|
|
124
|
+
let ratio = parseAspectRatio(aspectRatio, isPortrait: isPortrait)
|
|
125
|
+
|
|
126
|
+
// Calculate maximum size that fits the aspect ratio in available space
|
|
127
|
+
let maxWidthByHeight = availableHeight * ratio
|
|
128
|
+
let maxHeightByWidth = availableWidth / ratio
|
|
129
|
+
|
|
130
|
+
if maxWidthByHeight <= availableWidth {
|
|
131
|
+
// Height is the limiting factor
|
|
132
|
+
return (width: maxWidthByHeight, height: availableHeight)
|
|
133
|
+
} else {
|
|
134
|
+
// Width is the limiting factor
|
|
135
|
+
return (width: availableWidth, height: maxHeightByWidth)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// MARK: - Transparency Methods
|
|
140
|
+
|
|
141
|
+
private func makeWebViewTransparent() {
|
|
142
|
+
guard let webView = self.webView else { return }
|
|
143
|
+
|
|
144
|
+
DispatchQueue.main.async {
|
|
145
|
+
// Define a recursive function to traverse the view hierarchy
|
|
146
|
+
func makeSubviewsTransparent(_ view: UIView) {
|
|
147
|
+
// Set the background color to clear
|
|
148
|
+
view.backgroundColor = .clear
|
|
149
|
+
|
|
150
|
+
// Recurse for all subviews
|
|
151
|
+
for subview in view.subviews {
|
|
152
|
+
makeSubviewsTransparent(subview)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Set the main webView to be transparent
|
|
157
|
+
webView.isOpaque = false
|
|
158
|
+
webView.backgroundColor = .clear
|
|
159
|
+
|
|
160
|
+
// Recursively make all subviews transparent
|
|
161
|
+
makeSubviewsTransparent(webView)
|
|
162
|
+
|
|
163
|
+
// Also ensure the webview's container is transparent
|
|
164
|
+
webView.superview?.backgroundColor = .clear
|
|
165
|
+
|
|
166
|
+
// Force a layout pass to apply changes
|
|
167
|
+
webView.setNeedsLayout()
|
|
168
|
+
webView.layoutIfNeeded()
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@objc func getZoomButtonValues(_ call: CAPPluginCall) {
|
|
173
|
+
guard isInitialized else {
|
|
174
|
+
call.reject("Camera not initialized")
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Determine current device based on active position
|
|
179
|
+
var currentDevice: AVCaptureDevice?
|
|
180
|
+
switch self.cameraController.currentCameraPosition {
|
|
181
|
+
case .front:
|
|
182
|
+
currentDevice = self.cameraController.frontCamera
|
|
183
|
+
case .rear:
|
|
184
|
+
currentDevice = self.cameraController.rearCamera
|
|
185
|
+
default:
|
|
186
|
+
currentDevice = nil
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
guard let device = currentDevice else {
|
|
190
|
+
call.reject("No active camera device")
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
var hasUltraWide = false
|
|
195
|
+
var hasWide = false
|
|
196
|
+
var hasTele = false
|
|
197
|
+
|
|
198
|
+
let lenses = device.isVirtualDevice ? device.constituentDevices : [device]
|
|
199
|
+
for lens in lenses {
|
|
200
|
+
switch lens.deviceType {
|
|
201
|
+
case .builtInUltraWideCamera:
|
|
202
|
+
hasUltraWide = true
|
|
203
|
+
case .builtInWideAngleCamera:
|
|
204
|
+
hasWide = true
|
|
205
|
+
case .builtInTelephotoCamera:
|
|
206
|
+
hasTele = true
|
|
207
|
+
default:
|
|
208
|
+
break
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
var values: [Float] = []
|
|
213
|
+
if hasUltraWide {
|
|
214
|
+
values.append(0.5)
|
|
215
|
+
}
|
|
216
|
+
if hasWide {
|
|
217
|
+
values.append(1.0)
|
|
218
|
+
if self.isProModelSupportingOptical2x() {
|
|
219
|
+
values.append(2.0)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if hasTele {
|
|
223
|
+
// Use the virtual device's switch-over zoom factors when available
|
|
224
|
+
let displayMultiplier = self.cameraController.getDisplayZoomMultiplier()
|
|
225
|
+
var teleStep: Float
|
|
226
|
+
|
|
227
|
+
if #available(iOS 13.0, *) {
|
|
228
|
+
let switchFactors = device.virtualDeviceSwitchOverVideoZoomFactors
|
|
229
|
+
if !switchFactors.isEmpty {
|
|
230
|
+
// Choose the highest switch-over factor (typically the wide->tele threshold)
|
|
231
|
+
let maxSwitch = switchFactors.map { $0.floatValue }.max() ?? Float(device.maxAvailableVideoZoomFactor)
|
|
232
|
+
teleStep = maxSwitch * displayMultiplier
|
|
233
|
+
} else {
|
|
234
|
+
teleStep = Float(device.maxAvailableVideoZoomFactor) * displayMultiplier
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
teleStep = Float(device.maxAvailableVideoZoomFactor) * displayMultiplier
|
|
238
|
+
}
|
|
239
|
+
values.append(teleStep)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Deduplicate and sort
|
|
243
|
+
let uniqueSorted = Array(Set(values)).sorted()
|
|
244
|
+
call.resolve(["values": uniqueSorted])
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private func isProModelSupportingOptical2x() -> Bool {
|
|
248
|
+
// Detects iPhone 14 Pro/Pro Max, 15 Pro/Pro Max, and 16 Pro/Pro Max
|
|
249
|
+
var systemInfo = utsname()
|
|
250
|
+
uname(&systemInfo)
|
|
251
|
+
let mirror = Mirror(reflecting: systemInfo.machine)
|
|
252
|
+
let identifier = mirror.children.reduce("") { partialResult, element in
|
|
253
|
+
guard let value = element.value as? Int8, value != 0 else { return partialResult }
|
|
254
|
+
return partialResult + String(UnicodeScalar(UInt8(value)))
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Known identifiers: 14 Pro (iPhone15,2), 14 Pro Max (iPhone15,3),
|
|
258
|
+
// 15 Pro (iPhone16,1), 15 Pro Max (iPhone16,2),
|
|
259
|
+
// 16 Pro (iPhone17,1), 16 Pro Max (iPhone17,2),
|
|
260
|
+
// 17 Pro (iPhone18,1), 17 Pro Max (iPhone18,2)
|
|
261
|
+
let supportedIdentifiers: Set<String> = [
|
|
262
|
+
"iPhone15,2", "iPhone15,3", // 14 Pro / 14 Pro Max
|
|
263
|
+
"iPhone16,1", "iPhone16,2", // 15 Pro / 15 Pro Max
|
|
264
|
+
"iPhone17,1", "iPhone17,2" // 16 Pro / 16 Pro Max
|
|
265
|
+
]
|
|
266
|
+
return supportedIdentifiers.contains(identifier)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
@objc func rotated() {
|
|
270
|
+
guard let previewView = self.previewView,
|
|
271
|
+
let posX = self.posX,
|
|
272
|
+
let posY = self.posY,
|
|
273
|
+
let width = self.width,
|
|
274
|
+
let heightValue = self.height else {
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
let paddingBottom = self.paddingBottom ?? 0
|
|
278
|
+
let height = heightValue - paddingBottom
|
|
279
|
+
|
|
280
|
+
// Handle auto-centering during rotation
|
|
281
|
+
// Always use the factorized method for consistent positioning
|
|
282
|
+
self.updateCameraFrame()
|
|
283
|
+
|
|
284
|
+
// Centralize orientation update to use interface orientation consistently
|
|
285
|
+
cameraController.updateVideoOrientation()
|
|
286
|
+
|
|
287
|
+
// Update grid overlay frame if it exists - no animation
|
|
288
|
+
if let gridOverlay = self.cameraController.gridOverlayView {
|
|
289
|
+
CATransaction.begin()
|
|
290
|
+
CATransaction.setDisableActions(true)
|
|
291
|
+
gridOverlay.frame = previewView.bounds
|
|
292
|
+
CATransaction.commit()
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Ensure webview remains transparent after rotation
|
|
296
|
+
if self.isInitialized {
|
|
297
|
+
self.makeWebViewTransparent()
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
@objc func setAspectRatio(_ call: CAPPluginCall) {
|
|
302
|
+
guard self.isInitialized else {
|
|
303
|
+
call.reject("camera not started")
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
guard let newAspectRatio = call.getString("aspectRatio") else {
|
|
308
|
+
call.reject("aspectRatio parameter is required")
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
self.aspectRatio = newAspectRatio
|
|
313
|
+
DispatchQueue.main.async {
|
|
314
|
+
call.resolve(self.rawSetAspectRatio())
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
func rawSetAspectRatio() -> JSObject {
|
|
319
|
+
// When aspect ratio changes, always auto-center the view
|
|
320
|
+
// This ensures consistent behavior where changing aspect ratio recenters the view
|
|
321
|
+
self.posX = -1
|
|
322
|
+
self.posY = -1
|
|
323
|
+
|
|
324
|
+
// Calculate maximum size based on aspect ratio
|
|
325
|
+
let webViewWidth = self.webView?.frame.width ?? UIScreen.main.bounds.width
|
|
326
|
+
let webViewHeight = self.webView?.frame.height ?? UIScreen.main.bounds.height
|
|
327
|
+
let paddingBottom = self.paddingBottom ?? 0
|
|
328
|
+
let isPortrait = self.isPortrait()
|
|
329
|
+
|
|
330
|
+
// Calculate available space
|
|
331
|
+
let availableWidth: CGFloat
|
|
332
|
+
let availableHeight: CGFloat
|
|
333
|
+
|
|
334
|
+
if self.posX == -1 || self.posY == -1 {
|
|
335
|
+
// Auto-centering mode - use full dimensions
|
|
336
|
+
availableWidth = webViewWidth
|
|
337
|
+
availableHeight = webViewHeight - paddingBottom
|
|
338
|
+
} else {
|
|
339
|
+
// Manual positioning - calculate remaining space
|
|
340
|
+
availableWidth = webViewWidth - self.posX!
|
|
341
|
+
availableHeight = webViewHeight - self.posY! - paddingBottom
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Parse aspect ratio - convert to portrait orientation for camera use
|
|
345
|
+
// Use the centralized calculation method
|
|
346
|
+
if let aspectRatio = self.aspectRatio {
|
|
347
|
+
let dimensions = calculateDimensionsForAspectRatio(aspectRatio, availableWidth: availableWidth, availableHeight: availableHeight, isPortrait: isPortrait)
|
|
348
|
+
self.width = dimensions.width
|
|
349
|
+
self.height = dimensions.height
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
self.updateCameraFrame()
|
|
353
|
+
|
|
354
|
+
// Return the actual preview bounds
|
|
355
|
+
var result = JSObject()
|
|
356
|
+
result["x"] = Double(self.previewView.frame.origin.x)
|
|
357
|
+
result["y"] = Double(self.previewView.frame.origin.y)
|
|
358
|
+
result["width"] = Double(self.previewView.frame.width)
|
|
359
|
+
result["height"] = Double(self.previewView.frame.height)
|
|
360
|
+
return result
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
@objc func getAspectRatio(_ call: CAPPluginCall) {
|
|
364
|
+
guard self.isInitialized else {
|
|
365
|
+
call.reject("camera not started")
|
|
366
|
+
return
|
|
367
|
+
}
|
|
368
|
+
call.resolve(["aspectRatio": self.aspectRatio ?? "4:3"])
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
@objc func setGridMode(_ call: CAPPluginCall) {
|
|
372
|
+
guard self.isInitialized else {
|
|
373
|
+
call.reject("camera not started")
|
|
374
|
+
return
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
guard let gridMode = call.getString("gridMode") else {
|
|
378
|
+
call.reject("gridMode parameter is required")
|
|
379
|
+
return
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
self.gridMode = gridMode
|
|
383
|
+
|
|
384
|
+
// Update grid overlay
|
|
385
|
+
DispatchQueue.main.async {
|
|
386
|
+
if gridMode == "none" {
|
|
387
|
+
self.cameraController.removeGridOverlay()
|
|
388
|
+
} else {
|
|
389
|
+
self.cameraController.addGridOverlay(to: self.previewView, gridMode: gridMode)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
call.resolve()
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
@objc func getGridMode(_ call: CAPPluginCall) {
|
|
397
|
+
guard self.isInitialized else {
|
|
398
|
+
call.reject("camera not started")
|
|
399
|
+
return
|
|
400
|
+
}
|
|
401
|
+
call.resolve(["gridMode": self.gridMode])
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
@objc func appDidBecomeActive() {
|
|
405
|
+
if self.isInitialized {
|
|
406
|
+
DispatchQueue.main.async {
|
|
407
|
+
self.makeWebViewTransparent()
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
@objc func appWillEnterForeground() {
|
|
413
|
+
if self.isInitialized {
|
|
414
|
+
DispatchQueue.main.async {
|
|
415
|
+
self.makeWebViewTransparent()
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
struct CameraInfo {
|
|
421
|
+
let deviceID: String
|
|
422
|
+
let position: String
|
|
423
|
+
let pictureSizes: [CGSize]
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
func getSupportedPictureSizes() -> [CameraInfo] {
|
|
427
|
+
var cameraInfos = [CameraInfo]()
|
|
428
|
+
|
|
429
|
+
// Discover all available cameras
|
|
430
|
+
let deviceTypes: [AVCaptureDevice.DeviceType] = [
|
|
431
|
+
.builtInWideAngleCamera,
|
|
432
|
+
.builtInUltraWideCamera,
|
|
433
|
+
.builtInTelephotoCamera,
|
|
434
|
+
.builtInDualCamera,
|
|
435
|
+
.builtInDualWideCamera,
|
|
436
|
+
.builtInTripleCamera,
|
|
437
|
+
.builtInTrueDepthCamera
|
|
438
|
+
]
|
|
439
|
+
|
|
440
|
+
let session = AVCaptureDevice.DiscoverySession(
|
|
441
|
+
deviceTypes: deviceTypes,
|
|
442
|
+
mediaType: .video,
|
|
443
|
+
position: .unspecified
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
let devices = session.devices
|
|
447
|
+
|
|
448
|
+
for device in devices {
|
|
449
|
+
// Determine the position of the camera
|
|
450
|
+
var position = "Unknown"
|
|
451
|
+
switch device.position {
|
|
452
|
+
case .front:
|
|
453
|
+
position = "Front"
|
|
454
|
+
case .back:
|
|
455
|
+
position = "Back"
|
|
456
|
+
case .unspecified:
|
|
457
|
+
position = "Unspecified"
|
|
458
|
+
@unknown default:
|
|
459
|
+
position = "Unknown"
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
var pictureSizes = [CGSize]()
|
|
463
|
+
|
|
464
|
+
// Get supported formats
|
|
465
|
+
for format in device.formats {
|
|
466
|
+
let description = format.formatDescription
|
|
467
|
+
let dimensions = CMVideoFormatDescriptionGetDimensions(description)
|
|
468
|
+
let size = CGSize(width: CGFloat(dimensions.width), height: CGFloat(dimensions.height))
|
|
469
|
+
if !pictureSizes.contains(size) {
|
|
470
|
+
pictureSizes.append(size)
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Sort sizes in descending order (largest to smallest)
|
|
475
|
+
pictureSizes.sort { $0.width * $0.height > $1.width * $1.height }
|
|
476
|
+
|
|
477
|
+
let cameraInfo = CameraInfo(deviceID: device.uniqueID, position: position, pictureSizes: pictureSizes)
|
|
478
|
+
cameraInfos.append(cameraInfo)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return cameraInfos
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
@objc func getSupportedPictureSizes(_ call: CAPPluginCall) {
|
|
485
|
+
let cameraInfos = getSupportedPictureSizes()
|
|
486
|
+
call.resolve([
|
|
487
|
+
"supportedPictureSizes": cameraInfos.map {
|
|
488
|
+
return [
|
|
489
|
+
"facing": $0.position,
|
|
490
|
+
"supportedPictureSizes": $0.pictureSizes.map { size in
|
|
491
|
+
return [
|
|
492
|
+
"width": String(describing: size.width),
|
|
493
|
+
"height": String(describing: size.height)
|
|
494
|
+
]
|
|
495
|
+
}
|
|
496
|
+
]
|
|
497
|
+
}
|
|
498
|
+
])
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
@objc func start(_ call: CAPPluginCall) {
|
|
502
|
+
let startTime = CFAbsoluteTimeGetCurrent()
|
|
503
|
+
print("[CameraPreview] 🚀 START CALLED at \(Date())")
|
|
504
|
+
|
|
505
|
+
// Log all received settings
|
|
506
|
+
print("[CameraPreview] 📋 Settings received:")
|
|
507
|
+
print(" - position: \(call.getString("position") ?? "rear")")
|
|
508
|
+
print(" - deviceId: \(call.getString("deviceId") ?? "nil")")
|
|
509
|
+
print(" - cameraMode: \(call.getBool("cameraMode") ?? false)")
|
|
510
|
+
print(" - width: \(call.getInt("width") ?? 0)")
|
|
511
|
+
print(" - height: \(call.getInt("height") ?? 0)")
|
|
512
|
+
print(" - x: \(call.getInt("x") ?? -1)")
|
|
513
|
+
print(" - y: \(call.getInt("y") ?? -1)")
|
|
514
|
+
print(" - paddingBottom: \(call.getInt("paddingBottom") ?? 0)")
|
|
515
|
+
print(" - rotateWhenOrientationChanged: \(call.getBool("rotateWhenOrientationChanged") ?? true)")
|
|
516
|
+
print(" - toBack: \(call.getBool("toBack") ?? true)")
|
|
517
|
+
print(" - storeToFile: \(call.getBool("storeToFile") ?? false)")
|
|
518
|
+
print(" - enableZoom: \(call.getBool("enableZoom") ?? false)")
|
|
519
|
+
print(" - disableAudio: \(call.getBool("disableAudio") ?? true)")
|
|
520
|
+
print(" - aspectRatio: \(call.getString("aspectRatio") ?? "4:3")")
|
|
521
|
+
print(" - gridMode: \(call.getString("gridMode") ?? "none")")
|
|
522
|
+
print(" - positioning: \(call.getString("positioning") ?? "top")")
|
|
523
|
+
print(" - initialZoomLevel: \(call.getFloat("initialZoomLevel") ?? 1.0)")
|
|
524
|
+
|
|
525
|
+
if self.isInitializing {
|
|
526
|
+
call.reject("camera initialization in progress")
|
|
527
|
+
return
|
|
528
|
+
}
|
|
529
|
+
if self.isInitialized {
|
|
530
|
+
call.reject("camera already started")
|
|
531
|
+
return
|
|
532
|
+
}
|
|
533
|
+
self.isInitializing = true
|
|
534
|
+
|
|
535
|
+
self.cameraPosition = call.getString("position") ?? "rear"
|
|
536
|
+
let deviceId = call.getString("deviceId")
|
|
537
|
+
let cameraMode = call.getBool("cameraMode") ?? false
|
|
538
|
+
|
|
539
|
+
// Set width - use screen width if not provided or if 0
|
|
540
|
+
if let width = call.getInt("width"), width > 0 {
|
|
541
|
+
self.width = CGFloat(width)
|
|
542
|
+
} else {
|
|
543
|
+
self.width = UIScreen.main.bounds.size.width
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Set height - use screen height if not provided or if 0
|
|
547
|
+
if let height = call.getInt("height"), height > 0 {
|
|
548
|
+
self.height = CGFloat(height)
|
|
549
|
+
} else {
|
|
550
|
+
self.height = UIScreen.main.bounds.size.height
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Set x position - use exact CSS pixel value from web view, or mark for centering
|
|
554
|
+
if let x = call.getInt("x") {
|
|
555
|
+
self.posX = CGFloat(x)
|
|
556
|
+
} else {
|
|
557
|
+
self.posX = -1 // Use -1 to indicate auto-centering
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Set y position - use exact CSS pixel value from web view, or mark for centering
|
|
561
|
+
if let y = call.getInt("y") {
|
|
562
|
+
self.posY = CGFloat(y)
|
|
563
|
+
} else {
|
|
564
|
+
self.posY = -1 // Use -1 to indicate auto-centering
|
|
565
|
+
}
|
|
566
|
+
if call.getInt("paddingBottom") != nil {
|
|
567
|
+
self.paddingBottom = CGFloat(call.getInt("paddingBottom")!)
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
self.rotateWhenOrientationChanged = call.getBool("rotateWhenOrientationChanged") ?? true
|
|
571
|
+
self.toBack = call.getBool("toBack") ?? true
|
|
572
|
+
self.storeToFile = call.getBool("storeToFile") ?? false
|
|
573
|
+
self.enableZoom = call.getBool("enableZoom") ?? false
|
|
574
|
+
self.disableAudio = call.getBool("disableAudio") ?? true
|
|
575
|
+
// Default to 4:3 aspect ratio if not provided
|
|
576
|
+
self.aspectRatio = call.getString("aspectRatio") ?? "4:3"
|
|
577
|
+
self.gridMode = call.getString("gridMode") ?? "none"
|
|
578
|
+
self.positioning = call.getString("positioning") ?? "top"
|
|
579
|
+
|
|
580
|
+
let initialZoomLevel = call.getFloat("initialZoomLevel")
|
|
581
|
+
|
|
582
|
+
// Validate aspect ratio parameters using centralized method
|
|
583
|
+
if let validationError = validateAspectRatioParameters(aspectRatio: self.aspectRatio, width: call.getInt("width"), height: call.getInt("height")) {
|
|
584
|
+
call.reject(validationError)
|
|
585
|
+
return
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
AVCaptureDevice.requestAccess(for: .video, completionHandler: { (granted: Bool) in
|
|
589
|
+
|
|
590
|
+
guard granted else {
|
|
591
|
+
call.reject("permission failed")
|
|
592
|
+
return
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if self.cameraController.captureSession?.isRunning ?? false {
|
|
596
|
+
call.reject("camera already started")
|
|
597
|
+
} else {
|
|
598
|
+
self.cameraController.prepare(cameraPosition: self.cameraPosition, deviceId: deviceId, disableAudio: self.disableAudio, cameraMode: cameraMode, aspectRatio: self.aspectRatio, initialZoomLevel: initialZoomLevel) {error in
|
|
599
|
+
if let error = error {
|
|
600
|
+
print(error)
|
|
601
|
+
call.reject(error.localizedDescription)
|
|
602
|
+
return
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
DispatchQueue.main.async {
|
|
606
|
+
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
|
|
607
|
+
NotificationCenter.default.addObserver(self,
|
|
608
|
+
selector: #selector(self.handleOrientationChange),
|
|
609
|
+
name: UIDevice.orientationDidChangeNotification,
|
|
610
|
+
object: nil)
|
|
611
|
+
self.completeStartCamera(call: call)
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
})
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
private func completeStartCamera(call: CAPPluginCall) {
|
|
619
|
+
// Create and configure the preview view first
|
|
620
|
+
self.updateCameraFrame()
|
|
621
|
+
|
|
622
|
+
// Make webview transparent - comprehensive approach
|
|
623
|
+
self.makeWebViewTransparent()
|
|
624
|
+
|
|
625
|
+
// Add the preview view to the webview itself to use same coordinate system
|
|
626
|
+
self.webView?.addSubview(self.previewView)
|
|
627
|
+
if self.toBack! {
|
|
628
|
+
self.webView?.sendSubviewToBack(self.previewView)
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Display the camera preview on the configured view
|
|
632
|
+
try? self.cameraController.displayPreview(on: self.previewView)
|
|
633
|
+
|
|
634
|
+
// Ensure the preview orientation matches the current interface orientation at startup
|
|
635
|
+
self.cameraController.updateVideoOrientation()
|
|
636
|
+
|
|
637
|
+
self.cameraController.setupGestures(target: self.previewView, enableZoom: self.enableZoom!)
|
|
638
|
+
|
|
639
|
+
// Add grid overlay if enabled
|
|
640
|
+
if self.gridMode != "none" {
|
|
641
|
+
self.cameraController.addGridOverlay(to: self.previewView, gridMode: self.gridMode)
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if self.rotateWhenOrientationChanged == true {
|
|
645
|
+
NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.rotated), name: UIDevice.orientationDidChangeNotification, object: nil)
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Add observers for app state changes to maintain transparency
|
|
649
|
+
NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
|
|
650
|
+
NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.appWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
|
|
651
|
+
|
|
652
|
+
self.isInitializing = false
|
|
653
|
+
self.isInitialized = true
|
|
654
|
+
|
|
655
|
+
// Set up callback to wait for first frame before resolving
|
|
656
|
+
self.cameraController.firstFrameReadyCallback = { [weak self] in
|
|
657
|
+
guard let self = self else { return }
|
|
658
|
+
|
|
659
|
+
DispatchQueue.main.async {
|
|
660
|
+
var returnedObject = JSObject()
|
|
661
|
+
returnedObject["width"] = self.previewView.frame.width as any JSValue
|
|
662
|
+
returnedObject["height"] = self.previewView.frame.height as any JSValue
|
|
663
|
+
returnedObject["x"] = self.previewView.frame.origin.x as any JSValue
|
|
664
|
+
returnedObject["y"] = self.previewView.frame.origin.y as any JSValue
|
|
665
|
+
call.resolve(returnedObject)
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// If already received first frame (unlikely but possible), resolve immediately on main thread
|
|
670
|
+
if self.cameraController.hasReceivedFirstFrame {
|
|
671
|
+
DispatchQueue.main.async {
|
|
672
|
+
var returnedObject = JSObject()
|
|
673
|
+
returnedObject["width"] = self.previewView.frame.width as any JSValue
|
|
674
|
+
returnedObject["height"] = self.previewView.frame.height as any JSValue
|
|
675
|
+
returnedObject["x"] = self.previewView.frame.origin.x as any JSValue
|
|
676
|
+
returnedObject["y"] = self.previewView.frame.origin.y as any JSValue
|
|
677
|
+
call.resolve(returnedObject)
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
@objc func flip(_ call: CAPPluginCall) {
|
|
683
|
+
guard isInitialized else {
|
|
684
|
+
call.reject("Camera not initialized")
|
|
685
|
+
return
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Disable user interaction during flip
|
|
689
|
+
self.previewView.isUserInteractionEnabled = false
|
|
690
|
+
|
|
691
|
+
do {
|
|
692
|
+
try self.cameraController.switchCameras()
|
|
693
|
+
|
|
694
|
+
// Update preview layer frame without animation
|
|
695
|
+
CATransaction.begin()
|
|
696
|
+
CATransaction.setDisableActions(true)
|
|
697
|
+
self.cameraController.previewLayer?.frame = self.previewView.bounds
|
|
698
|
+
self.cameraController.previewLayer?.videoGravity = .resizeAspectFill
|
|
699
|
+
CATransaction.commit()
|
|
700
|
+
|
|
701
|
+
self.previewView.isUserInteractionEnabled = true
|
|
702
|
+
|
|
703
|
+
// Ensure webview remains transparent after flip
|
|
704
|
+
self.makeWebViewTransparent()
|
|
705
|
+
|
|
706
|
+
call.resolve()
|
|
707
|
+
} catch {
|
|
708
|
+
self.previewView.isUserInteractionEnabled = true
|
|
709
|
+
print("Failed to flip camera: \(error.localizedDescription)")
|
|
710
|
+
call.reject("Failed to flip camera: \(error.localizedDescription)")
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
@objc func stop(_ call: CAPPluginCall) {
|
|
715
|
+
if self.isInitializing {
|
|
716
|
+
call.reject("cannot stop camera while initialization is in progress")
|
|
717
|
+
return
|
|
718
|
+
}
|
|
719
|
+
if !self.isInitialized {
|
|
720
|
+
call.reject("camera not initialized")
|
|
721
|
+
return
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// UI operations must be on main thread
|
|
725
|
+
DispatchQueue.main.async {
|
|
726
|
+
// Always attempt to stop and clean up, regardless of captureSession state
|
|
727
|
+
self.cameraController.removeGridOverlay()
|
|
728
|
+
if let previewView = self.previewView {
|
|
729
|
+
previewView.removeFromSuperview()
|
|
730
|
+
self.previewView = nil
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
self.webView?.isOpaque = true
|
|
734
|
+
self.isInitialized = false
|
|
735
|
+
self.isInitializing = false
|
|
736
|
+
self.cameraController.cleanup()
|
|
737
|
+
|
|
738
|
+
// Remove notification observers
|
|
739
|
+
NotificationCenter.default.removeObserver(self)
|
|
740
|
+
|
|
741
|
+
NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil)
|
|
742
|
+
UIDevice.current.endGeneratingDeviceOrientationNotifications()
|
|
743
|
+
|
|
744
|
+
call.resolve()
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
// Get user's cache directory path
|
|
748
|
+
@objc func getTempFilePath() -> URL {
|
|
749
|
+
let path = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
|
750
|
+
let identifier = UUID()
|
|
751
|
+
let randomIdentifier = identifier.uuidString.replacingOccurrences(of: "-", with: "")
|
|
752
|
+
let finalIdentifier = String(randomIdentifier.prefix(8))
|
|
753
|
+
let fileName="cpcp_capture_"+finalIdentifier+".jpg"
|
|
754
|
+
let fileUrl=path.appendingPathComponent(fileName)
|
|
755
|
+
return fileUrl
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
@objc func capture(_ call: CAPPluginCall) {
|
|
759
|
+
print("[CameraPreview] capture called with options: \(call.options)")
|
|
760
|
+
let withExifLocation = call.getBool("withExifLocation", false)
|
|
761
|
+
print("[CameraPreview] capture called, withExifLocation: \(withExifLocation)")
|
|
762
|
+
|
|
763
|
+
if withExifLocation {
|
|
764
|
+
print("[CameraPreview] Location required for capture")
|
|
765
|
+
|
|
766
|
+
// Check location services before main thread dispatch
|
|
767
|
+
guard CLLocationManager.locationServicesEnabled() else {
|
|
768
|
+
print("[CameraPreview] Location services are disabled")
|
|
769
|
+
call.reject("Location services are disabled")
|
|
770
|
+
return
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Check if Info.plist has the required key
|
|
774
|
+
guard Bundle.main.object(forInfoDictionaryKey: "NSLocationWhenInUseUsageDescription") != nil else {
|
|
775
|
+
print("[CameraPreview] ERROR: NSLocationWhenInUseUsageDescription key missing from Info.plist")
|
|
776
|
+
call.reject("NSLocationWhenInUseUsageDescription key missing from Info.plist. Add this key with a description of how your app uses location.")
|
|
777
|
+
return
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Ensure location manager setup happens on main thread
|
|
781
|
+
DispatchQueue.main.async {
|
|
782
|
+
if self.locationManager == nil {
|
|
783
|
+
self.locationManager = CLLocationManager()
|
|
784
|
+
self.locationManager?.delegate = self
|
|
785
|
+
self.locationManager?.desiredAccuracy = kCLLocationAccuracyBest
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Check current authorization status
|
|
789
|
+
let currentStatus = self.locationManager?.authorizationStatus ?? .notDetermined
|
|
790
|
+
|
|
791
|
+
switch currentStatus {
|
|
792
|
+
case .authorizedWhenInUse, .authorizedAlways:
|
|
793
|
+
// Already authorized, get location and capture
|
|
794
|
+
self.getCurrentLocation { _ in
|
|
795
|
+
self.performCapture(call: call)
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
case .denied, .restricted:
|
|
799
|
+
// Permission denied
|
|
800
|
+
print("[CameraPreview] Location permission denied")
|
|
801
|
+
call.reject("Location permission denied")
|
|
802
|
+
|
|
803
|
+
case .notDetermined:
|
|
804
|
+
// Need to request permission
|
|
805
|
+
print("[CameraPreview] Location permission not determined, requesting...")
|
|
806
|
+
// Save the call for the delegate callback
|
|
807
|
+
print("[CameraPreview] Saving call for location authorization flow")
|
|
808
|
+
self.bridge?.saveCall(call)
|
|
809
|
+
self.permissionCallID = call.callbackId
|
|
810
|
+
self.waitingForLocation = true
|
|
811
|
+
|
|
812
|
+
// Request authorization - this will trigger locationManagerDidChangeAuthorization
|
|
813
|
+
print("[CameraPreview] Requesting location authorization...")
|
|
814
|
+
self.locationManager?.requestWhenInUseAuthorization()
|
|
815
|
+
// The delegate will handle the rest
|
|
816
|
+
|
|
817
|
+
@unknown default:
|
|
818
|
+
print("[CameraPreview] Unknown authorization status")
|
|
819
|
+
call.reject("Unknown location permission status")
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
} else {
|
|
823
|
+
print("[CameraPreview] No location required, performing capture directly")
|
|
824
|
+
self.performCapture(call: call)
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
private func performCapture(call: CAPPluginCall) {
|
|
829
|
+
print("[CameraPreview] performCapture called")
|
|
830
|
+
print("[CameraPreview] Call parameters: \(call.options)")
|
|
831
|
+
let quality = call.getFloat("quality", 85)
|
|
832
|
+
let saveToGallery = call.getBool("saveToGallery", false)
|
|
833
|
+
let withExifLocation = call.getBool("withExifLocation", false)
|
|
834
|
+
let width = call.getInt("width")
|
|
835
|
+
let height = call.getInt("height")
|
|
836
|
+
let aspectRatio = call.getString("aspectRatio")
|
|
837
|
+
|
|
838
|
+
print("[CameraPreview] Raw parameter values - width: \(String(describing: width)), height: \(String(describing: height)), aspectRatio: \(String(describing: aspectRatio))")
|
|
839
|
+
|
|
840
|
+
// Check for conflicting parameters using centralized validation
|
|
841
|
+
if let validationError = validateAspectRatioParameters(aspectRatio: aspectRatio, width: width, height: height) {
|
|
842
|
+
print("[CameraPreview] Error: \(validationError)")
|
|
843
|
+
call.reject(validationError)
|
|
844
|
+
return
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// When no dimensions are specified, we should capture exactly what's visible in the preview
|
|
848
|
+
// Don't pass aspectRatio in this case - let the capture method handle preview matching
|
|
849
|
+
print("[CameraPreview] Capture decision - width: \(width == nil), height: \(height == nil), aspectRatio param: \(aspectRatio == nil)")
|
|
850
|
+
print("[CameraPreview] Stored aspectRatio: \(self.aspectRatio ?? "nil")")
|
|
851
|
+
|
|
852
|
+
// Only pass aspectRatio if explicitly provided in the capture call
|
|
853
|
+
// Never use the stored aspectRatio when capturing without dimensions
|
|
854
|
+
let captureAspectRatio: String? = aspectRatio
|
|
855
|
+
|
|
856
|
+
print("[CameraPreview] Capture params - quality: \(quality), saveToGallery: \(saveToGallery), withExifLocation: \(withExifLocation), width: \(width ?? -1), height: \(height ?? -1), aspectRatio: \(aspectRatio ?? "nil"), using aspectRatio: \(captureAspectRatio ?? "nil")")
|
|
857
|
+
print("[CameraPreview] Current location: \(self.currentLocation?.description ?? "nil")")
|
|
858
|
+
// Safely read frame from main thread for logging
|
|
859
|
+
let (previewWidth, previewHeight): (CGFloat, CGFloat) = {
|
|
860
|
+
if Thread.isMainThread {
|
|
861
|
+
return (self.previewView.frame.width, self.previewView.frame.height)
|
|
862
|
+
}
|
|
863
|
+
var w: CGFloat = 0
|
|
864
|
+
var h: CGFloat = 0
|
|
865
|
+
DispatchQueue.main.sync {
|
|
866
|
+
w = self.previewView.frame.width
|
|
867
|
+
h = self.previewView.frame.height
|
|
868
|
+
}
|
|
869
|
+
return (w, h)
|
|
870
|
+
}()
|
|
871
|
+
print("[CameraPreview] Preview dimensions: \(previewWidth)x\(previewHeight)")
|
|
872
|
+
|
|
873
|
+
self.cameraController.captureImage(width: width, height: height, aspectRatio: captureAspectRatio, quality: quality, gpsLocation: self.currentLocation) { (image, originalPhotoData, _, error) in
|
|
874
|
+
print("[CameraPreview] captureImage callback received")
|
|
875
|
+
DispatchQueue.main.async {
|
|
876
|
+
print("[CameraPreview] Processing capture on main thread")
|
|
877
|
+
if let error = error {
|
|
878
|
+
print("[CameraPreview] Capture error: \(error.localizedDescription)")
|
|
879
|
+
call.reject(error.localizedDescription)
|
|
880
|
+
return
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
guard let image = image,
|
|
884
|
+
let imageDataWithExif = self.createImageDataWithExif(
|
|
885
|
+
from: image,
|
|
886
|
+
quality: Int(quality),
|
|
887
|
+
location: withExifLocation ? self.currentLocation : nil,
|
|
888
|
+
originalPhotoData: originalPhotoData
|
|
889
|
+
)
|
|
890
|
+
else {
|
|
891
|
+
print("[CameraPreview] Failed to create image data with EXIF")
|
|
892
|
+
call.reject("Failed to create image data with EXIF")
|
|
893
|
+
return
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
print("[CameraPreview] Image data created, size: \(imageDataWithExif.count) bytes")
|
|
897
|
+
|
|
898
|
+
if saveToGallery {
|
|
899
|
+
print("[CameraPreview] Saving to gallery...")
|
|
900
|
+
self.saveImageDataToGallery(imageData: imageDataWithExif) { success, error in
|
|
901
|
+
print("[CameraPreview] Save to gallery completed, success: \(success), error: \(error?.localizedDescription ?? "none")")
|
|
902
|
+
let exifData = self.getExifData(from: imageDataWithExif)
|
|
903
|
+
|
|
904
|
+
var result = JSObject()
|
|
905
|
+
result["exif"] = exifData
|
|
906
|
+
result["gallerySaved"] = success
|
|
907
|
+
if !success, let error = error {
|
|
908
|
+
result["galleryError"] = error.localizedDescription
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
if self.storeToFile == false {
|
|
912
|
+
let base64Image = imageDataWithExif.base64EncodedString()
|
|
913
|
+
result["value"] = base64Image
|
|
914
|
+
} else {
|
|
915
|
+
do {
|
|
916
|
+
let fileUrl = self.getTempFilePath()
|
|
917
|
+
try imageDataWithExif.write(to: fileUrl)
|
|
918
|
+
result["value"] = fileUrl.absoluteString
|
|
919
|
+
} catch {
|
|
920
|
+
call.reject("Error writing image to file")
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
print("[CameraPreview] Resolving capture call with gallery save")
|
|
925
|
+
call.resolve(result)
|
|
926
|
+
}
|
|
927
|
+
} else {
|
|
928
|
+
print("[CameraPreview] Not saving to gallery, returning image data")
|
|
929
|
+
let exifData = self.getExifData(from: imageDataWithExif)
|
|
930
|
+
|
|
931
|
+
if self.storeToFile == false {
|
|
932
|
+
let base64Image = imageDataWithExif.base64EncodedString()
|
|
933
|
+
var result = JSObject()
|
|
934
|
+
result["value"] = base64Image
|
|
935
|
+
result["exif"] = exifData
|
|
936
|
+
|
|
937
|
+
print("[CameraPreview] base64 - Resolving capture call")
|
|
938
|
+
call.resolve(result)
|
|
939
|
+
} else {
|
|
940
|
+
do {
|
|
941
|
+
let fileUrl = self.getTempFilePath()
|
|
942
|
+
try imageDataWithExif.write(to: fileUrl)
|
|
943
|
+
var result = JSObject()
|
|
944
|
+
result["value"] = fileUrl.absoluteString
|
|
945
|
+
result["exif"] = exifData
|
|
946
|
+
print("[CameraPreview] filePath - Resolving capture call")
|
|
947
|
+
call.resolve(result)
|
|
948
|
+
} catch {
|
|
949
|
+
call.reject("Error writing image to file")
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
private func getExifData(from imageData: Data) -> JSObject {
|
|
959
|
+
guard let imageSource = CGImageSourceCreateWithData(imageData as CFData, nil),
|
|
960
|
+
let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [String: Any],
|
|
961
|
+
let exifDict = imageProperties[kCGImagePropertyExifDictionary as String] as? [String: Any] else {
|
|
962
|
+
return [:]
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
var exifData = JSObject()
|
|
966
|
+
for (key, value) in exifDict {
|
|
967
|
+
// Convert value to JSValue-compatible type
|
|
968
|
+
if let stringValue = value as? String {
|
|
969
|
+
exifData[key] = stringValue
|
|
970
|
+
} else if let numberValue = value as? NSNumber {
|
|
971
|
+
exifData[key] = numberValue
|
|
972
|
+
} else if let boolValue = value as? Bool {
|
|
973
|
+
exifData[key] = boolValue
|
|
974
|
+
} else if let arrayValue = value as? [Any] {
|
|
975
|
+
exifData[key] = arrayValue
|
|
976
|
+
} else if let dictValue = value as? [String: Any] {
|
|
977
|
+
exifData[key] = JSObject(_immutableCocoaDictionary: NSMutableDictionary(dictionary: dictValue))
|
|
978
|
+
} else {
|
|
979
|
+
// Convert other types to string as fallback
|
|
980
|
+
exifData[key] = String(describing: value)
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
return exifData
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
@objc func getSafeAreaInsets(_ call: CAPPluginCall) {
|
|
988
|
+
DispatchQueue.main.async {
|
|
989
|
+
var notchInset: CGFloat = 0
|
|
990
|
+
var orientation: Int = 0
|
|
991
|
+
|
|
992
|
+
// Get the current interface orientation
|
|
993
|
+
let interfaceOrientation: UIInterfaceOrientation? = {
|
|
994
|
+
return (UIApplication.shared.connectedScenes.first as? UIWindowScene)?.interfaceOrientation
|
|
995
|
+
}()
|
|
996
|
+
|
|
997
|
+
// Convert to orientation number (matching Android values for consistency)
|
|
998
|
+
switch interfaceOrientation {
|
|
999
|
+
case .portrait, .portraitUpsideDown:
|
|
1000
|
+
orientation = 1 // Portrait
|
|
1001
|
+
case .landscapeLeft, .landscapeRight:
|
|
1002
|
+
orientation = 2 // Landscape
|
|
1003
|
+
default:
|
|
1004
|
+
orientation = 0 // Unknown
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Get safe area insets
|
|
1008
|
+
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
|
1009
|
+
let window = windowScene.windows.first {
|
|
1010
|
+
let safeAreaInsets = window.safeAreaInsets
|
|
1011
|
+
|
|
1012
|
+
switch interfaceOrientation {
|
|
1013
|
+
case .portrait:
|
|
1014
|
+
// Portrait: notch is at the top
|
|
1015
|
+
notchInset = safeAreaInsets.top
|
|
1016
|
+
case .portraitUpsideDown:
|
|
1017
|
+
// Portrait upside down: notch is at the bottom (but we still call it "top" for consistency)
|
|
1018
|
+
notchInset = safeAreaInsets.bottom
|
|
1019
|
+
case .landscapeLeft:
|
|
1020
|
+
// Landscape left: notch is typically on the left
|
|
1021
|
+
notchInset = safeAreaInsets.left
|
|
1022
|
+
case .landscapeRight:
|
|
1023
|
+
// Landscape right: notch is typically on the right (but we use left for consistency with Android)
|
|
1024
|
+
notchInset = safeAreaInsets.right
|
|
1025
|
+
default:
|
|
1026
|
+
// Unknown orientation, default to top
|
|
1027
|
+
notchInset = safeAreaInsets.top
|
|
1028
|
+
}
|
|
1029
|
+
} else {
|
|
1030
|
+
// Fallback: use status bar height as approximation
|
|
1031
|
+
notchInset = UIApplication.shared.statusBarFrame.height
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
let result: [String: Any] = [
|
|
1035
|
+
"orientation": orientation,
|
|
1036
|
+
"top": Double(notchInset)
|
|
1037
|
+
]
|
|
1038
|
+
|
|
1039
|
+
call.resolve(result)
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
private func createImageDataWithExif(from image: UIImage, quality: Int, location: CLLocation?, originalPhotoData: Data?) -> Data? {
|
|
1044
|
+
guard let jpegDataAtQuality = image.jpegData(compressionQuality: CGFloat(Double(quality) / 100.0)) else {
|
|
1045
|
+
return nil
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// Prefer metadata from the original AVCapturePhoto file data to preserve lens/EXIF
|
|
1049
|
+
let sourceDataForMetadata = (originalPhotoData ?? jpegDataAtQuality) as CFData
|
|
1050
|
+
guard let imageSource = CGImageSourceCreateWithData(sourceDataForMetadata, nil),
|
|
1051
|
+
let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [String: Any],
|
|
1052
|
+
let cgImage = image.cgImage else {
|
|
1053
|
+
return jpegDataAtQuality
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
let mutableData = NSMutableData()
|
|
1057
|
+
guard let destination = CGImageDestinationCreateWithData(mutableData, kUTTypeJPEG, 1, nil) else {
|
|
1058
|
+
return jpegDataAtQuality
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
var finalProperties = imageProperties
|
|
1062
|
+
|
|
1063
|
+
// Ensure orientation reflects the pixel data (we pass an orientation-fixed UIImage)
|
|
1064
|
+
finalProperties[kCGImagePropertyOrientation as String] = 1
|
|
1065
|
+
|
|
1066
|
+
// Add GPS location if available
|
|
1067
|
+
if let location = location {
|
|
1068
|
+
let formatter = DateFormatter()
|
|
1069
|
+
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
|
|
1070
|
+
formatter.timeZone = TimeZone(abbreviation: "UTC")
|
|
1071
|
+
|
|
1072
|
+
let gpsDict: [String: Any] = [
|
|
1073
|
+
kCGImagePropertyGPSLatitude as String: abs(location.coordinate.latitude),
|
|
1074
|
+
kCGImagePropertyGPSLatitudeRef as String: location.coordinate.latitude >= 0 ? "N" : "S",
|
|
1075
|
+
kCGImagePropertyGPSLongitude as String: abs(location.coordinate.longitude),
|
|
1076
|
+
kCGImagePropertyGPSLongitudeRef as String: location.coordinate.longitude >= 0 ? "E" : "W",
|
|
1077
|
+
kCGImagePropertyGPSTimeStamp as String: formatter.string(from: location.timestamp),
|
|
1078
|
+
kCGImagePropertyGPSAltitude as String: location.altitude,
|
|
1079
|
+
kCGImagePropertyGPSAltitudeRef as String: location.altitude >= 0 ? 0 : 1
|
|
1080
|
+
]
|
|
1081
|
+
|
|
1082
|
+
finalProperties[kCGImagePropertyGPSDictionary as String] = gpsDict
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// Create or update TIFF dictionary for device info and set orientation to Up
|
|
1086
|
+
var tiffDict = finalProperties[kCGImagePropertyTIFFDictionary as String] as? [String: Any] ?? [:]
|
|
1087
|
+
tiffDict[kCGImagePropertyTIFFMake as String] = "Apple"
|
|
1088
|
+
tiffDict[kCGImagePropertyTIFFModel as String] = UIDevice.current.model
|
|
1089
|
+
tiffDict[kCGImagePropertyTIFFOrientation as String] = 1
|
|
1090
|
+
finalProperties[kCGImagePropertyTIFFDictionary as String] = tiffDict
|
|
1091
|
+
|
|
1092
|
+
CGImageDestinationAddImage(destination, cgImage, finalProperties as CFDictionary)
|
|
1093
|
+
|
|
1094
|
+
if CGImageDestinationFinalize(destination) {
|
|
1095
|
+
return mutableData as Data
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
return jpegDataAtQuality
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
@objc func captureSample(_ call: CAPPluginCall) {
|
|
1102
|
+
let quality: Int? = call.getInt("quality", 85)
|
|
1103
|
+
|
|
1104
|
+
self.cameraController.captureSample { image, error in
|
|
1105
|
+
guard let image = image else {
|
|
1106
|
+
print("Image capture error: \(String(describing: error))")
|
|
1107
|
+
call.reject("Image capture error: \(String(describing: error))")
|
|
1108
|
+
return
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
let imageData: Data?
|
|
1112
|
+
if self.cameraPosition == "front" {
|
|
1113
|
+
let flippedImage = image.withHorizontallyFlippedOrientation()
|
|
1114
|
+
imageData = flippedImage.jpegData(compressionQuality: CGFloat(quality!/100))
|
|
1115
|
+
} else {
|
|
1116
|
+
imageData = image.jpegData(compressionQuality: CGFloat(quality!/100))
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
if self.storeToFile == false {
|
|
1120
|
+
let imageBase64 = imageData?.base64EncodedString()
|
|
1121
|
+
call.resolve(["value": imageBase64!])
|
|
1122
|
+
} else {
|
|
1123
|
+
do {
|
|
1124
|
+
let fileUrl = self.getTempFilePath()
|
|
1125
|
+
try imageData?.write(to: fileUrl)
|
|
1126
|
+
call.resolve(["value": fileUrl.absoluteString])
|
|
1127
|
+
} catch {
|
|
1128
|
+
call.reject("Error writing image to file")
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
@objc func getSupportedFlashModes(_ call: CAPPluginCall) {
|
|
1135
|
+
do {
|
|
1136
|
+
let supportedFlashModes = try self.cameraController.getSupportedFlashModes()
|
|
1137
|
+
call.resolve(["result": supportedFlashModes])
|
|
1138
|
+
} catch {
|
|
1139
|
+
call.reject("failed to get supported flash modes")
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
@objc func getHorizontalFov(_ call: CAPPluginCall) {
|
|
1144
|
+
do {
|
|
1145
|
+
let horizontalFov = try self.cameraController.getHorizontalFov()
|
|
1146
|
+
call.resolve(["result": horizontalFov])
|
|
1147
|
+
} catch {
|
|
1148
|
+
call.reject("failed to get FOV")
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
@objc func setFlashMode(_ call: CAPPluginCall) {
|
|
1153
|
+
guard let flashMode = call.getString("flashMode") else {
|
|
1154
|
+
call.reject("failed to set flash mode. required parameter flashMode is missing")
|
|
1155
|
+
return
|
|
1156
|
+
}
|
|
1157
|
+
do {
|
|
1158
|
+
var flashModeAsEnum: AVCaptureDevice.FlashMode?
|
|
1159
|
+
switch flashMode {
|
|
1160
|
+
case "off":
|
|
1161
|
+
flashModeAsEnum = AVCaptureDevice.FlashMode.off
|
|
1162
|
+
case "on":
|
|
1163
|
+
flashModeAsEnum = AVCaptureDevice.FlashMode.on
|
|
1164
|
+
case "auto":
|
|
1165
|
+
flashModeAsEnum = AVCaptureDevice.FlashMode.auto
|
|
1166
|
+
default: break
|
|
1167
|
+
}
|
|
1168
|
+
if flashModeAsEnum != nil {
|
|
1169
|
+
try self.cameraController.setFlashMode(flashMode: flashModeAsEnum!)
|
|
1170
|
+
} else if flashMode == "torch" {
|
|
1171
|
+
try self.cameraController.setTorchMode()
|
|
1172
|
+
} else {
|
|
1173
|
+
call.reject("Flash Mode not supported")
|
|
1174
|
+
return
|
|
1175
|
+
}
|
|
1176
|
+
call.resolve()
|
|
1177
|
+
} catch {
|
|
1178
|
+
call.reject("failed to set flash mode")
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
@objc func startRecordVideo(_ call: CAPPluginCall) {
|
|
1183
|
+
do {
|
|
1184
|
+
try self.cameraController.captureVideo()
|
|
1185
|
+
call.resolve()
|
|
1186
|
+
} catch {
|
|
1187
|
+
call.reject(error.localizedDescription)
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
@objc func stopRecordVideo(_ call: CAPPluginCall) {
|
|
1192
|
+
self.cameraController.stopRecording { (fileURL, error) in
|
|
1193
|
+
guard let fileURL = fileURL else {
|
|
1194
|
+
print(error ?? "Video capture error")
|
|
1195
|
+
guard let error = error else {
|
|
1196
|
+
call.reject("Video capture error")
|
|
1197
|
+
return
|
|
1198
|
+
}
|
|
1199
|
+
call.reject(error.localizedDescription)
|
|
1200
|
+
return
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
call.resolve(["videoFilePath": fileURL.absoluteString])
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
@objc func isRunning(_ call: CAPPluginCall) {
|
|
1208
|
+
let isRunning = self.isInitialized && (self.cameraController.captureSession?.isRunning ?? false)
|
|
1209
|
+
call.resolve(["isRunning": isRunning])
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
@objc func getAvailableDevices(_ call: CAPPluginCall) {
|
|
1213
|
+
let deviceTypes: [AVCaptureDevice.DeviceType] = [
|
|
1214
|
+
.builtInWideAngleCamera,
|
|
1215
|
+
.builtInUltraWideCamera,
|
|
1216
|
+
.builtInTelephotoCamera,
|
|
1217
|
+
.builtInDualCamera,
|
|
1218
|
+
.builtInDualWideCamera,
|
|
1219
|
+
.builtInTripleCamera,
|
|
1220
|
+
.builtInTrueDepthCamera
|
|
1221
|
+
]
|
|
1222
|
+
|
|
1223
|
+
let session = AVCaptureDevice.DiscoverySession(
|
|
1224
|
+
deviceTypes: deviceTypes,
|
|
1225
|
+
mediaType: .video,
|
|
1226
|
+
position: .unspecified
|
|
1227
|
+
)
|
|
1228
|
+
|
|
1229
|
+
var devices: [[String: Any]] = []
|
|
1230
|
+
|
|
1231
|
+
// Collect all devices by position
|
|
1232
|
+
for device in session.devices {
|
|
1233
|
+
var lenses: [[String: Any]] = []
|
|
1234
|
+
|
|
1235
|
+
let constituentDevices = device.isVirtualDevice ? device.constituentDevices : [device]
|
|
1236
|
+
|
|
1237
|
+
for lensDevice in constituentDevices {
|
|
1238
|
+
var deviceType: String
|
|
1239
|
+
switch lensDevice.deviceType {
|
|
1240
|
+
case .builtInWideAngleCamera: deviceType = "wideAngle"
|
|
1241
|
+
case .builtInUltraWideCamera: deviceType = "ultraWide"
|
|
1242
|
+
case .builtInTelephotoCamera: deviceType = "telephoto"
|
|
1243
|
+
case .builtInDualCamera: deviceType = "dual"
|
|
1244
|
+
case .builtInDualWideCamera: deviceType = "dualWide"
|
|
1245
|
+
case .builtInTripleCamera: deviceType = "triple"
|
|
1246
|
+
case .builtInTrueDepthCamera: deviceType = "trueDepth"
|
|
1247
|
+
default: deviceType = "unknown"
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
var baseZoomRatio: Float = 1.0
|
|
1251
|
+
if lensDevice.deviceType == .builtInUltraWideCamera {
|
|
1252
|
+
baseZoomRatio = 0.5
|
|
1253
|
+
} else if lensDevice.deviceType == .builtInTelephotoCamera {
|
|
1254
|
+
baseZoomRatio = 2.0 // A common value for telephoto lenses
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
let lensInfo: [String: Any] = [
|
|
1258
|
+
"label": lensDevice.localizedName,
|
|
1259
|
+
"deviceType": deviceType,
|
|
1260
|
+
"focalLength": 4.25, // Placeholder
|
|
1261
|
+
"baseZoomRatio": baseZoomRatio,
|
|
1262
|
+
"minZoom": Float(lensDevice.minAvailableVideoZoomFactor),
|
|
1263
|
+
"maxZoom": Float(lensDevice.maxAvailableVideoZoomFactor)
|
|
1264
|
+
]
|
|
1265
|
+
lenses.append(lensInfo)
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
let deviceData: [String: Any] = [
|
|
1269
|
+
"deviceId": device.uniqueID,
|
|
1270
|
+
"label": device.localizedName,
|
|
1271
|
+
"position": device.position == .front ? "front" : "rear",
|
|
1272
|
+
"lenses": lenses,
|
|
1273
|
+
"minZoom": Float(device.minAvailableVideoZoomFactor),
|
|
1274
|
+
"maxZoom": Float(device.maxAvailableVideoZoomFactor),
|
|
1275
|
+
"isLogical": device.isVirtualDevice
|
|
1276
|
+
]
|
|
1277
|
+
|
|
1278
|
+
devices.append(deviceData)
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
call.resolve(["devices": devices])
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
@objc func getZoom(_ call: CAPPluginCall) {
|
|
1285
|
+
guard isInitialized else {
|
|
1286
|
+
call.reject("Camera not initialized")
|
|
1287
|
+
return
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
do {
|
|
1291
|
+
let zoomInfo = try self.cameraController.getZoom()
|
|
1292
|
+
let lensInfo = try self.cameraController.getCurrentLensInfo()
|
|
1293
|
+
let displayMultiplier = self.cameraController.getDisplayZoomMultiplier()
|
|
1294
|
+
|
|
1295
|
+
var minZoom = zoomInfo.min
|
|
1296
|
+
var maxZoom = zoomInfo.max
|
|
1297
|
+
var currentZoom = zoomInfo.current
|
|
1298
|
+
|
|
1299
|
+
// Apply iOS 18+ display multiplier so UI sees the expected values
|
|
1300
|
+
if displayMultiplier != 1.0 {
|
|
1301
|
+
minZoom *= displayMultiplier
|
|
1302
|
+
maxZoom *= displayMultiplier
|
|
1303
|
+
currentZoom *= displayMultiplier
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
call.resolve([
|
|
1307
|
+
"min": minZoom,
|
|
1308
|
+
"max": maxZoom,
|
|
1309
|
+
"current": currentZoom,
|
|
1310
|
+
"lens": [
|
|
1311
|
+
"focalLength": lensInfo.focalLength,
|
|
1312
|
+
"deviceType": lensInfo.deviceType,
|
|
1313
|
+
"baseZoomRatio": lensInfo.baseZoomRatio,
|
|
1314
|
+
"digitalZoom": Float(currentZoom) / lensInfo.baseZoomRatio
|
|
1315
|
+
]
|
|
1316
|
+
])
|
|
1317
|
+
} catch {
|
|
1318
|
+
call.reject("Failed to get zoom: \(error.localizedDescription)")
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
@objc func setZoom(_ call: CAPPluginCall) {
|
|
1323
|
+
guard isInitialized else {
|
|
1324
|
+
call.reject("Camera not initialized")
|
|
1325
|
+
return
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
guard var level = call.getFloat("level") else {
|
|
1329
|
+
call.reject("level parameter is required")
|
|
1330
|
+
return
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// If using the multi-lens camera, translate the JS zoom value for the native layer
|
|
1334
|
+
// First, convert from UI/display zoom to native zoom using the iOS 18 multiplier
|
|
1335
|
+
let displayMultiplier = self.cameraController.getDisplayZoomMultiplier()
|
|
1336
|
+
if displayMultiplier != 1.0 {
|
|
1337
|
+
level = level / displayMultiplier
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
let ramp = call.getBool("ramp") ?? true
|
|
1341
|
+
let autoFocus = call.getBool("autoFocus") ?? false
|
|
1342
|
+
|
|
1343
|
+
do {
|
|
1344
|
+
try self.cameraController.setZoom(level: CGFloat(level), ramp: ramp, autoFocus: autoFocus)
|
|
1345
|
+
call.resolve()
|
|
1346
|
+
} catch {
|
|
1347
|
+
call.reject("Failed to set zoom: \(error.localizedDescription)")
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
@objc func getFlashMode(_ call: CAPPluginCall) {
|
|
1352
|
+
guard isInitialized else {
|
|
1353
|
+
call.reject("Camera not initialized")
|
|
1354
|
+
return
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
do {
|
|
1358
|
+
let flashMode = try self.cameraController.getFlashMode()
|
|
1359
|
+
call.resolve(["flashMode": flashMode])
|
|
1360
|
+
} catch {
|
|
1361
|
+
call.reject("Failed to get flash mode: \(error.localizedDescription)")
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
@objc func setDeviceId(_ call: CAPPluginCall) {
|
|
1366
|
+
guard isInitialized else {
|
|
1367
|
+
call.reject("Camera not initialized")
|
|
1368
|
+
return
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
guard let deviceId = call.getString("deviceId") else {
|
|
1372
|
+
call.reject("deviceId parameter is required")
|
|
1373
|
+
return
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// Disable user interaction during device swap
|
|
1377
|
+
self.previewView.isUserInteractionEnabled = false
|
|
1378
|
+
|
|
1379
|
+
do {
|
|
1380
|
+
try self.cameraController.swapToDevice(deviceId: deviceId)
|
|
1381
|
+
|
|
1382
|
+
// Update preview layer frame without animation
|
|
1383
|
+
CATransaction.begin()
|
|
1384
|
+
CATransaction.setDisableActions(true)
|
|
1385
|
+
self.cameraController.previewLayer?.frame = self.previewView.bounds
|
|
1386
|
+
self.cameraController.previewLayer?.videoGravity = .resizeAspectFill
|
|
1387
|
+
CATransaction.commit()
|
|
1388
|
+
|
|
1389
|
+
self.previewView.isUserInteractionEnabled = true
|
|
1390
|
+
|
|
1391
|
+
// Ensure webview remains transparent after device switch
|
|
1392
|
+
self.makeWebViewTransparent()
|
|
1393
|
+
|
|
1394
|
+
call.resolve()
|
|
1395
|
+
} catch {
|
|
1396
|
+
self.previewView.isUserInteractionEnabled = true
|
|
1397
|
+
call.reject("Failed to swap to device \(deviceId): \(error.localizedDescription)")
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
@objc func getDeviceId(_ call: CAPPluginCall) {
|
|
1402
|
+
guard isInitialized else {
|
|
1403
|
+
call.reject("Camera not initialized")
|
|
1404
|
+
return
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
do {
|
|
1408
|
+
let deviceId = try self.cameraController.getCurrentDeviceId()
|
|
1409
|
+
call.resolve(["deviceId": deviceId])
|
|
1410
|
+
} catch {
|
|
1411
|
+
call.reject("Failed to get device ID: \(error.localizedDescription)")
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// MARK: - Capacitor Permissions
|
|
1416
|
+
|
|
1417
|
+
private func requestLocationPermission(completion: @escaping (Bool) -> Void) {
|
|
1418
|
+
print("[CameraPreview] requestLocationPermission called")
|
|
1419
|
+
if self.locationManager == nil {
|
|
1420
|
+
print("[CameraPreview] Creating location manager")
|
|
1421
|
+
self.locationManager = CLLocationManager()
|
|
1422
|
+
self.locationManager?.delegate = self
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
let authStatus = self.locationManager?.authorizationStatus
|
|
1426
|
+
print("[CameraPreview] Current authorization status: \(String(describing: authStatus))")
|
|
1427
|
+
|
|
1428
|
+
switch authStatus {
|
|
1429
|
+
case .authorizedWhenInUse, .authorizedAlways:
|
|
1430
|
+
print("[CameraPreview] Location already authorized")
|
|
1431
|
+
completion(true)
|
|
1432
|
+
case .notDetermined:
|
|
1433
|
+
print("[CameraPreview] Location not determined, requesting authorization...")
|
|
1434
|
+
self.permissionCompletion = completion
|
|
1435
|
+
self.locationManager?.requestWhenInUseAuthorization()
|
|
1436
|
+
case .denied, .restricted:
|
|
1437
|
+
print("[CameraPreview] Location denied or restricted")
|
|
1438
|
+
completion(false)
|
|
1439
|
+
case .none:
|
|
1440
|
+
print("[CameraPreview] Location manager authorization status is nil")
|
|
1441
|
+
completion(false)
|
|
1442
|
+
@unknown default:
|
|
1443
|
+
print("[CameraPreview] Unknown authorization status")
|
|
1444
|
+
completion(false)
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
private var permissionCompletion: ((Bool) -> Void)?
|
|
1449
|
+
|
|
1450
|
+
public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
|
1451
|
+
let status = manager.authorizationStatus
|
|
1452
|
+
print("[CameraPreview] locationManagerDidChangeAuthorization called, status: \(status.rawValue), thread: \(Thread.current)")
|
|
1453
|
+
|
|
1454
|
+
// Handle pending capture call if we have one
|
|
1455
|
+
if let callID = self.permissionCallID, self.waitingForLocation {
|
|
1456
|
+
print("[CameraPreview] Found pending capture call ID: \(callID)")
|
|
1457
|
+
|
|
1458
|
+
let handleAuthorization = {
|
|
1459
|
+
print("[CameraPreview] Getting saved call on thread: \(Thread.current)")
|
|
1460
|
+
guard let call = self.bridge?.savedCall(withID: callID) else {
|
|
1461
|
+
print("[CameraPreview] ERROR: Could not retrieve saved call")
|
|
1462
|
+
self.permissionCallID = nil
|
|
1463
|
+
self.waitingForLocation = false
|
|
1464
|
+
return
|
|
1465
|
+
}
|
|
1466
|
+
print("[CameraPreview] Successfully retrieved saved call")
|
|
1467
|
+
|
|
1468
|
+
switch status {
|
|
1469
|
+
case .authorizedWhenInUse, .authorizedAlways:
|
|
1470
|
+
print("[CameraPreview] Location authorized, getting location for capture")
|
|
1471
|
+
self.getCurrentLocation { _ in
|
|
1472
|
+
self.performCapture(call: call)
|
|
1473
|
+
self.bridge?.releaseCall(call)
|
|
1474
|
+
self.permissionCallID = nil
|
|
1475
|
+
self.waitingForLocation = false
|
|
1476
|
+
}
|
|
1477
|
+
case .denied, .restricted:
|
|
1478
|
+
print("[CameraPreview] Location denied, rejecting capture")
|
|
1479
|
+
call.reject("Location permission denied")
|
|
1480
|
+
self.bridge?.releaseCall(call)
|
|
1481
|
+
self.permissionCallID = nil
|
|
1482
|
+
self.waitingForLocation = false
|
|
1483
|
+
case .notDetermined:
|
|
1484
|
+
print("[CameraPreview] Authorization not determined yet")
|
|
1485
|
+
// Don't do anything, wait for user response
|
|
1486
|
+
@unknown default:
|
|
1487
|
+
print("[CameraPreview] Unknown status, rejecting capture")
|
|
1488
|
+
call.reject("Unknown location permission status")
|
|
1489
|
+
self.bridge?.releaseCall(call)
|
|
1490
|
+
self.permissionCallID = nil
|
|
1491
|
+
self.waitingForLocation = false
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
// Check if we're already on main thread
|
|
1496
|
+
if Thread.isMainThread {
|
|
1497
|
+
print("[CameraPreview] Already on main thread")
|
|
1498
|
+
handleAuthorization()
|
|
1499
|
+
} else {
|
|
1500
|
+
print("[CameraPreview] Not on main thread, dispatching")
|
|
1501
|
+
DispatchQueue.main.async(execute: handleAuthorization)
|
|
1502
|
+
}
|
|
1503
|
+
} else {
|
|
1504
|
+
print("[CameraPreview] No pending capture call")
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
|
1509
|
+
print("[CameraPreview] locationManager didFailWithError: \(error.localizedDescription)")
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
private func getCurrentLocation(completion: @escaping (CLLocation?) -> Void) {
|
|
1513
|
+
print("[CameraPreview] getCurrentLocation called")
|
|
1514
|
+
self.locationCompletion = completion
|
|
1515
|
+
self.locationManager?.startUpdatingLocation()
|
|
1516
|
+
print("[CameraPreview] Started updating location")
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
private var locationCompletion: ((CLLocation?) -> Void)?
|
|
1520
|
+
|
|
1521
|
+
public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
|
1522
|
+
print("[CameraPreview] locationManager didUpdateLocations called, locations count: \(locations.count)")
|
|
1523
|
+
self.currentLocation = locations.last
|
|
1524
|
+
if let completion = locationCompletion {
|
|
1525
|
+
print("[CameraPreview] Calling location completion with location: \(self.currentLocation?.description ?? "nil")")
|
|
1526
|
+
self.locationManager?.stopUpdatingLocation()
|
|
1527
|
+
completion(self.currentLocation)
|
|
1528
|
+
locationCompletion = nil
|
|
1529
|
+
} else {
|
|
1530
|
+
print("[CameraPreview] No location completion handler found")
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
private func saveImageDataToGallery(imageData: Data, completion: @escaping (Bool, Error?) -> Void) {
|
|
1535
|
+
// Check if NSPhotoLibraryUsageDescription is present in Info.plist
|
|
1536
|
+
guard Bundle.main.object(forInfoDictionaryKey: "NSPhotoLibraryUsageDescription") != nil else {
|
|
1537
|
+
let error = NSError(domain: "CameraPreview", code: 2, userInfo: [
|
|
1538
|
+
NSLocalizedDescriptionKey: "NSPhotoLibraryUsageDescription key missing from Info.plist. Add this key with a description of how your app uses photo library access."
|
|
1539
|
+
])
|
|
1540
|
+
completion(false, error)
|
|
1541
|
+
return
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
let status = PHPhotoLibrary.authorizationStatus()
|
|
1545
|
+
|
|
1546
|
+
switch status {
|
|
1547
|
+
case .authorized:
|
|
1548
|
+
performSaveDataToGallery(imageData: imageData, completion: completion)
|
|
1549
|
+
case .notDetermined:
|
|
1550
|
+
PHPhotoLibrary.requestAuthorization { newStatus in
|
|
1551
|
+
if newStatus == .authorized {
|
|
1552
|
+
self.performSaveDataToGallery(imageData: imageData, completion: completion)
|
|
1553
|
+
} else {
|
|
1554
|
+
completion(false, NSError(domain: "CameraPreview", code: 1, userInfo: [NSLocalizedDescriptionKey: "Photo library access denied"]))
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
case .denied, .restricted:
|
|
1558
|
+
completion(false, NSError(domain: "CameraPreview", code: 1, userInfo: [NSLocalizedDescriptionKey: "Photo library access denied"]))
|
|
1559
|
+
case .limited:
|
|
1560
|
+
performSaveDataToGallery(imageData: imageData, completion: completion)
|
|
1561
|
+
@unknown default:
|
|
1562
|
+
completion(false, NSError(domain: "CameraPreview", code: 1, userInfo: [NSLocalizedDescriptionKey: "Unknown photo library authorization status"]))
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
private func performSaveDataToGallery(imageData: Data, completion: @escaping (Bool, Error?) -> Void) {
|
|
1567
|
+
// Create a temporary file to write the JPEG data with EXIF
|
|
1568
|
+
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".jpg")
|
|
1569
|
+
|
|
1570
|
+
do {
|
|
1571
|
+
try imageData.write(to: tempURL)
|
|
1572
|
+
|
|
1573
|
+
PHPhotoLibrary.shared().performChanges({
|
|
1574
|
+
PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: tempURL)
|
|
1575
|
+
}, completionHandler: { success, error in
|
|
1576
|
+
// Clean up temporary file
|
|
1577
|
+
try? FileManager.default.removeItem(at: tempURL)
|
|
1578
|
+
|
|
1579
|
+
completion(success, error)
|
|
1580
|
+
})
|
|
1581
|
+
} catch {
|
|
1582
|
+
completion(false, error)
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
private func isPortrait() -> Bool {
|
|
1587
|
+
let orientation = UIDevice.current.orientation
|
|
1588
|
+
if orientation.isValidInterfaceOrientation {
|
|
1589
|
+
return orientation.isPortrait
|
|
1590
|
+
} else {
|
|
1591
|
+
let interfaceOrientation: UIInterfaceOrientation? = {
|
|
1592
|
+
if Thread.isMainThread {
|
|
1593
|
+
return (UIApplication.shared.connectedScenes.first as? UIWindowScene)?.interfaceOrientation
|
|
1594
|
+
} else {
|
|
1595
|
+
var value: UIInterfaceOrientation?
|
|
1596
|
+
DispatchQueue.main.sync {
|
|
1597
|
+
value = (UIApplication.shared.connectedScenes.first as? UIWindowScene)?.interfaceOrientation
|
|
1598
|
+
}
|
|
1599
|
+
return value
|
|
1600
|
+
}
|
|
1601
|
+
}()
|
|
1602
|
+
return interfaceOrientation?.isPortrait ?? false
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
private func calculateCameraFrame(x: CGFloat? = nil, y: CGFloat? = nil, width: CGFloat? = nil, height: CGFloat? = nil, aspectRatio: String? = nil) -> CGRect {
|
|
1607
|
+
// Use provided values or existing ones
|
|
1608
|
+
let currentWidth = width ?? self.width ?? UIScreen.main.bounds.size.width
|
|
1609
|
+
let currentHeight = height ?? self.height ?? UIScreen.main.bounds.size.height
|
|
1610
|
+
let currentX = x ?? self.posX ?? -1
|
|
1611
|
+
let currentY = y ?? self.posY ?? -1
|
|
1612
|
+
let currentAspectRatio = aspectRatio ?? self.aspectRatio
|
|
1613
|
+
|
|
1614
|
+
let paddingBottom = self.paddingBottom ?? 0
|
|
1615
|
+
let adjustedHeight = currentHeight - CGFloat(paddingBottom)
|
|
1616
|
+
|
|
1617
|
+
// Cache webView dimensions for performance
|
|
1618
|
+
let webViewWidth = self.webView?.frame.width ?? UIScreen.main.bounds.width
|
|
1619
|
+
let webViewHeight = self.webView?.frame.height ?? UIScreen.main.bounds.height
|
|
1620
|
+
|
|
1621
|
+
let isPortrait = self.isPortrait()
|
|
1622
|
+
|
|
1623
|
+
var finalX = currentX
|
|
1624
|
+
var finalY = currentY
|
|
1625
|
+
var finalWidth = currentWidth
|
|
1626
|
+
var finalHeight = adjustedHeight
|
|
1627
|
+
|
|
1628
|
+
// Handle auto-centering when position is -1
|
|
1629
|
+
if currentX == -1 || currentY == -1 {
|
|
1630
|
+
// Only override dimensions if aspect ratio is provided and no explicit dimensions given
|
|
1631
|
+
if let ratio = currentAspectRatio,
|
|
1632
|
+
currentWidth == UIScreen.main.bounds.size.width &&
|
|
1633
|
+
currentHeight == UIScreen.main.bounds.size.height {
|
|
1634
|
+
finalWidth = webViewWidth
|
|
1635
|
+
|
|
1636
|
+
// width: 428.0 height: 926.0 - portrait
|
|
1637
|
+
|
|
1638
|
+
print("[CameraPreview] width: \(UIScreen.main.bounds.size.width) height: \(UIScreen.main.bounds.size.height)")
|
|
1639
|
+
|
|
1640
|
+
// Calculate dimensions using centralized method
|
|
1641
|
+
let dimensions = calculateDimensionsForAspectRatio(ratio, availableWidth: finalWidth, availableHeight: webViewHeight - paddingBottom, isPortrait: isPortrait)
|
|
1642
|
+
if isPortrait {
|
|
1643
|
+
finalHeight = dimensions.height
|
|
1644
|
+
finalWidth = dimensions.width
|
|
1645
|
+
} else {
|
|
1646
|
+
// In landscape, recalculate based on available space
|
|
1647
|
+
let landscapeDimensions = calculateDimensionsForAspectRatio(ratio, availableWidth: webViewWidth, availableHeight: webViewHeight - paddingBottom, isPortrait: isPortrait)
|
|
1648
|
+
finalWidth = landscapeDimensions.width
|
|
1649
|
+
finalHeight = landscapeDimensions.height
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
// Center horizontally if x is -1
|
|
1654
|
+
if currentX == -1 {
|
|
1655
|
+
finalX = (webViewWidth - finalWidth) / 2
|
|
1656
|
+
} else {
|
|
1657
|
+
finalX = currentX
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
// Position vertically if y is -1
|
|
1661
|
+
// TODO: fix top, bottom for landscape
|
|
1662
|
+
if currentY == -1 {
|
|
1663
|
+
// Use full screen height for positioning
|
|
1664
|
+
let screenHeight = UIScreen.main.bounds.size.height
|
|
1665
|
+
let screenWidth = UIScreen.main.bounds.size.width
|
|
1666
|
+
switch self.positioning {
|
|
1667
|
+
case "top":
|
|
1668
|
+
finalY = 0
|
|
1669
|
+
print("[CameraPreview] Positioning at top: finalY=0")
|
|
1670
|
+
case "bottom":
|
|
1671
|
+
finalY = screenHeight - finalHeight
|
|
1672
|
+
print("[CameraPreview] Positioning at bottom: screenHeight=\(screenHeight), finalHeight=\(finalHeight), finalY=\(finalY)")
|
|
1673
|
+
default: // "center"
|
|
1674
|
+
if isPortrait {
|
|
1675
|
+
finalY = (screenHeight - finalHeight) / 2
|
|
1676
|
+
print("[CameraPreview] Centering vertically: screenHeight=\(screenHeight), finalHeight=\(finalHeight), finalY=\(finalY)")
|
|
1677
|
+
} else {
|
|
1678
|
+
// In landscape, center both horizontally and vertically
|
|
1679
|
+
finalY = (screenHeight - finalHeight) / 2
|
|
1680
|
+
finalX = (screenWidth - finalWidth) / 2
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
} else {
|
|
1684
|
+
finalY = currentY
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
return CGRect(x: finalX, y: finalY, width: finalWidth, height: finalHeight)
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
private func updateCameraFrame() {
|
|
1692
|
+
guard let width = self.width, let height = self.height, let posX = self.posX, let posY = self.posY else {
|
|
1693
|
+
return
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
// Ensure UI operations happen on main thread
|
|
1697
|
+
guard Thread.isMainThread else {
|
|
1698
|
+
DispatchQueue.main.async {
|
|
1699
|
+
self.updateCameraFrame()
|
|
1700
|
+
}
|
|
1701
|
+
return
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
// Calculate the base frame using the factorized method
|
|
1705
|
+
var frame = calculateCameraFrame()
|
|
1706
|
+
|
|
1707
|
+
// Apply aspect ratio adjustments only if not auto-centering
|
|
1708
|
+
if posX != -1 && posY != -1, let aspectRatio = self.aspectRatio {
|
|
1709
|
+
let isPortrait = self.isPortrait()
|
|
1710
|
+
let ratio = parseAspectRatio(aspectRatio, isPortrait: isPortrait)
|
|
1711
|
+
let currentRatio = frame.width / frame.height
|
|
1712
|
+
|
|
1713
|
+
if currentRatio > ratio {
|
|
1714
|
+
let newWidth = frame.height * ratio
|
|
1715
|
+
frame.origin.x = frame.origin.x + (frame.width - newWidth) / 2
|
|
1716
|
+
frame.size.width = newWidth
|
|
1717
|
+
} else {
|
|
1718
|
+
let newHeight = frame.width / ratio
|
|
1719
|
+
frame.origin.y = frame.origin.y + (frame.height - newHeight) / 2
|
|
1720
|
+
frame.size.height = newHeight
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
// Disable ALL animations for frame updates - we want instant positioning
|
|
1725
|
+
CATransaction.begin()
|
|
1726
|
+
CATransaction.setDisableActions(true)
|
|
1727
|
+
|
|
1728
|
+
// Batch UI updates for better performance
|
|
1729
|
+
if self.previewView == nil {
|
|
1730
|
+
self.previewView = UIView(frame: frame)
|
|
1731
|
+
self.previewView.backgroundColor = UIColor.clear
|
|
1732
|
+
} else {
|
|
1733
|
+
self.previewView.frame = frame
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
// Update preview layer frame efficiently
|
|
1737
|
+
if let previewLayer = self.cameraController.previewLayer {
|
|
1738
|
+
previewLayer.frame = self.previewView.bounds
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
// Update grid overlay frame if it exists
|
|
1742
|
+
if let gridOverlay = self.cameraController.gridOverlayView {
|
|
1743
|
+
gridOverlay.frame = self.previewView.bounds
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
CATransaction.commit()
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
@objc func getPreviewSize(_ call: CAPPluginCall) {
|
|
1750
|
+
guard self.isInitialized else {
|
|
1751
|
+
call.reject("camera not started")
|
|
1752
|
+
return
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
DispatchQueue.main.async {
|
|
1756
|
+
var result = JSObject()
|
|
1757
|
+
result["x"] = Double(self.previewView.frame.origin.x)
|
|
1758
|
+
result["y"] = Double(self.previewView.frame.origin.y)
|
|
1759
|
+
result["width"] = Double(self.previewView.frame.width)
|
|
1760
|
+
result["height"] = Double(self.previewView.frame.height)
|
|
1761
|
+
call.resolve(result)
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
@objc func setPreviewSize(_ call: CAPPluginCall) {
|
|
1766
|
+
guard self.isInitialized else {
|
|
1767
|
+
call.reject("camera not started")
|
|
1768
|
+
return
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
// Always set to -1 for auto-centering if not explicitly provided
|
|
1772
|
+
if let x = call.getInt("x") {
|
|
1773
|
+
self.posX = CGFloat(x)
|
|
1774
|
+
} else {
|
|
1775
|
+
self.posX = -1 // Auto-center if X not provided
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
if let y = call.getInt("y") {
|
|
1779
|
+
self.posY = CGFloat(y)
|
|
1780
|
+
} else {
|
|
1781
|
+
self.posY = -1 // Auto-center if Y not provided
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
if let width = call.getInt("width") { self.width = CGFloat(width) }
|
|
1785
|
+
if let height = call.getInt("height") { self.height = CGFloat(height) }
|
|
1786
|
+
|
|
1787
|
+
DispatchQueue.main.async {
|
|
1788
|
+
// Direct update without animation for better performance
|
|
1789
|
+
self.updateCameraFrame()
|
|
1790
|
+
self.makeWebViewTransparent()
|
|
1791
|
+
|
|
1792
|
+
// Return the actual preview bounds
|
|
1793
|
+
var result = JSObject()
|
|
1794
|
+
result["x"] = Double(self.previewView.frame.origin.x)
|
|
1795
|
+
result["y"] = Double(self.previewView.frame.origin.y)
|
|
1796
|
+
result["width"] = Double(self.previewView.frame.width)
|
|
1797
|
+
result["height"] = Double(self.previewView.frame.height)
|
|
1798
|
+
call.resolve(result)
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
@objc func setFocus(_ call: CAPPluginCall) {
|
|
1803
|
+
guard isInitialized else {
|
|
1804
|
+
call.reject("Camera not initialized")
|
|
1805
|
+
return
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
guard let x = call.getFloat("x"), let y = call.getFloat("y") else {
|
|
1809
|
+
call.reject("x and y parameters are required")
|
|
1810
|
+
return
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
// Reject if values are outside 0-1 range
|
|
1814
|
+
if x < 0 || x > 1 || y < 0 || y > 1 {
|
|
1815
|
+
call.reject("Focus coordinates must be between 0 and 1")
|
|
1816
|
+
return
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
DispatchQueue.main.async {
|
|
1820
|
+
do {
|
|
1821
|
+
// Convert normalized coordinates to view coordinates
|
|
1822
|
+
let viewX = CGFloat(x) * self.previewView.bounds.width
|
|
1823
|
+
let viewY = CGFloat(y) * self.previewView.bounds.height
|
|
1824
|
+
let focusPoint = CGPoint(x: viewX, y: viewY)
|
|
1825
|
+
|
|
1826
|
+
// Convert view coordinates to device coordinates
|
|
1827
|
+
guard let previewLayer = self.cameraController.previewLayer else {
|
|
1828
|
+
call.reject("Preview layer not available")
|
|
1829
|
+
return
|
|
1830
|
+
}
|
|
1831
|
+
let devicePoint = previewLayer.captureDevicePointConverted(fromLayerPoint: focusPoint)
|
|
1832
|
+
|
|
1833
|
+
try self.cameraController.setFocus(at: devicePoint, showIndicator: true, in: self.previewView)
|
|
1834
|
+
call.resolve()
|
|
1835
|
+
} catch {
|
|
1836
|
+
call.reject("Failed to set focus: \(error.localizedDescription)")
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
@objc private func handleOrientationChange() {
|
|
1842
|
+
DispatchQueue.main.async {
|
|
1843
|
+
let result = self.rawSetAspectRatio()
|
|
1844
|
+
self.notifyListeners("screenResize", data: result)
|
|
1845
|
+
self.notifyListeners("orientationChange", data: ["orientation": self.currentOrientationString()])
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
@objc func deleteFile(_ call: CAPPluginCall) {
|
|
1850
|
+
guard let path = call.getString("path"), !path.isEmpty else {
|
|
1851
|
+
call.reject("path parameter is required")
|
|
1852
|
+
return
|
|
1853
|
+
}
|
|
1854
|
+
let url: URL?
|
|
1855
|
+
if path.hasPrefix("file://") {
|
|
1856
|
+
url = URL(string: path)
|
|
1857
|
+
} else {
|
|
1858
|
+
url = URL(fileURLWithPath: path)
|
|
1859
|
+
}
|
|
1860
|
+
guard let fileURL = url else {
|
|
1861
|
+
call.reject("Invalid path")
|
|
1862
|
+
return
|
|
1863
|
+
}
|
|
1864
|
+
do {
|
|
1865
|
+
if FileManager.default.fileExists(atPath: fileURL.path) {
|
|
1866
|
+
try FileManager.default.removeItem(at: fileURL)
|
|
1867
|
+
call.resolve(["success": true])
|
|
1868
|
+
} else {
|
|
1869
|
+
call.resolve(["success": false])
|
|
1870
|
+
}
|
|
1871
|
+
} catch {
|
|
1872
|
+
call.reject("Failed to delete file: \(error.localizedDescription)")
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
// MARK: - Orientation
|
|
1877
|
+
private func currentOrientationString() -> String {
|
|
1878
|
+
// Prefer interface orientation for UI-consistent results
|
|
1879
|
+
let orientation: UIInterfaceOrientation? = {
|
|
1880
|
+
if Thread.isMainThread {
|
|
1881
|
+
return (UIApplication.shared.connectedScenes.first as? UIWindowScene)?.interfaceOrientation
|
|
1882
|
+
} else {
|
|
1883
|
+
var value: UIInterfaceOrientation?
|
|
1884
|
+
DispatchQueue.main.sync {
|
|
1885
|
+
value = (UIApplication.shared.connectedScenes.first as? UIWindowScene)?.interfaceOrientation
|
|
1886
|
+
}
|
|
1887
|
+
return value
|
|
1888
|
+
}
|
|
1889
|
+
}()
|
|
1890
|
+
switch orientation {
|
|
1891
|
+
case .portrait: return "portrait"
|
|
1892
|
+
case .portraitUpsideDown: return "portrait-upside-down"
|
|
1893
|
+
case .landscapeLeft: return "landscape-left"
|
|
1894
|
+
case .landscapeRight: return "landscape-right"
|
|
1895
|
+
default: return "unknown"
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
@objc func getOrientation(_ call: CAPPluginCall) {
|
|
1900
|
+
call.resolve(["orientation": self.currentOrientationString()])
|
|
1901
|
+
}
|
|
1902
|
+
}
|