capacitor-camera-module 0.0.39 → 0.0.40
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.
|
@@ -700,7 +700,7 @@ public class CameraModulePlugin extends Plugin {
|
|
|
700
700
|
imageCapture,
|
|
701
701
|
imageAnalysis
|
|
702
702
|
);
|
|
703
|
-
|
|
703
|
+
|
|
704
704
|
});
|
|
705
705
|
}
|
|
706
706
|
|
|
@@ -754,6 +754,7 @@ public class CameraModulePlugin extends Plugin {
|
|
|
754
754
|
|
|
755
755
|
}
|
|
756
756
|
|
|
757
|
+
|
|
757
758
|
if (barcodeScanner != null) {
|
|
758
759
|
barcodeScanner.close();
|
|
759
760
|
barcodeScanner = null;
|
|
@@ -1,23 +1,284 @@
|
|
|
1
|
-
import Foundation
|
|
2
1
|
import Capacitor
|
|
2
|
+
import AVFoundation
|
|
3
|
+
import Vision
|
|
4
|
+
import UIKit
|
|
3
5
|
|
|
4
|
-
/**
|
|
5
|
-
* Please read the Capacitor iOS Plugin Development Guide
|
|
6
|
-
* here: https://capacitorjs.com/docs/plugins/ios
|
|
7
|
-
*/
|
|
8
6
|
@objc(CameraModulePlugin)
|
|
9
|
-
public class CameraModulePlugin: CAPPlugin,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
7
|
+
public class CameraModulePlugin: CAPPlugin,
|
|
8
|
+
AVCaptureVideoDataOutputSampleBufferDelegate {
|
|
9
|
+
|
|
10
|
+
private var previewView: UIView?
|
|
11
|
+
private var captureSession: AVCaptureSession?
|
|
12
|
+
private var videoPreviewLayer: AVCaptureVideoPreviewLayer?
|
|
13
|
+
|
|
14
|
+
private var photoOutput = AVCapturePhotoOutput()
|
|
15
|
+
private var isScanning = false
|
|
16
|
+
private var scanCall: CAPPluginCall?
|
|
17
|
+
|
|
18
|
+
private var photoDelegate: PhotoDelegate?
|
|
19
|
+
|
|
20
|
+
private var barcodeRequest: VNDetectBarcodesRequest?
|
|
21
|
+
|
|
22
|
+
private var videoDataOutput: AVCaptureVideoDataOutput?
|
|
23
|
+
|
|
24
|
+
private var lastScannedValue: String?
|
|
25
|
+
private var lastScanTime: Date?
|
|
26
|
+
private let scanDebounceInterval: TimeInterval = 0.5 // medio segundo
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
// MARK: - Lifecycle
|
|
30
|
+
|
|
31
|
+
public override func load() {
|
|
32
|
+
bridge?.webView?.isOpaque = false
|
|
33
|
+
bridge?.webView?.backgroundColor = .clear
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// MARK: - Permissions
|
|
37
|
+
|
|
38
|
+
@objc func checkPermission(_ call: CAPPluginCall) {
|
|
39
|
+
let status = AVCaptureDevice.authorizationStatus(for: .video)
|
|
40
|
+
|
|
19
41
|
call.resolve([
|
|
20
|
-
"
|
|
42
|
+
"granted": status == .authorized,
|
|
43
|
+
"status": status == .authorized ? "granted" : "prompt"
|
|
21
44
|
])
|
|
22
45
|
}
|
|
46
|
+
|
|
47
|
+
@objc func requestPermission(_ call: CAPPluginCall) {
|
|
48
|
+
AVCaptureDevice.requestAccess(for: .video) { granted in
|
|
49
|
+
DispatchQueue.main.async {
|
|
50
|
+
call.resolve([
|
|
51
|
+
"granted": granted,
|
|
52
|
+
"status": granted ? "granted" : "denied"
|
|
53
|
+
])
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// MARK: - Camera Preview
|
|
59
|
+
|
|
60
|
+
@objc func startPreview(_ call: CAPPluginCall) {
|
|
61
|
+
DispatchQueue.main.async {
|
|
62
|
+
|
|
63
|
+
if self.previewView != nil {
|
|
64
|
+
call.resolve()
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let session = AVCaptureSession()
|
|
69
|
+
session.sessionPreset = .photo
|
|
70
|
+
|
|
71
|
+
guard let camera = AVCaptureDevice.default(.builtInWideAngleCamera,
|
|
72
|
+
for: .video,
|
|
73
|
+
position: .back),
|
|
74
|
+
let input = try? AVCaptureDeviceInput(device: camera)
|
|
75
|
+
else {
|
|
76
|
+
call.reject("Camera not available")
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
session.addInput(input)
|
|
81
|
+
session.addOutput(self.photoOutput)
|
|
82
|
+
|
|
83
|
+
let view = UIView(frame: UIScreen.main.bounds)
|
|
84
|
+
let previewLayer = AVCaptureVideoPreviewLayer(session: session)
|
|
85
|
+
previewLayer.videoGravity = .resizeAspectFill
|
|
86
|
+
previewLayer.frame = view.bounds
|
|
87
|
+
|
|
88
|
+
view.layer.addSublayer(previewLayer)
|
|
89
|
+
|
|
90
|
+
self.bridge?.viewController?.view.insertSubview(view, at: 0)
|
|
91
|
+
|
|
92
|
+
self.previewView = view
|
|
93
|
+
self.videoPreviewLayer = previewLayer
|
|
94
|
+
self.captureSession = session
|
|
95
|
+
|
|
96
|
+
session.startRunning()
|
|
97
|
+
call.resolve()
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@objc func stopPreview(_ call: CAPPluginCall) {
|
|
102
|
+
DispatchQueue.main.async {
|
|
103
|
+
|
|
104
|
+
self.captureSession?.stopRunning()
|
|
105
|
+
self.captureSession = nil
|
|
106
|
+
|
|
107
|
+
self.videoPreviewLayer?.removeFromSuperlayer()
|
|
108
|
+
self.videoPreviewLayer = nil
|
|
109
|
+
|
|
110
|
+
self.previewView?.removeFromSuperview()
|
|
111
|
+
self.previewView = nil
|
|
112
|
+
|
|
113
|
+
call.resolve()
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// MARK: - Flash
|
|
118
|
+
|
|
119
|
+
@objc func toggleFlash(_ call: CAPPluginCall) {
|
|
120
|
+
guard let enable = call.getBool("enable") else {
|
|
121
|
+
call.reject("enable parameter is required")
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
guard let device = AVCaptureDevice.default(for: .video),
|
|
126
|
+
device.hasTorch
|
|
127
|
+
else {
|
|
128
|
+
call.reject("Flash not available")
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
do {
|
|
133
|
+
try device.lockForConfiguration()
|
|
134
|
+
device.torchMode = enable ? .on : .off
|
|
135
|
+
device.unlockForConfiguration()
|
|
136
|
+
call.resolve()
|
|
137
|
+
} catch {
|
|
138
|
+
call.reject("Flash error")
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@objc func hasFlash(_ call: CAPPluginCall) {
|
|
143
|
+
let hasFlash = AVCaptureDevice.default(for: .video)?.hasTorch ?? false
|
|
144
|
+
call.resolve(["hasFlash": hasFlash])
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// MARK: - Photo Capture
|
|
148
|
+
|
|
149
|
+
@objc func takePhotoBase64(_ call: CAPPluginCall) {
|
|
150
|
+
|
|
151
|
+
guard captureSession?.isRunning == true else {
|
|
152
|
+
call.reject("Camera not started")
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let settings = AVCapturePhotoSettings()
|
|
157
|
+
settings.flashMode = .off
|
|
158
|
+
|
|
159
|
+
self.photoDelegate = PhotoDelegate { [weak self] base64 in
|
|
160
|
+
call.resolve([
|
|
161
|
+
"base64": base64,
|
|
162
|
+
"mimeType": "image/jpeg"
|
|
163
|
+
])
|
|
164
|
+
|
|
165
|
+
self?.photoDelegate = nil
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
self.photoOutput.capturePhoto(
|
|
169
|
+
with: settings,
|
|
170
|
+
delegate: self.photoDelegate!
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
// MARK: - Barcode Scan
|
|
176
|
+
|
|
177
|
+
@objc func startBarcodeScan(_ call: CAPPluginCall) {
|
|
178
|
+
|
|
179
|
+
guard let session = captureSession else {
|
|
180
|
+
call.reject("Preview not started")
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if isScanning {
|
|
185
|
+
call.reject("Already scanning")
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
isScanning = true
|
|
190
|
+
scanCall = call
|
|
191
|
+
call.keepAlive = true
|
|
192
|
+
|
|
193
|
+
barcodeRequest = VNDetectBarcodesRequest { request, _ in
|
|
194
|
+
guard let results = request.results as? [VNBarcodeObservation],
|
|
195
|
+
let barcode = results.first,
|
|
196
|
+
let value = barcode.payloadStringValue
|
|
197
|
+
else { return }
|
|
198
|
+
|
|
199
|
+
// Debounce
|
|
200
|
+
let now = Date()
|
|
201
|
+
if let last = self.lastScannedValue,
|
|
202
|
+
let lastTime = self.lastScanTime,
|
|
203
|
+
last == value && now.timeIntervalSince(lastTime) < self.scanDebounceInterval {
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
self.lastScannedValue = value
|
|
208
|
+
self.lastScanTime = now
|
|
209
|
+
|
|
210
|
+
self.isScanning = false
|
|
211
|
+
self.scanCall?.resolve([
|
|
212
|
+
"rawValue": value,
|
|
213
|
+
"format": barcode.symbology.rawValue
|
|
214
|
+
])
|
|
215
|
+
self.scanCall?.keepAlive = false
|
|
216
|
+
self.scanCall = nil
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if videoDataOutput == nil {
|
|
221
|
+
let output = AVCaptureVideoDataOutput()
|
|
222
|
+
output.setSampleBufferDelegate(self, queue: DispatchQueue(label: "barcode.queue"))
|
|
223
|
+
session.addOutput(output)
|
|
224
|
+
videoDataOutput = output
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
@objc func stopBarcodeScan(_ call: CAPPluginCall) {
|
|
229
|
+
|
|
230
|
+
isScanning = false
|
|
231
|
+
|
|
232
|
+
if let output = videoDataOutput,
|
|
233
|
+
let session = captureSession {
|
|
234
|
+
session.removeOutput(output)
|
|
235
|
+
videoDataOutput = nil
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
scanCall?.keepAlive = false
|
|
239
|
+
scanCall = nil
|
|
240
|
+
|
|
241
|
+
lastScannedValue = nil
|
|
242
|
+
lastScanTime = nil
|
|
243
|
+
|
|
244
|
+
call.resolve()
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
public func captureOutput(
|
|
249
|
+
_ output: AVCaptureOutput,
|
|
250
|
+
didOutput sampleBuffer: CMSampleBuffer,
|
|
251
|
+
from connection: AVCaptureConnection
|
|
252
|
+
) {
|
|
253
|
+
guard isScanning,
|
|
254
|
+
let request = barcodeRequest
|
|
255
|
+
else { return }
|
|
256
|
+
|
|
257
|
+
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let handler = VNImageRequestHandler(
|
|
262
|
+
cvPixelBuffer: pixelBuffer,
|
|
263
|
+
orientation: cgOrientation(from: UIDevice.current.orientation),
|
|
264
|
+
options: [:]
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
do {
|
|
268
|
+
try handler.perform([request])
|
|
269
|
+
} catch {
|
|
270
|
+
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
func cgOrientation(from deviceOrientation: UIDeviceOrientation) -> CGImagePropertyOrientation {
|
|
275
|
+
switch deviceOrientation {
|
|
276
|
+
case .portrait: return .right
|
|
277
|
+
case .portraitUpsideDown: return .left
|
|
278
|
+
case .landscapeLeft: return .up
|
|
279
|
+
case .landscapeRight: return .down
|
|
280
|
+
default: return .right
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
23
284
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
class PhotoDelegate: NSObject, AVCapturePhotoCaptureDelegate {
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
private let callback: (String) -> Void
|
|
8
|
+
|
|
9
|
+
init(callback: @escaping (String) -> Void) {
|
|
10
|
+
self.callback = callback
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
func photoOutput(_ output: AVCapturePhotoOutput,
|
|
14
|
+
didFinishProcessingPhoto photo: AVCapturePhoto,
|
|
15
|
+
error: Error?) {
|
|
16
|
+
|
|
17
|
+
guard let data = photo.fileDataRepresentation(),
|
|
18
|
+
let image = UIImage(data: data),
|
|
19
|
+
let jpeg = image.jpegData(compressionQuality: 0.8)
|
|
20
|
+
else { return }
|
|
21
|
+
|
|
22
|
+
let base64 = jpeg.base64EncodedString()
|
|
23
|
+
callback(base64)
|
|
24
|
+
}
|
|
25
|
+
}
|