Ryan Duffy

Better memory footprint and using a dispatch queue

By not passing in the buffer to the barcode detect and using an image instead the footprint of the memory usage is significantly reduced.
@@ -2,6 +2,7 @@ import AVFoundation @@ -2,6 +2,7 @@ import AVFoundation
2 import FlutterMacOS 2 import FlutterMacOS
3 import Vision 3 import Vision
4 import AppKit 4 import AppKit
  5 +import VideoToolbox
5 6
6 public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, FlutterTexture, AVCaptureVideoDataOutputSampleBufferDelegate { 7 public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, FlutterTexture, AVCaptureVideoDataOutputSampleBufferDelegate {
7 8
@@ -17,8 +18,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -17,8 +18,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
17 var captureSession: AVCaptureSession! 18 var captureSession: AVCaptureSession!
18 19
19 // The selected camera 20 // The selected camera
20 - var device: AVCaptureDevice!  
21 - 21 + weak var device: AVCaptureDevice!
  22 +
22 // Image to be sent to the texture 23 // Image to be sent to the texture
23 var latestBuffer: CVImageBuffer! 24 var latestBuffer: CVImageBuffer!
24 25
@@ -95,7 +96,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -95,7 +96,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
95 } 96 }
96 97
97 var nextScanTime = 0.0 98 var nextScanTime = 0.0
98 - var imagesCurrentlyBeingProcessed = 0 99 + var imagesCurrentlyBeingProcessed = false
99 100
100 // Gets called when a new image is added to the buffer 101 // Gets called when a new image is added to the buffer
101 public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { 102 public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
@@ -111,50 +112,53 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -111,50 +112,53 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
111 registry.textureFrameAvailable(textureId) 112 registry.textureFrameAvailable(textureId)
112 113
113 let currentTime = Date().timeIntervalSince1970 114 let currentTime = Date().timeIntervalSince1970
114 - let eligibleForScan = currentTime > nextScanTime && imagesCurrentlyBeingProcessed == 0; 115 + let eligibleForScan = currentTime > nextScanTime && imagesCurrentlyBeingProcessed == false
115 if ((detectionSpeed == DetectionSpeed.normal || detectionSpeed == DetectionSpeed.noDuplicates) && eligibleForScan || detectionSpeed == DetectionSpeed.unrestricted) { 116 if ((detectionSpeed == DetectionSpeed.normal || detectionSpeed == DetectionSpeed.noDuplicates) && eligibleForScan || detectionSpeed == DetectionSpeed.unrestricted) {
116 nextScanTime = currentTime + timeoutSeconds 117 nextScanTime = currentTime + timeoutSeconds
117 - imagesCurrentlyBeingProcessed += 1  
118 - let imageRequestHandler = VNImageRequestHandler(  
119 - cvPixelBuffer: latestBuffer,  
120 - orientation: .right)  
121 - 118 + imagesCurrentlyBeingProcessed = true
  119 + DispatchQueue.global(qos: .userInitiated).async { [weak self] in
  120 + if(self!.latestBuffer == nil){
  121 + return
  122 + }
  123 + var cgImage: CGImage?
  124 + VTCreateCGImageFromCVPixelBuffer(self!.latestBuffer, options: nil, imageOut: &cgImage)
  125 + let imageRequestHandler = VNImageRequestHandler(cgImage: cgImage!)
122 do { 126 do {
123 - let barcodeRequest:VNDetectBarcodesRequest = VNDetectBarcodesRequest(completionHandler: { [self] (request, error) in  
124 - imagesCurrentlyBeingProcessed -= 1  
125 - if error == nil {  
126 - if let results = request.results as? [VNBarcodeObservation] { 127 + let barcodeRequest:VNDetectBarcodesRequest = VNDetectBarcodesRequest(completionHandler: { [weak self] (request, error) in
  128 + self?.imagesCurrentlyBeingProcessed = false
  129 + if error == nil {
  130 + if let results = request.results as? [VNBarcodeObservation] {
127 for barcode in results { 131 for barcode in results {
128 - if self.scanWindow != nil {  
129 - let match = self.isbarCodeInScanWindow(self.scanWindow!, barcode, self.latestBuffer)  
130 - if (!match) { 132 + if self?.scanWindow != nil && cgImage != nil {
  133 + let match = self?.isbarCodeInScanWindow(self!.scanWindow!, barcode, cgImage!)
  134 + if (match == false) {
131 continue 135 continue
132 } 136 }
133 } 137 }
134 138
135 - let barcodeType = String(barcode.symbology.rawValue).replacingOccurrences(of: "VNBarcodeSymbology", with: "")  
136 - let event: [String: Any?] = ["name": "barcodeMac", "data" : ["payload": barcode.payloadStringValue, "symbology": barcode.symbology.toInt as Any?]]  
137 - self.sink?(event)  
138 -  
139 - // if barcodeType == "QR" {  
140 - // let image = CIImage(image: source)  
141 - // image?.cropping(to: barcode.boundingBox)  
142 - // self.qrCodeDescriptor(qrCode: barcode, qrCodeImage: image!)  
143 - // }  
144 - }  
145 - }  
146 - } else {  
147 - print(error!.localizedDescription)  
148 - }  
149 - })  
150 - if(symbologies.isEmpty == false){  
151 - // add the symbologies the user wishes to support  
152 - barcodeRequest.symbologies = symbologies  
153 - }  
154 - try imageRequestHandler.perform([barcodeRequest]) 139 + DispatchQueue.main.async {
  140 + self?.sink?(["name": "barcodeMac", "data" : ["payload": barcode.payloadStringValue, "symbology": barcode.symbology.toInt as Any?]] as [String : Any])
  141 + }
  142 +// if barcodeType == "QR" {
  143 +// let image = CIImage(image: source)
  144 +// image?.cropping(to: barcode.boundingBox)
  145 +// self.qrCodeDescriptor(qrCode: barcode, qrCodeImage: image!)
  146 +// }
  147 + }
  148 + }
  149 + } else {
  150 + print(error!.localizedDescription)
  151 + }
  152 + })
  153 + if(self?.symbologies.isEmpty == false){
  154 + // add the symbologies the user wishes to support
  155 + barcodeRequest.symbologies = self!.symbologies
  156 + }
  157 + try imageRequestHandler.perform([barcodeRequest])
155 } catch { 158 } catch {
156 - print(error) 159 + print(error)
157 } 160 }
  161 + }
158 } 162 }
159 } 163 }
160 164
@@ -187,10 +191,10 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -187,10 +191,10 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
187 let scanWindowData: Array? = argReader.floatArray(key: "rect") 191 let scanWindowData: Array? = argReader.floatArray(key: "rect")
188 192
189 if (scanWindowData == nil) { 193 if (scanWindowData == nil) {
190 - return 194 + return
191 } 195 }
192 196
193 - let minX = scanWindowData![0] 197 + let minX = scanWindowData![0]
194 let minY = scanWindowData![1] 198 let minY = scanWindowData![1]
195 199
196 let width = scanWindowData![2] - minX 200 let width = scanWindowData![2] - minX
@@ -198,9 +202,23 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -198,9 +202,23 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
198 202
199 scanWindow = CGRect(x: minX, y: minY, width: width, height: height) 203 scanWindow = CGRect(x: minX, y: minY, width: width, height: height)
200 } 204 }
  205 +
  206 + func isbarCodeInScanWindow(_ scanWindow: CGRect, _ barcode: VNBarcodeObservation, _ inputImage: CGImage) -> Bool {
  207 +
  208 + let imageWidth = CGFloat(inputImage.width);
  209 + let imageHeight = CGFloat(inputImage.height);
  210 +
  211 + let minX = scanWindow.minX * imageWidth
  212 + let minY = scanWindow.minY * imageHeight
  213 + let width = scanWindow.width * imageWidth
  214 + let height = scanWindow.height * imageHeight
  215 +
  216 + let scaledScanWindow = CGRect(x: minX, y: minY, width: width, height: height)
  217 + return scaledScanWindow.contains(barcode.boundingBox)
  218 + }
201 219
202 func isbarCodeInScanWindow(_ scanWindow: CGRect, _ barcode: VNBarcodeObservation, _ inputImage: CVImageBuffer) -> Bool { 220 func isbarCodeInScanWindow(_ scanWindow: CGRect, _ barcode: VNBarcodeObservation, _ inputImage: CVImageBuffer) -> Bool {
203 - let size = CVImageBufferGetEncodedSize(inputImage) 221 + let size = CVImageBufferGetEncodedSize(inputImage)
204 222
205 let imageWidth = size.width; 223 let imageWidth = size.width;
206 let imageHeight = size.height; 224 let imageHeight = size.height;
@@ -217,22 +235,22 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -217,22 +235,22 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
217 func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { 235 func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
218 if (device != nil) { 236 if (device != nil) {
219 result(FlutterError(code: "MobileScanner", 237 result(FlutterError(code: "MobileScanner",
220 - message: "Called start() while already started!",  
221 - details: nil)) 238 + message: "Called start() while already started!",
  239 + details: nil))
222 return 240 return
223 } 241 }
224 - 242 +
225 textureId = registry.register(self) 243 textureId = registry.register(self)
226 captureSession = AVCaptureSession() 244 captureSession = AVCaptureSession()
227 - 245 +
228 let argReader = MapArgumentReader(call.arguments as? [String: Any]) 246 let argReader = MapArgumentReader(call.arguments as? [String: Any])
229 -  
230 -// let ratio: Int = argReader.int(key: "ratio")  
231 - let torch: Bool = argReader.bool(key: "torch") ?? false  
232 - let facing: Int = argReader.int(key: "facing") ?? 1  
233 - let speed: Int = (call.arguments as! Dictionary<String, Any?>)["speed"] as? Int ?? 0  
234 - let timeoutMs: Int = (call.arguments as! Dictionary<String, Any?>)["timeout"] as? Int ?? 0  
235 - symbologies = argReader.toSymbology() 247 +
  248 + // let ratio: Int = argReader.int(key: "ratio")
  249 + let torch:Bool = argReader.bool(key: "torch") ?? false
  250 + let facing:Int = argReader.int(key: "facing") ?? 1
  251 + let speed:Int = argReader.int(key: "speed") ?? 0
  252 + let timeoutMs:Int = argReader.int(key: "timeout") ?? 0
  253 + symbologies = argReader.toSymbology()
236 254
237 timeoutSeconds = Double(timeoutMs) / 1000.0 255 timeoutSeconds = Double(timeoutMs) / 1000.0
238 detectionSpeed = DetectionSpeed(rawValue: speed)! 256 detectionSpeed = DetectionSpeed(rawValue: speed)!
@@ -249,8 +267,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -249,8 +267,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
249 267
250 if (device == nil) { 268 if (device == nil) {
251 result(FlutterError(code: "MobileScanner", 269 result(FlutterError(code: "MobileScanner",
252 - message: "No camera found or failed to open camera!",  
253 - details: nil)) 270 + message: "No camera found or failed to open camera!",
  271 + details: nil))
254 return 272 return
255 } 273 }
256 274
@@ -258,10 +276,10 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -258,10 +276,10 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
258 if (device.hasTorch) { 276 if (device.hasTorch) {
259 do { 277 do {
260 try device.lockForConfiguration() 278 try device.lockForConfiguration()
261 - device.torchMode = torch ? .on : .off  
262 - device.unlockForConfiguration()  
263 - } catch {  
264 - result(FlutterError(code: error.localizedDescription, message: nil, details: nil)) 279 + device.torchMode = torch ? .on : .off
  280 + device.unlockForConfiguration()
  281 + } catch {
  282 + result(FlutterError(code: error.localizedDescription, message: nil, details: nil))
265 } 283 }
266 } 284 }
267 285
@@ -285,7 +303,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -285,7 +303,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
285 videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main) 303 videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main)
286 captureSession.addOutput(videoOutput) 304 captureSession.addOutput(videoOutput)
287 for connection in videoOutput.connections { 305 for connection in videoOutput.connections {
288 -// connection.videoOrientation = .portrait 306 + // connection.videoOrientation = .portrait
289 if position == .front && connection.isVideoMirroringSupported { 307 if position == .front && connection.isVideoMirroringSupported {
290 connection.isVideoMirrored = true 308 connection.isVideoMirrored = true
291 } 309 }
@@ -359,118 +377,117 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -359,118 +377,117 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
359 377
360 class MapArgumentReader { 378 class MapArgumentReader {
361 379
362 - let args: [String: Any]?  
363 -  
364 - init(_ args: [String: Any]?) {  
365 - self.args = args  
366 - }  
367 -  
368 - func string(key: String) -> String? {  
369 - return args?[key] as? String  
370 - }  
371 -  
372 - func int(key: String) -> Int? {  
373 - return (args?[key] as? NSNumber)?.intValue  
374 - }  
375 -  
376 - func bool(key: String) -> Bool? {  
377 - return (args?[key] as? NSNumber)?.boolValue  
378 - }  
379 -  
380 - func stringArray(key: String) -> [String]? {  
381 - return args?[key] as? [String]  
382 - }  
383 -  
384 - func toSymbology() -> [VNBarcodeSymbology] {  
385 - guard let syms:[Int] = args?["formats"] as? [Int] else {  
386 - return [] 380 + let args: [String: Any]?
  381 +
  382 + init(_ args: [String: Any]?) {
  383 + self.args = args
  384 + }
  385 +
  386 + func string(key: String) -> String? {
  387 + return args?[key] as? String
  388 + }
  389 +
  390 + func int(key: String) -> Int? {
  391 + return (args?[key] as? NSNumber)?.intValue
387 } 392 }
388 - if(syms.contains(0)){  
389 - return [] 393 +
  394 + func bool(key: String) -> Bool? {
  395 + return (args?[key] as? NSNumber)?.boolValue
390 } 396 }
391 - var barcodeFormats:[VNBarcodeSymbology] = []  
392 - syms.forEach { id in  
393 - if let bc:VNBarcodeSymbology = VNBarcodeSymbology.fromInt(id) {  
394 - barcodeFormats.append(bc)  
395 - } 397 +
  398 + func stringArray(key: String) -> [String]? {
  399 + return args?[key] as? [String]
  400 + }
  401 +
  402 + func toSymbology() -> [VNBarcodeSymbology] {
  403 + guard let syms:[Int] = args?["formats"] as? [Int] else {
  404 + return []
  405 + }
  406 + if(syms.contains(0)){
  407 + return []
  408 + }
  409 + var barcodeFormats:[VNBarcodeSymbology] = []
  410 + syms.forEach { id in
  411 + if let bc:VNBarcodeSymbology = VNBarcodeSymbology.fromInt(id) {
  412 + barcodeFormats.append(bc)
  413 + }
  414 + }
  415 + return barcodeFormats
396 } 416 }
397 - return barcodeFormats  
398 - }  
399 417
400 - func floatArray(key: String) -> [CGFloat]? {  
401 - return args?[key] as? [CGFloat]  
402 - } 418 + func floatArray(key: String) -> [CGFloat]? {
  419 + return args?[key] as? [CGFloat]
  420 + }
403 421
404 } 422 }
405 423
406 extension VNBarcodeSymbology { 424 extension VNBarcodeSymbology {
407 425
408 - static func fromInt(_ mapValue:Int) -> VNBarcodeSymbology? {  
409 - if #available(macOS 12.0, *) {  
410 - if(mapValue == 8){  
411 - return VNBarcodeSymbology.codabar  
412 - }  
413 - }  
414 - switch(mapValue){  
415 - case 1:  
416 - return VNBarcodeSymbology.code128  
417 - case 2:  
418 - return VNBarcodeSymbology.code39  
419 - case 4:  
420 - return VNBarcodeSymbology.code93  
421 - case 16:  
422 - return VNBarcodeSymbology.dataMatrix  
423 - case 32:  
424 - return VNBarcodeSymbology.ean13  
425 - case 64:  
426 - return VNBarcodeSymbology.ean8  
427 - case 128:  
428 - return VNBarcodeSymbology.itf14  
429 - case 256:  
430 - return VNBarcodeSymbology.qr  
431 - case 1024:  
432 - return VNBarcodeSymbology.upce  
433 - case 2048:  
434 - return VNBarcodeSymbology.pdf417  
435 - case 4096:  
436 - return VNBarcodeSymbology.aztec  
437 - default:  
438 - return nil  
439 - }  
440 - }  
441 -  
442 - var toInt:Int? {  
443 - if #available(macOS 12.0, *) {  
444 - if(self == VNBarcodeSymbology.codabar){  
445 - return 8  
446 - } 426 + static func fromInt(_ mapValue:Int) -> VNBarcodeSymbology? {
  427 + if #available(macOS 12.0, *) {
  428 + if(mapValue == 8){
  429 + return VNBarcodeSymbology.codabar
  430 + }
  431 + }
  432 + switch(mapValue){
  433 + case 1:
  434 + return VNBarcodeSymbology.code128
  435 + case 2:
  436 + return VNBarcodeSymbology.code39
  437 + case 4:
  438 + return VNBarcodeSymbology.code93
  439 + case 16:
  440 + return VNBarcodeSymbology.dataMatrix
  441 + case 32:
  442 + return VNBarcodeSymbology.ean13
  443 + case 64:
  444 + return VNBarcodeSymbology.ean8
  445 + case 128:
  446 + return VNBarcodeSymbology.itf14
  447 + case 256:
  448 + return VNBarcodeSymbology.qr
  449 + case 1024:
  450 + return VNBarcodeSymbology.upce
  451 + case 2048:
  452 + return VNBarcodeSymbology.pdf417
  453 + case 4096:
  454 + return VNBarcodeSymbology.aztec
  455 + default:
  456 + return nil
  457 + }
447 } 458 }
448 - switch(self){  
449 - case VNBarcodeSymbology.code128:  
450 - return 1  
451 - case VNBarcodeSymbology.code39:  
452 - return 2  
453 - case VNBarcodeSymbology.code93:  
454 - return 4  
455 - case VNBarcodeSymbology.dataMatrix:  
456 - return 16  
457 - case VNBarcodeSymbology.ean13:  
458 - return 32  
459 - case VNBarcodeSymbology.ean8:  
460 - return 64  
461 - case VNBarcodeSymbology.itf14:  
462 - return 128  
463 - case VNBarcodeSymbology.qr:  
464 - return 256  
465 - case VNBarcodeSymbology.upce:  
466 - return 1024  
467 - case VNBarcodeSymbology.pdf417:  
468 - return 2048  
469 - case VNBarcodeSymbology.aztec:  
470 - return 4096  
471 - default:  
472 - return -1; 459 +
  460 + var toInt:Int? {
  461 + if #available(macOS 12.0, *) {
  462 + if(self == VNBarcodeSymbology.codabar){
  463 + return 8
  464 + }
  465 + }
  466 + switch(self){
  467 + case VNBarcodeSymbology.code128:
  468 + return 1
  469 + case VNBarcodeSymbology.code39:
  470 + return 2
  471 + case VNBarcodeSymbology.code93:
  472 + return 4
  473 + case VNBarcodeSymbology.dataMatrix:
  474 + return 16
  475 + case VNBarcodeSymbology.ean13:
  476 + return 32
  477 + case VNBarcodeSymbology.ean8:
  478 + return 64
  479 + case VNBarcodeSymbology.itf14:
  480 + return 128
  481 + case VNBarcodeSymbology.qr:
  482 + return 256
  483 + case VNBarcodeSymbology.upce:
  484 + return 1024
  485 + case VNBarcodeSymbology.pdf417:
  486 + return 2048
  487 + case VNBarcodeSymbology.aztec:
  488 + return 4096
  489 + default:
  490 + return -1;
  491 + }
473 } 492 }
474 - }  
475 -  
476 } 493 }