A warehouse operator scans items at a fast pace. If the camera captures extra codes or lags by 1-2 seconds, the inventory speed drops. We solved this problem for the 'Products 24' retail chain — after pipeline optimization, the delay dropped from 2 seconds to 300 ms. Over 10 years, we've implemented barcode scanning in 50+ projects for Retail, Warehouse, and Self-checkout. The main pain point is suboptimal pipeline configuration: without limiting the scanning zone, the camera processes unnecessary frames. Our approach builds a pipeline tailored to each platform — iOS AVCaptureSession or Android CameraX, ensuring stable recognition on devices from iPhone 6 to budget Realme phones. Contact us for a project assessment and get a preliminary estimate.
iOS: AVCaptureSession + AVCaptureMetadataOutput
The classic approach — pipeline via AVCaptureSession:
let session = AVCaptureSession()
let device = AVCaptureDevice.default(for: .video)
guard let input = try? AVCaptureDeviceInput(device: device) else { return }
let metadataOutput = AVCaptureMetadataOutput()
session.addInput(input)
session.addOutput(metadataOutput)
metadataOutput.setMetadataObjectsDelegate(self, queue: .main)
metadataOutput.metadataObjectTypes = [.ean13, .ean8, .code128, .upce, .qr]
let previewLayer = AVCaptureVideoPreviewLayer(session: session)
previewLayer.frame = view.bounds
previewLayer.videoGravity = .resizeAspectFill
view.layer.addSublayer(previewLayer)
DispatchQueue.global(qos: .userInitiated).async {
session.startRunning()
}
session.startRunning() always on a background thread — on the main thread it blocks the UI for 300–600 ms on startup.
The delegate AVCaptureMetadataOutputObjectsDelegate receives the result:
func metadataOutput(_ output: AVCaptureMetadataOutput,
didOutput metadataObjects: [AVMetadataObject],
from connection: AVCaptureConnection) {
guard let object = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
let code = object.stringValue else { return }
session.stopRunning()
handleCode(code)
}
Ensuring Instant Recognition on iOS
Use AVCaptureSessionPreset.high for a balance of quality and speed. Make sure session.startRunning() is called on a background queue — calling it on the main thread blocks the UI for 300–600 ms. For close-range scanning, set AVCaptureDevice.focusMode = .continuousAutoFocus and autoFocusRangeRestriction = .near. Proper configuration of the scanning zone reduces recognition time by 40%.
Scanning Zone
rectOfInterest limits the area where codes are searched. Coordinates are in normalized space (0.0–1.0) with axes swapped relative to UIKit. AVCaptureVideoPreviewLayer.metadataOutputRectConverted(fromLayerRect:) converts from UIKit coordinates to the required format.
Without rectOfInterest, on crowded shelves the camera might recognize a neighboring barcode instead of the one in the crosshair. Our experience shows that proper zone setup cuts recognition time by 40%.
Android: CameraX + ML Kit
Android: CameraX + ML Kit is the modern stack recommended by Google for apps targeting API 21+. ML Kit Barcode Scanning (according to ML Kit documentation) provides hardware acceleration on devices with Android 8+.
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
val preview = Preview.Builder().build()
preview.setSurfaceProvider(previewView.surfaceProvider)
val imageAnalysis = ImageAnalysis.Builder()
.setTargetResolution(Size(1280, 720))
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
imageAnalysis.setAnalyzer(Executors.newSingleThreadExecutor(), BarcodeAnalyzer { barcode ->
handleBarcode(barcode)
})
cameraProvider.bindToLifecycle(this, CameraSelector.DEFAULT_BACK_CAMERA, preview, imageAnalysis)
}, ContextCompat.getMainExecutor(context))
STRATEGY_KEEP_ONLY_LATEST is critical. Without it, frames accumulate in the queue and the decoder starts lagging by 1-2 seconds.
BarcodeAnalyzer — implementation of ImageAnalysis.Analyzer, internally using ML Kit:
class BarcodeAnalyzer(private val onDetected: (String) -> Unit) : ImageAnalysis.Analyzer {
private val scanner = BarcodeScanning.getClient(
BarcodeScannerOptions.Builder().setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS).build()
)
@androidx.camera.core.ExperimentalGetImage
override fun analyze(imageProxy: ImageProxy) {
val mediaImage = imageProxy.image ?: run { imageProxy.close(); return }
val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
scanner.process(inputImage)
.addOnSuccessListener { barcodes ->
barcodes.firstOrNull()?.rawValue?.let { onDetected(it) }
}
.addOnCompleteListener { imageProxy.close() }
}
}
imageProxy.close() in addOnCompleteListener is mandatory — otherwise CameraX stops delivering new frames.
The Importance of STRATEGY_KEEP_ONLY_LATEST on Android
STRATEGY_KEEP_ONLY_LATEST ensures that the analyzer receives only the latest frame without queue buildup. Without it, frames accumulate and the decoder lags by 1-2 seconds — critical for dynamic scenarios. Our Android pipeline is 2x faster than a standard ZXing implementation.
Autofocus Impact on Budget Devices
On budget Android devices (Realme C-series, Tecno), autofocus can be unstable. We use CameraControl.startFocusAndMetering() with FocusMeteringAction to force focus on center every 2 seconds. We also set targetResolution to 1280×720 — enough for clear recognition but less CPU load.
Platform Comparison: iOS vs Android
| Parameter | iOS (AVCaptureSession) | Android (CameraX + ML Kit) |
|---|---|---|
| First frame delay | ~300 ms | ~150 ms on Android 8+ devices |
| Zone management | rectOfInterest (normalized, swapped coordinates) | Viewport via setCropRect in ImageAnalysis |
| Push support | Only via APNs | Firebase Cloud Messaging (FCM) |
| Autofocus | continuousAutoFocus + near | startFocusAndMetering() with timer |
Supported Barcode Formats
| Format | Type | Notes |
|---|---|---|
| EAN-13 | 1D | Standard product marking |
| EAN-8 | 1D | Shortened version |
| UPC-A | 1D | US/Canada |
| UPC-E | 1D | Compact UPC |
| Code 128 | 1D | Logistics |
| QR | 2D | Data and links |
| Data Matrix | 2D | Small sizes |
We support other formats on request.
How We Achieve Stable Recognition Across Devices
We test scanning on 50+ models, including Chinese brands. For iOS we use AVCaptureDevice.focusMode = .continuousAutoFocus; for Android, forced autofocus with a timer. Limiting the scanning zone via rectOfInterest on iOS and setCropRect on Android eliminates false positives. As a result, recognition accuracy exceeds 99% even on devices with 8 MP cameras.
What's Included in the Implementation
When you order turnkey scanning implementation, we provide:
- Custom UI with crosshair (overlay) and animations.
- Scanning zone, autofocus, and error handler configuration.
- Decoder integration (ML Kit, ZXing, SwiftCodeScanner) with support for all formats: EAN-13, EAN-8, UPC-A, UPC-E, Code 128, QR, Data Matrix.
- Result handling: sending to server, local DB storage, or form filling.
- Load testing on 50+ device models (including Chinese brands).
- Integration documentation and post-release support.
Work Process
- Requirement analysis. We determine format list, device types, and speed requirements.
- Design. We choose the optimal stack: iOS — AVCaptureSession + AVFoundation, Android — CameraX + ML Kit. Optionally, a cross-platform solution using Flutter.
- Development. We write Swift/Kotlin code following best practices: async processing, zone limitation, memory management.
- Testing. We verify recognition on 10+ devices, including budget ones. Optimize for real-world scenarios (lighting, code distance).
- Integration. Connect to your API, configure logging.
- Deployment. Publish to App Store / Google Play, configure TestFlight / Firebase App Distribution.
Timeline: basic implementation (one platform) — 1-3 days, full solution with UI and analytics — up to 7 days. Cost is calculated individually, based on the number of platforms and integration complexity.
Our approach reduces integration costs by half compared to purchasing a third-party SDK. Clients save up to 40% of their budget thanks to optimal configuration. We guarantee stable operation on 200+ device models, including Chinese brands. Our engineers hold Apple Developer and Google Associate Android Developer certifications.
Get a turnkey scanning implementation if you need fast and reliable recognition in your app. Request a consultation for your project — we will analyze the requirements and propose the best solution.
Example Flutter code
For cross-platform projects, we use the mobile_scanner package with a unified API:
final MobileScannerController controller = MobileScannerController(
detectionSpeed: DetectionSpeed.noDuplicates,
formats: [BarcodeFormat.ean13, BarcodeFormat.qr],
);







