juliansteenbakker / mobile_scanner

A universal scanner for Flutter based on MLKit. Uses CameraX on Android and AVFoundation on iOS.
BSD 3-Clause "New" or "Revised" License
897 stars 516 forks source link

Scanner stops working after reopening screen #493

Open SaulSDS opened 1 year ago

SaulSDS commented 1 year ago

The first time the scan screen is opened it works fine, but after i close it and open it again it stops detecting codes.

I'm trying disposing the controller and starting it on initstate

@override
void initState() {
  controller.start();
  super.initState();
}

@override
void dispose() {
  controller.dispose();
  super.dispose();
}

These are the logs when opening the scan screen for the second time.

D/TransportRuntime.SQLiteEventStore(11912): Storing event with priority=DEFAULT, name=FIREBASE_ML_SDK for destination cct D/TransportRuntime.JobInfoScheduler(11912): Upload for context TransportContext(cct, DEFAULT, MSRodHRwczovL2ZpcmViYXNlbG9nZ2luZy5nb29nbGVhcGlzLmNvbS92MGNjL2xvZy9iYXRjaD9mb3JtYXQ9anNvbl9wcm90bzNc) is already scheduled. Returning... D/DeferrableSurface(11912): Surface created[total_surfaces=1, used_surfaces=0](androidx.camera.core.SurfaceRequest$2@3c19fab} D/CameraOrientationUtil(11912): getRelativeImageRotation: destRotationDegrees=0, sourceRotationDegrees=90, isOppositeFacing=true, result=90 I/chatty (11912): uid=10479(com.vavastone.driver) identical 1 line D/CameraOrientationUtil(11912): getRelativeImageRotation: destRotationDegrees=0, sourceRotationDegrees=90, isOppositeFacing=true, result=90 D/DeferrableSurface(11912): Surface created[total_surfaces=2, used_surfaces=0](androidx.camera.core.impl.ImmediateSurface@6aa9fc6} D/Camera2CameraImpl(11912): {Camera@b772065[id=0]} Use case androidx.camera.core.Preview-af7e5d6f-aa02-4810-bb5a-025f31827062154886382 ACTIVE D/UseCaseAttachState(11912): Active and attached use case: [] for camera: 0 D/CameraOrientationUtil(11912): getRelativeImageRotation: destRotationDegrees=0, sourceRotationDegrees=90, isOppositeFacing=true, result=90 D/Camera2CameraImpl(11912): {Camera@b772065[id=0]} Use case androidx.camera.core.ImageAnalysis-2840e1fa-af4c-416c-bb47-c00f37a77cdb199555471 ACTIVE D/CameraOrientationUtil(11912): getRelativeImageRotation: destRotationDegrees=0, sourceRotationDegrees=90, isOppositeFacing=true, result=90 D/UseCaseAttachState(11912): Active and attached use case: [] for camera: 0 D/Camera2CameraImpl(11912): {Camera@b772065[id=0]} Use cases [androidx.camera.core.Preview-af7e5d6f-aa02-4810-bb5a-025f31827062154886382, androidx.camera.core.ImageAnalysis-2840e1fa-af4c-416c-bb47-c00f37a77cdb199555471] now ATTACHED D/UseCaseAttachState(11912): All use case: [androidx.camera.core.Preview-af7e5d6f-aa02-4810-bb5a-025f31827062154886382, androidx.camera.core.ImageAnalysis-2840e1fa-af4c-416c-bb47-c00f37a77cdb199555471] for camera: 0 D/UseCaseAttachState(11912): Active and attached use case: [androidx.camera.core.Preview-af7e5d6f-aa02-4810-bb5a-025f31827062154886382, androidx.camera.core.ImageAnalysis-2840e1fa-af4c-416c-bb47-c00f37a77cdb199555471] for camera: 0 D/Camera2CameraImpl(11912): {Camera@b772065[id=0]} Resetting Capture Session D/Camera2CameraImpl(11912): {Camera@b772065[id=0]} Releasing session in state INITIALIZED D/Camera2CameraImpl(11912): {Camera@b772065[id=0]} Attempting to force open the camera. D/CameraStateRegistry(11912): tryOpenCamera(Camera@b772065[id=0]) [Available Cameras: 1, Already Open: false (Previous state: CLOSED)] --> SUCCESS D/CameraStateRegistry(11912): Recalculating open cameras: D/CameraStateRegistry(11912): Camera State
D/CameraStateRegistry(11912): ------------------------------------------------------------------- D/CameraStateRegistry(11912): Camera@b772065[id=0] OPENING
D/CameraStateRegistry(11912): Camera@acbf1c7[id=1] UNKNOWN
D/CameraStateRegistry(11912): ------------------------------------------------------------------- D/CameraStateRegistry(11912): Open count: 1 (Max allowed: 1) D/Camera2CameraImpl(11912): {Camera@b772065[id=0]} Opening camera. D/Camera2CameraImpl(11912): {Camera@b772065[id=0]} Transitioning camera internal state: INITIALIZED --> OPENING D/CameraStateMachine(11912): New public camera state CameraState{type=OPENING, error=null} from OPENING and null D/CameraStateMachine(11912): Publishing new public camera state CameraState{type=OPENING, error=null} D/UseCaseAttachState(11912): All use case: [androidx.camera.core.Preview-af7e5d6f-aa02-4810-bb5a-025f31827062154886382, androidx.camera.core.ImageAnalysis-2840e1fa-af4c-416c-bb47-c00f37a77cdb199555471] for camera: 0 E/libc (11912): Access denied finding property "persist.vendor.camera.privapp.list" W/CameraX-core_ca(11912): type=1400 audit(0.0:10551729): avc: denied { read } for name="u:object_r:vendor_persist_camera_prop:s0" dev="tmpfs" ino=23634 scontext=u:r:untrusted_app:s0:c223,c257,c512,c768 tcontext=u:object_r:vendor_persist_camera_prop:s0 tclass=file permissive=0 W/Binder:11912_5(11912): type=1400 audit(0.0:10551730): avc: denied { read } for name="u:object_r:vendor_persist_camera_prop:s0" dev="tmpfs" ino=23634 scontext=u:r:untrusted_app:s0:c223,c257,c512,c768 tcontext=u:object_r:vendor_persist_camera_prop:s0 tclass=file permissive=0 W/Binder:11912_5(11912): type=1400 audit(0.0:10551732): avc: denied { read } for name="u:object_r:vendor_persist_camera_prop:s0" dev="tmpfs" ino=23634 scontext=u:r:untrusted_app:s0:c223,c257,c512,c768 tcontext=u:object_r:vendor_persist_camera_prop:s0 tclass=file permissive=0 E/libc (11912): Access denied finding property "vendor.camera.aux.packagelist" E/CameraManagerGlobal(11912): Camera 61 is not available. Ignore physical camera status change E/libc (11912): Access denied finding property "vendor.camera.aux.packagelist" D/Camera2CameraImpl(11912): {Camera@b772065[id=0]} Use case androidx.camera.core.Preview-af7e5d6f-aa02-4810-bb5a-025f31827062154886382 ACTIVE D/UseCaseAttachState(11912): Active and attached use case: [androidx.camera.core.Preview-af7e5d6f-aa02-4810-bb5a-025f31827062154886382, androidx.camera.core.ImageAnalysis-2840e1fa-af4c-416c-bb47-c00f37a77cdb199555471] for camera: 0 D/Camera2CameraImpl(11912): {Camera@b772065[id=0]} Use case androidx.camera.core.ImageAnalysis-2840e1fa-af4c-416c-bb47-c00f37a77cdb199555471 ACTIVE D/UseCaseAttachState(11912): Active and attached use case: [androidx.camera.core.Preview-af7e5d6f-aa02-4810-bb5a-025f31827062154886382, androidx.camera.core.ImageAnalysis-2840e1fa-af4c-416c-bb47-c00f37a77cdb199555471] for camera: 0 D/Camera2CameraImpl(11912): {Camera@b772065[id=0]} Issue capture request D/UseCaseAttachState(11912): Active and attached use case: [androidx.camera.core.Preview-af7e5d6f-aa02-4810-bb5a-025f31827062154886382, androidx.camera.core.ImageAnalysis-2840e1fa-af4c-416c-bb47-c00f37a77cdb199555471] for camera: 0 D/Camera2CameraImpl(11912): {Camera@b772065[id=0]} CameraDevice.onOpened() D/Camera2CameraImpl(11912): {Camera@b772065[id=0]} Transitioning camera internal state: OPENING --> OPENED D/CameraStateRegistry(11912): Recalculating open cameras: D/CameraStateRegistry(11912): Camera State
D/CameraStateRegistry(11912): ------------------------------------------------------------------- D/CameraStateRegistry(11912): Camera@b772065[id=0] OPEN
D/CameraStateRegistry(11912): Camera@acbf1c7[id=1] UNKNOWN
D/CameraStateRegistry(11912): ------------------------------------------------------------------- D/CameraStateRegistry(11912): Open count: 1 (Max allowed: 1) D/CameraStateMachine(11912): New public camera state CameraState{type=OPEN, error=null} from OPEN and null D/CameraStateMachine(11912): Publishing new public camera state CameraState{type=OPEN, error=null} D/UseCaseAttachState(11912): All use case: [androidx.camera.core.Preview-af7e5d6f-aa02-4810-bb5a-025f31827062154886382, androidx.camera.core.ImageAnalysis-2840e1fa-af4c-416c-bb47-c00f37a77cdb199555471] for camera: 0 D/UseCaseAttachState(11912): Active and attached use case: [androidx.camera.core.Preview-af7e5d6f-aa02-4810-bb5a-025f31827062154886382, androidx.camera.core.ImageAnalysis-2840e1fa-af4c-416c-bb47-c00f37a77cdb199555471] for camera: 0 D/SyncCaptureSessionBase(11912): [androidx.camera.camera2.internal.SynchronizedCaptureSessionBaseImpl@36dee4e] getSurface...done D/CaptureSession(11912): Opening capture session. D/DeferrableSurface(11912): New surface in use[total_surfaces=2, used_surfaces=1](androidx.camera.core.SurfaceRequest$2@3c19fab} D/DeferrableSurface(11912): use count+1, useCount=1 androidx.camera.core.SurfaceRequest$2@3c19fab D/DeferrableSurface(11912): New surface in use[total_surfaces=2, used_surfaces=2](androidx.camera.core.impl.ImmediateSurface@6aa9fc6} D/DeferrableSurface(11912): use count+1, useCount=1 androidx.camera.core.impl.ImmediateSurface@6aa9fc6 D/CaptureSession(11912): Attempting to send capture request onConfigured D/CaptureSession(11912): Issuing request for session. D/Camera2CaptureRequestBuilder(11912): createCaptureRequest D/CaptureSession(11912): Issuing capture request. D/Camera2CaptureRequestBuilder(11912): createCaptureRequest D/CaptureSession(11912): CameraCaptureSession.onConfigured() mState=OPENED D/CaptureSession(11912): CameraCaptureSession.onReady() OPENED D/TransportRuntime.CctTransportBackend(11912): Making request to: https://firebaselogging.googleapis.com/v0cc/log/batch?format=json_proto3 I/DpmTcmClient(11912): RegisterTcmMonitor from: $Proxy1 W/System (11912): A resource failed to call release. I/BpBinder(11912): onLastStrongRef automatically unlinking death recipients: I/TransportRuntime.CctTransportBackend(11912): Status Code: 200 I/TransportRuntime.CctTransportBackend(11912): Content-Type: application/json; charset=UTF-8 I/TransportRuntime.CctTransportBackend(11912): Content-Encoding: gzip

allanwolski-openco commented 1 year ago

The same issue here

robsoncardozorebel commented 1 year ago

I'm facing the same issue, with the allowDuplicates param kinda worked but i dont think that is the best way to solve this problem

jartos commented 1 year ago

You can use this as a workaround, e.g. three different controllers, if it is possible in the implementation.

 final MobileScannerController _cameraController1 = MobileScannerController();
  final MobileScannerController _cameraController2 = MobileScannerController();
  final MobileScannerController _cameraController3 = MobileScannerController();

  int cameraOpenCount = 1;

cameraOpenCount == 1 ? MobileScanner(
                                                controller: _cameraController1,
                                                onDetect: (capture) {})
                                            : cameraOpenCount == 2
                                                ? MobileScanner(
                                                    controller:
                                                        _cameraController2,
                                                    onDetect: (capture) {})
                                                : MobileScanner(
                                                    controller:
                                                        _cameraController3,
                                                    onDetect: (capture) {}),
                                      ),
                                    ),
                                  ),
...
                                  GestureDetector(
                                    onTap: () {
                                      if (cameraOpenCount == 1) {
                                        _cameraController1.stop().then((_) =>
                                            _cameraController1.dispose());
                                        setState(() {
                                          cameraOpenCount = 2;
                                        });
                                      } else {
                                        _cameraController2.stop().then((_) =>
                                            _cameraController2.dispose());
                                        setState(() {
                                          cameraOpenCount = 3;
                                        });
                                      }

This code allows 3 camera starts. Just add more if needed. Tested on reading QR-code and it works.

PabloPadilla5 commented 1 year ago

Same problem here when I stop and re-start the camera with the controller

ferreque commented 9 months ago

Same problem here

kuma0605 commented 7 months ago

same issue. Does any official maintainers give some solution?

kuma0605 commented 7 months ago

when reopen and scan fails, always get Error:

W/System (13336): A resource failed to call release. I/BpBinder(13336): onLastStrongRef automatically unlinking death recipients:

It seems last MobileScannerController instance not destroyed successfully. May be that's the point why MobileScanner not work when reopen.

Here is my code

import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:medical_instrument_manage/screen_adaptive/int_extension.dart';
import 'package:mobile_scanner/mobile_scanner.dart';

import '../../constants.dart';

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

  @override
  State<ScanSign> createState() => _ScanSignState();
}

class _ScanSignState extends State<ScanSign> with SingleTickerProviderStateMixin {
  MobileScannerController cameraController = MobileScannerController(
    detectionSpeed: DetectionSpeed.noDuplicates,
  );

  // ### 1. **AnimationController Statement**
  late AnimationController _controller;
  late Animation<double> _animation;

  String showTitle = "扫码签名";

  @override
  void initState() {
    // TODO: implement initState
    super.initState();

    // ### 2. **AnimationController Initialization**
    // The  AnimationController  is initialized in the  _ScannerAnimationState  class. It controls the animation, including its duration and repeating behavior. The  vsync  parameter prevents off-screen animations from consuming unnecessary resources.
    _controller = AnimationController(
      duration: const Duration(seconds: 3),
      vsync: this,
    );

    // ### 3. **Tween and Animation**
    // A  Tween  defines the range between the starting and ending points of the animation. In this case, the animation moves the line from  0.0  (top) to  1.0  (bottom) relative to the container's height. The  Tween  is animated by the  AnimationController .
    _animation = Tween<double>(begin: 0.0, end: 1.0).animate(_controller)
      ..addListener(() {
        // ### 8. **Rebuilding the Widget with setState**
        // The  addListener  callback attached to the animation triggers a call to  setState  every time the animation value changes, prompting the widget to rebuild with the new line position.
        setState(() {});
      })
      ..addStatusListener((status) {
        // ### 4. **Listening to Animation Status**
        // The  addStatusListener  listens to the animation status. When the animation completes, it resets the  AnimationController  and starts the animation again, creating a loop. This is crucial for achieving the continuous scanning effect.
        if (status == AnimationStatus.completed) {
          _controller.reset();
          _controller.forward();
        }
      });

    // ### 6. **Starting the Animation**
    // The animation is started by calling  .forward()  on the  AnimationController  from within the  initState  method. This begins the animation when the widget is inserted into the widget tree.
    _controller.forward();
  }

  @override
  void dispose() {
    cameraController.dispose();
    // ### 7. **Disposing of the Controller**
    // To avoid memory leaks and ensure resources are released when the widget is destroyed, the  AnimationController  is disposed of in the  dispose  method.
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    dynamic appBar = AppBar(
      centerTitle: true,
      title: Text(showTitle,
          style: const TextStyle(
            // fontSize: 32.rpx,
              color: Color(0xFF333333),
              fontWeight: FontWeight.bold)),
      elevation: 0,
      backgroundColor: Colors.white,
      leading: IconButton(
        // iconSize: 32.rpx,
        onPressed: () {
          Navigator.pop(context);
        },
        icon: const Icon(
          // size: 32.rpx,
            color: Color(0xFF000000),
            Icons.arrow_back_ios),),
      actions: [
        IconButton(
          color: Colors.white,
          icon: ValueListenableBuilder(
            valueListenable: cameraController.torchState,
            builder: (context, state, child) {
              switch (state) {
                case TorchState.off:
                  return const Icon(Icons.flash_off, color: Colors.grey);
                case TorchState.on:
                  return const Icon(Icons.flash_on, color: Colors.yellow);
              }
            },
          ),
          // iconSize: 32.0,
          onPressed: () => cameraController.toggleTorch(),
        ),
      ],
    );
    return Scaffold(
      appBar: appBar,
      body: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
        return Stack(
            alignment: Alignment.center,
            children: [
              MobileScanner(
                scanWindow: Rect.fromCenter(
                  center: Offset(constraints.maxWidth/2,constraints.maxHeight/2),
                  width: 650.rpx,
                  height: 650.rpx,
                ),
                fit: BoxFit.contain,
                controller: cameraController,
                onDetect: (capture) {
                  final List<Barcode> barcodes = capture.barcodes;
                  debugPrint('皮蛋守护!');

                  for (final barcode in barcodes) {
                    debugPrint('皮蛋守护!Barcode found! ${barcode.rawValue}');
                    cameraController.stop();
                    cameraController.dispose();
                    Future.delayed(const Duration(seconds:1), () {
                      context.pushReplacement(Uri(path: "/sign-name", queryParameters: {
                        "code":barcode.rawValue,
                      }).toString());
                    });
                  }
                },
              ),
              ClipPath(
                clipper: RectClipper(),
                child: BackdropFilter(
                  filter: ImageFilter.blur(sigmaX: 2.5, sigmaY: 2.5),
                  child: CustomPaint(
                    painter: ScannerLinePainter(_animation.value),
                    child: Container(
                      decoration: BoxDecoration(
                        color: Colors.black.withOpacity(0.25),
                      ),
                    ),
                  ),
                  /*child: Container(
                    decoration: BoxDecoration(
                      color: Colors.black.withOpacity(0.5),
                    ),
                  ),*/
                ),
              ),
            ]
        );
      },),
    );
  }
}

class RectClipper extends CustomClipper<Path> {
  @override
  Path getClip(Size size) {
    var path = Path();
    var rect = Rect.fromCenter(center: Offset(size.width / 2, size.height / 2), width: 650.rpx, height: 650.rpx);
    path.addRect(Rect.fromLTRB(0, 0, size.width, size.height));
    path.addRect(rect);
    path.fillType = PathFillType.evenOdd;
    return path;
  }

  @override
  bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}

// ### 5. **CustomPainter for Drawing**
// The  ScannerLinePainter  class, which extends  CustomPainter , is used to draw the horizontal line on the canvas. It takes the current animation value ( position ) to determine the line's vertical position within the container.
class ScannerLinePainter extends CustomPainter {
  final double position;

  ScannerLinePainter(this.position);

  @override
  void paint(Canvas canvas, Size size) {
    const int linesCount = 10; // Number of lines to create the splash effect
    final paint = Paint()
      ..color = Colors.green.withOpacity(0.5) // Starting color of the lines
      ..strokeWidth = 2; // Thickness of the lines

    for (int i = 0; i < linesCount; i++) {
      // Calculate the opacity for each line based on its index
      final opacity = (1 - (i / linesCount)).clamp(0.0, 1.0);
      paint.color = paint.color.withOpacity(opacity);

      // Calculate the vertical position for each line
      final yOffset = size.height * position + (i * 5.0) - (linesCount * 2.5); // Adjust the multiplier for spacing

      // Ensure the lines are drawn within the container
      if (yOffset >= 0 && yOffset <= size.height) {
        canvas.drawLine(
          Offset(0, yOffset),
          Offset(size.width, yOffset),
          paint,
        );
      }
    }
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
jartos commented 7 months ago

Hi. I suggest that you create a new StatefulWidget that will manage the MobileScanner. This widget will handle the scanner initialization, barcode detection, and any other related tasks.

In your code you can use it for example like this: bool _showMobileScanner = true/false; _showMobileScanner ? MobileScannerWidget() : Container(),

This way MobileScanner gets initialized and disposed properly every time and there will be no errors.

kuma0605 commented 7 months ago

I updated mobile scanner to 5.00. That works well.