fumin65

add pause function

@@ -19,7 +19,6 @@ import androidx.camera.core.ExperimentalGetImage @@ -19,7 +19,6 @@ import androidx.camera.core.ExperimentalGetImage
19 import androidx.camera.core.ImageAnalysis 19 import androidx.camera.core.ImageAnalysis
20 import androidx.camera.core.ImageProxy 20 import androidx.camera.core.ImageProxy
21 import androidx.camera.core.Preview 21 import androidx.camera.core.Preview
22 -import androidx.camera.core.resolutionselector.AspectRatioStrategy  
23 import androidx.camera.core.resolutionselector.ResolutionSelector 22 import androidx.camera.core.resolutionselector.ResolutionSelector
24 import androidx.camera.core.resolutionselector.ResolutionStrategy 23 import androidx.camera.core.resolutionselector.ResolutionStrategy
25 import androidx.camera.lifecycle.ProcessCameraProvider 24 import androidx.camera.lifecycle.ProcessCameraProvider
@@ -259,7 +258,7 @@ class MobileScanner( @@ -259,7 +258,7 @@ class MobileScanner(
259 } 258 }
260 259
261 cameraProvider?.unbindAll() 260 cameraProvider?.unbindAll()
262 - textureEntry = textureRegistry.createSurfaceTexture() 261 + textureEntry = textureEntry ?: textureRegistry.createSurfaceTexture()
263 262
264 // Preview 263 // Preview
265 val surfaceProvider = Preview.SurfaceProvider { request -> 264 val surfaceProvider = Preview.SurfaceProvider { request ->
@@ -380,14 +379,33 @@ class MobileScanner( @@ -380,14 +379,33 @@ class MobileScanner(
380 }, executor) 379 }, executor)
381 380
382 } 381 }
  382 +
  383 + /**
  384 + * Pause barcode scanning.
  385 + */
  386 + fun pause() {
  387 + if (isPaused()) {
  388 + throw AlreadyPaused()
  389 + } else if (isStopped()) {
  390 + throw AlreadyStopped()
  391 + }
  392 +
  393 + releaseCamera()
  394 + }
  395 +
383 /** 396 /**
384 * Stop barcode scanning. 397 * Stop barcode scanning.
385 */ 398 */
386 fun stop() { 399 fun stop() {
387 - if (isStopped()) { 400 + if (!isPaused() && isStopped()) {
388 throw AlreadyStopped() 401 throw AlreadyStopped()
389 } 402 }
390 403
  404 + releaseCamera()
  405 + releaseTexture()
  406 + }
  407 +
  408 + private fun releaseCamera() {
391 if (displayListener != null) { 409 if (displayListener != null) {
392 val displayManager = activity.applicationContext.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager 410 val displayManager = activity.applicationContext.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
393 411
@@ -398,15 +416,19 @@ class MobileScanner( @@ -398,15 +416,19 @@ class MobileScanner(
398 val owner = activity as LifecycleOwner 416 val owner = activity as LifecycleOwner
399 camera?.cameraInfo?.torchState?.removeObservers(owner) 417 camera?.cameraInfo?.torchState?.removeObservers(owner)
400 cameraProvider?.unbindAll() 418 cameraProvider?.unbindAll()
401 - textureEntry?.release()  
402 419
403 camera = null 420 camera = null
404 preview = null 421 preview = null
405 - textureEntry = null  
406 cameraProvider = null 422 cameraProvider = null
407 } 423 }
408 424
  425 + private fun releaseTexture() {
  426 + textureEntry?.release()
  427 + textureEntry = null
  428 + }
  429 +
409 private fun isStopped() = camera == null && preview == null 430 private fun isStopped() = camera == null && preview == null
  431 + private fun isPaused() = isStopped() && textureEntry != null
410 432
411 /** 433 /**
412 * Toggles the flash light on or off. 434 * 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()
@@ -122,6 +122,7 @@ class MobileScannerHandler( @@ -122,6 +122,7 @@ class MobileScannerHandler(
122 }) 122 })
123 "start" -> start(call, result) 123 "start" -> start(call, result)
124 "torch" -> toggleTorch(call, result) 124 "torch" -> toggleTorch(call, result)
  125 + "pause" -> pause(result)
125 "stop" -> stop(result) 126 "stop" -> stop(result)
126 "analyzeImage" -> analyzeImage(call, result) 127 "analyzeImage" -> analyzeImage(call, result)
127 "setScale" -> setScale(call, result) 128 "setScale" -> setScale(call, result)
@@ -227,6 +228,18 @@ class MobileScannerHandler( @@ -227,6 +228,18 @@ class MobileScannerHandler(
227 ) 228 )
228 } 229 }
229 230
  231 + private fun pause(result: MethodChannel.Result) {
  232 + try {
  233 + mobileScanner!!.pause()
  234 + result.success(null)
  235 + } catch (e: Exception) {
  236 + when (e) {
  237 + is AlreadyPaused, is AlreadyStopped -> result.success(null)
  238 + else -> throw e
  239 + }
  240 + }
  241 + }
  242 +
230 private fun stop(result: MethodChannel.Result) { 243 private fun stop(result: MethodChannel.Result) {
231 try { 244 try {
232 mobileScanner!!.stop() 245 mobileScanner!!.stop()
@@ -106,6 +106,7 @@ class _BarcodeScannerWithControllerState @@ -106,6 +106,7 @@ class _BarcodeScannerWithControllerState
106 children: [ 106 children: [
107 ToggleFlashlightButton(controller: controller), 107 ToggleFlashlightButton(controller: controller),
108 StartStopMobileScannerButton(controller: controller), 108 StartStopMobileScannerButton(controller: controller),
  109 + PauseMobileScannerButton(controller: controller),
109 Expanded(child: Center(child: _buildBarcode(_barcode))), 110 Expanded(child: Center(child: _buildBarcode(_barcode))),
110 SwitchCameraButton(controller: controller), 111 SwitchCameraButton(controller: controller),
111 AnalyzeImageFromGalleryButton(controller: controller), 112 AnalyzeImageFromGalleryButton(controller: controller),
@@ -166,3 +166,30 @@ class ToggleFlashlightButton extends StatelessWidget { @@ -166,3 +166,30 @@ class ToggleFlashlightButton extends StatelessWidget {
166 ); 166 );
167 } 167 }
168 } 168 }
  169 +
  170 +class PauseMobileScannerButton extends StatelessWidget {
  171 + const PauseMobileScannerButton({required this.controller, super.key});
  172 +
  173 + final MobileScannerController controller;
  174 +
  175 + @override
  176 + Widget build(BuildContext context) {
  177 + return ValueListenableBuilder(
  178 + valueListenable: controller,
  179 + builder: (context, state, child) {
  180 + if (!state.isInitialized || !state.isRunning) {
  181 + return const SizedBox.shrink();
  182 + }
  183 +
  184 + return IconButton(
  185 + color: Colors.white,
  186 + iconSize: 32.0,
  187 + icon: const Icon(Icons.pause),
  188 + onPressed: () async {
  189 + await controller.pause();
  190 + },
  191 + );
  192 + },
  193 + );
  194 + }
  195 +}
@@ -61,6 +61,14 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -61,6 +61,14 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
61 61
62 public var timeoutSeconds: Double = 0 62 public var timeoutSeconds: Double = 0
63 63
  64 + private var stopped: Bool {
  65 + return device == nil || captureSession == nil
  66 + }
  67 +
  68 + private var paused: Bool {
  69 + return stopped && textureId != nil
  70 + }
  71 +
64 init(registry: FlutterTextureRegistry?, mobileScannerCallback: @escaping MobileScannerCallback, torchModeChangeCallback: @escaping TorchModeChangeCallback, zoomScaleChangeCallback: @escaping ZoomScaleChangeCallback) { 72 init(registry: FlutterTextureRegistry?, mobileScannerCallback: @escaping MobileScannerCallback, torchModeChangeCallback: @escaping TorchModeChangeCallback, zoomScaleChangeCallback: @escaping ZoomScaleChangeCallback) {
65 self.registry = registry 73 self.registry = registry
66 self.mobileScannerCallback = mobileScannerCallback 74 self.mobileScannerCallback = mobileScannerCallback
@@ -126,6 +134,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -126,6 +134,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
126 134
127 /// Gets called when a new image is added to the buffer 135 /// Gets called when a new image is added to the buffer
128 public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { 136 public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
  137 +
129 guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { 138 guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
130 print("Failed to get image buffer from sample buffer.") 139 print("Failed to get image buffer from sample buffer.")
131 return 140 return
@@ -180,7 +189,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -180,7 +189,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
180 barcodesString = nil 189 barcodesString = nil
181 scanner = barcodeScannerOptions != nil ? BarcodeScanner.barcodeScanner(options: barcodeScannerOptions!) : BarcodeScanner.barcodeScanner() 190 scanner = barcodeScannerOptions != nil ? BarcodeScanner.barcodeScanner(options: barcodeScannerOptions!) : BarcodeScanner.barcodeScanner()
182 captureSession = AVCaptureSession() 191 captureSession = AVCaptureSession()
183 - textureId = registry?.register(self) 192 + textureId = textureId ?? registry?.register(self)
184 193
185 // Open the camera device 194 // Open the camera device
186 device = getDefaultCameraDevice(position: cameraPosition) 195 device = getDefaultCameraDevice(position: cameraPosition)
@@ -301,27 +310,49 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -301,27 +310,49 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
301 } 310 }
302 } 311 }
303 312
  313 + /// Pause scanning for barcodes
  314 + func pause() throws {
  315 + if (paused) {
  316 + throw MobileScannerError.alreadyPaused
  317 + } else if (stopped) {
  318 + throw MobileScannerError.alreadyStopped
  319 + }
  320 + releaseCamera()
  321 + }
  322 +
304 /// Stop scanning for barcodes 323 /// Stop scanning for barcodes
305 func stop() throws { 324 func stop() throws {
306 - if (device == nil || captureSession == nil) { 325 + if (!paused && stopped) {
307 throw MobileScannerError.alreadyStopped 326 throw MobileScannerError.alreadyStopped
308 } 327 }
  328 + releaseCamera()
  329 + releaseTexture()
  330 + }
  331 +
  332 + private func releaseCamera() {
  333 +
  334 + guard let captureSession = captureSession else {
  335 + return
  336 + }
309 337
310 - captureSession!.stopRunning()  
311 - for input in captureSession!.inputs {  
312 - captureSession!.removeInput(input) 338 + captureSession.stopRunning()
  339 + for input in captureSession.inputs {
  340 + captureSession.removeInput(input)
313 } 341 }
314 - for output in captureSession!.outputs {  
315 - captureSession!.removeOutput(output) 342 + for output in captureSession.outputs {
  343 + captureSession.removeOutput(output)
316 } 344 }
317 345
318 latestBuffer = nil 346 latestBuffer = nil
319 device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode)) 347 device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode))
320 device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.videoZoomFactor)) 348 device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.videoZoomFactor))
  349 + self.captureSession = nil
  350 + device = nil
  351 + }
  352 +
  353 + private func releaseTexture() {
321 registry?.unregisterTexture(textureId) 354 registry?.unregisterTexture(textureId)
322 textureId = nil 355 textureId = nil
323 - captureSession = nil  
324 - device = nil  
325 } 356 }
326 357
327 /// Set the torch mode. 358 /// Set the torch mode.
@@ -10,6 +10,7 @@ enum MobileScannerError: Error { @@ -10,6 +10,7 @@ enum MobileScannerError: Error {
10 case noCamera 10 case noCamera
11 case alreadyStarted 11 case alreadyStarted
12 case alreadyStopped 12 case alreadyStopped
  13 + case alreadyPaused
13 case cameraError(_ error: Error) 14 case cameraError(_ error: Error)
14 case zoomWhenStopped 15 case zoomWhenStopped
15 case zoomError(_ error: Error) 16 case zoomError(_ error: Error)
@@ -78,6 +78,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -78,6 +78,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
78 AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) }) 78 AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) })
79 case "start": 79 case "start":
80 start(call, result) 80 start(call, result)
  81 + case "pause":
  82 + pause(result)
81 case "stop": 83 case "stop":
82 stop(result) 84 stop(result)
83 case "torch": 85 case "torch":
@@ -147,6 +149,14 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -147,6 +149,14 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
147 } 149 }
148 } 150 }
149 151
  152 + /// Stops the mobileScanner without closing the texture.
  153 + private func pause(_ result: @escaping FlutterResult) {
  154 + do {
  155 + try mobileScanner.pause()
  156 + } catch {}
  157 + result(nil)
  158 + }
  159 +
150 /// Stops the mobileScanner and closes the texture. 160 /// Stops the mobileScanner and closes the texture.
151 private func stop(_ result: @escaping FlutterResult) { 161 private func stop(_ result: @escaping FlutterResult) {
152 do { 162 do {
@@ -38,6 +38,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @@ -38,6 +38,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
38 } 38 }
39 39
40 int? _textureId; 40 int? _textureId;
  41 + bool _pausing = false;
41 42
42 /// Parse a [BarcodeCapture] from the given [event]. 43 /// Parse a [BarcodeCapture] from the given [event].
43 BarcodeCapture? _parseBarcode(Map<Object?, Object?>? event) { 44 BarcodeCapture? _parseBarcode(Map<Object?, Object?>? event) {
@@ -206,7 +207,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @@ -206,7 +207,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
206 207
207 @override 208 @override
208 Future<MobileScannerViewAttributes> start(StartOptions startOptions) async { 209 Future<MobileScannerViewAttributes> start(StartOptions startOptions) async {
209 - if (_textureId != null) { 210 + if (!_pausing && _textureId != null) {
210 throw const MobileScannerException( 211 throw const MobileScannerException(
211 errorCode: MobileScannerErrorCode.controllerAlreadyInitialized, 212 errorCode: MobileScannerErrorCode.controllerAlreadyInitialized,
212 errorDetails: MobileScannerErrorDetails( 213 errorDetails: MobileScannerErrorDetails(
@@ -274,6 +275,8 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @@ -274,6 +275,8 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
274 size = Size(width, height); 275 size = Size(width, height);
275 } 276 }
276 277
  278 + _pausing = false;
  279 +
277 return MobileScannerViewAttributes( 280 return MobileScannerViewAttributes(
278 hasTorch: hasTorch, 281 hasTorch: hasTorch,
279 numberOfCameras: numberOfCameras, 282 numberOfCameras: numberOfCameras,
@@ -288,10 +291,23 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @@ -288,10 +291,23 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
288 } 291 }
289 292
290 _textureId = null; 293 _textureId = null;
  294 + _pausing = false;
291 295
292 await methodChannel.invokeMethod<void>('stop'); 296 await methodChannel.invokeMethod<void>('stop');
293 } 297 }
294 298
  299 +
  300 + @override
  301 + Future<void> pause() async {
  302 + if (_pausing) {
  303 + return;
  304 + }
  305 +
  306 + _pausing = true;
  307 +
  308 + await methodChannel.invokeMethod<void>('pause');
  309 + }
  310 +
295 @override 311 @override
296 Future<void> updateScanWindow(Rect? window) async { 312 Future<void> updateScanWindow(Rect? window) async {
297 if (_textureId == null) { 313 if (_textureId == null) {
@@ -166,6 +166,25 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -166,6 +166,25 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
166 } 166 }
167 } 167 }
168 168
  169 + void _stop() {
  170 + // Do nothing if not initialized or already stopped.
  171 + // On the web, the permission popup triggers a lifecycle change from resumed to inactive,
  172 + // due to the permission popup gaining focus.
  173 + // This would 'stop' the camera while it is not ready yet.
  174 + if (!value.isInitialized || !value.isRunning || _isDisposed) {
  175 + return;
  176 + }
  177 +
  178 + _disposeListeners();
  179 +
  180 + // After the camera stopped, set the torch state to off,
  181 + // as the torch state callback is never called when the camera is stopped.
  182 + value = value.copyWith(
  183 + isRunning: false,
  184 + torchState: TorchState.off,
  185 + );
  186 + }
  187 +
169 /// Analyze an image file. 188 /// Analyze an image file.
170 /// 189 ///
171 /// The [path] points to a file on the device. 190 /// The [path] points to a file on the device.
@@ -301,24 +320,19 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -301,24 +320,19 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
301 /// 320 ///
302 /// Does nothing if the camera is already stopped. 321 /// Does nothing if the camera is already stopped.
303 Future<void> stop() async { 322 Future<void> stop() async {
304 - // Do nothing if not initialized or already stopped.  
305 - // On the web, the permission popup triggers a lifecycle change from resumed to inactive,  
306 - // due to the permission popup gaining focus.  
307 - // This would 'stop' the camera while it is not ready yet.  
308 - if (!value.isInitialized || !value.isRunning || _isDisposed) {  
309 - return; 323 + _stop();
  324 + await MobileScannerPlatform.instance.stop();
310 } 325 }
311 326
312 - _disposeListeners();  
313 -  
314 - // After the camera stopped, set the torch state to off,  
315 - // as the torch state callback is never called when the camera is stopped.  
316 - value = value.copyWith(  
317 - isRunning: false,  
318 - torchState: TorchState.off,  
319 - );  
320 -  
321 - await MobileScannerPlatform.instance.stop(); 327 + /// Pause the camera.
  328 + ///
  329 + /// This method stops to update camera frame and scan barcodes.
  330 + /// After calling this method, the camera can be restarted using [start].
  331 + ///
  332 + /// Does nothing if the camera is already paused or stopped.
  333 + Future<void> pause() async {
  334 + _stop();
  335 + await MobileScannerPlatform.instance.pause();
322 } 336 }
323 337
324 /// Switch between the front and back camera. 338 /// Switch between the front and back camera.
@@ -95,6 +95,12 @@ abstract class MobileScannerPlatform extends PlatformInterface { @@ -95,6 +95,12 @@ abstract class MobileScannerPlatform extends PlatformInterface {
95 throw UnimplementedError('stop() has not been implemented.'); 95 throw UnimplementedError('stop() has not been implemented.');
96 } 96 }
97 97
  98 + /// Pause the camera.
  99 + Future<void> pause() {
  100 + throw UnimplementedError('pause() has not been implemented.');
  101 + }
  102 +
  103 +
98 /// Update the scan window to the given [window] rectangle. 104 /// Update the scan window to the given [window] rectangle.
99 /// 105 ///
100 /// Any barcodes that do not intersect with the given [window] will be ignored. 106 /// Any barcodes that do not intersect with the given [window] will be ignored.