fumin65

add pause function

... ... @@ -19,7 +19,6 @@ import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview
import androidx.camera.core.resolutionselector.AspectRatioStrategy
import androidx.camera.core.resolutionselector.ResolutionSelector
import androidx.camera.core.resolutionselector.ResolutionStrategy
import androidx.camera.lifecycle.ProcessCameraProvider
... ... @@ -259,7 +258,7 @@ class MobileScanner(
}
cameraProvider?.unbindAll()
textureEntry = textureRegistry.createSurfaceTexture()
textureEntry = textureEntry ?: textureRegistry.createSurfaceTexture()
// Preview
val surfaceProvider = Preview.SurfaceProvider { request ->
... ... @@ -380,14 +379,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
... ... @@ -398,15 +416,19 @@ class MobileScanner(
val owner = activity as LifecycleOwner
camera?.cameraInfo?.torchState?.removeObservers(owner)
cameraProvider?.unbindAll()
textureEntry?.release()
camera = null
preview = null
textureEntry = null
cameraProvider = 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
... ...
... ... @@ -122,6 +122,7 @@ class MobileScannerHandler(
})
"start" -> start(call, result)
"torch" -> toggleTorch(call, result)
"pause" -> pause(result)
"stop" -> stop(result)
"analyzeImage" -> analyzeImage(call, result)
"setScale" -> setScale(call, result)
... ... @@ -227,6 +228,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()
... ...
... ... @@ -106,6 +106,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),
... ...
... ... @@ -166,3 +166,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();
},
);
},
);
}
}
... ...
... ... @@ -61,6 +61,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
... ... @@ -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
... ... @@ -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)
... ... @@ -301,27 +310,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
}
/// Set the torch mode.
... ...
... ... @@ -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)
... ...
... ... @@ -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 "torch":
... ... @@ -147,6 +149,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 {
... ...
... ... @@ -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) {
... ... @@ -206,7 +207,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(
... ... @@ -274,6 +275,8 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
size = Size(width, height);
}
_pausing = false;
return MobileScannerViewAttributes(
hasTorch: hasTorch,
numberOfCameras: numberOfCameras,
... ... @@ -288,10 +291,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> updateScanWindow(Rect? window) async {
if (_textureId == null) {
... ...
... ... @@ -166,6 +166,25 @@ 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();
// After the camera stopped, set the torch state to off,
// as the torch state callback is never called when the camera is stopped.
value = value.copyWith(
isRunning: false,
torchState: TorchState.off,
);
}
/// Analyze an image file.
///
/// The [path] points to a file on the device.
... ... @@ -301,24 +320,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();
// After the camera stopped, set the torch state to off,
// as the torch state callback is never called when the camera is stopped.
value = value.copyWith(
isRunning: false,
torchState: 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.
... ...
... ... @@ -95,6 +95,12 @@ 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.');
}
/// Update the scan window to the given [window] rectangle.
///
/// Any barcodes that do not intersect with the given [window] will be ignored.
... ...