Committed by
GitHub
Merge pull request #994 from fumin65/pause_function
feat: add pause feature
Showing
15 changed files
with
276 additions
and
84 deletions
| @@ -273,7 +273,7 @@ class MobileScanner( | @@ -273,7 +273,7 @@ class MobileScanner( | ||
| 273 | } | 273 | } |
| 274 | 274 | ||
| 275 | cameraProvider?.unbindAll() | 275 | cameraProvider?.unbindAll() |
| 276 | - textureEntry = textureRegistry.createSurfaceTexture() | 276 | + textureEntry = textureEntry ?: textureRegistry.createSurfaceTexture() |
| 277 | 277 | ||
| 278 | // Preview | 278 | // Preview |
| 279 | val surfaceProvider = Preview.SurfaceProvider { request -> | 279 | val surfaceProvider = Preview.SurfaceProvider { request -> |
| @@ -405,14 +405,33 @@ class MobileScanner( | @@ -405,14 +405,33 @@ class MobileScanner( | ||
| 405 | }, executor) | 405 | }, executor) |
| 406 | 406 | ||
| 407 | } | 407 | } |
| 408 | + | ||
| 409 | + /** | ||
| 410 | + * Pause barcode scanning. | ||
| 411 | + */ | ||
| 412 | + fun pause() { | ||
| 413 | + if (isPaused()) { | ||
| 414 | + throw AlreadyPaused() | ||
| 415 | + } else if (isStopped()) { | ||
| 416 | + throw AlreadyStopped() | ||
| 417 | + } | ||
| 418 | + | ||
| 419 | + releaseCamera() | ||
| 420 | + } | ||
| 421 | + | ||
| 408 | /** | 422 | /** |
| 409 | * Stop barcode scanning. | 423 | * Stop barcode scanning. |
| 410 | */ | 424 | */ |
| 411 | fun stop() { | 425 | fun stop() { |
| 412 | - if (isStopped()) { | 426 | + if (!isPaused() && isStopped()) { |
| 413 | throw AlreadyStopped() | 427 | throw AlreadyStopped() |
| 414 | } | 428 | } |
| 415 | 429 | ||
| 430 | + releaseCamera() | ||
| 431 | + releaseTexture() | ||
| 432 | + } | ||
| 433 | + | ||
| 434 | + private fun releaseCamera() { | ||
| 416 | if (displayListener != null) { | 435 | if (displayListener != null) { |
| 417 | val displayManager = activity.applicationContext.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager | 436 | val displayManager = activity.applicationContext.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager |
| 418 | 437 | ||
| @@ -430,9 +449,6 @@ class MobileScanner( | @@ -430,9 +449,6 @@ class MobileScanner( | ||
| 430 | // Unbind the camera use cases, the preview is a use case. | 449 | // Unbind the camera use cases, the preview is a use case. |
| 431 | // The camera will be closed when the last use case is unbound. | 450 | // The camera will be closed when the last use case is unbound. |
| 432 | cameraProvider?.unbindAll() | 451 | cameraProvider?.unbindAll() |
| 433 | - cameraProvider = null | ||
| 434 | - camera = null | ||
| 435 | - preview = null | ||
| 436 | 452 | ||
| 437 | // Release the texture for the preview. | 453 | // Release the texture for the preview. |
| 438 | textureEntry?.release() | 454 | textureEntry?.release() |
| @@ -444,7 +460,13 @@ class MobileScanner( | @@ -444,7 +460,13 @@ class MobileScanner( | ||
| 444 | lastScanned = null | 460 | lastScanned = null |
| 445 | } | 461 | } |
| 446 | 462 | ||
| 463 | + private fun releaseTexture() { | ||
| 464 | + textureEntry?.release() | ||
| 465 | + textureEntry = null | ||
| 466 | + } | ||
| 467 | + | ||
| 447 | private fun isStopped() = camera == null && preview == null | 468 | private fun isStopped() = camera == null && preview == null |
| 469 | + private fun isPaused() = isStopped() && textureEntry != null | ||
| 448 | 470 | ||
| 449 | /** | 471 | /** |
| 450 | * Toggles the flash light on or off. | 472 | * Toggles the flash light on or off. |
| @@ -3,6 +3,7 @@ package dev.steenbakker.mobile_scanner | @@ -3,6 +3,7 @@ package dev.steenbakker.mobile_scanner | ||
| 3 | class NoCamera : Exception() | 3 | class NoCamera : Exception() |
| 4 | class AlreadyStarted : Exception() | 4 | class AlreadyStarted : Exception() |
| 5 | class AlreadyStopped : Exception() | 5 | class AlreadyStopped : Exception() |
| 6 | +class AlreadyPaused : Exception() | ||
| 6 | class CameraError : Exception() | 7 | class CameraError : Exception() |
| 7 | class ZoomWhenStopped : Exception() | 8 | class ZoomWhenStopped : Exception() |
| 8 | class ZoomNotInRange : Exception() | 9 | class ZoomNotInRange : Exception() |
| @@ -118,6 +118,7 @@ class MobileScannerHandler( | @@ -118,6 +118,7 @@ class MobileScannerHandler( | ||
| 118 | } | 118 | } |
| 119 | }) | 119 | }) |
| 120 | "start" -> start(call, result) | 120 | "start" -> start(call, result) |
| 121 | + "pause" -> pause(result) | ||
| 121 | "stop" -> stop(result) | 122 | "stop" -> stop(result) |
| 122 | "toggleTorch" -> toggleTorch(result) | 123 | "toggleTorch" -> toggleTorch(result) |
| 123 | "analyzeImage" -> analyzeImage(call, result) | 124 | "analyzeImage" -> analyzeImage(call, result) |
| @@ -213,6 +214,18 @@ class MobileScannerHandler( | @@ -213,6 +214,18 @@ class MobileScannerHandler( | ||
| 213 | ) | 214 | ) |
| 214 | } | 215 | } |
| 215 | 216 | ||
| 217 | + private fun pause(result: MethodChannel.Result) { | ||
| 218 | + try { | ||
| 219 | + mobileScanner!!.pause() | ||
| 220 | + result.success(null) | ||
| 221 | + } catch (e: Exception) { | ||
| 222 | + when (e) { | ||
| 223 | + is AlreadyPaused, is AlreadyStopped -> result.success(null) | ||
| 224 | + else -> throw e | ||
| 225 | + } | ||
| 226 | + } | ||
| 227 | + } | ||
| 228 | + | ||
| 216 | private fun stop(result: MethodChannel.Result) { | 229 | private fun stop(result: MethodChannel.Result) { |
| 217 | try { | 230 | try { |
| 218 | mobileScanner!!.stop() | 231 | mobileScanner!!.stop() |
| @@ -105,6 +105,7 @@ class _BarcodeScannerWithControllerState | @@ -105,6 +105,7 @@ class _BarcodeScannerWithControllerState | ||
| 105 | children: [ | 105 | children: [ |
| 106 | ToggleFlashlightButton(controller: controller), | 106 | ToggleFlashlightButton(controller: controller), |
| 107 | StartStopMobileScannerButton(controller: controller), | 107 | StartStopMobileScannerButton(controller: controller), |
| 108 | + PauseMobileScannerButton(controller: controller), | ||
| 108 | Expanded(child: Center(child: _buildBarcode(_barcode))), | 109 | Expanded(child: Center(child: _buildBarcode(_barcode))), |
| 109 | SwitchCameraButton(controller: controller), | 110 | SwitchCameraButton(controller: controller), |
| 110 | AnalyzeImageFromGalleryButton(controller: controller), | 111 | AnalyzeImageFromGalleryButton(controller: controller), |
| @@ -180,3 +180,30 @@ class ToggleFlashlightButton extends StatelessWidget { | @@ -180,3 +180,30 @@ class ToggleFlashlightButton extends StatelessWidget { | ||
| 180 | ); | 180 | ); |
| 181 | } | 181 | } |
| 182 | } | 182 | } |
| 183 | + | ||
| 184 | +class PauseMobileScannerButton extends StatelessWidget { | ||
| 185 | + const PauseMobileScannerButton({required this.controller, super.key}); | ||
| 186 | + | ||
| 187 | + final MobileScannerController controller; | ||
| 188 | + | ||
| 189 | + @override | ||
| 190 | + Widget build(BuildContext context) { | ||
| 191 | + return ValueListenableBuilder( | ||
| 192 | + valueListenable: controller, | ||
| 193 | + builder: (context, state, child) { | ||
| 194 | + if (!state.isInitialized || !state.isRunning) { | ||
| 195 | + return const SizedBox.shrink(); | ||
| 196 | + } | ||
| 197 | + | ||
| 198 | + return IconButton( | ||
| 199 | + color: Colors.white, | ||
| 200 | + iconSize: 32.0, | ||
| 201 | + icon: const Icon(Icons.pause), | ||
| 202 | + onPressed: () async { | ||
| 203 | + await controller.pause(); | ||
| 204 | + }, | ||
| 205 | + ); | ||
| 206 | + }, | ||
| 207 | + ); | ||
| 208 | + } | ||
| 209 | +} |
| @@ -58,6 +58,14 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -58,6 +58,14 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 58 | 58 | ||
| 59 | public var timeoutSeconds: Double = 0 | 59 | public var timeoutSeconds: Double = 0 |
| 60 | 60 | ||
| 61 | + private var stopped: Bool { | ||
| 62 | + return device == nil || captureSession == nil | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | + private var paused: Bool { | ||
| 66 | + return stopped && textureId != nil | ||
| 67 | + } | ||
| 68 | + | ||
| 61 | init(registry: FlutterTextureRegistry?, mobileScannerCallback: @escaping MobileScannerCallback, torchModeChangeCallback: @escaping TorchModeChangeCallback, zoomScaleChangeCallback: @escaping ZoomScaleChangeCallback) { | 69 | init(registry: FlutterTextureRegistry?, mobileScannerCallback: @escaping MobileScannerCallback, torchModeChangeCallback: @escaping TorchModeChangeCallback, zoomScaleChangeCallback: @escaping ZoomScaleChangeCallback) { |
| 62 | self.registry = registry | 70 | self.registry = registry |
| 63 | self.mobileScannerCallback = mobileScannerCallback | 71 | self.mobileScannerCallback = mobileScannerCallback |
| @@ -123,6 +131,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -123,6 +131,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 123 | 131 | ||
| 124 | /// Gets called when a new image is added to the buffer | 132 | /// Gets called when a new image is added to the buffer |
| 125 | public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { | 133 | public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { |
| 134 | + | ||
| 126 | guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { | 135 | guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { |
| 127 | return | 136 | return |
| 128 | } | 137 | } |
| @@ -157,7 +166,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -157,7 +166,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 157 | if (error == nil && barcodesString != nil && newScannedBarcodes != nil && barcodesString!.elementsEqual(newScannedBarcodes!)) { | 166 | if (error == nil && barcodesString != nil && newScannedBarcodes != nil && barcodesString!.elementsEqual(newScannedBarcodes!)) { |
| 158 | return | 167 | return |
| 159 | } | 168 | } |
| 160 | - | 169 | + |
| 161 | if (newScannedBarcodes?.isEmpty == false) { | 170 | if (newScannedBarcodes?.isEmpty == false) { |
| 162 | barcodesString = newScannedBarcodes | 171 | barcodesString = newScannedBarcodes |
| 163 | } | 172 | } |
| @@ -178,7 +187,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -178,7 +187,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 178 | barcodesString = nil | 187 | barcodesString = nil |
| 179 | scanner = barcodeScannerOptions != nil ? BarcodeScanner.barcodeScanner(options: barcodeScannerOptions!) : BarcodeScanner.barcodeScanner() | 188 | scanner = barcodeScannerOptions != nil ? BarcodeScanner.barcodeScanner(options: barcodeScannerOptions!) : BarcodeScanner.barcodeScanner() |
| 180 | captureSession = AVCaptureSession() | 189 | captureSession = AVCaptureSession() |
| 181 | - textureId = registry?.register(self) | 190 | + textureId = textureId ?? registry?.register(self) |
| 182 | 191 | ||
| 183 | // Open the camera device | 192 | // Open the camera device |
| 184 | device = getDefaultCameraDevice(position: cameraPosition) | 193 | device = getDefaultCameraDevice(position: cameraPosition) |
| @@ -293,27 +302,49 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -293,27 +302,49 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 293 | } | 302 | } |
| 294 | } | 303 | } |
| 295 | 304 | ||
| 305 | + /// Pause scanning for barcodes | ||
| 306 | + func pause() throws { | ||
| 307 | + if (paused) { | ||
| 308 | + throw MobileScannerError.alreadyPaused | ||
| 309 | + } else if (stopped) { | ||
| 310 | + throw MobileScannerError.alreadyStopped | ||
| 311 | + } | ||
| 312 | + releaseCamera() | ||
| 313 | + } | ||
| 314 | + | ||
| 296 | /// Stop scanning for barcodes | 315 | /// Stop scanning for barcodes |
| 297 | func stop() throws { | 316 | func stop() throws { |
| 298 | - if (device == nil || captureSession == nil) { | 317 | + if (!paused && stopped) { |
| 299 | throw MobileScannerError.alreadyStopped | 318 | throw MobileScannerError.alreadyStopped |
| 300 | } | 319 | } |
| 301 | - | ||
| 302 | - captureSession!.stopRunning() | ||
| 303 | - for input in captureSession!.inputs { | ||
| 304 | - captureSession!.removeInput(input) | 320 | + releaseCamera() |
| 321 | + releaseTexture() | ||
| 322 | + } | ||
| 323 | + | ||
| 324 | + private func releaseCamera() { | ||
| 325 | + | ||
| 326 | + guard let captureSession = captureSession else { | ||
| 327 | + return | ||
| 328 | + } | ||
| 329 | + | ||
| 330 | + captureSession.stopRunning() | ||
| 331 | + for input in captureSession.inputs { | ||
| 332 | + captureSession.removeInput(input) | ||
| 305 | } | 333 | } |
| 306 | - for output in captureSession!.outputs { | ||
| 307 | - captureSession!.removeOutput(output) | 334 | + for output in captureSession.outputs { |
| 335 | + captureSession.removeOutput(output) | ||
| 308 | } | 336 | } |
| 309 | 337 | ||
| 310 | latestBuffer = nil | 338 | latestBuffer = nil |
| 311 | device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode)) | 339 | device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode)) |
| 312 | device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.videoZoomFactor)) | 340 | device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.videoZoomFactor)) |
| 341 | + self.captureSession = nil | ||
| 342 | + device = nil | ||
| 343 | + } | ||
| 344 | + | ||
| 345 | + private func releaseTexture() { | ||
| 313 | registry?.unregisterTexture(textureId) | 346 | registry?.unregisterTexture(textureId) |
| 314 | textureId = nil | 347 | textureId = nil |
| 315 | - captureSession = nil | ||
| 316 | - device = nil | ||
| 317 | scanner = nil | 348 | scanner = nil |
| 318 | } | 349 | } |
| 319 | 350 | ||
| @@ -440,7 +471,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -440,7 +471,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 440 | defaultOrientation: .portrait, | 471 | defaultOrientation: .portrait, |
| 441 | position: position | 472 | position: position |
| 442 | ) | 473 | ) |
| 443 | - | 474 | + |
| 444 | let scanner: BarcodeScanner = barcodeScannerOptions != nil ? BarcodeScanner.barcodeScanner(options: barcodeScannerOptions!) : BarcodeScanner.barcodeScanner() | 475 | let scanner: BarcodeScanner = barcodeScannerOptions != nil ? BarcodeScanner.barcodeScanner(options: barcodeScannerOptions!) : BarcodeScanner.barcodeScanner() |
| 445 | 476 | ||
| 446 | scanner.process(image, completion: callback) | 477 | scanner.process(image, completion: callback) |
| @@ -16,6 +16,7 @@ enum MobileScannerError: Error { | @@ -16,6 +16,7 @@ enum MobileScannerError: Error { | ||
| 16 | case noCamera | 16 | case noCamera |
| 17 | case alreadyStarted | 17 | case alreadyStarted |
| 18 | case alreadyStopped | 18 | case alreadyStopped |
| 19 | + case alreadyPaused | ||
| 19 | case cameraError(_ error: Error) | 20 | case cameraError(_ error: Error) |
| 20 | case zoomWhenStopped | 21 | case zoomWhenStopped |
| 21 | case zoomError(_ error: Error) | 22 | case zoomError(_ error: Error) |
| @@ -105,6 +105,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | @@ -105,6 +105,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | ||
| 105 | AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) }) | 105 | AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) }) |
| 106 | case "start": | 106 | case "start": |
| 107 | start(call, result) | 107 | start(call, result) |
| 108 | + case "pause": | ||
| 109 | + pause(result) | ||
| 108 | case "stop": | 110 | case "stop": |
| 109 | stop(result) | 111 | stop(result) |
| 110 | case "toggleTorch": | 112 | case "toggleTorch": |
| @@ -166,6 +168,14 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | @@ -166,6 +168,14 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | ||
| 166 | details: nil)) | 168 | details: nil)) |
| 167 | } | 169 | } |
| 168 | } | 170 | } |
| 171 | + | ||
| 172 | + /// Stops the mobileScanner without closing the texture. | ||
| 173 | + private func pause(_ result: @escaping FlutterResult) { | ||
| 174 | + do { | ||
| 175 | + try mobileScanner.pause() | ||
| 176 | + } catch {} | ||
| 177 | + result(nil) | ||
| 178 | + } | ||
| 169 | 179 | ||
| 170 | /// Stops the mobileScanner and closes the texture. | 180 | /// Stops the mobileScanner and closes the texture. |
| 171 | private func stop(_ result: @escaping FlutterResult) { | 181 | private func stop(_ result: @escaping FlutterResult) { |
| @@ -46,6 +46,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | @@ -46,6 +46,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | ||
| 46 | } | 46 | } |
| 47 | 47 | ||
| 48 | int? _textureId; | 48 | int? _textureId; |
| 49 | + bool _pausing = false; | ||
| 49 | 50 | ||
| 50 | /// Parse a [BarcodeCapture] from the given [event]. | 51 | /// Parse a [BarcodeCapture] from the given [event]. |
| 51 | BarcodeCapture? _parseBarcode(Map<Object?, Object?>? event) { | 52 | BarcodeCapture? _parseBarcode(Map<Object?, Object?>? event) { |
| @@ -216,7 +217,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | @@ -216,7 +217,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | ||
| 216 | 217 | ||
| 217 | @override | 218 | @override |
| 218 | Future<MobileScannerViewAttributes> start(StartOptions startOptions) async { | 219 | Future<MobileScannerViewAttributes> start(StartOptions startOptions) async { |
| 219 | - if (_textureId != null) { | 220 | + if (!_pausing && _textureId != null) { |
| 220 | throw const MobileScannerException( | 221 | throw const MobileScannerException( |
| 221 | errorCode: MobileScannerErrorCode.controllerAlreadyInitialized, | 222 | errorCode: MobileScannerErrorCode.controllerAlreadyInitialized, |
| 222 | errorDetails: MobileScannerErrorDetails( | 223 | errorDetails: MobileScannerErrorDetails( |
| @@ -281,6 +282,8 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | @@ -281,6 +282,8 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | ||
| 281 | size = Size.zero; | 282 | size = Size.zero; |
| 282 | } | 283 | } |
| 283 | 284 | ||
| 285 | + _pausing = false; | ||
| 286 | + | ||
| 284 | return MobileScannerViewAttributes( | 287 | return MobileScannerViewAttributes( |
| 285 | currentTorchMode: currentTorchState, | 288 | currentTorchMode: currentTorchState, |
| 286 | numberOfCameras: numberOfCameras, | 289 | numberOfCameras: numberOfCameras, |
| @@ -295,11 +298,23 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | @@ -295,11 +298,23 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | ||
| 295 | } | 298 | } |
| 296 | 299 | ||
| 297 | _textureId = null; | 300 | _textureId = null; |
| 301 | + _pausing = false; | ||
| 298 | 302 | ||
| 299 | await methodChannel.invokeMethod<void>('stop'); | 303 | await methodChannel.invokeMethod<void>('stop'); |
| 300 | } | 304 | } |
| 301 | 305 | ||
| 302 | @override | 306 | @override |
| 307 | + Future<void> pause() async { | ||
| 308 | + if (_pausing) { | ||
| 309 | + return; | ||
| 310 | + } | ||
| 311 | + | ||
| 312 | + _pausing = true; | ||
| 313 | + | ||
| 314 | + await methodChannel.invokeMethod<void>('pause'); | ||
| 315 | + } | ||
| 316 | + | ||
| 317 | + @override | ||
| 303 | Future<void> toggleTorch() async { | 318 | Future<void> toggleTorch() async { |
| 304 | await methodChannel.invokeMethod<void>('toggleTorch'); | 319 | await methodChannel.invokeMethod<void>('toggleTorch'); |
| 305 | } | 320 | } |
| @@ -183,6 +183,30 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | @@ -183,6 +183,30 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | ||
| 183 | } | 183 | } |
| 184 | } | 184 | } |
| 185 | 185 | ||
| 186 | + void _stop() { | ||
| 187 | + // Do nothing if not initialized or already stopped. | ||
| 188 | + // On the web, the permission popup triggers a lifecycle change from resumed to inactive, | ||
| 189 | + // due to the permission popup gaining focus. | ||
| 190 | + // This would 'stop' the camera while it is not ready yet. | ||
| 191 | + if (!value.isInitialized || !value.isRunning || _isDisposed) { | ||
| 192 | + return; | ||
| 193 | + } | ||
| 194 | + | ||
| 195 | + _disposeListeners(); | ||
| 196 | + | ||
| 197 | + final TorchState oldTorchState = value.torchState; | ||
| 198 | + | ||
| 199 | + // After the camera stopped, set the torch state to off, | ||
| 200 | + // as the torch state callback is never called when the camera is stopped. | ||
| 201 | + // If the device does not have a torch, do not report "off". | ||
| 202 | + value = value.copyWith( | ||
| 203 | + isRunning: false, | ||
| 204 | + torchState: oldTorchState == TorchState.unavailable | ||
| 205 | + ? TorchState.unavailable | ||
| 206 | + : TorchState.off, | ||
| 207 | + ); | ||
| 208 | + } | ||
| 209 | + | ||
| 186 | /// Analyze an image file. | 210 | /// Analyze an image file. |
| 187 | /// | 211 | /// |
| 188 | /// The [path] points to a file on the device. | 212 | /// The [path] points to a file on the device. |
| @@ -336,31 +360,21 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | @@ -336,31 +360,21 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | ||
| 336 | /// | 360 | /// |
| 337 | /// Does nothing if the camera is already stopped. | 361 | /// Does nothing if the camera is already stopped. |
| 338 | Future<void> stop() async { | 362 | Future<void> stop() async { |
| 339 | - // Do nothing if not initialized or already stopped. | ||
| 340 | - // On the web, the permission popup triggers a lifecycle change from resumed to inactive, | ||
| 341 | - // due to the permission popup gaining focus. | ||
| 342 | - // This would 'stop' the camera while it is not ready yet. | ||
| 343 | - if (!value.isInitialized || !value.isRunning || _isDisposed) { | ||
| 344 | - return; | ||
| 345 | - } | ||
| 346 | - | ||
| 347 | - _disposeListeners(); | ||
| 348 | - | ||
| 349 | - final TorchState oldTorchState = value.torchState; | ||
| 350 | - | ||
| 351 | - // After the camera stopped, set the torch state to off, | ||
| 352 | - // as the torch state callback is never called when the camera is stopped. | ||
| 353 | - // If the device does not have a torch, do not report "off". | ||
| 354 | - value = value.copyWith( | ||
| 355 | - isRunning: false, | ||
| 356 | - torchState: oldTorchState == TorchState.unavailable | ||
| 357 | - ? TorchState.unavailable | ||
| 358 | - : TorchState.off, | ||
| 359 | - ); | ||
| 360 | - | 363 | + _stop(); |
| 361 | await MobileScannerPlatform.instance.stop(); | 364 | await MobileScannerPlatform.instance.stop(); |
| 362 | } | 365 | } |
| 363 | 366 | ||
| 367 | + /// Pause the camera. | ||
| 368 | + /// | ||
| 369 | + /// This method stops to update camera frame and scan barcodes. | ||
| 370 | + /// After calling this method, the camera can be restarted using [start]. | ||
| 371 | + /// | ||
| 372 | + /// Does nothing if the camera is already paused or stopped. | ||
| 373 | + Future<void> pause() async { | ||
| 374 | + _stop(); | ||
| 375 | + await MobileScannerPlatform.instance.pause(); | ||
| 376 | + } | ||
| 377 | + | ||
| 364 | /// Switch between the front and back camera. | 378 | /// Switch between the front and back camera. |
| 365 | /// | 379 | /// |
| 366 | /// Does nothing if the device has less than 2 cameras. | 380 | /// Does nothing if the device has less than 2 cameras. |
| @@ -97,6 +97,11 @@ abstract class MobileScannerPlatform extends PlatformInterface { | @@ -97,6 +97,11 @@ abstract class MobileScannerPlatform extends PlatformInterface { | ||
| 97 | throw UnimplementedError('stop() has not been implemented.'); | 97 | throw UnimplementedError('stop() has not been implemented.'); |
| 98 | } | 98 | } |
| 99 | 99 | ||
| 100 | + /// Pause the camera. | ||
| 101 | + Future<void> pause() { | ||
| 102 | + throw UnimplementedError('pause() has not been implemented.'); | ||
| 103 | + } | ||
| 104 | + | ||
| 100 | /// Toggle the torch on the active camera on or off. | 105 | /// Toggle the torch on the active camera on or off. |
| 101 | Future<void> toggleTorch() { | 106 | Future<void> toggleTorch() { |
| 102 | throw UnimplementedError('toggleTorch() has not been implemented.'); | 107 | throw UnimplementedError('toggleTorch() has not been implemented.'); |
| @@ -128,6 +128,11 @@ abstract class BarcodeReader { | @@ -128,6 +128,11 @@ abstract class BarcodeReader { | ||
| 128 | throw UnimplementedError('start() has not been implemented.'); | 128 | throw UnimplementedError('start() has not been implemented.'); |
| 129 | } | 129 | } |
| 130 | 130 | ||
| 131 | + /// Pause the barcode reader. | ||
| 132 | + Future<void> pause() { | ||
| 133 | + throw UnimplementedError('pause() has not been implemented.'); | ||
| 134 | + } | ||
| 135 | + | ||
| 131 | /// Stop the barcode reader and dispose of the video stream. | 136 | /// Stop the barcode reader and dispose of the video stream. |
| 132 | Future<void> stop() { | 137 | Future<void> stop() { |
| 133 | throw UnimplementedError('stop() has not been implemented.'); | 138 | throw UnimplementedError('stop() has not been implemented.'); |
| @@ -271,6 +271,11 @@ class MobileScannerWeb extends MobileScannerPlatform { | @@ -271,6 +271,11 @@ class MobileScannerWeb extends MobileScannerPlatform { | ||
| 271 | ); | 271 | ); |
| 272 | } | 272 | } |
| 273 | 273 | ||
| 274 | + // If the previous state is a pause, reset scanner. | ||
| 275 | + if (_barcodesSubscription != null && _barcodesSubscription!.isPaused) { | ||
| 276 | + await stop(); | ||
| 277 | + } | ||
| 278 | + | ||
| 274 | _barcodeReader = ZXingBarcodeReader(); | 279 | _barcodeReader = ZXingBarcodeReader(); |
| 275 | 280 | ||
| 276 | await _barcodeReader?.maybeLoadLibrary( | 281 | await _barcodeReader?.maybeLoadLibrary( |
| @@ -358,6 +363,12 @@ class MobileScannerWeb extends MobileScannerPlatform { | @@ -358,6 +363,12 @@ class MobileScannerWeb extends MobileScannerPlatform { | ||
| 358 | } | 363 | } |
| 359 | 364 | ||
| 360 | @override | 365 | @override |
| 366 | + Future<void> pause() async { | ||
| 367 | + _barcodesSubscription?.pause(); | ||
| 368 | + await _barcodeReader?.pause(); | ||
| 369 | + } | ||
| 370 | + | ||
| 371 | + @override | ||
| 361 | Future<void> stop() async { | 372 | Future<void> stop() async { |
| 362 | // Ensure the barcode scanner is stopped, by cancelling the subscription. | 373 | // Ensure the barcode scanner is stopped, by cancelling the subscription. |
| 363 | await _barcodesSubscription?.cancel(); | 374 | await _barcodesSubscription?.cancel(); |
| @@ -169,6 +169,11 @@ final class ZXingBarcodeReader extends BarcodeReader { | @@ -169,6 +169,11 @@ final class ZXingBarcodeReader extends BarcodeReader { | ||
| 169 | } | 169 | } |
| 170 | 170 | ||
| 171 | @override | 171 | @override |
| 172 | + Future<void> pause() async { | ||
| 173 | + _reader?.videoElement?.pause(); | ||
| 174 | + } | ||
| 175 | + | ||
| 176 | + @override | ||
| 172 | Future<void> stop() async { | 177 | Future<void> stop() async { |
| 173 | _onMediaTrackSettingsChanged = null; | 178 | _onMediaTrackSettingsChanged = null; |
| 174 | _reader?.stopContinuousDecode.callAsFunction(_reader); | 179 | _reader?.stopContinuousDecode.callAsFunction(_reader); |
| @@ -25,7 +25,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -25,7 +25,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 25 | 25 | ||
| 26 | // optional window to limit scan search | 26 | // optional window to limit scan search |
| 27 | var scanWindow: CGRect? | 27 | var scanWindow: CGRect? |
| 28 | - | 28 | + |
| 29 | /// Whether to return the input image with the barcode event. | 29 | /// Whether to return the input image with the barcode event. |
| 30 | /// This is static to avoid accessing `self` in the `VNDetectBarcodesRequest` callback. | 30 | /// This is static to avoid accessing `self` in the `VNDetectBarcodesRequest` callback. |
| 31 | private static var returnImage: Bool = false | 31 | private static var returnImage: Bool = false |
| @@ -35,9 +35,17 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -35,9 +35,17 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 35 | var timeoutSeconds: Double = 0 | 35 | var timeoutSeconds: Double = 0 |
| 36 | 36 | ||
| 37 | var symbologies:[VNBarcodeSymbology] = [] | 37 | var symbologies:[VNBarcodeSymbology] = [] |
| 38 | - | 38 | + |
| 39 | var position = AVCaptureDevice.Position.back | 39 | var position = AVCaptureDevice.Position.back |
| 40 | 40 | ||
| 41 | + private var stopped: Bool { | ||
| 42 | + return device == nil || captureSession == nil | ||
| 43 | + } | ||
| 44 | + | ||
| 45 | + private var paused: Bool { | ||
| 46 | + return stopped && textureId != nil | ||
| 47 | + } | ||
| 48 | + | ||
| 41 | public static func register(with registrar: FlutterPluginRegistrar) { | 49 | public static func register(with registrar: FlutterPluginRegistrar) { |
| 42 | let instance = MobileScannerPlugin(registrar.textures) | 50 | let instance = MobileScannerPlugin(registrar.textures) |
| 43 | let method = FlutterMethodChannel(name: | 51 | let method = FlutterMethodChannel(name: |
| @@ -67,6 +75,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -67,6 +75,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 67 | setScale(call, result) | 75 | setScale(call, result) |
| 68 | case "resetScale": | 76 | case "resetScale": |
| 69 | resetScale(call, result) | 77 | resetScale(call, result) |
| 78 | + case "pause": | ||
| 79 | + pause(result) | ||
| 70 | case "stop": | 80 | case "stop": |
| 71 | stop(result) | 81 | stop(result) |
| 72 | case "updateScanWindow": | 82 | case "updateScanWindow": |
| @@ -128,7 +138,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -128,7 +138,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 128 | do { | 138 | do { |
| 129 | let barcodeRequest: VNDetectBarcodesRequest = VNDetectBarcodesRequest(completionHandler: { [weak self] (request, error) in | 139 | let barcodeRequest: VNDetectBarcodesRequest = VNDetectBarcodesRequest(completionHandler: { [weak self] (request, error) in |
| 130 | self?.imagesCurrentlyBeingProcessed = false | 140 | self?.imagesCurrentlyBeingProcessed = false |
| 131 | - | 141 | + |
| 132 | if error != nil { | 142 | if error != nil { |
| 133 | DispatchQueue.main.async { | 143 | DispatchQueue.main.async { |
| 134 | self?.sink?(FlutterError( | 144 | self?.sink?(FlutterError( |
| @@ -137,24 +147,24 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -137,24 +147,24 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 137 | } | 147 | } |
| 138 | return | 148 | return |
| 139 | } | 149 | } |
| 140 | - | 150 | + |
| 141 | guard let results: [VNBarcodeObservation] = request.results as? [VNBarcodeObservation] else { | 151 | guard let results: [VNBarcodeObservation] = request.results as? [VNBarcodeObservation] else { |
| 142 | return | 152 | return |
| 143 | } | 153 | } |
| 144 | - | 154 | + |
| 145 | if results.isEmpty { | 155 | if results.isEmpty { |
| 146 | return | 156 | return |
| 147 | } | 157 | } |
| 148 | - | 158 | + |
| 149 | let barcodes: [VNBarcodeObservation] = results.compactMap({ barcode in | 159 | let barcodes: [VNBarcodeObservation] = results.compactMap({ barcode in |
| 150 | // If there is a scan window, check if the barcode is within said scan window. | 160 | // If there is a scan window, check if the barcode is within said scan window. |
| 151 | if self?.scanWindow != nil && cgImage != nil && !(self?.isBarCodeInScanWindow(self!.scanWindow!, barcode, cgImage!) ?? false) { | 161 | if self?.scanWindow != nil && cgImage != nil && !(self?.isBarCodeInScanWindow(self!.scanWindow!, barcode, cgImage!) ?? false) { |
| 152 | return nil | 162 | return nil |
| 153 | } | 163 | } |
| 154 | - | 164 | + |
| 155 | return barcode | 165 | return barcode |
| 156 | }) | 166 | }) |
| 157 | - | 167 | + |
| 158 | DispatchQueue.main.async { | 168 | DispatchQueue.main.async { |
| 159 | guard let image = cgImage else { | 169 | guard let image = cgImage else { |
| 160 | self?.sink?([ | 170 | self?.sink?([ |
| @@ -163,7 +173,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -163,7 +173,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 163 | ]) | 173 | ]) |
| 164 | return | 174 | return |
| 165 | } | 175 | } |
| 166 | - | 176 | + |
| 167 | // The image dimensions are always provided. | 177 | // The image dimensions are always provided. |
| 168 | // The image bytes are only non-null when `returnImage` is true. | 178 | // The image bytes are only non-null when `returnImage` is true. |
| 169 | let imageData: [String: Any?] = [ | 179 | let imageData: [String: Any?] = [ |
| @@ -171,7 +181,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -171,7 +181,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 171 | "width": Double(image.width), | 181 | "width": Double(image.width), |
| 172 | "height": Double(image.height), | 182 | "height": Double(image.height), |
| 173 | ] | 183 | ] |
| 174 | - | 184 | + |
| 175 | self?.sink?([ | 185 | self?.sink?([ |
| 176 | "name": "barcode", | 186 | "name": "barcode", |
| 177 | "data": barcodes.map({ $0.toMap() }), | 187 | "data": barcodes.map({ $0.toMap() }), |
| @@ -179,7 +189,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -179,7 +189,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 179 | ]) | 189 | ]) |
| 180 | } | 190 | } |
| 181 | }) | 191 | }) |
| 182 | - | 192 | + |
| 183 | if self?.symbologies.isEmpty == false { | 193 | if self?.symbologies.isEmpty == false { |
| 184 | // Add the symbologies the user wishes to support. | 194 | // Add the symbologies the user wishes to support. |
| 185 | barcodeRequest.symbologies = self!.symbologies | 195 | barcodeRequest.symbologies = self!.symbologies |
| @@ -276,7 +286,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -276,7 +286,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 276 | return | 286 | return |
| 277 | } | 287 | } |
| 278 | 288 | ||
| 279 | - textureId = registry.register(self) | 289 | + textureId = textureId ?? registry.register(self) |
| 280 | captureSession = AVCaptureSession() | 290 | captureSession = AVCaptureSession() |
| 281 | 291 | ||
| 282 | let argReader = MapArgumentReader(call.arguments as? [String: Any]) | 292 | let argReader = MapArgumentReader(call.arguments as? [String: Any]) |
| @@ -438,52 +448,73 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -438,52 +448,73 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 438 | result(nil) | 448 | result(nil) |
| 439 | } | 449 | } |
| 440 | 450 | ||
| 451 | + func pause(_ result: FlutterResult) { | ||
| 452 | + if (paused || stopped) { | ||
| 453 | + result(nil) | ||
| 454 | + | ||
| 455 | + return | ||
| 456 | + } | ||
| 457 | + releaseCamera() | ||
| 458 | + } | ||
| 459 | + | ||
| 441 | func stop(_ result: FlutterResult) { | 460 | func stop(_ result: FlutterResult) { |
| 442 | - if (device == nil || captureSession == nil) { | 461 | + if (!paused && stopped) { |
| 443 | result(nil) | 462 | result(nil) |
| 444 | 463 | ||
| 445 | return | 464 | return |
| 446 | } | 465 | } |
| 447 | - captureSession!.stopRunning() | ||
| 448 | - for input in captureSession!.inputs { | ||
| 449 | - captureSession!.removeInput(input) | 466 | + releaseCamera() |
| 467 | + releaseTexture() | ||
| 468 | + | ||
| 469 | + result(nil) | ||
| 470 | + } | ||
| 471 | + | ||
| 472 | + private func releaseCamera() { | ||
| 473 | + guard let captureSession = captureSession else { | ||
| 474 | + return | ||
| 475 | + } | ||
| 476 | + | ||
| 477 | + captureSession.stopRunning() | ||
| 478 | + for input in captureSession.inputs { | ||
| 479 | + captureSession.removeInput(input) | ||
| 450 | } | 480 | } |
| 451 | - for output in captureSession!.outputs { | ||
| 452 | - captureSession!.removeOutput(output) | 481 | + for output in captureSession.outputs { |
| 482 | + captureSession.removeOutput(output) | ||
| 453 | } | 483 | } |
| 454 | device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode)) | 484 | device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode)) |
| 455 | - registry.unregisterTexture(textureId) | ||
| 456 | - | 485 | + |
| 457 | latestBuffer = nil | 486 | latestBuffer = nil |
| 458 | - captureSession = nil | 487 | + self.captureSession = nil |
| 459 | device = nil | 488 | device = nil |
| 489 | + } | ||
| 490 | + | ||
| 491 | + private func releaseTexture() { | ||
| 492 | + registry.unregisterTexture(textureId) | ||
| 460 | textureId = nil | 493 | textureId = nil |
| 461 | - | ||
| 462 | - result(nil) | ||
| 463 | } | 494 | } |
| 464 | - | 495 | + |
| 465 | func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | 496 | func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { |
| 466 | let argReader = MapArgumentReader(call.arguments as? [String: Any]) | 497 | let argReader = MapArgumentReader(call.arguments as? [String: Any]) |
| 467 | let symbologies:[VNBarcodeSymbology] = argReader.toSymbology() | 498 | let symbologies:[VNBarcodeSymbology] = argReader.toSymbology() |
| 468 | - | 499 | + |
| 469 | guard let filePath: String = argReader.string(key: "filePath") else { | 500 | guard let filePath: String = argReader.string(key: "filePath") else { |
| 470 | result(nil) | 501 | result(nil) |
| 471 | return | 502 | return |
| 472 | } | 503 | } |
| 473 | - | 504 | + |
| 474 | let fileUrl = URL(fileURLWithPath: filePath) | 505 | let fileUrl = URL(fileURLWithPath: filePath) |
| 475 | - | 506 | + |
| 476 | guard let ciImage = CIImage(contentsOf: fileUrl) else { | 507 | guard let ciImage = CIImage(contentsOf: fileUrl) else { |
| 477 | result(nil) | 508 | result(nil) |
| 478 | return | 509 | return |
| 479 | } | 510 | } |
| 480 | - | 511 | + |
| 481 | let imageRequestHandler = VNImageRequestHandler(ciImage: ciImage, orientation: CGImagePropertyOrientation.up, options: [:]) | 512 | let imageRequestHandler = VNImageRequestHandler(ciImage: ciImage, orientation: CGImagePropertyOrientation.up, options: [:]) |
| 482 | - | 513 | + |
| 483 | do { | 514 | do { |
| 484 | let barcodeRequest: VNDetectBarcodesRequest = VNDetectBarcodesRequest( | 515 | let barcodeRequest: VNDetectBarcodesRequest = VNDetectBarcodesRequest( |
| 485 | completionHandler: { [] (request, error) in | 516 | completionHandler: { [] (request, error) in |
| 486 | - | 517 | + |
| 487 | if error != nil { | 518 | if error != nil { |
| 488 | DispatchQueue.main.async { | 519 | DispatchQueue.main.async { |
| 489 | result(FlutterError( | 520 | result(FlutterError( |
| @@ -492,26 +523,26 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -492,26 +523,26 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 492 | } | 523 | } |
| 493 | return | 524 | return |
| 494 | } | 525 | } |
| 495 | - | 526 | + |
| 496 | guard let barcodes: [VNBarcodeObservation] = request.results as? [VNBarcodeObservation] else { | 527 | guard let barcodes: [VNBarcodeObservation] = request.results as? [VNBarcodeObservation] else { |
| 497 | return | 528 | return |
| 498 | } | 529 | } |
| 499 | - | 530 | + |
| 500 | if barcodes.isEmpty { | 531 | if barcodes.isEmpty { |
| 501 | return | 532 | return |
| 502 | } | 533 | } |
| 503 | - | 534 | + |
| 504 | result([ | 535 | result([ |
| 505 | "name": "barcode", | 536 | "name": "barcode", |
| 506 | "data": barcodes.map({ $0.toMap() }), | 537 | "data": barcodes.map({ $0.toMap() }), |
| 507 | ]) | 538 | ]) |
| 508 | }) | 539 | }) |
| 509 | - | 540 | + |
| 510 | if !symbologies.isEmpty { | 541 | if !symbologies.isEmpty { |
| 511 | // Add the symbologies the user wishes to support. | 542 | // Add the symbologies the user wishes to support. |
| 512 | barcodeRequest.symbologies = symbologies | 543 | barcodeRequest.symbologies = symbologies |
| 513 | } | 544 | } |
| 514 | - | 545 | + |
| 515 | try imageRequestHandler.perform([barcodeRequest]) | 546 | try imageRequestHandler.perform([barcodeRequest]) |
| 516 | } catch let error { | 547 | } catch let error { |
| 517 | DispatchQueue.main.async { | 548 | DispatchQueue.main.async { |
| @@ -521,7 +552,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -521,7 +552,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 521 | } | 552 | } |
| 522 | } | 553 | } |
| 523 | } | 554 | } |
| 524 | - | 555 | + |
| 525 | // Observer for torch state | 556 | // Observer for torch state |
| 526 | public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { | 557 | public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { |
| 527 | switch keyPath { | 558 | switch keyPath { |
| @@ -584,29 +615,29 @@ class MapArgumentReader { | @@ -584,29 +615,29 @@ class MapArgumentReader { | ||
| 584 | extension CGImage { | 615 | extension CGImage { |
| 585 | public func jpegData(compressionQuality: CGFloat) -> Data? { | 616 | public func jpegData(compressionQuality: CGFloat) -> Data? { |
| 586 | let mutableData = CFDataCreateMutable(nil, 0) | 617 | let mutableData = CFDataCreateMutable(nil, 0) |
| 587 | - | 618 | + |
| 588 | let formatHint: CFString | 619 | let formatHint: CFString |
| 589 | - | 620 | + |
| 590 | if #available(macOS 11.0, *) { | 621 | if #available(macOS 11.0, *) { |
| 591 | formatHint = UTType.jpeg.identifier as CFString | 622 | formatHint = UTType.jpeg.identifier as CFString |
| 592 | } else { | 623 | } else { |
| 593 | formatHint = kUTTypeJPEG | 624 | formatHint = kUTTypeJPEG |
| 594 | } | 625 | } |
| 595 | - | 626 | + |
| 596 | guard let destination = CGImageDestinationCreateWithData(mutableData!, formatHint, 1, nil) else { | 627 | guard let destination = CGImageDestinationCreateWithData(mutableData!, formatHint, 1, nil) else { |
| 597 | return nil | 628 | return nil |
| 598 | } | 629 | } |
| 599 | - | 630 | + |
| 600 | let options: NSDictionary = [ | 631 | let options: NSDictionary = [ |
| 601 | kCGImageDestinationLossyCompressionQuality: compressionQuality, | 632 | kCGImageDestinationLossyCompressionQuality: compressionQuality, |
| 602 | ] | 633 | ] |
| 603 | - | 634 | + |
| 604 | CGImageDestinationAddImage(destination, self, options) | 635 | CGImageDestinationAddImage(destination, self, options) |
| 605 | - | 636 | + |
| 606 | if !CGImageDestinationFinalize(destination) { | 637 | if !CGImageDestinationFinalize(destination) { |
| 607 | return nil | 638 | return nil |
| 608 | } | 639 | } |
| 609 | - | 640 | + |
| 610 | return mutableData as Data? | 641 | return mutableData as Data? |
| 611 | } | 642 | } |
| 612 | } | 643 | } |
| @@ -615,7 +646,7 @@ extension VNBarcodeObservation { | @@ -615,7 +646,7 @@ extension VNBarcodeObservation { | ||
| 615 | private func distanceBetween(_ p1: CGPoint, _ p2: CGPoint) -> CGFloat { | 646 | private func distanceBetween(_ p1: CGPoint, _ p2: CGPoint) -> CGFloat { |
| 616 | return sqrt(pow(p1.x - p2.x, 2) + pow(p1.y - p2.y, 2)) | 647 | return sqrt(pow(p1.x - p2.x, 2) + pow(p1.y - p2.y, 2)) |
| 617 | } | 648 | } |
| 618 | - | 649 | + |
| 619 | public func toMap() -> [String: Any?] { | 650 | public func toMap() -> [String: Any?] { |
| 620 | return [ | 651 | return [ |
| 621 | "corners": [ | 652 | "corners": [ |
-
Please register or login to post a comment