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