@scr2em/capacitor-scanner 6.0.3 → 6.0.5
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.
|
@@ -1,71 +1,69 @@
|
|
|
1
1
|
|
|
2
|
-
import Foundation
|
|
3
2
|
import AVFoundation
|
|
3
|
+
import Vision
|
|
4
4
|
|
|
5
|
-
public typealias BarcodeFormat =
|
|
5
|
+
public typealias BarcodeFormat = VNBarcodeSymbology
|
|
6
6
|
|
|
7
7
|
public class CapacitorScannerHelpers {
|
|
8
|
+
public static func getAllSupportedFormats() -> [BarcodeFormat] {
|
|
9
|
+
return [.aztec, .code39, .code93, .code128, .dataMatrix, .ean8, .ean13, .itf14, .pdf417, .qr, .upce]
|
|
10
|
+
}
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
default:
|
|
38
|
-
return nil
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
public static func convertBarcodeScannerFormatToString(_ format: BarcodeFormat) -> String? {
|
|
43
|
-
switch format {
|
|
44
|
-
case BarcodeFormat.aztec:
|
|
45
|
-
return "AZTEC"
|
|
46
|
-
case BarcodeFormat.code39:
|
|
47
|
-
return "CODE_39"
|
|
48
|
-
case BarcodeFormat.code93:
|
|
49
|
-
return "CODE_93"
|
|
50
|
-
case BarcodeFormat.code128:
|
|
51
|
-
return "CODE_128"
|
|
52
|
-
case BarcodeFormat.dataMatrix:
|
|
53
|
-
return "DATA_MATRIX"
|
|
54
|
-
case BarcodeFormat.ean8:
|
|
55
|
-
return "EAN_8"
|
|
56
|
-
case BarcodeFormat.ean13:
|
|
57
|
-
return "EAN_13"
|
|
58
|
-
case BarcodeFormat.itf14:
|
|
59
|
-
return "ITF14"
|
|
60
|
-
case BarcodeFormat.pdf417:
|
|
61
|
-
return "PDF_417"
|
|
62
|
-
case BarcodeFormat.qr:
|
|
63
|
-
return "QR_CODE"
|
|
64
|
-
case BarcodeFormat.upce:
|
|
65
|
-
return "UPC_E"
|
|
66
|
-
default:
|
|
67
|
-
return nil
|
|
68
|
-
}
|
|
69
|
-
}
|
|
12
|
+
public static func convertStringToBarcodeScannerFormat(_ value: String) -> BarcodeFormat? {
|
|
13
|
+
switch value {
|
|
14
|
+
case "AZTEC":
|
|
15
|
+
return BarcodeFormat.aztec
|
|
16
|
+
case "CODE_39":
|
|
17
|
+
return BarcodeFormat.code39
|
|
18
|
+
case "CODE_93":
|
|
19
|
+
return BarcodeFormat.code93
|
|
20
|
+
case "CODE_128":
|
|
21
|
+
return BarcodeFormat.code128
|
|
22
|
+
case "DATA_MATRIX":
|
|
23
|
+
return BarcodeFormat.dataMatrix
|
|
24
|
+
case "EAN_8":
|
|
25
|
+
return BarcodeFormat.ean8
|
|
26
|
+
case "EAN_13":
|
|
27
|
+
return BarcodeFormat.ean13
|
|
28
|
+
case "ITF14":
|
|
29
|
+
return BarcodeFormat.itf14
|
|
30
|
+
case "PDF_417":
|
|
31
|
+
return BarcodeFormat.pdf417
|
|
32
|
+
case "QR_CODE":
|
|
33
|
+
return BarcodeFormat.qr
|
|
34
|
+
case "UPC_E":
|
|
35
|
+
return BarcodeFormat.upce
|
|
36
|
+
default:
|
|
37
|
+
return nil
|
|
38
|
+
}
|
|
39
|
+
}
|
|
70
40
|
|
|
41
|
+
public static func convertBarcodeScannerFormatToString(_ format: BarcodeFormat) -> String? {
|
|
42
|
+
switch format {
|
|
43
|
+
case BarcodeFormat.aztec:
|
|
44
|
+
return "AZTEC"
|
|
45
|
+
case BarcodeFormat.code39:
|
|
46
|
+
return "CODE_39"
|
|
47
|
+
case BarcodeFormat.code93:
|
|
48
|
+
return "CODE_93"
|
|
49
|
+
case BarcodeFormat.code128:
|
|
50
|
+
return "CODE_128"
|
|
51
|
+
case BarcodeFormat.dataMatrix:
|
|
52
|
+
return "DATA_MATRIX"
|
|
53
|
+
case BarcodeFormat.ean8:
|
|
54
|
+
return "EAN_8"
|
|
55
|
+
case BarcodeFormat.ean13:
|
|
56
|
+
return "EAN_13"
|
|
57
|
+
case BarcodeFormat.itf14:
|
|
58
|
+
return "ITF14"
|
|
59
|
+
case BarcodeFormat.pdf417:
|
|
60
|
+
return "PDF_417"
|
|
61
|
+
case BarcodeFormat.qr:
|
|
62
|
+
return "QR_CODE"
|
|
63
|
+
case BarcodeFormat.upce:
|
|
64
|
+
return "UPC_E"
|
|
65
|
+
default:
|
|
66
|
+
return nil
|
|
67
|
+
}
|
|
68
|
+
}
|
|
71
69
|
}
|
|
@@ -1,346 +1,402 @@
|
|
|
1
1
|
import AVFoundation
|
|
2
2
|
import Capacitor
|
|
3
3
|
import Foundation
|
|
4
|
+
import Vision // For barcode detection
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Please read the Capacitor iOS Plugin Development Guide
|
|
7
8
|
* here: https://capacitorjs.com/docs/plugins/ios
|
|
8
9
|
*/
|
|
9
10
|
struct VoteStatus {
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
var votes: Int
|
|
12
|
+
var done: Bool
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
@objc(CapacitorScannerPlugin)
|
|
15
16
|
public class CapacitorScannerPlugin: CAPPlugin, CAPBridgedPlugin, AVCaptureMetadataOutputObjectsDelegate, AVCapturePhotoCaptureDelegate {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
17
|
+
public let identifier = "CapacitorScannerPlugin"
|
|
18
|
+
public let jsName = "CapacitorScanner"
|
|
19
|
+
public let pluginMethods: [CAPPluginMethod] = [
|
|
20
|
+
CAPPluginMethod(name: "startScanning", returnType: CAPPluginReturnPromise),
|
|
21
|
+
CAPPluginMethod(name: "stopScanning", returnType: CAPPluginReturnPromise),
|
|
22
|
+
CAPPluginMethod(name: "checkPermissions", returnType: CAPPluginReturnPromise),
|
|
23
|
+
CAPPluginMethod(name: "requestPermissions", returnType: CAPPluginReturnPromise),
|
|
24
|
+
CAPPluginMethod(name: "openSettings", returnType: CAPPluginReturnPromise),
|
|
25
|
+
CAPPluginMethod(name: "capturePhoto", returnType: CAPPluginReturnPromise),
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
private var captureSession: AVCaptureSession?
|
|
29
|
+
private var cameraView: UIView?
|
|
30
|
+
private var previewLayer: AVCaptureVideoPreviewLayer?
|
|
31
|
+
|
|
32
|
+
private let voteThreshold = 3
|
|
33
|
+
private var scannedCodesVotes: [String: VoteStatus] = [:]
|
|
34
|
+
|
|
35
|
+
// for capturing images
|
|
36
|
+
private var photoOutput: AVCapturePhotoOutput?
|
|
37
|
+
private var capturePhotoCall: CAPPluginCall?
|
|
38
|
+
|
|
39
|
+
private var orientationObserver: NSObjectProtocol?
|
|
40
|
+
private var videoDataOutput: AVCaptureVideoDataOutput? // For getting video frames
|
|
41
|
+
|
|
42
|
+
// This is how we create a Vision request for detecting barcodes
|
|
43
|
+
private lazy var barcodeDetectionRequest: VNDetectBarcodesRequest = {
|
|
44
|
+
let request = VNDetectBarcodesRequest { [weak self] request, error in
|
|
45
|
+
// This closure is called when Vision finds barcodes
|
|
46
|
+
self?.processClassification(request, error: error)
|
|
47
|
+
}
|
|
48
|
+
return request
|
|
49
|
+
}()
|
|
50
|
+
|
|
51
|
+
@objc func capturePhoto(_ call: CAPPluginCall) {
|
|
52
|
+
guard let photoOutput = self.photoOutput, captureSession?.isRunning == true else {
|
|
53
|
+
call.reject("Camera is not set up or running")
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let photoSettings = AVCapturePhotoSettings()
|
|
58
|
+
self.capturePhotoCall = call
|
|
59
|
+
photoOutput.capturePhoto(with: photoSettings, delegate: self)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
|
|
63
|
+
if let captureError = error {
|
|
64
|
+
self.capturePhotoCall?.reject("Photo capture failed: \(captureError.localizedDescription)")
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
guard let imageData = photo.fileDataRepresentation() else {
|
|
69
|
+
self.capturePhotoCall?.reject("Unable to get image data")
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let base64String = imageData.base64EncodedString()
|
|
74
|
+
self.capturePhotoCall?.resolve(["imageBase64": "data:image/jpeg;base64, \(base64String)"])
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@objc func startScanning(_ call: CAPPluginCall) {
|
|
78
|
+
self.stopScanning(call)
|
|
79
|
+
|
|
80
|
+
DispatchQueue.main.async {
|
|
81
|
+
let captureSession = AVCaptureSession()
|
|
82
|
+
|
|
83
|
+
if captureSession.canSetSessionPreset(.hd1920x1080) {
|
|
84
|
+
captureSession.sessionPreset = .hd1920x1080
|
|
85
|
+
} else {
|
|
86
|
+
captureSession.sessionPreset = .hd1280x720
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let cameraDirection: AVCaptureDevice.Position = call.getString("cameraDirection", "BACK") == "BACK" ? .back : .front
|
|
90
|
+
|
|
91
|
+
guard let videoCaptureDevice = self.getCaptureDevice(position: cameraDirection) else {
|
|
92
|
+
print("No camera available")
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
guard let videoInput = try? AVCaptureDeviceInput(device: videoCaptureDevice) else {
|
|
96
|
+
print("Could not create video input")
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
captureSession.addInput(videoInput)
|
|
100
|
+
|
|
101
|
+
// 2. Setup video output for Vision
|
|
102
|
+
let videoOutput = AVCaptureVideoDataOutput()
|
|
103
|
+
videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.global(qos: .userInitiated))
|
|
104
|
+
captureSession.addOutput(videoOutput)
|
|
105
|
+
self.videoDataOutput = videoOutput
|
|
106
|
+
|
|
107
|
+
let formats = call.getArray("formats", String.self) ?? []
|
|
108
|
+
|
|
109
|
+
self.barcodeDetectionRequest.symbologies = self.getSupportedFormats(from: formats)
|
|
110
|
+
|
|
111
|
+
// Add photo output
|
|
112
|
+
let photoOutput = AVCapturePhotoOutput()
|
|
113
|
+
self.photoOutput = photoOutput
|
|
114
|
+
if captureSession.canAddOutput(photoOutput) {
|
|
115
|
+
captureSession.addOutput(photoOutput)
|
|
116
|
+
} else {
|
|
117
|
+
call.reject("Unable to add photo output to capture session")
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
self.hideWebViewBackground()
|
|
122
|
+
|
|
123
|
+
if let webView = self.webView, let superView = webView.superview {
|
|
124
|
+
let cameraView = UIView(frame: superView.bounds)
|
|
125
|
+
let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
|
|
126
|
+
previewLayer.videoGravity = .resizeAspectFill
|
|
127
|
+
previewLayer.frame = superView.bounds
|
|
128
|
+
cameraView.layer.addSublayer(previewLayer)
|
|
129
|
+
|
|
130
|
+
webView.superview?.insertSubview(cameraView, belowSubview: webView)
|
|
131
|
+
|
|
132
|
+
self.captureSession = captureSession
|
|
133
|
+
self.cameraView = cameraView
|
|
134
|
+
self.previewLayer = previewLayer
|
|
135
|
+
|
|
136
|
+
// Add orientation change observer
|
|
137
|
+
self.addOrientationChangeObserver()
|
|
138
|
+
|
|
139
|
+
// Initial orientation setup
|
|
140
|
+
self.updatePreviewOrientation()
|
|
141
|
+
|
|
142
|
+
DispatchQueue.global(qos: .background).async {
|
|
143
|
+
captureSession.startRunning()
|
|
144
|
+
call.resolve()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
} else {
|
|
148
|
+
call.reject("unknown")
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@objc func stopScanning(_ call: CAPPluginCall) {
|
|
154
|
+
DispatchQueue.main.async {
|
|
155
|
+
if let captureSession = self.captureSession {
|
|
156
|
+
captureSession.stopRunning()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
self.previewLayer?.removeFromSuperlayer()
|
|
160
|
+
self.cameraView?.removeFromSuperview()
|
|
161
|
+
self.videoDataOutput?.setSampleBufferDelegate(nil, queue: nil)
|
|
162
|
+
|
|
163
|
+
self.captureSession = nil
|
|
164
|
+
self.cameraView = nil
|
|
165
|
+
self.previewLayer = nil
|
|
166
|
+
self.videoDataOutput = nil
|
|
167
|
+
self.scannedCodesVotes = [:]
|
|
168
|
+
self.showWebViewBackground()
|
|
169
|
+
|
|
170
|
+
self.removeOrientationChangeObserver()
|
|
171
|
+
|
|
172
|
+
call.resolve()
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private func addOrientationChangeObserver() {
|
|
177
|
+
self.orientationObserver = NotificationCenter.default.addObserver(
|
|
178
|
+
forName: UIDevice.orientationDidChangeNotification,
|
|
179
|
+
object: nil,
|
|
180
|
+
queue: .main
|
|
181
|
+
) { [weak self] _ in
|
|
182
|
+
self?.updatePreviewOrientation()
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private func removeOrientationChangeObserver() {
|
|
187
|
+
if let observer = self.orientationObserver {
|
|
188
|
+
NotificationCenter.default.removeObserver(observer)
|
|
189
|
+
self.orientationObserver = nil
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private func updatePreviewOrientation() {
|
|
194
|
+
guard let previewLayer = self.previewLayer,
|
|
195
|
+
let connection = previewLayer.connection,
|
|
196
|
+
let cameraView = self.cameraView
|
|
197
|
+
else {
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let deviceOrientation = UIDevice.current.orientation
|
|
202
|
+
|
|
203
|
+
if deviceOrientation.isFlat == true || deviceOrientation == .portraitUpsideDown {
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let newOrientation: AVCaptureVideoOrientation
|
|
208
|
+
|
|
209
|
+
switch deviceOrientation {
|
|
210
|
+
case .landscapeLeft:
|
|
211
|
+
newOrientation = .landscapeRight
|
|
212
|
+
case .landscapeRight:
|
|
213
|
+
newOrientation = .landscapeLeft
|
|
214
|
+
default:
|
|
215
|
+
newOrientation = .portrait
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
connection.videoOrientation = newOrientation
|
|
219
|
+
|
|
220
|
+
// Update camera view and preview layer frames
|
|
221
|
+
let screenBounds = UIScreen.main.bounds
|
|
222
|
+
let screenWidth = screenBounds.width
|
|
223
|
+
let screenHeight = screenBounds.height
|
|
224
|
+
|
|
225
|
+
// Determine the correct dimensions based on orientation
|
|
226
|
+
let width: CGFloat
|
|
227
|
+
let height: CGFloat
|
|
228
|
+
if newOrientation == .portrait {
|
|
229
|
+
width = min(screenWidth, screenHeight)
|
|
230
|
+
height = max(screenWidth, screenHeight)
|
|
231
|
+
} else {
|
|
232
|
+
width = max(screenWidth, screenHeight)
|
|
233
|
+
height = min(screenWidth, screenHeight)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Update frames
|
|
237
|
+
cameraView.frame = CGRect(x: 0, y: 0, width: width, height: height)
|
|
238
|
+
previewLayer.frame = cameraView.bounds
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private func getSupportedFormats(from formats: [String]) -> [BarcodeFormat] {
|
|
242
|
+
if formats.isEmpty {
|
|
243
|
+
return CapacitorScannerHelpers.getAllSupportedFormats()
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return formats.compactMap { format in
|
|
247
|
+
CapacitorScannerHelpers.convertStringToBarcodeScannerFormat(format)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Must run on UI thread.
|
|
253
|
+
*/
|
|
254
|
+
private func hideWebViewBackground() {
|
|
255
|
+
guard let webView = self.webView else {
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
webView.isOpaque = false
|
|
259
|
+
webView.backgroundColor = UIColor.clear
|
|
260
|
+
webView.scrollView.backgroundColor = UIColor.clear
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Must run on UI thread.
|
|
265
|
+
*/
|
|
266
|
+
private func showWebViewBackground() {
|
|
267
|
+
guard let webView = self.webView else {
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
webView.isOpaque = true
|
|
271
|
+
webView.backgroundColor = UIColor.white
|
|
272
|
+
webView.scrollView.backgroundColor = UIColor.white
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
@objc override public func checkPermissions(_ call: CAPPluginCall) {
|
|
276
|
+
let status = AVCaptureDevice.authorizationStatus(for: .video)
|
|
277
|
+
|
|
278
|
+
var stringStatus = "prompt"
|
|
279
|
+
|
|
280
|
+
if status == .denied || status == .restricted {
|
|
281
|
+
stringStatus = "denied"
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if status == .authorized {
|
|
285
|
+
stringStatus = "granted"
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
call.resolve(["camera": stringStatus])
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
@objc override public func requestPermissions(_ call: CAPPluginCall) {
|
|
292
|
+
AVCaptureDevice.requestAccess(for: .video) { _ in
|
|
293
|
+
self.checkPermissions(call)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Keep the enhanced device selection method
|
|
298
|
+
private func getCaptureDevice(position: AVCaptureDevice.Position) -> AVCaptureDevice? {
|
|
299
|
+
let discoverySession = AVCaptureDevice.DiscoverySession(
|
|
300
|
+
deviceTypes: [.builtInDualCamera, .builtInTripleCamera, .builtInWideAngleCamera],
|
|
301
|
+
mediaType: .video,
|
|
302
|
+
position: position
|
|
303
|
+
)
|
|
304
|
+
// Prioritize higher quality cameras first
|
|
305
|
+
if let device = discoverySession.devices.first(where: { $0.deviceType == .builtInTripleCamera }) ??
|
|
306
|
+
discoverySession.devices.first(where: { $0.deviceType == .builtInDualCamera }) ??
|
|
307
|
+
discoverySession.devices.first(where: { $0.deviceType == .builtInWideAngleCamera })
|
|
308
|
+
{
|
|
309
|
+
return device
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return nil
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
@objc func openSettings(_ call: CAPPluginCall) {
|
|
316
|
+
let url = URL(string: UIApplication.openSettingsURLString)
|
|
317
|
+
DispatchQueue.main.async {
|
|
318
|
+
if let url = url, UIApplication.shared.canOpenURL(url) {
|
|
319
|
+
UIApplication.shared.open(url)
|
|
320
|
+
call.resolve()
|
|
321
|
+
} else {
|
|
322
|
+
call.reject("unknown")
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private func processClassification(_ request: VNRequest, error: Error?) {
|
|
328
|
+
// Handle any errors
|
|
329
|
+
if let error = error {
|
|
330
|
+
print("Vision error: \(error)")
|
|
331
|
+
return
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Get the barcode observations
|
|
335
|
+
guard let observations = request.results as? [VNBarcodeObservation] else {
|
|
336
|
+
return
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// First find the barcode with highest confidence
|
|
340
|
+
let highestConfidenceBarcode = observations
|
|
341
|
+
.filter { $0.payloadStringValue != nil }
|
|
342
|
+
.max(by: { $0.confidence < $1.confidence })
|
|
343
|
+
|
|
344
|
+
// Then process only that barcode if found
|
|
345
|
+
if let bestObservation = highestConfidenceBarcode,
|
|
346
|
+
let payload = bestObservation.payloadStringValue
|
|
347
|
+
{
|
|
348
|
+
/*
|
|
349
|
+
this is a voting system to
|
|
350
|
+
1. avoid scanning the same code
|
|
351
|
+
2. avoid scanning any code that doesn't appear for at least in 10 frames
|
|
352
|
+
of the video stream to reduce the number of false positives
|
|
353
|
+
*/
|
|
354
|
+
|
|
355
|
+
var voteStatus = self.scannedCodesVotes[payload] ?? VoteStatus(votes: 0, done: false)
|
|
356
|
+
|
|
357
|
+
if !voteStatus.done {
|
|
358
|
+
voteStatus.votes += 1
|
|
359
|
+
|
|
360
|
+
if voteStatus.votes >= self.voteThreshold {
|
|
361
|
+
voteStatus.done = true
|
|
362
|
+
|
|
363
|
+
self.notifyListeners("barcodeScanned", data: [
|
|
364
|
+
"scannedCode": payload,
|
|
365
|
+
"format": CapacitorScannerHelpers.convertBarcodeScannerFormatToString(bestObservation.symbology),
|
|
366
|
+
])
|
|
367
|
+
|
|
368
|
+
// Reset votes after successful scan
|
|
369
|
+
self.scannedCodesVotes = [:]
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
self.scannedCodesVotes[payload] = voteStatus
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
extension CapacitorScannerPlugin: AVCaptureVideoDataOutputSampleBufferDelegate {
|
|
379
|
+
public func captureOutput(_ output: AVCaptureOutput,
|
|
380
|
+
didOutput sampleBuffer: CMSampleBuffer,
|
|
381
|
+
from connection: AVCaptureConnection)
|
|
382
|
+
{
|
|
383
|
+
// This is called for every frame from the camera
|
|
384
|
+
|
|
385
|
+
// 1. Get the pixel buffer from the frame
|
|
386
|
+
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
|
|
387
|
+
return
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// 2. Create a Vision image handler
|
|
391
|
+
let imageRequestHandler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer,
|
|
392
|
+
orientation: .up,
|
|
393
|
+
options: [:])
|
|
394
|
+
|
|
395
|
+
// 3. Perform the barcode detection
|
|
396
|
+
do {
|
|
397
|
+
try imageRequestHandler.perform([self.barcodeDetectionRequest])
|
|
398
|
+
} catch {
|
|
399
|
+
print("Failed to perform Vision request: \(error)")
|
|
400
|
+
}
|
|
401
|
+
}
|
|
346
402
|
}
|