Committed by
GitHub
Merge pull request #347 from juliansteenbakker/feature/return-image-ios
feat: add new error and event handling
Showing
6 changed files
with
341 additions
and
8 deletions
| @@ -37,5 +37,9 @@ end | @@ -37,5 +37,9 @@ end | ||
| 37 | post_install do |installer| | 37 | post_install do |installer| |
| 38 | installer.pods_project.targets.each do |target| | 38 | installer.pods_project.targets.each do |target| |
| 39 | flutter_additional_ios_build_settings(target) | 39 | flutter_additional_ios_build_settings(target) |
| 40 | + target.build_configurations.each do |config| | ||
| 41 | + config.build_settings['ENABLE_BITCODE'] = 'NO' | ||
| 42 | + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0' | ||
| 43 | + end | ||
| 40 | end | 44 | end |
| 41 | end | 45 | end |
ios/Classes/BarcodeHandler.swift
0 → 100644
| 1 | +// | ||
| 2 | +// BarcodeHandler.swift | ||
| 3 | +// mobile_scanner | ||
| 4 | +// | ||
| 5 | +// Created by Julian Steenbakker on 24/08/2022. | ||
| 6 | +// | ||
| 7 | + | ||
| 8 | +import Foundation | ||
| 9 | + | ||
| 10 | +public class BarcodeHandler: NSObject, FlutterStreamHandler { | ||
| 11 | + | ||
| 12 | + var event: [String: Any?] = [:] | ||
| 13 | + | ||
| 14 | + private var eventSink: FlutterEventSink? | ||
| 15 | + private let eventChannel: FlutterEventChannel | ||
| 16 | + | ||
| 17 | + init(registrar: FlutterPluginRegistrar) { | ||
| 18 | + eventChannel = FlutterEventChannel(name: | ||
| 19 | + "dev.steenbakker.mobile_scanner/scanner/event", binaryMessenger: registrar.messenger()) | ||
| 20 | + super.init() | ||
| 21 | + eventChannel.setStreamHandler(self) | ||
| 22 | + } | ||
| 23 | + | ||
| 24 | + func publishEvent(_ event: [String: Any?]) { | ||
| 25 | + self.event = event | ||
| 26 | + eventSink?(event) | ||
| 27 | + } | ||
| 28 | + | ||
| 29 | + public func onListen(withArguments arguments: Any?, | ||
| 30 | + eventSink: @escaping FlutterEventSink) -> FlutterError? { | ||
| 31 | + self.eventSink = eventSink | ||
| 32 | + return nil | ||
| 33 | + } | ||
| 34 | + | ||
| 35 | + public func onCancel(withArguments arguments: Any?) -> FlutterError? { | ||
| 36 | + eventSink = nil | ||
| 37 | + return nil | ||
| 38 | + } | ||
| 39 | +} |
ios/Classes/MobileScanner.swift
0 → 100644
| 1 | +// | ||
| 2 | +// SwiftMobileScanner.swift | ||
| 3 | +// mobile_scanner | ||
| 4 | +// | ||
| 5 | +// Created by Julian Steenbakker on 15/02/2022. | ||
| 6 | +// | ||
| 7 | + | ||
| 8 | +import Foundation | ||
| 9 | + | ||
| 10 | +import AVFoundation | ||
| 11 | +import MLKitVision | ||
| 12 | +import MLKitBarcodeScanning | ||
| 13 | + | ||
| 14 | +typealias MobileScannerCallback = ((Array<Barcode>?, Error?, UIImage) -> ()) | ||
| 15 | + | ||
| 16 | +public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, FlutterTexture { | ||
| 17 | + /// Capture session of the camera | ||
| 18 | + var captureSession: AVCaptureSession! | ||
| 19 | + | ||
| 20 | + /// The selected camera | ||
| 21 | + var device: AVCaptureDevice! | ||
| 22 | + | ||
| 23 | + /// Barcode scanner for results | ||
| 24 | + var scanner = BarcodeScanner.barcodeScanner() | ||
| 25 | + | ||
| 26 | + /// Return image buffer with the Barcode event | ||
| 27 | + var returnImage: Bool = false | ||
| 28 | + | ||
| 29 | + /// Default position of camera | ||
| 30 | + var videoPosition: AVCaptureDevice.Position = AVCaptureDevice.Position.back | ||
| 31 | + | ||
| 32 | + /// When results are found, this callback will be called | ||
| 33 | + let mobileScannerCallback: MobileScannerCallback | ||
| 34 | + | ||
| 35 | + /// If provided, the Flutter registry will be used to send the output of the CaptureOutput to a Flutter texture. | ||
| 36 | + private let registry: FlutterTextureRegistry? | ||
| 37 | + | ||
| 38 | + /// Image to be sent to the texture | ||
| 39 | + var latestBuffer: CVImageBuffer! | ||
| 40 | + | ||
| 41 | + /// Texture id of the camera preview for Flutter | ||
| 42 | + private var textureId: Int64! | ||
| 43 | + | ||
| 44 | + var detectionSpeed: DetectionSpeed = DetectionSpeed.noDuplicates | ||
| 45 | + | ||
| 46 | + init(registry: FlutterTextureRegistry?, mobileScannerCallback: @escaping MobileScannerCallback) { | ||
| 47 | + self.registry = registry | ||
| 48 | + self.mobileScannerCallback = mobileScannerCallback | ||
| 49 | + super.init() | ||
| 50 | + } | ||
| 51 | + | ||
| 52 | + /// Check permissions for video | ||
| 53 | + func checkPermission() -> Int { | ||
| 54 | + let status = AVCaptureDevice.authorizationStatus(for: .video) | ||
| 55 | + switch status { | ||
| 56 | + case .notDetermined: | ||
| 57 | + return 0 | ||
| 58 | + case .authorized: | ||
| 59 | + return 1 | ||
| 60 | + default: | ||
| 61 | + return 2 | ||
| 62 | + } | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | + /// Request permissions for video | ||
| 66 | + func requestPermission(_ result: @escaping FlutterResult) { | ||
| 67 | + AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) }) | ||
| 68 | + } | ||
| 69 | + | ||
| 70 | + /// Start scanning for barcodes | ||
| 71 | + func start(barcodeScannerOptions: BarcodeScannerOptions?, returnImage: Bool, cameraPosition: AVCaptureDevice.Position, torch: AVCaptureDevice.TorchMode, detectionSpeed: DetectionSpeed) throws -> MobileScannerStartParameters { | ||
| 72 | + self.detectionSpeed = detectionSpeed | ||
| 73 | + if (device != nil) { | ||
| 74 | + throw MobileScannerError.alreadyStarted | ||
| 75 | + } | ||
| 76 | + | ||
| 77 | + scanner = barcodeScannerOptions != nil ? BarcodeScanner.barcodeScanner(options: barcodeScannerOptions!) : BarcodeScanner.barcodeScanner() | ||
| 78 | + captureSession = AVCaptureSession() | ||
| 79 | + textureId = registry?.register(self) | ||
| 80 | + | ||
| 81 | + // Open the camera device | ||
| 82 | + if #available(iOS 10.0, *) { | ||
| 83 | + device = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: cameraPosition).devices.first | ||
| 84 | + } else { | ||
| 85 | + device = AVCaptureDevice.devices(for: .video).filter({$0.position == cameraPosition}).first | ||
| 86 | + } | ||
| 87 | + | ||
| 88 | + if (device == nil) { | ||
| 89 | + throw MobileScannerError.noCamera | ||
| 90 | + } | ||
| 91 | + | ||
| 92 | + // Enable the torch if parameter is set and torch is available | ||
| 93 | + if (device.hasTorch && device.isTorchAvailable) { | ||
| 94 | + do { | ||
| 95 | + try device.lockForConfiguration() | ||
| 96 | + device.torchMode = torch | ||
| 97 | + device.unlockForConfiguration() | ||
| 98 | + } catch { | ||
| 99 | + throw MobileScannerError.torchError(error) | ||
| 100 | + } | ||
| 101 | + } | ||
| 102 | + | ||
| 103 | + device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode), options: .new, context: nil) | ||
| 104 | + captureSession.beginConfiguration() | ||
| 105 | + | ||
| 106 | + // Add device input | ||
| 107 | + do { | ||
| 108 | + let input = try AVCaptureDeviceInput(device: device) | ||
| 109 | + captureSession.addInput(input) | ||
| 110 | + } catch { | ||
| 111 | + throw MobileScannerError.cameraError(error) | ||
| 112 | + } | ||
| 113 | + | ||
| 114 | + captureSession.sessionPreset = AVCaptureSession.Preset.photo; | ||
| 115 | + // Add video output. | ||
| 116 | + let videoOutput = AVCaptureVideoDataOutput() | ||
| 117 | + | ||
| 118 | + videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA] | ||
| 119 | + videoOutput.alwaysDiscardsLateVideoFrames = true | ||
| 120 | + | ||
| 121 | + videoPosition = cameraPosition | ||
| 122 | + // calls captureOutput() | ||
| 123 | + videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main) | ||
| 124 | + | ||
| 125 | + captureSession.addOutput(videoOutput) | ||
| 126 | + for connection in videoOutput.connections { | ||
| 127 | + connection.videoOrientation = .portrait | ||
| 128 | + if cameraPosition == .front && connection.isVideoMirroringSupported { | ||
| 129 | + connection.isVideoMirrored = true | ||
| 130 | + } | ||
| 131 | + } | ||
| 132 | + captureSession.commitConfiguration() | ||
| 133 | + captureSession.startRunning() | ||
| 134 | + let dimensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription) | ||
| 135 | + | ||
| 136 | + return MobileScannerStartParameters(width: Double(dimensions.height), height: Double(dimensions.width), hasTorch: device.hasTorch, textureId: textureId) | ||
| 137 | + } | ||
| 138 | + | ||
| 139 | + struct MobileScannerStartParameters { | ||
| 140 | + var width: Double = 0.0 | ||
| 141 | + var height: Double = 0.0 | ||
| 142 | + var hasTorch = false | ||
| 143 | + var textureId: Int64 = 0 | ||
| 144 | + } | ||
| 145 | + | ||
| 146 | + /// Stop scanning for barcodes | ||
| 147 | + func stop() throws { | ||
| 148 | + if (device == nil) { | ||
| 149 | + throw MobileScannerError.alreadyStopped | ||
| 150 | + } | ||
| 151 | + captureSession.stopRunning() | ||
| 152 | + for input in captureSession.inputs { | ||
| 153 | + captureSession.removeInput(input) | ||
| 154 | + } | ||
| 155 | + for output in captureSession.outputs { | ||
| 156 | + captureSession.removeOutput(output) | ||
| 157 | + } | ||
| 158 | + device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode)) | ||
| 159 | + registry?.unregisterTexture(textureId) | ||
| 160 | + textureId = nil | ||
| 161 | + captureSession = nil | ||
| 162 | + device = nil | ||
| 163 | + } | ||
| 164 | + | ||
| 165 | + /// Toggle the flashlight between on and off | ||
| 166 | + func toggleTorch(_ torch: AVCaptureDevice.TorchMode) throws { | ||
| 167 | + if (device == nil) { | ||
| 168 | + throw MobileScannerError.torchWhenStopped | ||
| 169 | + } | ||
| 170 | + do { | ||
| 171 | + try device.lockForConfiguration() | ||
| 172 | + device.torchMode = torch | ||
| 173 | + device.unlockForConfiguration() | ||
| 174 | + } catch { | ||
| 175 | + throw MobileScannerError.torchError(error) | ||
| 176 | + } | ||
| 177 | + } | ||
| 178 | + | ||
| 179 | + /// Analyze a single image | ||
| 180 | + func analyzeImage(image: UIImage, position: AVCaptureDevice.Position, callback: @escaping BarcodeScanningCallback) { | ||
| 181 | + let image = VisionImage(image: image) | ||
| 182 | + image.orientation = imageOrientation( | ||
| 183 | + deviceOrientation: UIDevice.current.orientation, | ||
| 184 | + defaultOrientation: .portrait, | ||
| 185 | + position: position | ||
| 186 | + ) | ||
| 187 | + | ||
| 188 | + scanner.process(image, completion: callback) | ||
| 189 | + } | ||
| 190 | + | ||
| 191 | + var i = 0 | ||
| 192 | + | ||
| 193 | + var barcodesString: Array<String?>? | ||
| 194 | + | ||
| 195 | + /// Gets called when a new image is added to the buffer | ||
| 196 | + public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { | ||
| 197 | + guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { | ||
| 198 | + print("Failed to get image buffer from sample buffer.") | ||
| 199 | + return | ||
| 200 | + } | ||
| 201 | + latestBuffer = imageBuffer | ||
| 202 | + registry?.textureFrameAvailable(textureId) | ||
| 203 | + if ((detectionSpeed == DetectionSpeed.normal || detectionSpeed == DetectionSpeed.noDuplicates) && i > 10 || detectionSpeed == DetectionSpeed.unrestricted) { | ||
| 204 | + i = 0 | ||
| 205 | + let ciImage = latestBuffer.image | ||
| 206 | + | ||
| 207 | + let image = VisionImage(image: ciImage) | ||
| 208 | + image.orientation = imageOrientation( | ||
| 209 | + deviceOrientation: UIDevice.current.orientation, | ||
| 210 | + defaultOrientation: .portrait, | ||
| 211 | + position: videoPosition | ||
| 212 | + ) | ||
| 213 | + | ||
| 214 | + scanner.process(image) { [self] barcodes, error in | ||
| 215 | + if (detectionSpeed == DetectionSpeed.noDuplicates) { | ||
| 216 | + let newScannedBarcodes = barcodes?.map { barcode in | ||
| 217 | + return barcode.rawValue | ||
| 218 | + } | ||
| 219 | + if (error == nil && barcodesString != nil && newScannedBarcodes != nil && barcodesString!.elementsEqual(newScannedBarcodes!)) { | ||
| 220 | + return | ||
| 221 | + } else { | ||
| 222 | + barcodesString = newScannedBarcodes | ||
| 223 | + } | ||
| 224 | + } | ||
| 225 | + | ||
| 226 | + mobileScannerCallback(barcodes, error, ciImage) | ||
| 227 | + } | ||
| 228 | + } else { | ||
| 229 | + i+=1 | ||
| 230 | + } | ||
| 231 | + } | ||
| 232 | + | ||
| 233 | + /// Convert image buffer to jpeg | ||
| 234 | + private func ciImageToJpeg(ciImage: CIImage) -> Data { | ||
| 235 | + | ||
| 236 | + // let ciImage = CIImage(cvPixelBuffer: latestBuffer) | ||
| 237 | + let context:CIContext = CIContext.init(options: nil) | ||
| 238 | + let cgImage:CGImage = context.createCGImage(ciImage, from: ciImage.extent)! | ||
| 239 | + let uiImage:UIImage = UIImage(cgImage: cgImage, scale: 1, orientation: UIImage.Orientation.up) | ||
| 240 | + | ||
| 241 | + return uiImage.jpegData(compressionQuality: 0.8)!; | ||
| 242 | + } | ||
| 243 | + | ||
| 244 | + /// Rotates images accordingly | ||
| 245 | + func imageOrientation( | ||
| 246 | + deviceOrientation: UIDeviceOrientation, | ||
| 247 | + defaultOrientation: UIDeviceOrientation, | ||
| 248 | + position: AVCaptureDevice.Position | ||
| 249 | + ) -> UIImage.Orientation { | ||
| 250 | + switch deviceOrientation { | ||
| 251 | + case .portrait: | ||
| 252 | + return position == .front ? .leftMirrored : .right | ||
| 253 | + case .landscapeLeft: | ||
| 254 | + return position == .front ? .downMirrored : .up | ||
| 255 | + case .portraitUpsideDown: | ||
| 256 | + return position == .front ? .rightMirrored : .left | ||
| 257 | + case .landscapeRight: | ||
| 258 | + return position == .front ? .upMirrored : .down | ||
| 259 | + case .faceDown, .faceUp, .unknown: | ||
| 260 | + return .up | ||
| 261 | + @unknown default: | ||
| 262 | + return imageOrientation(deviceOrientation: defaultOrientation, defaultOrientation: .portrait, position: .back) | ||
| 263 | + } | ||
| 264 | + } | ||
| 265 | + | ||
| 266 | + /// Sends output of OutputBuffer to a Flutter texture | ||
| 267 | + public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? { | ||
| 268 | + if latestBuffer == nil { | ||
| 269 | + return nil | ||
| 270 | + } | ||
| 271 | + return Unmanaged<CVPixelBuffer>.passRetained(latestBuffer) | ||
| 272 | + } | ||
| 273 | + | ||
| 274 | +} | ||
| 275 | + |
ios/Classes/MobileScannerError.swift
0 → 100644
| 1 | +// | ||
| 2 | +// MobileScannerError.swift | ||
| 3 | +// mobile_scanner | ||
| 4 | +// | ||
| 5 | +// Created by Julian Steenbakker on 24/08/2022. | ||
| 6 | +// | ||
| 7 | +import Foundation | ||
| 8 | + | ||
| 9 | +enum MobileScannerError: Error { | ||
| 10 | + case noCamera | ||
| 11 | + case alreadyStarted | ||
| 12 | + case alreadyStopped | ||
| 13 | + case torchError(_ error: Error) | ||
| 14 | + case cameraError(_ error: Error) | ||
| 15 | + case torchWhenStopped | ||
| 16 | + case analyzerError(_ error: Error) | ||
| 17 | +} |
ios/Classes/SwiftMobileScanner.swift
deleted
100644 → 0
-
Please register or login to post a comment