Julian Steenbakker
Committed by GitHub

Merge pull request #994 from fumin65/pause_function

feat: add pause feature
@@ -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": [