@scr2em/capacitor-scanner 6.0.2 → 6.0.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 CHANGED
@@ -121,7 +121,7 @@ removeAllListeners() => Promise<void>
121
121
 
122
122
  #### ScannerOptions
123
123
 
124
- <code>{ formats?: BarcodeFormat[]; cameraDirection?: 'BACK' | 'FRONT'; }</code>
124
+ <code>{ formats?: BarcodeFormat[]; cameraDirection?: 'BACK' | 'FRONT'; debounceTimeInMilli?: number }</code>
125
125
 
126
126
 
127
127
  #### CapturePhotoResult
@@ -169,7 +169,7 @@ public class CapacitorScannerPlugin extends Plugin {
169
169
  cameraProviderFuture.addListener(() -> {
170
170
  try {
171
171
  cameraProvider = cameraProviderFuture.get();
172
- bindCamera(cameraProvider, previewView, lensFacing);
172
+ bindCamera(cameraProvider, previewView, lensFacing, call);
173
173
 
174
174
  orientationEventListener = new OrientationEventListener(getContext()) {
175
175
  @Override
@@ -209,7 +209,7 @@ public class CapacitorScannerPlugin extends Plugin {
209
209
  });
210
210
  }
211
211
 
212
- private void bindCamera(@NonNull ProcessCameraProvider cameraProvider, PreviewView previewView, int lensFacing) {
212
+ private void bindCamera(@NonNull ProcessCameraProvider cameraProvider, PreviewView previewView, int lensFacing, PluginCall call) {
213
213
  cameraProvider.unbindAll();
214
214
 
215
215
  DisplayMetrics metrics = new DisplayMetrics();
@@ -240,7 +240,7 @@ public class CapacitorScannerPlugin extends Plugin {
240
240
  .setTargetRotation(rotation)
241
241
  .build();
242
242
 
243
- imageAnalysis.setAnalyzer(executor, new BarcodeAnalyzer());
243
+ imageAnalysis.setAnalyzer(executor, new BarcodeAnalyzer(call));
244
244
 
245
245
  imageCapture = new ImageCapture.Builder()
246
246
  .setTargetResolution(targetResolution)
@@ -260,18 +260,30 @@ public class CapacitorScannerPlugin extends Plugin {
260
260
  }
261
261
 
262
262
  @ExperimentalGetImage private class BarcodeAnalyzer implements ImageAnalysis.Analyzer {
263
+ final PluginCall call;
264
+ BarcodeAnalyzer(PluginCall call) {
265
+ this.call = call;
266
+ }
267
+
263
268
  @Override
264
269
  public void analyze(@NonNull ImageProxy imageProxy) {
265
270
  @androidx.camera.core.ExperimentalGetImage
266
271
  android.media.Image mediaImage = imageProxy.getImage();
267
272
  if (mediaImage != null) {
268
273
  InputImage image = InputImage.fromMediaImage(mediaImage, imageProxy.getImageInfo().getRotationDegrees());
269
- scanner.process(image)
270
- .addOnSuccessListener(executor,CapacitorScannerPlugin.this::processBarcodes)
271
- .addOnFailureListener(executor,e -> {
272
- echo("Failed to process image: " + e.getMessage());
273
- })
274
- .addOnCompleteListener(task -> imageProxy.close());
274
+ if (scanner != null) {
275
+ scanner.process(image)
276
+ .addOnSuccessListener(executor, CapacitorScannerPlugin.this::processBarcodes)
277
+ .addOnFailureListener(executor, e -> {
278
+ echo("Failed to process image: " + e.getMessage());
279
+ call.reject("Failed to process image: " + e.getMessage());
280
+ })
281
+ .addOnCompleteListener(task -> imageProxy.close());
282
+ } else {
283
+ imageProxy.close();
284
+ echo("Scanner is null, skipping analysis");
285
+ call.reject("Scanner is null, skipping analysis");
286
+ }
275
287
  } else {
276
288
  imageProxy.close();
277
289
  }
package/dist/docs.json CHANGED
@@ -197,7 +197,7 @@
197
197
  "docs": "",
198
198
  "types": [
199
199
  {
200
- "text": "{\n formats?: BarcodeFormat[];\n cameraDirection?: 'BACK' | 'FRONT';\n}",
200
+ "text": "{\n formats?: BarcodeFormat[];\n cameraDirection?: 'BACK' | 'FRONT';\n debounceTimeInMilli?: number\n}",
201
201
  "complexTypes": [
202
202
  "BarcodeFormat"
203
203
  ]
@@ -11,6 +11,7 @@ export interface CapacitorScannerPlugin {
11
11
  export declare type ScannerOptions = {
12
12
  formats?: BarcodeFormat[];
13
13
  cameraDirection?: 'BACK' | 'FRONT';
14
+ debounceTimeInMilli?: number;
14
15
  };
15
16
  export declare type BarcodeScannedEvent = {
16
17
  scannedCode: string;
@@ -1 +1 @@
1
- {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"AAsBA,MAAM,CAAN,IAAY,aAYX;AAZD,WAAY,aAAa;IACvB,gCAAe,CAAA;IACf,mCAAkB,CAAA;IAClB,mCAAkB,CAAA;IAClB,qCAAoB,CAAA;IACpB,2CAA0B,CAAA;IAC1B,+BAAc,CAAA;IACd,iCAAgB,CAAA;IAChB,gCAAe,CAAA;IACf,mCAAkB,CAAA;IAClB,mCAAkB,CAAA;IAClB,+BAAc,CAAA;AAChB,CAAC,EAZW,aAAa,KAAb,aAAa,QAYxB","sourcesContent":["export interface CapacitorScannerPlugin {\n startScanning(options?: ScannerOptions): Promise<void>;\n stopScanning(): Promise<void>;\n openSettings(): Promise<void>;\n capturePhoto(): Promise<CapturePhotoResult>;\n checkPermissions(): Promise<PermissionsResult>;\n requestPermissions(): Promise<PermissionsResult>;\n addListener(event: 'barcodeScanned', listenerFunc: (result: BarcodeScannedEvent) => void): Promise<void>;\n removeAllListeners(): Promise<void>;\n}\n\nexport type ScannerOptions = {\n formats?: BarcodeFormat[];\n cameraDirection?: 'BACK' | 'FRONT';\n};\n\nexport type BarcodeScannedEvent = { scannedCode: string; format: string };\n\nexport type PermissionsResult = { camera: 'prompt' | 'denied' | 'granted' };\n\nexport type CapturePhotoResult = { imageBase64: string };\n\nexport enum BarcodeFormat {\n Aztec = 'AZTEC',\n Code39 = 'CODE_39',\n Code93 = 'CODE_93',\n Code128 = 'CODE_128',\n DataMatrix = 'DATA_MATRIX',\n Ean8 = 'EAN_8',\n Ean13 = 'EAN_13',\n Itf14 = 'ITF14',\n Pdf417 = 'PDF_417',\n QrCode = 'QR_CODE',\n UpcE = 'UPC_E',\n}\n\ndeclare global {\n interface PluginRegistry {\n QRScanner: CapacitorScannerPlugin;\n }\n}\n"]}
1
+ {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"AAuBA,MAAM,CAAN,IAAY,aAYX;AAZD,WAAY,aAAa;IACvB,gCAAe,CAAA;IACf,mCAAkB,CAAA;IAClB,mCAAkB,CAAA;IAClB,qCAAoB,CAAA;IACpB,2CAA0B,CAAA;IAC1B,+BAAc,CAAA;IACd,iCAAgB,CAAA;IAChB,gCAAe,CAAA;IACf,mCAAkB,CAAA;IAClB,mCAAkB,CAAA;IAClB,+BAAc,CAAA;AAChB,CAAC,EAZW,aAAa,KAAb,aAAa,QAYxB","sourcesContent":["export interface CapacitorScannerPlugin {\n startScanning(options?: ScannerOptions): Promise<void>;\n stopScanning(): Promise<void>;\n openSettings(): Promise<void>;\n capturePhoto(): Promise<CapturePhotoResult>;\n checkPermissions(): Promise<PermissionsResult>;\n requestPermissions(): Promise<PermissionsResult>;\n addListener(event: 'barcodeScanned', listenerFunc: (result: BarcodeScannedEvent) => void): Promise<void>;\n removeAllListeners(): Promise<void>;\n}\n\nexport type ScannerOptions = {\n formats?: BarcodeFormat[];\n cameraDirection?: 'BACK' | 'FRONT';\n debounceTimeInMilli?: number\n};\n\nexport type BarcodeScannedEvent = { scannedCode: string; format: string };\n\nexport type PermissionsResult = { camera: 'prompt' | 'denied' | 'granted' };\n\nexport type CapturePhotoResult = { imageBase64: string };\n\nexport enum BarcodeFormat {\n Aztec = 'AZTEC',\n Code39 = 'CODE_39',\n Code93 = 'CODE_93',\n Code128 = 'CODE_128',\n DataMatrix = 'DATA_MATRIX',\n Ean8 = 'EAN_8',\n Ean13 = 'EAN_13',\n Itf14 = 'ITF14',\n Pdf417 = 'PDF_417',\n QrCode = 'QR_CODE',\n UpcE = 'UPC_E',\n}\n\ndeclare global {\n interface PluginRegistry {\n QRScanner: CapacitorScannerPlugin;\n }\n}\n"]}
@@ -7,340 +7,358 @@ import Foundation
7
7
  * here: https://capacitorjs.com/docs/plugins/ios
8
8
  */
9
9
  struct VoteStatus {
10
- var votes: Int
11
- var done: Bool
10
+ var votes: Int
11
+ var done: Bool
12
12
  }
13
13
 
14
14
  @objc(CapacitorScannerPlugin)
15
15
  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
- }
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
+
72
+ // Always check if the device supports 4K first
73
+ if captureSession.canSetSessionPreset(.hd4K3840x2160) {
74
+ captureSession.sessionPreset = .hd4K3840x2160
75
+ } else if captureSession.canSetSessionPreset(.hd1920x1080) {
76
+ captureSession.sessionPreset = .hd1920x1080
77
+ } else {
78
+ captureSession.sessionPreset = .hd1280x720
79
+ }
80
+
81
+ captureSession.sessionPreset = AVCaptureSession.Preset.hd4K3840x2160
82
+
83
+ let cameraDirection: AVCaptureDevice.Position = call.getString("cameraDirection", "BACK") == "BACK" ? .back : .front
84
+
85
+ guard let videoCaptureDevice = self.getCaptureDevice(position: cameraDirection) else {
86
+ call.reject("Unable to access the camera")
87
+ return
88
+ }
89
+
90
+ let videoInput: AVCaptureDeviceInput
91
+
92
+ do {
93
+ videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
94
+ } catch {
95
+ call.reject("Unable to initialize video input")
96
+ return
97
+ }
98
+
99
+ if captureSession.canAddInput(videoInput) {
100
+ captureSession.addInput(videoInput)
101
+ } else {
102
+ call.reject("Unable to add video input to capture session")
103
+ return
104
+ }
105
+
106
+ let metadataOutput = AVCaptureMetadataOutput()
107
+
108
+ if captureSession.canAddOutput(metadataOutput) {
109
+ captureSession.addOutput(metadataOutput)
110
+
111
+ metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
112
+
113
+ let formats = call.getArray("formats", String.self) ?? []
114
+ let metadataObjectTypes = self.getMetadataObjectTypes(from: formats)
115
+ metadataOutput.metadataObjectTypes = metadataObjectTypes
116
+ } else {
117
+ call.reject("Unable to add metadata output to capture session")
118
+ return
119
+ }
120
+
121
+ // Add photo output
122
+ let photoOutput = AVCapturePhotoOutput()
123
+ self.photoOutput = photoOutput
124
+ if captureSession.canAddOutput(photoOutput) {
125
+ captureSession.addOutput(photoOutput)
126
+ } else {
127
+ call.reject("Unable to add photo output to capture session")
128
+ return
129
+ }
130
+
131
+ self.hideWebViewBackground()
132
+
133
+ if let webView = self.webView, let superView = webView.superview {
134
+ let cameraView = UIView(frame: superView.bounds)
135
+ let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
136
+ previewLayer.videoGravity = .resizeAspectFill
137
+ previewLayer.frame = superView.bounds
138
+ cameraView.layer.addSublayer(previewLayer)
139
+
140
+ webView.superview?.insertSubview(cameraView, belowSubview: webView)
141
+
142
+ self.captureSession = captureSession
143
+ self.cameraView = cameraView
144
+ self.previewLayer = previewLayer
145
+
146
+ // Add orientation change observer
147
+ self.addOrientationChangeObserver()
148
+
149
+ // Initial orientation setup
150
+ self.updatePreviewOrientation()
151
+
152
+ DispatchQueue.global(qos: .background).async {
153
+ captureSession.startRunning()
154
+ call.resolve()
155
+ }
156
+
157
+ } else {
158
+ call.reject("unknown")
159
+ }
160
+ }
161
+ }
162
+
163
+ @objc func stopScanning(_ call: CAPPluginCall) {
164
+ DispatchQueue.main.async {
165
+ if let captureSession = self.captureSession {
166
+ captureSession.stopRunning()
167
+ }
168
+
169
+ self.previewLayer?.removeFromSuperlayer()
170
+ self.cameraView?.removeFromSuperview()
171
+
172
+ self.captureSession = nil
173
+ self.cameraView = nil
174
+ self.previewLayer = nil
175
+ self.scannedCodesVotes = [:]
176
+ self.showWebViewBackground()
177
+
178
+ self.removeOrientationChangeObserver()
179
+
180
+ call.resolve()
181
+ }
182
+ }
183
+
184
+ private func addOrientationChangeObserver() {
185
+ self.orientationObserver = NotificationCenter.default.addObserver(
186
+ forName: UIDevice.orientationDidChangeNotification,
187
+ object: nil,
188
+ queue: .main
189
+ ) { [weak self] _ in
190
+ self?.updatePreviewOrientation()
191
+ }
192
+ }
193
+
194
+ private func removeOrientationChangeObserver() {
195
+ if let observer = self.orientationObserver {
196
+ NotificationCenter.default.removeObserver(observer)
197
+ self.orientationObserver = nil
198
+ }
199
+ }
200
+
201
+ private func updatePreviewOrientation() {
202
+ guard let previewLayer = self.previewLayer,
203
+ let connection = previewLayer.connection,
204
+ let cameraView = self.cameraView
205
+ else {
206
+ return
207
+ }
208
+
209
+ let deviceOrientation = UIDevice.current.orientation
210
+
211
+ if deviceOrientation.isFlat == true || deviceOrientation == .portraitUpsideDown {
212
+ return
213
+ }
214
+
215
+ let newOrientation: AVCaptureVideoOrientation
216
+
217
+ switch deviceOrientation {
218
+ case .landscapeLeft:
219
+ newOrientation = .landscapeRight
220
+ case .landscapeRight:
221
+ newOrientation = .landscapeLeft
222
+ default:
223
+ newOrientation = .portrait
224
+ }
225
+
226
+ connection.videoOrientation = newOrientation
227
+
228
+ // Update camera view and preview layer frames
229
+ let screenBounds = UIScreen.main.bounds
230
+ let screenWidth = screenBounds.width
231
+ let screenHeight = screenBounds.height
232
+
233
+ // Determine the correct dimensions based on orientation
234
+ let width: CGFloat
235
+ let height: CGFloat
236
+ if newOrientation == .portrait {
237
+ width = min(screenWidth, screenHeight)
238
+ height = max(screenWidth, screenHeight)
239
+ } else {
240
+ width = max(screenWidth, screenHeight)
241
+ height = min(screenWidth, screenHeight)
242
+ }
243
+
244
+ // Update frames
245
+ cameraView.frame = CGRect(x: 0, y: 0, width: width, height: height)
246
+ previewLayer.frame = cameraView.bounds
247
+ }
248
+
249
+ public func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
250
+ guard let metadataObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
251
+ let stringValue = metadataObject.stringValue
252
+ else {
253
+ return
254
+ }
255
+
256
+ /*
257
+ this is a voting system to
258
+ 1. avoid scanning the same code
259
+ 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
260
+ */
261
+
262
+ var voteStatus = self.scannedCodesVotes[stringValue] ?? VoteStatus(votes: 0, done: false)
263
+
264
+ if !voteStatus.done {
265
+ voteStatus.votes += 1
266
+
267
+ if voteStatus.votes >= self.voteThreshold {
268
+ voteStatus.done = true
269
+
270
+ self.notifyListeners("barcodeScanned", data: [
271
+ "scannedCode": stringValue,
272
+ "format": CapacitorScannerHelpers.convertBarcodeScannerFormatToString(metadataObject.type)
273
+ ])
274
+ }
275
+ }
276
+
277
+ self.scannedCodesVotes[stringValue] = voteStatus
278
+ }
279
+
280
+ private func getCaptureDevice(position: AVCaptureDevice.Position) -> AVCaptureDevice? {
281
+ let discoverySession = AVCaptureDevice.DiscoverySession(
282
+ deviceTypes: [.builtInDualCamera, .builtInTripleCamera, .builtInWideAngleCamera],
283
+ mediaType: .video,
284
+ position: position
285
+ )
286
+ // Prioritize higher quality cameras first
287
+ if let device = discoverySession.devices.first(where: { $0.deviceType == .builtInTripleCamera }) ??
288
+ discoverySession.devices.first(where: { $0.deviceType == .builtInDualCamera }) ??
289
+ discoverySession.devices.first(where: { $0.deviceType == .builtInWideAngleCamera })
290
+ {
291
+ return device
292
+ }
293
+
294
+ return nil
295
+ }
296
+
297
+ private func getMetadataObjectTypes(from formats: [String]) -> [BarcodeFormat] {
298
+ if formats.isEmpty {
299
+ return CapacitorScannerHelpers.getAllSupportedFormats()
300
+ }
301
+
302
+ return formats.compactMap { format in
303
+ CapacitorScannerHelpers.convertStringToBarcodeScannerFormat(format)
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Must run on UI thread.
309
+ */
310
+ private func hideWebViewBackground() {
311
+ guard let webView = self.webView else {
312
+ return
313
+ }
314
+ webView.isOpaque = false
315
+ webView.backgroundColor = UIColor.clear
316
+ webView.scrollView.backgroundColor = UIColor.clear
317
+ }
318
+
319
+ /**
320
+ * Must run on UI thread.
321
+ */
322
+ private func showWebViewBackground() {
323
+ guard let webView = self.webView else {
324
+ return
325
+ }
326
+ webView.isOpaque = true
327
+ webView.backgroundColor = UIColor.white
328
+ webView.scrollView.backgroundColor = UIColor.white
329
+ }
330
+
331
+ @objc override public func checkPermissions(_ call: CAPPluginCall) {
332
+ let status = AVCaptureDevice.authorizationStatus(for: .video)
333
+
334
+ var stringStatus = "prompt"
335
+
336
+ if status == .denied || status == .restricted {
337
+ stringStatus = "denied"
338
+ }
339
+
340
+ if status == .authorized {
341
+ stringStatus = "granted"
342
+ }
343
+
344
+ call.resolve(["camera": stringStatus])
345
+ }
346
+
347
+ @objc override public func requestPermissions(_ call: CAPPluginCall) {
348
+ AVCaptureDevice.requestAccess(for: .video) { _ in
349
+ self.checkPermissions(call)
350
+ }
351
+ }
352
+
353
+ @objc func openSettings(_ call: CAPPluginCall) {
354
+ let url = URL(string: UIApplication.openSettingsURLString)
355
+ DispatchQueue.main.async {
356
+ if let url = url, UIApplication.shared.canOpenURL(url) {
357
+ UIApplication.shared.open(url)
358
+ call.resolve()
359
+ } else {
360
+ call.reject("unknown")
361
+ }
362
+ }
363
+ }
346
364
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scr2em/capacitor-scanner",
3
- "version": "6.0.2",
3
+ "version": "6.0.4",
4
4
  "description": "scan codes",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",