Search code examples
kotlinandroid-jetpack-composecompose-multiplatformcompose-multiplatform-ios

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

Solution

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