Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add pause feature #994

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ class MobileScanner(
}

cameraProvider?.unbindAll()
textureEntry = textureRegistry.createSurfaceTexture()
textureEntry = textureEntry ?: textureRegistry.createSurfaceTexture()

// Preview
val surfaceProvider = Preview.SurfaceProvider { request ->
Expand Down Expand Up @@ -406,14 +406,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

Expand All @@ -431,9 +450,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()
Expand All @@ -445,7 +461,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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ class MobileScannerHandler(
}
})
"start" -> start(call, result)
"pause" -> pause(result)
"stop" -> stop(result)
"toggleTorch" -> toggleTorch(result)
"analyzeImage" -> analyzeImage(call, result)
Expand Down Expand Up @@ -230,6 +231,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()
Expand Down
1 change: 1 addition & 0 deletions example/lib/barcode_scanner_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
27 changes: 27 additions & 0 deletions example/lib/scanner_button_widgets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,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();
},
);
},
);
}
}
49 changes: 40 additions & 9 deletions ios/Classes/MobileScanner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
private var imagesCurrentlyBeingProcessed = false

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
Expand Down Expand Up @@ -126,6 +134,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 {
print("Failed to get image buffer from sample buffer.")
return
Expand Down Expand Up @@ -180,7 +189,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)
Expand Down Expand Up @@ -294,28 +303,50 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
completion(MobileScannerStartParameters())
}
}

/// 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
}

/// Toggle the torch.
Expand Down
1 change: 1 addition & 0 deletions ios/Classes/MobileScannerError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ enum MobileScannerError: Error {
case noCamera
case alreadyStarted
case alreadyStopped
case alreadyPaused
case cameraError(_ error: Error)
case zoomWhenStopped
case zoomError(_ error: Error)
Expand Down
10 changes: 10 additions & 0 deletions ios/Classes/MobileScannerPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,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":
Expand Down Expand Up @@ -147,6 +149,14 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
details: nil))
}
}

/// 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) {
Expand Down
17 changes: 16 additions & 1 deletion lib/src/method_channel/mobile_scanner_method_channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
}

int? _textureId;
bool _pausing = false;

/// Parse a [BarcodeCapture] from the given [event].
BarcodeCapture? _parseBarcode(Map<Object?, Object?>? event) {
Expand Down Expand Up @@ -184,7 +185,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(
Expand Down Expand Up @@ -254,6 +255,8 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
size = Size(width, height);
}

_pausing = false;

return MobileScannerViewAttributes(
currentTorchMode: currentTorchState,
numberOfCameras: numberOfCameras,
Expand All @@ -268,10 +271,22 @@ 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');
Expand Down
58 changes: 36 additions & 22 deletions lib/src/mobile_scanner_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,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.
Expand Down Expand Up @@ -317,31 +341,21 @@ 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;
}

_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,
);

_stop();
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.
///
/// Does nothing if the device has less than 2 cameras.
Expand Down
Loading
Loading