@capgo/camera-preview 7.4.0-beta.2 → 7.4.0-beta.4
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 +120 -30
- package/android/.gradle/8.14.2/checksums/checksums.lock +0 -0
- package/android/.gradle/8.14.2/checksums/md5-checksums.bin +0 -0
- package/android/.gradle/8.14.2/checksums/sha1-checksums.bin +0 -0
- package/android/.gradle/8.14.2/executionHistory/executionHistory.bin +0 -0
- package/android/.gradle/8.14.2/executionHistory/executionHistory.lock +0 -0
- package/android/.gradle/8.14.2/fileHashes/fileHashes.bin +0 -0
- package/android/.gradle/8.14.2/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.14.2/fileHashes/resourceHashesCache.bin +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/file-system.probe +0 -0
- package/android/build.gradle +2 -1
- package/android/src/main/AndroidManifest.xml +1 -4
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java +111 -29
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraXView.java +152 -17
- package/android/src/main/java/com/ahm/capacitor/camera/preview/GridOverlayView.java +80 -0
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraSessionConfiguration.java +19 -5
- package/dist/docs.json +117 -4
- package/dist/esm/definitions.d.ts +52 -3
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +16 -2
- package/dist/esm/web.js +180 -78
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +178 -78
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +178 -78
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapgoCameraPreview/CameraController.swift +131 -13
- package/ios/Sources/CapgoCameraPreview/GridOverlayView.swift +65 -0
- package/ios/Sources/CapgoCameraPreview/Plugin.swift +176 -37
- package/package.json +1 -1
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import AVFoundation
|
|
10
10
|
import UIKit
|
|
11
|
+
import CoreLocation
|
|
11
12
|
|
|
12
13
|
class CameraController: NSObject {
|
|
13
14
|
var captureSession: AVCaptureSession?
|
|
@@ -26,6 +27,7 @@ class CameraController: NSObject {
|
|
|
26
27
|
var fileVideoOutput: AVCaptureMovieFileOutput?
|
|
27
28
|
|
|
28
29
|
var previewLayer: AVCaptureVideoPreviewLayer?
|
|
30
|
+
var gridOverlayView: GridOverlayView?
|
|
29
31
|
|
|
30
32
|
var flashMode = AVCaptureDevice.FlashMode.off
|
|
31
33
|
var photoCaptureCompletionBlock: ((UIImage?, Error?) -> Void)?
|
|
@@ -50,7 +52,7 @@ class CameraController: NSObject {
|
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
extension CameraController {
|
|
53
|
-
func prepare(cameraPosition: String, deviceId: String? = nil, disableAudio: Bool, cameraMode: Bool, completionHandler: @escaping (Error?) -> Void) {
|
|
55
|
+
func prepare(cameraPosition: String, deviceId: String? = nil, disableAudio: Bool, cameraMode: Bool, aspectRatio: String? = nil, completionHandler: @escaping (Error?) -> Void) {
|
|
54
56
|
func createCaptureSession() {
|
|
55
57
|
self.captureSession = AVCaptureSession()
|
|
56
58
|
}
|
|
@@ -203,11 +205,54 @@ extension CameraController {
|
|
|
203
205
|
func configurePhotoOutput(cameraMode: Bool) throws {
|
|
204
206
|
guard let captureSession = self.captureSession else { throw CameraControllerError.captureSessionIsMissing }
|
|
205
207
|
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
208
|
+
// Configure session preset based on aspect ratio and other settings
|
|
209
|
+
var targetPreset: AVCaptureSession.Preset = .photo // Default preset
|
|
210
|
+
|
|
211
|
+
if let aspectRatio = aspectRatio {
|
|
212
|
+
switch aspectRatio {
|
|
213
|
+
case "16:9":
|
|
214
|
+
// Use HD presets for 16:9 aspect ratio
|
|
215
|
+
if self.highResolutionOutput && captureSession.canSetSessionPreset(.hd1920x1080) {
|
|
216
|
+
targetPreset = .hd1920x1080
|
|
217
|
+
} else if captureSession.canSetSessionPreset(.hd1280x720) {
|
|
218
|
+
targetPreset = .hd1280x720
|
|
219
|
+
} else {
|
|
220
|
+
targetPreset = .high
|
|
221
|
+
}
|
|
222
|
+
case "4:3":
|
|
223
|
+
// Use photo preset for 4:3 aspect ratio (traditional photo format)
|
|
224
|
+
if self.highResolutionOutput && captureSession.canSetSessionPreset(.photo) {
|
|
225
|
+
targetPreset = .photo
|
|
226
|
+
} else {
|
|
227
|
+
targetPreset = .medium
|
|
228
|
+
}
|
|
229
|
+
default:
|
|
230
|
+
// Default behavior for unrecognized aspect ratios
|
|
231
|
+
if !cameraMode && self.highResolutionOutput && captureSession.canSetSessionPreset(.photo) {
|
|
232
|
+
targetPreset = .photo
|
|
233
|
+
} else if cameraMode && self.highResolutionOutput && captureSession.canSetSessionPreset(.high) {
|
|
234
|
+
targetPreset = .high
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
// Original logic when no aspect ratio is specified
|
|
239
|
+
if !cameraMode && self.highResolutionOutput && captureSession.canSetSessionPreset(.photo) {
|
|
240
|
+
targetPreset = .photo
|
|
241
|
+
} else if cameraMode && self.highResolutionOutput && captureSession.canSetSessionPreset(.high) {
|
|
242
|
+
targetPreset = .high
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Apply the determined preset
|
|
247
|
+
if captureSession.canSetSessionPreset(targetPreset) {
|
|
248
|
+
captureSession.sessionPreset = targetPreset
|
|
249
|
+
print("[CameraPreview] Set session preset to \(targetPreset) for aspect ratio: \(aspectRatio ?? "default")")
|
|
250
|
+
} else {
|
|
251
|
+
// Fallback to a basic preset if the target preset is not supported
|
|
252
|
+
print("[CameraPreview] Target preset \(targetPreset) not supported, falling back to .medium")
|
|
253
|
+
if captureSession.canSetSessionPreset(.medium) {
|
|
254
|
+
captureSession.sessionPreset = .medium
|
|
255
|
+
}
|
|
211
256
|
}
|
|
212
257
|
|
|
213
258
|
self.photoOutput = AVCapturePhotoOutput()
|
|
@@ -275,6 +320,19 @@ extension CameraController {
|
|
|
275
320
|
updateVideoOrientation()
|
|
276
321
|
}
|
|
277
322
|
|
|
323
|
+
func addGridOverlay(to view: UIView, gridMode: String) {
|
|
324
|
+
removeGridOverlay()
|
|
325
|
+
|
|
326
|
+
gridOverlayView = GridOverlayView(frame: view.bounds)
|
|
327
|
+
gridOverlayView?.gridMode = gridMode
|
|
328
|
+
view.addSubview(gridOverlayView!)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
func removeGridOverlay() {
|
|
332
|
+
gridOverlayView?.removeFromSuperview()
|
|
333
|
+
gridOverlayView = nil
|
|
334
|
+
}
|
|
335
|
+
|
|
278
336
|
func setupGestures(target: UIView, enableZoom: Bool) {
|
|
279
337
|
setupTapGesture(target: target, selector: #selector(handleTap(_:)), delegate: self)
|
|
280
338
|
if enableZoom {
|
|
@@ -427,15 +485,75 @@ extension CameraController {
|
|
|
427
485
|
}
|
|
428
486
|
}
|
|
429
487
|
|
|
430
|
-
func captureImage(completion: @escaping (UIImage?, Error?) -> Void) {
|
|
431
|
-
guard let
|
|
488
|
+
func captureImage(width: Int?, height: Int?, quality: Float, gpsLocation: CLLocation?, completion: @escaping (UIImage?, Error?) -> Void) {
|
|
489
|
+
guard let photoOutput = self.photoOutput else {
|
|
490
|
+
completion(nil, NSError(domain: "Camera", code: 0, userInfo: [NSLocalizedDescriptionKey: "Photo output is not available"]))
|
|
491
|
+
return
|
|
492
|
+
}
|
|
493
|
+
|
|
432
494
|
let settings = AVCapturePhotoSettings()
|
|
433
495
|
|
|
434
|
-
|
|
435
|
-
|
|
496
|
+
self.photoCaptureCompletionBlock = { (image, error) in
|
|
497
|
+
if let error = error {
|
|
498
|
+
completion(nil, error)
|
|
499
|
+
return
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
guard let image = image else {
|
|
503
|
+
completion(nil, NSError(domain: "Camera", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to capture image"]))
|
|
504
|
+
return
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if let location = gpsLocation {
|
|
508
|
+
self.addGPSMetadata(to: image, location: location)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if let width = width, let height = height {
|
|
512
|
+
let resizedImage = self.resizeImage(image: image, to: CGSize(width: width, height: height))
|
|
513
|
+
completion(resizedImage, nil)
|
|
514
|
+
} else {
|
|
515
|
+
completion(image, nil)
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
photoOutput.capturePhoto(with: settings, delegate: self)
|
|
520
|
+
}
|
|
436
521
|
|
|
437
|
-
|
|
438
|
-
|
|
522
|
+
func addGPSMetadata(to image: UIImage, location: CLLocation) {
|
|
523
|
+
guard let jpegData = image.jpegData(compressionQuality: 1.0),
|
|
524
|
+
let source = CGImageSourceCreateWithData(jpegData as CFData, nil),
|
|
525
|
+
let uti = CGImageSourceGetType(source) else { return }
|
|
526
|
+
|
|
527
|
+
var metadata = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any] ?? [:]
|
|
528
|
+
|
|
529
|
+
let formatter = DateFormatter()
|
|
530
|
+
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
|
|
531
|
+
formatter.timeZone = TimeZone(abbreviation: "UTC")
|
|
532
|
+
|
|
533
|
+
let gpsDict: [String: Any] = [
|
|
534
|
+
kCGImagePropertyGPSLatitude as String: abs(location.coordinate.latitude),
|
|
535
|
+
kCGImagePropertyGPSLatitudeRef as String: location.coordinate.latitude >= 0 ? "N" : "S",
|
|
536
|
+
kCGImagePropertyGPSLongitude as String: abs(location.coordinate.longitude),
|
|
537
|
+
kCGImagePropertyGPSLongitudeRef as String: location.coordinate.longitude >= 0 ? "E" : "W",
|
|
538
|
+
kCGImagePropertyGPSTimeStamp as String: formatter.string(from: location.timestamp),
|
|
539
|
+
kCGImagePropertyGPSAltitude as String: location.altitude,
|
|
540
|
+
kCGImagePropertyGPSAltitudeRef as String: location.altitude >= 0 ? 0 : 1
|
|
541
|
+
]
|
|
542
|
+
|
|
543
|
+
metadata[kCGImagePropertyGPSDictionary as String] = gpsDict
|
|
544
|
+
|
|
545
|
+
let destData = NSMutableData()
|
|
546
|
+
guard let destination = CGImageDestinationCreateWithData(destData, uti, 1, nil) else { return }
|
|
547
|
+
CGImageDestinationAddImageFromSource(destination, source, 0, metadata as CFDictionary)
|
|
548
|
+
CGImageDestinationFinalize(destination)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
func resizeImage(image: UIImage, to size: CGSize) -> UIImage? {
|
|
552
|
+
let renderer = UIGraphicsImageRenderer(size: size)
|
|
553
|
+
let resizedImage = renderer.image { (context) in
|
|
554
|
+
image.draw(in: CGRect(origin: .zero, size: size))
|
|
555
|
+
}
|
|
556
|
+
return resizedImage
|
|
439
557
|
}
|
|
440
558
|
|
|
441
559
|
func captureSample(completion: @escaping (UIImage?, Error?) -> Void) {
|
|
@@ -677,7 +795,7 @@ extension CameraController {
|
|
|
677
795
|
|
|
678
796
|
return device.uniqueID
|
|
679
797
|
}
|
|
680
|
-
|
|
798
|
+
|
|
681
799
|
func getCurrentLensInfo() throws -> (focalLength: Float, deviceType: String, baseZoomRatio: Float) {
|
|
682
800
|
var currentCamera: AVCaptureDevice?
|
|
683
801
|
switch currentCameraPosition {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
class GridOverlayView: UIView {
|
|
4
|
+
|
|
5
|
+
var gridMode: String = "none" {
|
|
6
|
+
didSet {
|
|
7
|
+
isHidden = gridMode == "none"
|
|
8
|
+
setNeedsDisplay()
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
override init(frame: CGRect) {
|
|
13
|
+
super.init(frame: frame)
|
|
14
|
+
setup()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
required init?(coder: NSCoder) {
|
|
18
|
+
super.init(coder: coder)
|
|
19
|
+
setup()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private func setup() {
|
|
23
|
+
backgroundColor = UIColor.clear
|
|
24
|
+
isUserInteractionEnabled = false
|
|
25
|
+
isHidden = true
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
override func draw(_ rect: CGRect) {
|
|
29
|
+
guard let context = UIGraphicsGetCurrentContext() else { return }
|
|
30
|
+
|
|
31
|
+
if gridMode == "none" {
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
context.setStrokeColor(UIColor.white.withAlphaComponent(0.5).cgColor)
|
|
36
|
+
context.setLineWidth(1.0)
|
|
37
|
+
|
|
38
|
+
if gridMode == "3x3" {
|
|
39
|
+
drawGrid(context: context, rect: rect, divisions: 3)
|
|
40
|
+
} else if gridMode == "4x4" {
|
|
41
|
+
drawGrid(context: context, rect: rect, divisions: 4)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private func drawGrid(context: CGContext, rect: CGRect, divisions: Int) {
|
|
46
|
+
let stepX = rect.width / CGFloat(divisions)
|
|
47
|
+
let stepY = rect.height / CGFloat(divisions)
|
|
48
|
+
|
|
49
|
+
// Draw vertical lines
|
|
50
|
+
for i in 1..<divisions {
|
|
51
|
+
let x = CGFloat(i) * stepX
|
|
52
|
+
context.move(to: CGPoint(x: x, y: 0))
|
|
53
|
+
context.addLine(to: CGPoint(x: x, y: rect.height))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Draw horizontal lines
|
|
57
|
+
for i in 1..<divisions {
|
|
58
|
+
let y = CGFloat(i) * stepY
|
|
59
|
+
context.move(to: CGPoint(x: 0, y: y))
|
|
60
|
+
context.addLine(to: CGPoint(x: rect.width, y: y))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
context.strokePath()
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -3,6 +3,8 @@ import Capacitor
|
|
|
3
3
|
import AVFoundation
|
|
4
4
|
import Photos
|
|
5
5
|
import CoreImage
|
|
6
|
+
import CoreLocation
|
|
7
|
+
|
|
6
8
|
|
|
7
9
|
extension UIWindow {
|
|
8
10
|
static var isLandscape: Bool {
|
|
@@ -34,7 +36,7 @@ extension UIWindow {
|
|
|
34
36
|
* here: https://capacitor.ionicframework.com/docs/plugins/ios
|
|
35
37
|
*/
|
|
36
38
|
@objc(CameraPreview)
|
|
37
|
-
public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
39
|
+
public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelegate {
|
|
38
40
|
public let identifier = "CameraPreviewPlugin"
|
|
39
41
|
public let jsName = "CameraPreview"
|
|
40
42
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
@@ -56,7 +58,9 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
56
58
|
CAPPluginMethod(name: "setZoom", returnType: CAPPluginReturnPromise),
|
|
57
59
|
CAPPluginMethod(name: "getFlashMode", returnType: CAPPluginReturnPromise),
|
|
58
60
|
CAPPluginMethod(name: "setDeviceId", returnType: CAPPluginReturnPromise),
|
|
59
|
-
CAPPluginMethod(name: "getDeviceId", returnType: CAPPluginReturnPromise)
|
|
61
|
+
CAPPluginMethod(name: "getDeviceId", returnType: CAPPluginReturnPromise),
|
|
62
|
+
CAPPluginMethod(name: "setAspectRatio", returnType: CAPPluginReturnPromise),
|
|
63
|
+
CAPPluginMethod(name: "getAspectRatio", returnType: CAPPluginReturnPromise)
|
|
60
64
|
]
|
|
61
65
|
// Camera state tracking
|
|
62
66
|
private var isInitializing: Bool = false
|
|
@@ -76,33 +80,43 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
76
80
|
var enableZoom: Bool?
|
|
77
81
|
var highResolutionOutput: Bool = false
|
|
78
82
|
var disableAudio: Bool = false
|
|
79
|
-
|
|
80
|
-
|
|
83
|
+
var locationManager: CLLocationManager?
|
|
84
|
+
var currentLocation: CLLocation?
|
|
85
|
+
private var aspectRatio: String?
|
|
81
86
|
|
|
87
|
+
// MARK: - Transparency Methods
|
|
88
|
+
|
|
89
|
+
|
|
82
90
|
private func makeWebViewTransparent() {
|
|
83
91
|
guard let webView = self.webView else { return }
|
|
84
|
-
|
|
92
|
+
|
|
93
|
+
|
|
85
94
|
// Define a recursive function to traverse the view hierarchy
|
|
86
95
|
func makeSubviewsTransparent(_ view: UIView) {
|
|
87
96
|
// Set the background color to clear
|
|
88
97
|
view.backgroundColor = .clear
|
|
89
|
-
|
|
98
|
+
|
|
99
|
+
|
|
90
100
|
// Recurse for all subviews
|
|
91
101
|
for subview in view.subviews {
|
|
92
102
|
makeSubviewsTransparent(subview)
|
|
93
103
|
}
|
|
94
104
|
}
|
|
95
|
-
|
|
105
|
+
|
|
106
|
+
|
|
96
107
|
// Set the main webView to be transparent
|
|
97
108
|
webView.isOpaque = false
|
|
98
109
|
webView.backgroundColor = .clear
|
|
99
|
-
|
|
110
|
+
|
|
111
|
+
|
|
100
112
|
// Recursively make all subviews transparent
|
|
101
113
|
makeSubviewsTransparent(webView)
|
|
102
|
-
|
|
114
|
+
|
|
115
|
+
|
|
103
116
|
// Also ensure the webview's container is transparent
|
|
104
117
|
webView.superview?.backgroundColor = .clear
|
|
105
|
-
|
|
118
|
+
|
|
119
|
+
|
|
106
120
|
// Force a layout pass to apply changes
|
|
107
121
|
DispatchQueue.main.async {
|
|
108
122
|
webView.setNeedsLayout()
|
|
@@ -147,13 +161,38 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
147
161
|
}
|
|
148
162
|
|
|
149
163
|
cameraController.updateVideoOrientation()
|
|
150
|
-
|
|
164
|
+
|
|
165
|
+
cameraController.updateVideoOrientation()
|
|
166
|
+
|
|
167
|
+
// Update grid overlay frame if it exists
|
|
168
|
+
if let gridOverlay = self.cameraController.gridOverlayView {
|
|
169
|
+
gridOverlay.frame = previewView.bounds
|
|
170
|
+
}
|
|
171
|
+
|
|
151
172
|
// Ensure webview remains transparent after rotation
|
|
152
173
|
if self.isInitialized {
|
|
153
174
|
self.makeWebViewTransparent()
|
|
154
175
|
}
|
|
155
176
|
}
|
|
156
177
|
|
|
178
|
+
@objc func setAspectRatio(_ call: CAPPluginCall) {
|
|
179
|
+
guard self.isInitialized else {
|
|
180
|
+
call.reject("camera not started")
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
self.aspectRatio = call.getString("aspectRatio")
|
|
184
|
+
self.updateCameraFrame()
|
|
185
|
+
call.resolve()
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
@objc func getAspectRatio(_ call: CAPPluginCall) {
|
|
189
|
+
guard self.isInitialized else {
|
|
190
|
+
call.reject("camera not started")
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
call.resolve(["aspectRatio": self.aspectRatio ?? "fill"])
|
|
194
|
+
}
|
|
195
|
+
|
|
157
196
|
@objc func appDidBecomeActive() {
|
|
158
197
|
if self.isInitialized {
|
|
159
198
|
DispatchQueue.main.async {
|
|
@@ -161,7 +200,8 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
161
200
|
}
|
|
162
201
|
}
|
|
163
202
|
}
|
|
164
|
-
|
|
203
|
+
|
|
204
|
+
|
|
165
205
|
@objc func appWillEnterForeground() {
|
|
166
206
|
if self.isInitialized {
|
|
167
207
|
DispatchQueue.main.async {
|
|
@@ -288,7 +328,9 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
288
328
|
self.toBack = call.getBool("toBack") ?? true
|
|
289
329
|
self.storeToFile = call.getBool("storeToFile") ?? false
|
|
290
330
|
self.enableZoom = call.getBool("enableZoom") ?? false
|
|
291
|
-
self.disableAudio = call.getBool("disableAudio") ??
|
|
331
|
+
self.disableAudio = call.getBool("disableAudio") ?? true
|
|
332
|
+
self.aspectRatio = call.getString("aspectRatio")
|
|
333
|
+
let gridMode = call.getString("gridMode") ?? "none"
|
|
292
334
|
|
|
293
335
|
AVCaptureDevice.requestAccess(for: .video, completionHandler: { (granted: Bool) in
|
|
294
336
|
guard granted else {
|
|
@@ -300,18 +342,18 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
300
342
|
if self.cameraController.captureSession?.isRunning ?? false {
|
|
301
343
|
call.reject("camera already started")
|
|
302
344
|
} else {
|
|
303
|
-
self.cameraController.prepare(cameraPosition: self.cameraPosition, deviceId: deviceId, disableAudio: self.disableAudio, cameraMode: cameraMode) {error in
|
|
345
|
+
self.cameraController.prepare(cameraPosition: self.cameraPosition, deviceId: deviceId, disableAudio: self.disableAudio, cameraMode: cameraMode, aspectRatio: self.aspectRatio) {error in
|
|
304
346
|
if let error = error {
|
|
305
347
|
print(error)
|
|
306
348
|
call.reject(error.localizedDescription)
|
|
307
349
|
return
|
|
308
350
|
}
|
|
309
|
-
|
|
310
|
-
self.previewView = UIView(frame: CGRect(x: self.posX ?? 0, y: self.posY ?? 0, width: self.width!, height: height))
|
|
351
|
+
self.updateCameraFrame()
|
|
311
352
|
|
|
312
353
|
// Make webview transparent - comprehensive approach
|
|
313
354
|
self.makeWebViewTransparent()
|
|
314
|
-
|
|
355
|
+
|
|
356
|
+
|
|
315
357
|
self.webView?.superview?.addSubview(self.previewView)
|
|
316
358
|
if self.toBack! {
|
|
317
359
|
self.webView?.superview?.bringSubviewToFront(self.webView!)
|
|
@@ -321,17 +363,34 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
321
363
|
let frontView = self.toBack! ? self.webView : self.previewView
|
|
322
364
|
self.cameraController.setupGestures(target: frontView ?? self.previewView, enableZoom: self.enableZoom!)
|
|
323
365
|
|
|
366
|
+
// Add grid overlay if enabled
|
|
367
|
+
if gridMode != "none" {
|
|
368
|
+
self.cameraController.addGridOverlay(to: self.previewView, gridMode: gridMode)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Add grid overlay if enabled
|
|
372
|
+
if gridMode != "none" {
|
|
373
|
+
self.cameraController.addGridOverlay(to: self.previewView, gridMode: gridMode)
|
|
374
|
+
}
|
|
375
|
+
|
|
324
376
|
if self.rotateWhenOrientationChanged == true {
|
|
325
377
|
NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.rotated), name: UIDevice.orientationDidChangeNotification, object: nil)
|
|
326
378
|
}
|
|
327
|
-
|
|
379
|
+
|
|
380
|
+
|
|
328
381
|
// Add observers for app state changes to maintain transparency
|
|
329
382
|
NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
|
|
330
383
|
NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.appWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
|
|
331
384
|
|
|
332
385
|
self.isInitializing = false
|
|
333
386
|
self.isInitialized = true
|
|
334
|
-
|
|
387
|
+
|
|
388
|
+
var returnedObject = JSObject()
|
|
389
|
+
returnedObject["width"] = self.previewView.frame.width as any JSValue
|
|
390
|
+
returnedObject["height"] = self.previewView.frame.height as any JSValue
|
|
391
|
+
returnedObject["x"] = self.previewView.frame.origin.x as any JSValue
|
|
392
|
+
returnedObject["y"] = self.previewView.frame.origin.y as any JSValue
|
|
393
|
+
call.resolve(returnedObject)
|
|
335
394
|
|
|
336
395
|
}
|
|
337
396
|
}
|
|
@@ -368,10 +427,12 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
368
427
|
self.cameraController.previewLayer?.frame = self.previewView.bounds
|
|
369
428
|
self.cameraController.previewLayer?.videoGravity = .resizeAspectFill
|
|
370
429
|
self.previewView.isUserInteractionEnabled = true
|
|
371
|
-
|
|
430
|
+
|
|
431
|
+
|
|
372
432
|
// Ensure webview remains transparent after flip
|
|
373
433
|
self.makeWebViewTransparent()
|
|
374
|
-
|
|
434
|
+
|
|
435
|
+
|
|
375
436
|
call.resolve()
|
|
376
437
|
}
|
|
377
438
|
} catch {
|
|
@@ -408,6 +469,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
408
469
|
}
|
|
409
470
|
|
|
410
471
|
// Always attempt to stop and clean up, regardless of captureSession state
|
|
472
|
+
self.cameraController.removeGridOverlay()
|
|
411
473
|
if let previewView = self.previewView {
|
|
412
474
|
previewView.removeFromSuperview()
|
|
413
475
|
self.previewView = nil
|
|
@@ -417,7 +479,8 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
417
479
|
self.isInitialized = false
|
|
418
480
|
self.isInitializing = false
|
|
419
481
|
self.cameraController.cleanup()
|
|
420
|
-
|
|
482
|
+
|
|
483
|
+
|
|
421
484
|
// Remove notification observers
|
|
422
485
|
NotificationCenter.default.removeObserver(self)
|
|
423
486
|
|
|
@@ -440,8 +503,18 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
440
503
|
|
|
441
504
|
let quality = call.getFloat("quality", 85)
|
|
442
505
|
let saveToGallery = call.getBool("saveToGallery", false)
|
|
506
|
+
let withExifLocation = call.getBool("withExifLocation", false)
|
|
507
|
+
let width = call.getInt("width")
|
|
508
|
+
let height = call.getInt("height")
|
|
509
|
+
|
|
510
|
+
if withExifLocation {
|
|
511
|
+
self.locationManager = CLLocationManager()
|
|
512
|
+
self.locationManager?.delegate = self
|
|
513
|
+
self.locationManager?.requestWhenInUseAuthorization()
|
|
514
|
+
self.locationManager?.startUpdatingLocation()
|
|
515
|
+
}
|
|
443
516
|
|
|
444
|
-
self.cameraController.captureImage(quality: quality) { (image, error) in
|
|
517
|
+
self.cameraController.captureImage(width: width, height: height, quality: quality, gpsLocation: self.currentLocation) { (image, error) in
|
|
445
518
|
if let error = error {
|
|
446
519
|
call.reject(error.localizedDescription)
|
|
447
520
|
return
|
|
@@ -452,7 +525,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
452
525
|
PHAssetChangeRequest.creationRequestForAsset(from: image!)
|
|
453
526
|
}, completionHandler: { (success, error) in
|
|
454
527
|
if !success {
|
|
455
|
-
|
|
528
|
+
print("CameraPreview: Error saving image to gallery: \(error?.localizedDescription ?? "Unknown error")")
|
|
456
529
|
}
|
|
457
530
|
})
|
|
458
531
|
}
|
|
@@ -461,10 +534,12 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
461
534
|
call.reject("Failed to get JPEG data from image")
|
|
462
535
|
return
|
|
463
536
|
}
|
|
464
|
-
|
|
537
|
+
|
|
538
|
+
|
|
465
539
|
let exifData = self.getExifData(from: imageData)
|
|
466
540
|
let base64Image = imageData.base64EncodedString()
|
|
467
|
-
|
|
541
|
+
|
|
542
|
+
|
|
468
543
|
var result = JSObject()
|
|
469
544
|
result["value"] = base64Image
|
|
470
545
|
result["exif"] = exifData
|
|
@@ -479,12 +554,28 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
479
554
|
let exifDict = imageProperties[kCGImagePropertyExifDictionary as String] as? [String: Any] else {
|
|
480
555
|
return [:]
|
|
481
556
|
}
|
|
482
|
-
|
|
557
|
+
|
|
558
|
+
|
|
483
559
|
var exifData = JSObject()
|
|
484
560
|
for (key, value) in exifDict {
|
|
485
|
-
|
|
561
|
+
// Convert value to JSValue-compatible type
|
|
562
|
+
if let stringValue = value as? String {
|
|
563
|
+
exifData[key] = stringValue
|
|
564
|
+
} else if let numberValue = value as? NSNumber {
|
|
565
|
+
exifData[key] = numberValue
|
|
566
|
+
} else if let boolValue = value as? Bool {
|
|
567
|
+
exifData[key] = boolValue
|
|
568
|
+
} else if let arrayValue = value as? [Any] {
|
|
569
|
+
exifData[key] = arrayValue
|
|
570
|
+
} else if let dictValue = value as? [String: Any] {
|
|
571
|
+
exifData[key] = JSObject(_immutableCocoaDictionary: NSMutableDictionary(dictionary: dictValue))
|
|
572
|
+
} else {
|
|
573
|
+
// Convert other types to string as fallback
|
|
574
|
+
exifData[key] = String(describing: value)
|
|
575
|
+
}
|
|
486
576
|
}
|
|
487
|
-
|
|
577
|
+
|
|
578
|
+
|
|
488
579
|
return exifData
|
|
489
580
|
}
|
|
490
581
|
|
|
@@ -627,9 +718,11 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
627
718
|
// Collect all devices by position
|
|
628
719
|
for device in session.devices {
|
|
629
720
|
var lenses: [[String: Any]] = []
|
|
630
|
-
|
|
721
|
+
|
|
722
|
+
|
|
631
723
|
let constituentDevices = device.isVirtualDevice ? device.constituentDevices : [device]
|
|
632
|
-
|
|
724
|
+
|
|
725
|
+
|
|
633
726
|
for lensDevice in constituentDevices {
|
|
634
727
|
var deviceType: String
|
|
635
728
|
switch lensDevice.deviceType {
|
|
@@ -649,7 +742,8 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
649
742
|
} else if lensDevice.deviceType == .builtInTelephotoCamera {
|
|
650
743
|
baseZoomRatio = 2.0 // A common value for telephoto lenses
|
|
651
744
|
}
|
|
652
|
-
|
|
745
|
+
|
|
746
|
+
|
|
653
747
|
let lensInfo: [String: Any] = [
|
|
654
748
|
"label": lensDevice.localizedName,
|
|
655
749
|
"deviceType": deviceType,
|
|
@@ -660,7 +754,8 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
660
754
|
]
|
|
661
755
|
lenses.append(lensInfo)
|
|
662
756
|
}
|
|
663
|
-
|
|
757
|
+
|
|
758
|
+
|
|
664
759
|
let deviceData: [String: Any] = [
|
|
665
760
|
"deviceId": device.uniqueID,
|
|
666
761
|
"label": device.localizedName,
|
|
@@ -670,7 +765,8 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
670
765
|
"maxZoom": Float(device.maxAvailableVideoZoomFactor),
|
|
671
766
|
"isLogical": device.isVirtualDevice
|
|
672
767
|
]
|
|
673
|
-
|
|
768
|
+
|
|
769
|
+
|
|
674
770
|
devices.append(deviceData)
|
|
675
771
|
}
|
|
676
772
|
|
|
@@ -686,7 +782,8 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
686
782
|
do {
|
|
687
783
|
let zoomInfo = try self.cameraController.getZoom()
|
|
688
784
|
let lensInfo = try self.cameraController.getCurrentLensInfo()
|
|
689
|
-
|
|
785
|
+
|
|
786
|
+
|
|
690
787
|
var minZoom = zoomInfo.min
|
|
691
788
|
var maxZoom = zoomInfo.max
|
|
692
789
|
var currentZoom = zoomInfo.current
|
|
@@ -782,10 +879,12 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
782
879
|
self.cameraController.previewLayer?.frame = self.previewView.bounds
|
|
783
880
|
self.cameraController.previewLayer?.videoGravity = .resizeAspectFill
|
|
784
881
|
self.previewView.isUserInteractionEnabled = true
|
|
785
|
-
|
|
882
|
+
|
|
883
|
+
|
|
786
884
|
// Ensure webview remains transparent after device switch
|
|
787
885
|
self.makeWebViewTransparent()
|
|
788
|
-
|
|
886
|
+
|
|
887
|
+
|
|
789
888
|
call.resolve()
|
|
790
889
|
}
|
|
791
890
|
} catch {
|
|
@@ -812,6 +911,46 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
|
|
|
812
911
|
}
|
|
813
912
|
}
|
|
814
913
|
|
|
914
|
+
public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
|
915
|
+
self.currentLocation = locations.last
|
|
916
|
+
self.locationManager?.stopUpdatingLocation()
|
|
917
|
+
}
|
|
815
918
|
|
|
919
|
+
public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
|
920
|
+
print("CameraPreview: Failed to get location: \(error.localizedDescription)")
|
|
921
|
+
}
|
|
816
922
|
|
|
923
|
+
private func updateCameraFrame() {
|
|
924
|
+
guard let width = self.width, var height = self.height, let posX = self.posX, let posY = self.posY else { return }
|
|
925
|
+
let paddingBottom = self.paddingBottom ?? 0
|
|
926
|
+
height -= paddingBottom
|
|
927
|
+
|
|
928
|
+
var frame = CGRect(x: posX, y: posY, width: width, height: height)
|
|
929
|
+
|
|
930
|
+
if let aspectRatio = self.aspectRatio, aspectRatio != "fill" {
|
|
931
|
+
let ratioParts = aspectRatio.split(separator: ":").map { Double($0) ?? 1.0 }
|
|
932
|
+
let ratio = ratioParts[0] / ratioParts[1]
|
|
933
|
+
let viewWidth = Double(width)
|
|
934
|
+
let viewHeight = Double(height)
|
|
935
|
+
|
|
936
|
+
if viewWidth / ratio > viewHeight {
|
|
937
|
+
let newWidth = viewHeight * ratio
|
|
938
|
+
frame.origin.x += (viewWidth - newWidth) / 2
|
|
939
|
+
frame.size.width = newWidth
|
|
940
|
+
} else {
|
|
941
|
+
let newHeight = viewWidth / ratio
|
|
942
|
+
frame.origin.y += (viewHeight - newHeight) / 2
|
|
943
|
+
frame.size.height = newHeight
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
if self.previewView == nil {
|
|
948
|
+
self.previewView = UIView(frame: frame)
|
|
949
|
+
} else {
|
|
950
|
+
self.previewView.frame = frame
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// Update the preview layer frame to match the preview view
|
|
954
|
+
self.cameraController.previewLayer?.frame = frame
|
|
955
|
+
}
|
|
817
956
|
}
|