@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 = AVMetadataObject.ObjectType
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
- public static func getAllSupportedFormats() -> [BarcodeFormat] {
10
- return [.aztec, .code39, .code93, .code128, .dataMatrix, .ean8, .ean13, .itf14, .pdf417, .qr, .upce]
11
- }
12
-
13
- public static func convertStringToBarcodeScannerFormat(_ value: String) -> BarcodeFormat? {
14
- switch value {
15
- case "AZTEC":
16
- return BarcodeFormat.aztec
17
- case "CODE_39":
18
- return BarcodeFormat.code39
19
- case "CODE_93":
20
- return BarcodeFormat.code93
21
- case "CODE_128":
22
- return BarcodeFormat.code128
23
- case "DATA_MATRIX":
24
- return BarcodeFormat.dataMatrix
25
- case "EAN_8":
26
- return BarcodeFormat.ean8
27
- case "EAN_13":
28
- return BarcodeFormat.ean13
29
- case "ITF14":
30
- return BarcodeFormat.itf14
31
- case "PDF_417":
32
- return BarcodeFormat.pdf417
33
- case "QR_CODE":
34
- return BarcodeFormat.qr
35
- case "UPC_E":
36
- return BarcodeFormat.upce
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
- var votes: Int
11
- var done: Bool
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
- public let identifier = "CapacitorScannerPlugin"
17
- public let jsName = "CapacitorScanner"
18
- public let pluginMethods: [CAPPluginMethod] = [
19
- CAPPluginMethod(name: "startScanning", returnType: CAPPluginReturnPromise),
20
- CAPPluginMethod(name: "stopScanning", returnType: CAPPluginReturnPromise),
21
- CAPPluginMethod(name: "checkPermissions", returnType: CAPPluginReturnPromise),
22
- CAPPluginMethod(name: "requestPermissions", returnType: CAPPluginReturnPromise),
23
- CAPPluginMethod(name: "openSettings", returnType: CAPPluginReturnPromise),
24
- CAPPluginMethod(name: "capturePhoto", returnType: CAPPluginReturnPromise)
25
- ]
26
-
27
- private var captureSession: AVCaptureSession?
28
- private var cameraView: UIView?
29
- private var previewLayer: AVCaptureVideoPreviewLayer?
30
-
31
- private let voteThreshold = 5
32
- private var scannedCodesVotes: [String: VoteStatus] = [:]
33
-
34
- // for capturing images
35
- private var photoOutput: AVCapturePhotoOutput?
36
- private var capturePhotoCall: CAPPluginCall?
37
-
38
- private var orientationObserver: NSObjectProtocol?
39
-
40
- @objc func capturePhoto(_ call: CAPPluginCall) {
41
- guard let photoOutput = self.photoOutput, captureSession?.isRunning == true else {
42
- call.reject("Camera is not set up or running")
43
- return
44
- }
45
-
46
- let photoSettings = AVCapturePhotoSettings()
47
- self.capturePhotoCall = call
48
- photoOutput.capturePhoto(with: photoSettings, delegate: self)
49
- }
50
-
51
- public func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
52
- if let captureError = error {
53
- self.capturePhotoCall?.reject("Photo capture failed: \(captureError.localizedDescription)")
54
- return
55
- }
56
-
57
- guard let imageData = photo.fileDataRepresentation() else {
58
- self.capturePhotoCall?.reject("Unable to get image data")
59
- return
60
- }
61
-
62
- let base64String = imageData.base64EncodedString()
63
- self.capturePhotoCall?.resolve(["imageBase64": "data:image/jpeg;base64, \(base64String)"])
64
- }
65
-
66
- @objc func startScanning(_ call: CAPPluginCall) {
67
- self.stopScanning(call)
68
-
69
- DispatchQueue.main.async {
70
- let captureSession = AVCaptureSession()
71
- captureSession.sessionPreset = AVCaptureSession.Preset.hd1280x720
72
-
73
- let cameraDirection: AVCaptureDevice.Position = call.getString("cameraDirection", "BACK") == "BACK" ? .back : .front
74
-
75
- guard let videoCaptureDevice = self.getCaptureDevice(position: cameraDirection) else {
76
- call.reject("Unable to access the camera")
77
- return
78
- }
79
-
80
- let videoInput: AVCaptureDeviceInput
81
-
82
- do {
83
- videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
84
- } catch {
85
- call.reject("Unable to initialize video input")
86
- return
87
- }
88
-
89
- if captureSession.canAddInput(videoInput) {
90
- captureSession.addInput(videoInput)
91
- } else {
92
- call.reject("Unable to add video input to capture session")
93
- return
94
- }
95
-
96
- let metadataOutput = AVCaptureMetadataOutput()
97
-
98
- if captureSession.canAddOutput(metadataOutput) {
99
- captureSession.addOutput(metadataOutput)
100
-
101
- metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
102
-
103
- let formats = call.getArray("formats", String.self) ?? []
104
- let metadataObjectTypes = self.getMetadataObjectTypes(from: formats)
105
- metadataOutput.metadataObjectTypes = metadataObjectTypes
106
- } else {
107
- call.reject("Unable to add metadata output to capture session")
108
- return
109
- }
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
-
162
- self.captureSession = nil
163
- self.cameraView = nil
164
- self.previewLayer = nil
165
- self.scannedCodesVotes = [:]
166
- self.showWebViewBackground()
167
-
168
- self.removeOrientationChangeObserver()
169
-
170
- call.resolve()
171
- }
172
- }
173
-
174
- private func addOrientationChangeObserver() {
175
- self.orientationObserver = NotificationCenter.default.addObserver(
176
- forName: UIDevice.orientationDidChangeNotification,
177
- object: nil,
178
- queue: .main
179
- ) { [weak self] _ in
180
- self?.updatePreviewOrientation()
181
- }
182
- }
183
-
184
- private func removeOrientationChangeObserver() {
185
- if let observer = self.orientationObserver {
186
- NotificationCenter.default.removeObserver(observer)
187
- self.orientationObserver = nil
188
- }
189
- }
190
-
191
- private func updatePreviewOrientation() {
192
- guard let previewLayer = self.previewLayer,
193
- let connection = previewLayer.connection,
194
- let cameraView = self.cameraView
195
- else {
196
- return
197
- }
198
-
199
- let deviceOrientation = UIDevice.current.orientation
200
-
201
- if deviceOrientation.isFlat == true || deviceOrientation == .portraitUpsideDown {
202
- return
203
- }
204
-
205
- let newOrientation: AVCaptureVideoOrientation
206
-
207
- switch deviceOrientation {
208
- case .landscapeLeft:
209
- newOrientation = .landscapeRight
210
- case .landscapeRight:
211
- newOrientation = .landscapeLeft
212
- default:
213
- newOrientation = .portrait
214
- }
215
-
216
- connection.videoOrientation = newOrientation
217
-
218
- // Update camera view and preview layer frames
219
- let screenBounds = UIScreen.main.bounds
220
- let screenWidth = screenBounds.width
221
- let screenHeight = screenBounds.height
222
-
223
- // Determine the correct dimensions based on orientation
224
- let width: CGFloat
225
- let height: CGFloat
226
- if newOrientation == .portrait {
227
- width = min(screenWidth, screenHeight)
228
- height = max(screenWidth, screenHeight)
229
- } else {
230
- width = max(screenWidth, screenHeight)
231
- height = min(screenWidth, screenHeight)
232
- }
233
-
234
- // Update frames
235
- cameraView.frame = CGRect(x: 0, y: 0, width: width, height: height)
236
- previewLayer.frame = cameraView.bounds
237
- }
238
-
239
- public func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
240
- guard let metadataObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
241
- let stringValue = metadataObject.stringValue
242
- else {
243
- return
244
- }
245
-
246
- /*
247
- this is a voting system to
248
- 1. avoid scanning the same code
249
- 2. avoid scanning any code that doesn't appear for at least in 10 frames of the video stream to reduce the number of false positives
250
- */
251
-
252
- var voteStatus = self.scannedCodesVotes[stringValue] ?? VoteStatus(votes: 0, done: false)
253
-
254
- if !voteStatus.done {
255
- voteStatus.votes += 1
256
-
257
- if voteStatus.votes >= self.voteThreshold {
258
- voteStatus.done = true
259
-
260
- self.notifyListeners("barcodeScanned", data: [
261
- "scannedCode": stringValue,
262
- "format": CapacitorScannerHelpers.convertBarcodeScannerFormatToString(metadataObject.type)
263
- ])
264
- }
265
- }
266
-
267
- self.scannedCodesVotes[stringValue] = voteStatus
268
- }
269
-
270
- private func getCaptureDevice(position: AVCaptureDevice.Position) -> AVCaptureDevice? {
271
- let discoverySession = AVCaptureDevice.DiscoverySession(
272
- deviceTypes: [.builtInWideAngleCamera],
273
- mediaType: .video,
274
- position: position
275
- )
276
- return discoverySession.devices.first
277
- }
278
-
279
- private func getMetadataObjectTypes(from formats: [String]) -> [BarcodeFormat] {
280
- if formats.isEmpty {
281
- return CapacitorScannerHelpers.getAllSupportedFormats()
282
- }
283
-
284
- return formats.compactMap { format in
285
- CapacitorScannerHelpers.convertStringToBarcodeScannerFormat(format)
286
- }
287
- }
288
-
289
- /**
290
- * Must run on UI thread.
291
- */
292
- private func hideWebViewBackground() {
293
- guard let webView = self.webView else {
294
- return
295
- }
296
- webView.isOpaque = false
297
- webView.backgroundColor = UIColor.clear
298
- webView.scrollView.backgroundColor = UIColor.clear
299
- }
300
-
301
- /**
302
- * Must run on UI thread.
303
- */
304
- private func showWebViewBackground() {
305
- guard let webView = self.webView else {
306
- return
307
- }
308
- webView.isOpaque = true
309
- webView.backgroundColor = UIColor.white
310
- webView.scrollView.backgroundColor = UIColor.white
311
- }
312
-
313
- @objc override public func checkPermissions(_ call: CAPPluginCall) {
314
- let status = AVCaptureDevice.authorizationStatus(for: .video)
315
-
316
- var stringStatus = "prompt"
317
-
318
- if status == .denied || status == .restricted {
319
- stringStatus = "denied"
320
- }
321
-
322
- if status == .authorized {
323
- stringStatus = "granted"
324
- }
325
-
326
- call.resolve(["camera": stringStatus])
327
- }
328
-
329
- @objc override public func requestPermissions(_ call: CAPPluginCall) {
330
- AVCaptureDevice.requestAccess(for: .video) { _ in
331
- self.checkPermissions(call)
332
- }
333
- }
334
-
335
- @objc func openSettings(_ call: CAPPluginCall) {
336
- let url = URL(string: UIApplication.openSettingsURLString)
337
- DispatchQueue.main.async {
338
- if let url = url, UIApplication.shared.canOpenURL(url) {
339
- UIApplication.shared.open(url)
340
- call.resolve()
341
- } else {
342
- call.reject("unknown")
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scr2em/capacitor-scanner",
3
- "version": "6.0.3",
3
+ "version": "6.0.5",
4
4
  "description": "scan codes",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",