From 60858c2831da931b3cdd507d5ee2ad23fe08416f Mon Sep 17 00:00:00 2001 From: J Caso Date: Fri, 13 Dec 2024 12:09:07 +0100 Subject: [PATCH 1/4] fix: DispatchQueue for synchronizing the session configuration and start operations --- .../ios/Plugin/BarcodeScannerView.swift | 101 +++++++++++------- 1 file changed, 61 insertions(+), 40 deletions(-) diff --git a/packages/barcode-scanning/ios/Plugin/BarcodeScannerView.swift b/packages/barcode-scanning/ios/Plugin/BarcodeScannerView.swift index 919ede0..98635b2 100644 --- a/packages/barcode-scanning/ios/Plugin/BarcodeScannerView.swift +++ b/packages/barcode-scanning/ios/Plugin/BarcodeScannerView.swift @@ -28,63 +28,84 @@ public protocol BarcodeScannerViewDelegate { private var torchButton: UIButton? private var detectionAreaView: UIView? private var detectionAreaViewFrame: CGRect? + private var error: Error? init (implementation: BarcodeScanner, settings: ScanSettings) throws { self.implementation = implementation self.settings = settings - + // creates a serial DispatchQueue, which ensures operations are executed in a First In, First Out + // (FIFO) order, meaning tasks are completed one at a time in the exact order they were added to + // the queue. + let captureSessionQueue = DispatchQueue(label: "com.google.mlkit.visiondetector.CaptureSessionQueue") + super.init(frame: UIScreen.main.bounds) let captureSession = AVCaptureSession() - captureSession.beginConfiguration() - captureSession.sessionPreset = AVCaptureSession.Preset.hd1280x720 + // Prepare capture session and preview layer + // It executes tasks one at a time in the order they are added (FIFO), ensuring that no other + // tasks on the same queue can run simultaneously or out of order with respect to the synchronous + // block + captureSessionQueue.sync { + do { + captureSession.beginConfiguration() + captureSession.sessionPreset = AVCaptureSession.Preset.hd1280x720 - let captureDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: AVMediaType.video, position: settings.lensFacing) - guard let captureDevice = captureDevice else { - throw RuntimeError(implementation.plugin.errorNoCaptureDeviceAvailable) - } - var deviceInput: AVCaptureDeviceInput - deviceInput = try AVCaptureDeviceInput(device: captureDevice) - if captureSession.canAddInput(deviceInput) { - captureSession.addInput(deviceInput) - } else { - throw RuntimeError(implementation.plugin.errorCannotAddCaptureInput) + guard let captureDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: settings.lensFacing), + let deviceInput = try? AVCaptureDeviceInput(device: captureDevice) else { + throw RuntimeError(implementation.plugin.errorNoCaptureDeviceAvailable) + } + + if captureSession.canAddInput(deviceInput) { + captureSession.addInput(deviceInput) + } else { + throw RuntimeError(implementation.plugin.errorCannotAddCaptureInput) + } + + let deviceOutput = AVCaptureVideoDataOutput() + deviceOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32BGRA)] + deviceOutput.alwaysDiscardsLateVideoFrames = true + let outputQueue = DispatchQueue(label: "com.google.mlkit.visiondetector.VideoDataOutputQueue") + deviceOutput.setSampleBufferDelegate(self, queue: outputQueue) + + if captureSession.canAddOutput(deviceOutput) { + captureSession.addOutput(deviceOutput) + } else { + throw RuntimeError(implementation.plugin.errorCannotAddCaptureOutput) + } + + captureSession.commitConfiguration() + } catch { + print("Failed to configure AVCaptureSession: \(error)") + self.error = error + } } - let deviceOutput = AVCaptureVideoDataOutput() - deviceOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32BGRA)] - deviceOutput.alwaysDiscardsLateVideoFrames = true - let outputQueue = DispatchQueue(label: "com.google.mlkit.visiondetector.VideoDataOutputQueue") - deviceOutput.setSampleBufferDelegate(self, queue: outputQueue) - if captureSession.canAddOutput(deviceOutput) { - captureSession.addOutput(deviceOutput) - } else { - throw RuntimeError(implementation.plugin.errorCannotAddCaptureOutput) + + if let error = self.error { + throw error } - captureSession.commitConfiguration() - // `session.startRunning()` should be called after `session.commitConfiguration()` is complete. - // However, occacsionally `commitConfiguration()` runs asynchronously, so when `startRunning()` - // is called, `commitConfiguration()` is still in progress and the state is still `uncommited`. - // This can be reproduced by repeatedly switching or toggling the camera using the plugin demo. - // Adding a 100ms delay ensures that `session.commitConfiguration()` is complete before calling - // `session.startRunning()`. - Thread.sleep(forTimeInterval: 0.1) - DispatchQueue.global(qos: .background).async { + + // Add Start task to the queue in the order, each task starts only after the previous task has + // finished, ensuring captureSession.startRunning() starts after the sync block + captureSessionQueue.async { captureSession.startRunning() } - self.captureSession = captureSession - let formats = settings.formats.count == 0 ? BarcodeFormat.all : BarcodeFormat(settings.formats) - self.barcodeScannerInstance = MLKitBarcodeScanner.barcodeScanner(options: BarcodeScannerOptions(formats: formats)) + + DispatchQueue.main.async { + self.captureSession = captureSession + let formats = settings.formats.count == 0 ? BarcodeFormat.all : BarcodeFormat(settings.formats) + self.barcodeScannerInstance = MLKitBarcodeScanner.barcodeScanner(options: BarcodeScannerOptions(formats: formats)) + self.setVideoPreviewLayer(AVCaptureVideoPreviewLayer(session: captureSession)) - self.setVideoPreviewLayer(AVCaptureVideoPreviewLayer(session: captureSession)) - - if settings.showUIElements { - self.addCancelButton() - if implementation.isTorchAvailable() { - self.addTorchButton() + if settings.showUIElements { + self.addCancelButton() + if implementation.isTorchAvailable() { + self.addTorchButton() + } } } } + required init?(coder: NSCoder) { fatalError("coder initialization not supported.") } From dbace2ea4166e5fd196b6ad353fc06b5587bd8d6 Mon Sep 17 00:00:00 2001 From: J Caso Date: Fri, 13 Dec 2024 13:04:01 +0100 Subject: [PATCH 2/4] fix: npm run changeset --- .changeset/forty-avocados-allow.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/forty-avocados-allow.md diff --git a/.changeset/forty-avocados-allow.md b/.changeset/forty-avocados-allow.md new file mode 100644 index 0000000..d5b42ee --- /dev/null +++ b/.changeset/forty-avocados-allow.md @@ -0,0 +1,9 @@ +--- +'@capacitor-mlkit/barcode-scanning': minor +--- + +Introduce a serial DispatchQueue for managing AVCaptureSession operations in a FIFO (First In, First Out) sequence. Configuration tasks are encapsulated within a synchronous block to ensure complete setup before proceeding. Following this, startRunning() is scheduled asynchronously, guaranteeing it executes only after the configuration is fully committed. + +This approach not only prevents the NSGenericException by ensuring proper sequence of operations but also maintains high performance and responsiveness of the application. + +Related PR [fix(barcode-scanning): add delay before starting camera session #188](https://github.com/capawesome-team/capacitor-mlkit/pull/188) From 1a1b2f39db57e7bc1da05143a17f06b3448a78d9 Mon Sep 17 00:00:00 2001 From: J Caso Date: Wed, 18 Dec 2024 12:39:32 +0100 Subject: [PATCH 3/4] fix: remove duplicated empty line --- packages/barcode-scanning/ios/Plugin/BarcodeScannerView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/barcode-scanning/ios/Plugin/BarcodeScannerView.swift b/packages/barcode-scanning/ios/Plugin/BarcodeScannerView.swift index 98635b2..a8f11ea 100644 --- a/packages/barcode-scanning/ios/Plugin/BarcodeScannerView.swift +++ b/packages/barcode-scanning/ios/Plugin/BarcodeScannerView.swift @@ -105,7 +105,6 @@ public protocol BarcodeScannerViewDelegate { } } - required init?(coder: NSCoder) { fatalError("coder initialization not supported.") } From afe647596b9182e2aed7b4af35820d5b7768ac83 Mon Sep 17 00:00:00 2001 From: Jaime Caso Date: Wed, 18 Dec 2024 11:40:33 +0000 Subject: [PATCH 4/4] Update .changeset/forty-avocados-allow.md Co-authored-by: Robin Genz --- .changeset/forty-avocados-allow.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.changeset/forty-avocados-allow.md b/.changeset/forty-avocados-allow.md index d5b42ee..a5ea79e 100644 --- a/.changeset/forty-avocados-allow.md +++ b/.changeset/forty-avocados-allow.md @@ -2,8 +2,4 @@ '@capacitor-mlkit/barcode-scanning': minor --- -Introduce a serial DispatchQueue for managing AVCaptureSession operations in a FIFO (First In, First Out) sequence. Configuration tasks are encapsulated within a synchronous block to ensure complete setup before proceeding. Following this, startRunning() is scheduled asynchronously, guaranteeing it executes only after the configuration is fully committed. - -This approach not only prevents the NSGenericException by ensuring proper sequence of operations but also maintains high performance and responsiveness of the application. - -Related PR [fix(barcode-scanning): add delay before starting camera session #188](https://github.com/capawesome-team/capacitor-mlkit/pull/188) +fix(ios): use queue for synchronizing the session configuration and start operations of the `scan(...)` method