Julian Steenbakker
Committed by GitHub

Merge pull request #994 from fumin65/pause_function

feat: add pause feature
... ... @@ -273,7 +273,7 @@ class MobileScanner(
}
cameraProvider?.unbindAll()
textureEntry = textureRegistry.createSurfaceTexture()
textureEntry = textureEntry ?: textureRegistry.createSurfaceTexture()
// Preview
val surfaceProvider = Preview.SurfaceProvider { request ->
... ... @@ -405,14 +405,33 @@ class MobileScanner(
}, executor)
}
/**
* Pause barcode scanning.
*/
fun pause() {
if (isPaused()) {
throw AlreadyPaused()
} else if (isStopped()) {
throw AlreadyStopped()
}
releaseCamera()
}
/**
* Stop barcode scanning.
*/
fun stop() {
if (isStopped()) {
if (!isPaused() && isStopped()) {
throw AlreadyStopped()
}
releaseCamera()
releaseTexture()
}
private fun releaseCamera() {
if (displayListener != null) {
val displayManager = activity.applicationContext.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
... ... @@ -430,9 +449,6 @@ class MobileScanner(
// Unbind the camera use cases, the preview is a use case.
// The camera will be closed when the last use case is unbound.
cameraProvider?.unbindAll()
cameraProvider = null
camera = null
preview = null
// Release the texture for the preview.
textureEntry?.release()
... ... @@ -444,7 +460,13 @@ class MobileScanner(
lastScanned = null
}
private fun releaseTexture() {
textureEntry?.release()
textureEntry = null
}
private fun isStopped() = camera == null && preview == null
private fun isPaused() = isStopped() && textureEntry != null
/**
* Toggles the flash light on or off.
... ...
... ... @@ -3,6 +3,7 @@ package dev.steenbakker.mobile_scanner
class NoCamera : Exception()
class AlreadyStarted : Exception()
class AlreadyStopped : Exception()
class AlreadyPaused : Exception()
class CameraError : Exception()
class ZoomWhenStopped : Exception()
class ZoomNotInRange : Exception()
\ No newline at end of file
... ...
... ... @@ -118,6 +118,7 @@ class MobileScannerHandler(
}
})
"start" -> start(call, result)
"pause" -> pause(result)
"stop" -> stop(result)
"toggleTorch" -> toggleTorch(result)
"analyzeImage" -> analyzeImage(call, result)
... ... @@ -213,6 +214,18 @@ class MobileScannerHandler(
)
}
private fun pause(result: MethodChannel.Result) {
try {
mobileScanner!!.pause()
result.success(null)
} catch (e: Exception) {
when (e) {
is AlreadyPaused, is AlreadyStopped -> result.success(null)
else -> throw e
}
}
}
private fun stop(result: MethodChannel.Result) {
try {
mobileScanner!!.stop()
... ...
... ... @@ -105,6 +105,7 @@ class _BarcodeScannerWithControllerState
children: [
ToggleFlashlightButton(controller: controller),
StartStopMobileScannerButton(controller: controller),
PauseMobileScannerButton(controller: controller),
Expanded(child: Center(child: _buildBarcode(_barcode))),
SwitchCameraButton(controller: controller),
AnalyzeImageFromGalleryButton(controller: controller),
... ...
... ... @@ -180,3 +180,30 @@ class ToggleFlashlightButton extends StatelessWidget {
);
}
}
class PauseMobileScannerButton extends StatelessWidget {
const PauseMobileScannerButton({required this.controller, super.key});
final MobileScannerController controller;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: controller,
builder: (context, state, child) {
if (!state.isInitialized || !state.isRunning) {
return const SizedBox.shrink();
}
return IconButton(
color: Colors.white,
iconSize: 32.0,
icon: const Icon(Icons.pause),
onPressed: () async {
await controller.pause();
},
);
},
);
}
}
... ...
... ... @@ -58,6 +58,14 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
public var timeoutSeconds: Double = 0
private var stopped: Bool {
return device == nil || captureSession == nil
}
private var paused: Bool {
return stopped && textureId != nil
}
init(registry: FlutterTextureRegistry?, mobileScannerCallback: @escaping MobileScannerCallback, torchModeChangeCallback: @escaping TorchModeChangeCallback, zoomScaleChangeCallback: @escaping ZoomScaleChangeCallback) {
self.registry = registry
self.mobileScannerCallback = mobileScannerCallback
... ... @@ -123,6 +131,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
/// Gets called when a new image is added to the buffer
public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
return
}
... ... @@ -178,7 +187,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
barcodesString = nil
scanner = barcodeScannerOptions != nil ? BarcodeScanner.barcodeScanner(options: barcodeScannerOptions!) : BarcodeScanner.barcodeScanner()
captureSession = AVCaptureSession()
textureId = registry?.register(self)
textureId = textureId ?? registry?.register(self)
// Open the camera device
device = getDefaultCameraDevice(position: cameraPosition)
... ... @@ -293,27 +302,49 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
}
}
/// Pause scanning for barcodes
func pause() throws {
if (paused) {
throw MobileScannerError.alreadyPaused
} else if (stopped) {
throw MobileScannerError.alreadyStopped
}
releaseCamera()
}
/// Stop scanning for barcodes
func stop() throws {
if (device == nil || captureSession == nil) {
if (!paused && stopped) {
throw MobileScannerError.alreadyStopped
}
releaseCamera()
releaseTexture()
}
private func releaseCamera() {
guard let captureSession = captureSession else {
return
}
captureSession!.stopRunning()
for input in captureSession!.inputs {
captureSession!.removeInput(input)
captureSession.stopRunning()
for input in captureSession.inputs {
captureSession.removeInput(input)
}
for output in captureSession!.outputs {
captureSession!.removeOutput(output)
for output in captureSession.outputs {
captureSession.removeOutput(output)
}
latestBuffer = nil
device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode))
device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.videoZoomFactor))
self.captureSession = nil
device = nil
}
private func releaseTexture() {
registry?.unregisterTexture(textureId)
textureId = nil
captureSession = nil
device = nil
scanner = nil
}
... ...
... ... @@ -16,6 +16,7 @@ enum MobileScannerError: Error {
case noCamera
case alreadyStarted
case alreadyStopped
case alreadyPaused
case cameraError(_ error: Error)
case zoomWhenStopped
case zoomError(_ error: Error)
... ...
... ... @@ -105,6 +105,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) })
case "start":
start(call, result)
case "pause":
pause(result)
case "stop":
stop(result)
case "toggleTorch":
... ... @@ -167,6 +169,14 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
}
}
/// Stops the mobileScanner without closing the texture.
private func pause(_ result: @escaping FlutterResult) {
do {
try mobileScanner.pause()
} catch {}
result(nil)
}
/// Stops the mobileScanner and closes the texture.
private func stop(_ result: @escaping FlutterResult) {
do {
... ...
... ... @@ -46,6 +46,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
}
int? _textureId;
bool _pausing = false;
/// Parse a [BarcodeCapture] from the given [event].
BarcodeCapture? _parseBarcode(Map<Object?, Object?>? event) {
... ... @@ -216,7 +217,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
@override
Future<MobileScannerViewAttributes> start(StartOptions startOptions) async {
if (_textureId != null) {
if (!_pausing && _textureId != null) {
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.controllerAlreadyInitialized,
errorDetails: MobileScannerErrorDetails(
... ... @@ -281,6 +282,8 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
size = Size.zero;
}
_pausing = false;
return MobileScannerViewAttributes(
currentTorchMode: currentTorchState,
numberOfCameras: numberOfCameras,
... ... @@ -295,11 +298,23 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
}
_textureId = null;
_pausing = false;
await methodChannel.invokeMethod<void>('stop');
}
@override
Future<void> pause() async {
if (_pausing) {
return;
}
_pausing = true;
await methodChannel.invokeMethod<void>('pause');
}
@override
Future<void> toggleTorch() async {
await methodChannel.invokeMethod<void>('toggleTorch');
}
... ...
... ... @@ -183,6 +183,30 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
}
}
void _stop() {
// Do nothing if not initialized or already stopped.
// On the web, the permission popup triggers a lifecycle change from resumed to inactive,
// due to the permission popup gaining focus.
// This would 'stop' the camera while it is not ready yet.
if (!value.isInitialized || !value.isRunning || _isDisposed) {
return;
}
_disposeListeners();
final TorchState oldTorchState = value.torchState;
// After the camera stopped, set the torch state to off,
// as the torch state callback is never called when the camera is stopped.
// If the device does not have a torch, do not report "off".
value = value.copyWith(
isRunning: false,
torchState: oldTorchState == TorchState.unavailable
? TorchState.unavailable
: TorchState.off,
);
}
/// Analyze an image file.
///
/// The [path] points to a file on the device.
... ... @@ -336,29 +360,19 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
///
/// Does nothing if the camera is already stopped.
Future<void> stop() async {
// Do nothing if not initialized or already stopped.
// On the web, the permission popup triggers a lifecycle change from resumed to inactive,
// due to the permission popup gaining focus.
// This would 'stop' the camera while it is not ready yet.
if (!value.isInitialized || !value.isRunning || _isDisposed) {
return;
_stop();
await MobileScannerPlatform.instance.stop();
}
_disposeListeners();
final TorchState oldTorchState = value.torchState;
// After the camera stopped, set the torch state to off,
// as the torch state callback is never called when the camera is stopped.
// If the device does not have a torch, do not report "off".
value = value.copyWith(
isRunning: false,
torchState: oldTorchState == TorchState.unavailable
? TorchState.unavailable
: TorchState.off,
);
await MobileScannerPlatform.instance.stop();
/// Pause the camera.
///
/// This method stops to update camera frame and scan barcodes.
/// After calling this method, the camera can be restarted using [start].
///
/// Does nothing if the camera is already paused or stopped.
Future<void> pause() async {
_stop();
await MobileScannerPlatform.instance.pause();
}
/// Switch between the front and back camera.
... ...
... ... @@ -97,6 +97,11 @@ abstract class MobileScannerPlatform extends PlatformInterface {
throw UnimplementedError('stop() has not been implemented.');
}
/// Pause the camera.
Future<void> pause() {
throw UnimplementedError('pause() has not been implemented.');
}
/// Toggle the torch on the active camera on or off.
Future<void> toggleTorch() {
throw UnimplementedError('toggleTorch() has not been implemented.');
... ...
... ... @@ -128,6 +128,11 @@ abstract class BarcodeReader {
throw UnimplementedError('start() has not been implemented.');
}
/// Pause the barcode reader.
Future<void> pause() {
throw UnimplementedError('pause() has not been implemented.');
}
/// Stop the barcode reader and dispose of the video stream.
Future<void> stop() {
throw UnimplementedError('stop() has not been implemented.');
... ...
... ... @@ -271,6 +271,11 @@ class MobileScannerWeb extends MobileScannerPlatform {
);
}
// If the previous state is a pause, reset scanner.
if (_barcodesSubscription != null && _barcodesSubscription!.isPaused) {
await stop();
}
_barcodeReader = ZXingBarcodeReader();
await _barcodeReader?.maybeLoadLibrary(
... ... @@ -358,6 +363,12 @@ class MobileScannerWeb extends MobileScannerPlatform {
}
@override
Future<void> pause() async {
_barcodesSubscription?.pause();
await _barcodeReader?.pause();
}
@override
Future<void> stop() async {
// Ensure the barcode scanner is stopped, by cancelling the subscription.
await _barcodesSubscription?.cancel();
... ...
... ... @@ -169,6 +169,11 @@ final class ZXingBarcodeReader extends BarcodeReader {
}
@override
Future<void> pause() async {
_reader?.videoElement?.pause();
}
@override
Future<void> stop() async {
_onMediaTrackSettingsChanged = null;
_reader?.stopContinuousDecode.callAsFunction(_reader);
... ...
... ... @@ -38,6 +38,14 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
var position = AVCaptureDevice.Position.back
private var stopped: Bool {
return device == nil || captureSession == nil
}
private var paused: Bool {
return stopped && textureId != nil
}
public static func register(with registrar: FlutterPluginRegistrar) {
let instance = MobileScannerPlugin(registrar.textures)
let method = FlutterMethodChannel(name:
... ... @@ -67,6 +75,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
setScale(call, result)
case "resetScale":
resetScale(call, result)
case "pause":
pause(result)
case "stop":
stop(result)
case "updateScanWindow":
... ... @@ -276,7 +286,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
return
}
textureId = registry.register(self)
textureId = textureId ?? registry.register(self)
captureSession = AVCaptureSession()
let argReader = MapArgumentReader(call.arguments as? [String: Any])
... ... @@ -438,28 +448,49 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
result(nil)
}
func pause(_ result: FlutterResult) {
if (paused || stopped) {
result(nil)
return
}
releaseCamera()
}
func stop(_ result: FlutterResult) {
if (device == nil || captureSession == nil) {
if (!paused && stopped) {
result(nil)
return
}
captureSession!.stopRunning()
for input in captureSession!.inputs {
captureSession!.removeInput(input)
releaseCamera()
releaseTexture()
result(nil)
}
for output in captureSession!.outputs {
captureSession!.removeOutput(output)
private func releaseCamera() {
guard let captureSession = captureSession else {
return
}
captureSession.stopRunning()
for input in captureSession.inputs {
captureSession.removeInput(input)
}
for output in captureSession.outputs {
captureSession.removeOutput(output)
}
device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode))
registry.unregisterTexture(textureId)
latestBuffer = nil
captureSession = nil
self.captureSession = nil
device = nil
textureId = nil
}
result(nil)
private func releaseTexture() {
registry.unregisterTexture(textureId)
textureId = nil
}
func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
... ...