#if os(macOS) import AppKit import AVFoundation import SwiftUI import Vision @MainActor final class QRScannerController: NSObject, ObservableObject { private var captureSession: AVCaptureSession? private var previewLayer: AVCaptureVideoPreviewLayer? private var videoOutput: AVCaptureVideoDataOutput? private var didFinishScanning = false @Published var availableCameras: [AVCaptureDevice] = [] @Published var selectedCamera: AVCaptureDevice? @Published var qrsMode = false @Published var decoder: LubyTransformDecoder? @Published var progress: Double = 0 @Published var framesScanned = 0 private var seenBlockIds = Set() private let decodingQueue = DispatchQueue(label: "QRSDecoding", qos: .userInitiated) let previewView = NSView() var onScan: ((Result) -> Void)? override init() { super.init() previewView.wantsLayer = true previewView.layer?.backgroundColor = NSColor.black.cgColor refreshCameraList() } func refreshCameraList() { let discoverySession = AVCaptureDevice.DiscoverySession( deviceTypes: [.builtInWideAngleCamera, .externalUnknown], mediaType: .video, position: .unspecified ) availableCameras = discoverySession.devices if selectedCamera == nil { selectedCamera = availableCameras.first } } func selectCamera(_ camera: AVCaptureDevice) { guard camera.uniqueID != selectedCamera?.uniqueID else { return } selectedCamera = camera didFinishScanning = false if captureSession != nil { stopScanning() captureSession = nil previewLayer?.removeFromSuperlayer() previewLayer = nil startScanning() } } func startScanning() { guard captureSession == nil else { if captureSession?.isRunning == false { DispatchQueue.global(qos: .userInteractive).async { [weak self] in self?.captureSession?.startRunning() } } return } switch AVCaptureDevice.authorizationStatus(for: .video) { case .authorized: setupCaptureSession() case .notDetermined: AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in DispatchQueue.main.async { if granted { self?.setupCaptureSession() } else { self?.onScan?(.failure(.permissionDenied)) } } } case .denied, .restricted: onScan?(.failure(.permissionDenied)) @unknown default: onScan?(.failure(.cameraUnavailable)) } } func stopScanning() { DispatchQueue.global(qos: .userInteractive).async { [weak self] in self?.captureSession?.stopRunning() } } func reset() { didFinishScanning = false qrsMode = false decoder = nil progress = 0 framesScanned = 0 seenBlockIds.removeAll() } private func setupCaptureSession() { let session = AVCaptureSession() guard let videoCaptureDevice = selectedCamera ?? AVCaptureDevice.default(for: .video) else { onScan?(.failure(.cameraUnavailable)) return } let videoInput: AVCaptureDeviceInput do { videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice) } catch { onScan?(.failure(.scanFailed(error))) return } guard session.canAddInput(videoInput) else { onScan?(.failure(.cameraUnavailable)) return } session.addInput(videoInput) let videoOutput = AVCaptureVideoDataOutput() videoOutput.videoSettings = [ kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA, ] videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "QRScannerQueue")) guard session.canAddOutput(videoOutput) else { onScan?(.failure(.cameraUnavailable)) return } session.addOutput(videoOutput) self.videoOutput = videoOutput captureSession = session let previewLayer = AVCaptureVideoPreviewLayer(session: session) previewLayer.videoGravity = .resizeAspectFill previewLayer.frame = previewView.bounds previewView.layer?.addSublayer(previewLayer) self.previewLayer = previewLayer DispatchQueue.global(qos: .userInteractive).async { [weak self] in self?.captureSession?.startRunning() } } func updatePreviewFrame(_ frame: CGRect) { previewLayer?.frame = frame } private func processScannedContent(_ content: String) { DispatchQueue.main.async { [weak self] in guard let self, !didFinishScanning else { return } if let block = EncodedBlock.fromQRSString(content) { processQRSBlock(block) } else if !qrsMode { didFinishScanning = true NSSound.beep() onScan?(.success(.qrCode(string: content, type: .qr))) } } } private func processQRSBlock(_ block: EncodedBlock) { if let currentChecksum = decoder?.meta?.checksum, block.checksum != currentChecksum { decoder = LubyTransformDecoder() seenBlockIds.removeAll() progress = 0 framesScanned = 0 } if !qrsMode { qrsMode = true decoder = LubyTransformDecoder() } let blockId = "\(block.checksum):\(block.indices.sorted().map(String.init).joined(separator: ","))" guard !seenBlockIds.contains(blockId) else { return } seenBlockIds.insert(blockId) framesScanned += 1 guard let decoder else { return } decodingQueue.async { [weak self] in do { let complete = try decoder.addBlock(block) let currentProgress = decoder.progress DispatchQueue.main.async { guard let self, !self.didFinishScanning else { return } self.progress = currentProgress if complete { self.didFinishScanning = true self.stopScanning() NSSound.beep() if let data = try? decoder.getDecoded() { self.onScan?(.success(.qrsData(data))) } else { self.onScan?(.failure(.qrsDecodeFailed)) } } } } catch { // Checksum mismatch is handled above, ignore other errors } } } } extension QRScannerController: AVCaptureVideoDataOutputSampleBufferDelegate { nonisolated func captureOutput( _: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from _: AVCaptureConnection ) { guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return } let request = VNDetectBarcodesRequest { [weak self] request, _ in guard let results = request.results as? [VNBarcodeObservation] else { return } for result in results { if result.symbology == .qr, let payload = result.payloadStringValue { self?.processScannedContent(payload) } } } request.symbologies = [.qr] let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, options: [:]) try? handler.perform([request]) } } struct QRScannerControllerView: NSViewControllerRepresentable { let controller: QRScannerController func makeNSViewController(context _: Context) -> NSViewController { QRScannerViewController(controller: controller) } func updateNSViewController(_: NSViewController, context _: Context) {} } private class QRScannerViewController: NSViewController { let controller: QRScannerController init(controller: QRScannerController) { self.controller = controller super.init(nibName: nil, bundle: nil) } @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } override func loadView() { view = NSView() view.wantsLayer = true view.layer?.backgroundColor = NSColor.black.cgColor } override func viewDidLoad() { super.viewDidLoad() view.addSubview(controller.previewView) controller.previewView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ controller.previewView.topAnchor.constraint(equalTo: view.topAnchor), controller.previewView.bottomAnchor.constraint(equalTo: view.bottomAnchor), controller.previewView.leadingAnchor.constraint(equalTo: view.leadingAnchor), controller.previewView.trailingAnchor.constraint(equalTo: view.trailingAnchor), ]) } override func viewDidLayout() { super.viewDidLayout() controller.updatePreviewFrame(view.bounds) } override func viewWillAppear() { super.viewWillAppear() controller.startScanning() } override func viewWillDisappear() { super.viewWillDisappear() controller.stopScanning() } } #endif