Skip to content

iOS: CaptureVisionRouter.stopCapturing() blocks for ~5 seconds when capture is active #4

@chethas-qb

Description

@chethas-qb

Environment

Package: dynamsoft_barcode_reader_bundle_flutter: ^11.0.5201
Platform: iOS
Flutter: [Flutter 3.41.6]
iOS Version: [26]
Device: [IPhone 16]

Description

We are experiencing a significant delay when closing the scanner screen on iOS.

After profiling the official Flutter sample application, we found that the delay is entirely caused by CaptureVisionRouter.stopCapturing() when barcode capture is active.

The issue is reproducible even with the sample code and does not appear to be related to Flutter UI, camera preview rendering, widget disposal, or application-specific logic.

Profiling Results

Scenario 1: Active capture session

Initialization:

_cvr.setInput(): 33 ms
_cvr.addResultReceiver(): 0 ms
_camera.open(): 1 ms
_cvr.startCapturing(): 14 ms
TOTAL initSdk(): 52 ms

Disposal:

stopCapturing(): 5010 ms
camera.close(): 6 ms
cvr.dispose(): 11 ms
camera.dispose(): 97 ms
TOTAL cleanup(): 5127 ms

Scenario 2: No active capture session

Initialization:

_cvr.setInput(): 27 ms
_cvr.addResultReceiver(): 0 ms
_camera.open(): 1 ms
_cvr.startCapturing(): 0 ms
TOTAL initSdk(): 30 ms

Disposal:

stopCapturing(): 2 ms
camera.close(): 0 ms
cvr.dispose(): 5 ms
camera.dispose(): 0 ms
TOTAL cleanup(): 9 ms

Observations

Camera initialization is extremely fast.
Camera closing is extremely fast.
Router disposal is extremely fast.
Camera disposal is extremely fast.
The delay occurs only when an active capture session exists.
Nearly the entire 5-second delay is spent inside CaptureVisionRouter.stopCapturing().

Expected Behavior

Calling stopCapturing() should return quickly and allow the scanner screen to close immediately.

Actual Behavior

When barcode capture is active, stopCapturing() takes approximately 5 seconds to return. Since our application frequently closes and reopens the scanner screen, this delay significantly impacts the user experience and can contribute to stability issues due to repeated scanner shutdown and reinitialization cycles.

Questions

  • Is a ~5 second blocking time in stopCapturing() expected on iOS?
  • Does stopCapturing() wait for internal worker threads or processing pipelines to terminate before returning?
  • Is there a recommended way to immediately stop scanning without waiting for the full shutdown process?
  • Are there any configuration options or newer SDK versions that improve this behavior?

Any guidance would be appreciated.

Sample Code

 class DynamsoftScanner extends StatefulWidget {
  const DynamsoftScanner({
    super.key,
  });

  @override
  State<DynamsoftScanner> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<DynamsoftScanner> {
  final CaptureVisionRouter _cvr = CaptureVisionRouter.instance;
  final CameraEnhancer _camera = CameraEnhancer.instance;
  final String _templateName = EnumPresetTemplate.readBarcodes;
  late final CapturedResultReceiver _receiver = CapturedResultReceiver()
    ..onDecodedBarcodesReceived = (DecodedBarcodesResult result) async {
      if (result.items?.isNotEmpty ?? false) {
        _cvr.stopCapturing();
        var displayString = result.items
            ?.map((item) => "Format: ${item.formatString}\nText: ${item.text}")
            .join('\n\n');
        showTextDialog("Barcodes Count: ${result.items?.length ?? 0}", displayString ?? "", () {
          _cvr.startCapturing(_templateName);
        });
      }
    };

  @override
  void initState() {
    super.initState();
    PermissionUtil.requestCameraPermission();
    // Initialize the license.
    // The license string here is a trial license. Note that network connection is required for this license to work.
    // You can request an extension via the following link: https://www.dynamsoft.com/customer/license/trialLicense?product=dbr&utm_source=samples&package=flutter
    LicenseManager.initLicense('XXXXXX').then((data) {
      final (isSuccess, message) = data;
      if (!isSuccess) {
        print("license error: $message");
      }
    });
    initSdk();
  }

  void initSdk() async {
    final totalSw = Stopwatch()..start();

    try {
      final sw1 = Stopwatch()..start();
      await _cvr.setInput(_camera);
      debugPrint(
        '[TIMING] _cvr.setInput(): ${sw1.elapsedMilliseconds} ms',
      );

      final sw2 = Stopwatch()..start();
      _cvr.addResultReceiver(_receiver);
      debugPrint(
        '[TIMING] _cvr.addResultReceiver(): ${sw2.elapsedMilliseconds} ms',
      );

      final sw3 = Stopwatch()..start();
      await _camera.open();
      debugPrint(
        '[TIMING] _camera.open(): ${sw3.elapsedMilliseconds} ms',
      );

      final sw4 = Stopwatch()..start();
      // await _cvr.startCapturing(_templateName);
      debugPrint(
        '[TIMING] _cvr.startCapturing(): ${sw4.elapsedMilliseconds} ms',
      );

      debugPrint(
        '[TIMING] TOTAL initSdk(): ${totalSw.elapsedMilliseconds} ms',
      );
    } catch (e) {
      debugPrint('[TIMING] initSdk ERROR: $e');
      showTextDialog("StartCapturing Error", e.toString(), null);
    }
  }

  void showTextDialog(String title, String message, VoidCallback? onDismiss) {
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(title: Text(title), content: Text(message));
      },
    ).then((_) {
      //Callback when dialog dismissed
      onDismiss?.call();
    });
  }

  @override
  void deactivate() {
    debugPrint('[TIMING] Widget deactivate');
    super.deactivate();
  }

  Future<void> _disposeScanner() async {
    final totalSw = Stopwatch()..start();

    final sw1 = Stopwatch()..start();
    await _cvr.stopCapturing();
    debugPrint(
      '[TIMING] stopCapturing: ${sw1.elapsedMilliseconds} ms',
    );

    final sw2 = Stopwatch()..start();
    await _camera.close();
    debugPrint(
      '[TIMING] camera.close: ${sw2.elapsedMilliseconds} ms',
    );

    final sw3 = Stopwatch()..start();
    await _cvr.dispose();
    debugPrint(
      '[TIMING] cvr.dispose: ${sw3.elapsedMilliseconds} ms',
    );

    final sw4 = Stopwatch()..start();
    await _camera.dispose();
    debugPrint(
      '[TIMING] camera.dispose: ${sw4.elapsedMilliseconds} ms',
    );

    debugPrint(
      '[TIMING] TOTAL cleanup: ${totalSw.elapsedMilliseconds} ms',
    );
  }

  @override
  void dispose() {
    debugPrint('[TIMING] Widget dispose START');
    _disposeScanner();
    debugPrint('[TIMING] Widget dispose END');
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
        height: 399, width: 200, child: Center(child: CameraView(cameraEnhancer: _camera)));
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions