Реалізація сканування штрих-кодів через камеру мобільного застосунку
Сканування штрих-кодів у реальному часі через відеопотік — завдання, у якому платформові API сильно відрізняються за підходом. iOS дає AVCaptureSession, Android — Camera2 API або CameraX. Мета одна: захопити кадр, передати в декодер, отримати результат з мінімальною затримкою.
iOS: AVCaptureSession + AVCaptureMetadataOutput
Класичний підхід — конвеєр через AVCaptureSession:
let session = AVCaptureSession()
guard let device = AVCaptureDevice.default(for: .video),
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() завжди у фоновому потоці — на головному потоці це блокує UI на 300-600 мс при запуску.
Делегат AVCaptureMetadataOutputObjectsDelegate отримує результат:
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)
}
Зона сканування
metadataOutput.rectOfInterest — обмежує область, у якій шукаються коди. Координати в нормалізованому просторі (0.0-1.0), причому осі перевернені порівняно з UIKit. AVCaptureVideoPreviewLayer.metadataOutputRectConverted(fromLayerRect:) конвертує з UIKit-координат в потрібний формат.
Без rectOfInterest на густих полицях з товарами камера може розпізнати сусідній штрих-код замість того, на який наведений прицел.
Android: CameraX + ML Kit
CameraX — рекомендований підхід з API 21+:
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 — критично важливо. Без цього кадри накопичуються в черзі та декодер починає відставати на 1-2 секунди.
BarcodeAnalyzer — реалізація ImageAnalysis.Analyzer, всередину 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() в addOnCompleteListener — обов'язково, інакше CameraX припинить подачу нових кадрів.
Дозвіл камери та автофокус
На бюджетних Android-пристроях (Realme C-серія, Tecno) автофокус може працювати нестабільно. CameraControl.startFocusAndMetering() з FocusMeteringAction допомагає примусово сфокусуватися по центру кожні 2 секунди.
На iOS — AVCaptureDevice.focusMode = .continuousAutoFocus та autoFocusRangeRestriction = .near для сканування на близькій відстані.
Час реалізації: 1-3 дні. Вартість розраховується індивідуально.







