Murodjon Abdukholikov
Murodjon Abdukholikov

Reputation: 587

Troubleshooting AVFoundation QR Scanner Stops in Compose Multiplatform

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()
        },
    )
}

Upvotes: 0

Views: 107

Answers (1)

Murodjon Abdukholikov
Murodjon Abdukholikov

Reputation: 587

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()
        })
}

Upvotes: 0

Related Questions