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
880 stars 510 forks source link

Multiple instance issue : black screen #446

Closed EArminjon closed 1 year ago

EArminjon commented 1 year ago

Issue :

Cannot start scanner after a dispose from an other instance (or related).

What we see

We can push one or several pages which contain a scanner When we pop this page and go back, previous scanner won't start, despite the .start() method...

Version

Flutter 3.3.4 mobile_scanner: ^3.0.0-beta.4

untitled.webm

import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';

void main() => runApp(const MaterialApp(home: MyHome()));

class MyHome extends StatelessWidget {
  const MyHome({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Flutter Demo Home Page')),
      body: SizedBox(
        width: MediaQuery.of(context).size.width,
        height: MediaQuery.of(context).size.height,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            ElevatedButton(
              onPressed: () {
                Navigator.of(context).push(
                  MaterialPageRoute<dynamic>(
                    builder: (BuildContext context) =>
                        const BarcodeScannerWithController(),
                  ),
                );
              },
              child: const Text('MobileScanner with Controller'),
            ),
          ],
        ),
      ),
    );
  }
}

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

  @override
  _BarcodeScannerWithControllerState createState() =>
      _BarcodeScannerWithControllerState();
}

class _BarcodeScannerWithControllerState
    extends State<BarcodeScannerWithController>
    with SingleTickerProviderStateMixin {
  BarcodeCapture? barcode;

  final MobileScannerController controller = MobileScannerController(
    torchEnabled: true,
    // formats: [BarcodeFormat.qrCode]
    // facing: CameraFacing.front,
    // detectionSpeed: DetectionSpeed.normal
    // detectionTimeoutMs: 1000,
    // returnImage: false,
  );

  bool isStarted = true;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(controller.hashCode.toString()),
      ),
      backgroundColor: Colors.black,
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.camera),
        onPressed: () async {
          await controller.stop();
          await Navigator.of(context).push(
            MaterialPageRoute<dynamic>(
              builder: (BuildContext context) =>
                  const BarcodeScannerWithController(),
            ),
          );
          await controller.start();
        },
      ),
      body: Builder(
        builder: (BuildContext context) {
          return Stack(
            children: <Widget>[
              MobileScanner(
                controller: controller,
                errorBuilder: (
                  BuildContext context,
                  MobileScannerException error,
                  Widget? child,
                ) {
                  return ScannerErrorWidget(error: error);
                },
                fit: BoxFit.contain,
                onDetect: (BarcodeCapture barcode) {
                  setState(() {
                    this.barcode = barcode;
                  });
                },
              ),
            ],
          );
        },
      ),
    );
  }

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

class ScannerErrorWidget extends StatelessWidget {
  const ScannerErrorWidget({super.key, required this.error});

  final MobileScannerException error;

  @override
  Widget build(BuildContext context) {
    String errorMessage;

    switch (error.errorCode) {
      case MobileScannerErrorCode.controllerUninitialized:
        errorMessage = 'Controller not ready.';
        break;
      case MobileScannerErrorCode.permissionDenied:
        errorMessage = 'Permission denied';
        break;
      default:
        errorMessage = 'Generic Error';
        break;
    }

    return ColoredBox(
      color: Colors.black,
      child: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            const Padding(
              padding: EdgeInsets.only(bottom: 16),
              child: Icon(Icons.error, color: Colors.white),
            ),
            Text(
              errorMessage,
              style: const TextStyle(color: Colors.white),
            ),
          ],
        ),
      ),
    );
  }
}
cody1024d commented 1 year ago

I'm having a similar issue. Albeit mine is not back-to-back scanners, but a scanner, and then a standard camera view.

I call controller.dispose() before navigating to the next screen, however, I still see this being thrown (on an Android device) in the logs:

01-12 19:52:05.561  1090  2244 I CameraService: CameraService::connect call (PID -1 "com.verfico.app", camera ID 1) for HAL version default and Camera API version 2
01-12 19:52:05.562  1090  2244 E CameraService: CameraService::connect X (PID 1642) rejected (existing client(s) with higher priority).

I have confirmed that if I remove the scanning screen, and just load the user into the camera widget, that it loads properly.

EArminjon commented 1 year ago

I'm having a similar issue. Albeit mine is not back-to-back scanners, but a scanner, and then a standard camera view.

I call controller.dispose() before navigating to the next screen, however, I still see this being thrown (on an Android device) in the logs:

01-12 19:52:05.561  1090  2244 I CameraService: CameraService::connect call (PID -1 "com.verfico.app", camera ID 1) for HAL version default and Camera API version 2
01-12 19:52:05.562  1090  2244 E CameraService: CameraService::connect X (PID 1642) rejected (existing client(s) with higher priority).

I have confirmed that if I remove the scanning screen, and just load the user into the camera widget, that it loads properly.

I've updated the post with the version used. Did you use same as me ?

cody1024d commented 1 year ago

@EArminjon

I am pulling straight from the master branch, but yes it would be the latest code (similar to beta pub). Have you tried any earlier versions?

EArminjon commented 1 year ago

@EArminjon

I am pulling straight from the master branch, but yes it would be the latest code (similar to beta pub). Have you tried any earlier versions?

Can't use older versions, many bugs or missing features which didn't allow me to use this library to feat my business. (I'm coming from qr_code_scanner...).

Which is weird is, we can push and push again scanner view, if we stop or dispose the scan controller we didn't see issue. But when poping, it didn't work. I think because when poping a route : the new one appear and after that the old one is deleted. So during X amount of time, both views exist so we have 2 instances at the same time...

EArminjon commented 1 year ago

This issue is major for us :'( I tried to investigate your library but i think it's more native part and i can't help more :/

NAME-NikhilPatil commented 1 year ago

start controller.start() in initstate

EArminjon commented 1 year ago

start controller.start() in initstate

Have you tried my example and try with your solution ? Do it allow to open many windows with camera without issue and pop them one by one restarting each camera when screen come back to visibility ?

cody1024d commented 1 year ago

Hey @EArminjon ,

So I ended up resolving my issue; but I'm not sure it will work in your usecase.

So the problem is that the controller.stop/dispose (that stops the controller) calls into the native Android code, which ends up unbinding the CameraX lifecycle listener. I did some research, and basically, this unbinding is asynchronous, and doesn't notify the caller, at all. So it's very possible, that while you're opening new screens, that the previous is still disconnecting.

I figured this out based on the errors I was specifically seeing from my device logs (client rejected, etc.).

In my case, I closed the camera sooner; as well as added retry mechanisms when trying to "start" the camera on the next screen

EArminjon commented 1 year ago

Hum could be interesting to create a PR to await this asynchronous method call. Can you @cody1024d ?

cody1024d commented 1 year ago

Unfortunately, the Android platform itself has no hook back (as far as I could tell) when unbinding, so I'm not sure that'd be possible

EArminjon commented 1 year ago

@juliansteenbakker issue above is still reproducible with stable 3.0.0 (Android 13) :/

I tried to add startDelay: trueinside MobileScannerbut same issue.

Can we reopen this issue please ?

Open 2 pages, pop one, scanner is broke.

EArminjon commented 1 year ago

I created on my app a temporary fix : i made a loop around the .start to retry 3 times with a little delay. Not clean at all but it work for us.

cody1024d commented 1 year ago

I created on my app a temporary fix : i made a loop around the .start to retry 3 times with a little delay. Not clean at all but it work for us.

I've actually done something similar, although I can still get my app into an unusable state :( Specifically with very aggressive use of the home button, and app switching (in Android). I believe this framework is somehow holding onto a lifecycle binding somewhere too long.