I have integrated the Camera Scanner from AVFoundation into a Compose Multiplatform project. The scanner scans a few times (randomly 5-6 times) before ceasing to scan further, although the live preview remains active. How can I address this issue? I'll provide the CameraScanner code.
@OptIn(ExperimentalForeignApi::class)
@Composable
actual fun QRCameraView(
modifier: Modifier,
onScanResult: (String) -> Unit
) {
val captureSession = AVCaptureSession()
val captureDevice =
AVCaptureDevice.devicesWithMediaType(AVMediaTypeVideo).firstOrNull { device ->
(device as AVCaptureDevice).position == AVCaptureDevicePositionBack
}!! as AVCaptureDevice
val input =
AVCaptureDeviceInput.deviceInputWithDevice(captureDevice, null) as AVCaptureDeviceInput
captureSession.addInput(input)
//Initialize an AVCaptureMetadataOutput object and set it as the output device to the capture session.
val metadataOutput = AVCaptureMetadataOutput()
if (captureSession.canAddOutput(metadataOutput)) {
//Set delegate and use default dispatch queue to execute the call back
// fixed with https://youtrack.jetbrains.com/issue/KT-45755/iOS-delegate-protocol-is-empty
captureSession.addOutput(metadataOutput)
metadataOutput.setMetadataObjectsDelegate(objectsDelegate = object : NSObject(),
AVCaptureMetadataOutputObjectsDelegateProtocol {
override fun captureOutput(
output: AVCaptureOutput,
didOutputMetadataObjects: List<*>,
fromConnection: AVCaptureConnection,
) {
if (didOutputMetadataObjects.isNotEmpty()) {
val readableObject =
didOutputMetadataObjects[0] as? AVMetadataMachineReadableCodeObject
if (readableObject?.type == AVMetadataObjectTypeQRCode) {
val code = readableObject?.stringValue
onScanResult(code ?: "")
}
return
}
}
}, queue = dispatch_get_main_queue())
metadataOutput.metadataObjectTypes = listOf(AVMetadataObjectTypeQRCode)
}
val cameraPreviewLayer = remember {
AVCaptureVideoPreviewLayer(
session =
captureSession
)
}
UIKitView(
modifier = modifier,
background = Color.Black,
factory = {
val container = UIView()
container.layer.addSublayer(cameraPreviewLayer)
cameraPreviewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill
CoroutineScope(Dispatchers.IO).launch {
captureSession.startRunning()
}
container
},
onResize = { container: UIView, rect: CValue<CGRect> ->
CATransaction.begin()
CATransaction.setValue(true, kCATransactionDisableActions)
container.layer.setFrame(rect)
cameraPreviewLayer.setFrame(rect)
CATransaction.commit()
},
)
}
I found the solution for my question here
It's because the metadataObjectDelegate doesn’t have a reference from Kotlin and getting garbage collected. Here's the fix for my problem.
class ScannerMetadataOutputDelegate(val onScanResult: (String) -> Unit = {}) : NSObject(),
AVCaptureMetadataOutputObjectsDelegateProtocol {
override fun captureOutput(
output: AVCaptureOutput,
didOutputMetadataObjects: List<*>,
fromConnection: AVCaptureConnection
) {
if (didOutputMetadataObjects.isNotEmpty()) {
val readableObject =
didOutputMetadataObjects[0] as? AVMetadataMachineReadableCodeObject
if (readableObject?.type == AVMetadataObjectTypeQRCode) {
val code = readableObject?.stringValue
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
onScanResult(code ?: "")
}
return
}
}
}
@OptIn(ExperimentalForeignApi::class)
@Composable
actual fun QRCameraView(
modifier: Modifier,
onScanResult: (String) -> Unit
) {
val scannerDelegate = remember {
ScannerMetadataOutputDelegate(
onScanResult = onScanResult
)
}
val captureSession = remember {
AVCaptureSession().also { captureSession ->
captureSession.sessionPreset = AVCaptureSessionPresetPhoto
val captureDevice =
AVCaptureDevice.devicesWithMediaType(AVMediaTypeVideo).firstOrNull { device ->
(device as AVCaptureDevice).position == AVCaptureDevicePositionBack
}!! as AVCaptureDevice
val input = AVCaptureDeviceInput.deviceInputWithDevice(
captureDevice,
null
) as AVCaptureDeviceInput
captureSession.addInput(input)
//Initialize an AVCaptureMetadataOutput object and set it as the output device to the capture session.
val metadataOutput = AVCaptureMetadataOutput()
if (captureSession.canAddOutput(metadataOutput)) {
//Set delegate and use default dispatch queue to execute the call back
// fixed with https://youtrack.jetbrains.com/issue/KT-45755/iOS-delegate-protocol-is-empty
captureSession.addOutput(metadataOutput)
metadataOutput.setMetadataObjectsDelegate(
objectsDelegate = scannerDelegate,
queue = dispatch_get_main_queue()
)
metadataOutput.metadataObjectTypes = listOf(AVMetadataObjectTypeQRCode)
}
}
}
val cameraPreviewLayer = remember { AVCaptureVideoPreviewLayer(session = captureSession) }
UIKitView(
modifier = modifier,
background = Color.Black,
factory = {
val container = UIView()
container.layer.addSublayer(cameraPreviewLayer)
cameraPreviewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill
captureSession.startRunning()
container
},
onResize = { container: UIView, rect: CValue<CGRect> ->
CATransaction.begin()
CATransaction.setValue(true, kCATransactionDisableActions)
container.layer.setFrame(rect)
cameraPreviewLayer.setFrame(rect)
CATransaction.commit()
})
}