Redth / ZXing.Net.Maui

Barcode Scanning for MAUI?
MIT License
434 stars 143 forks source link

Camera is not released when using the CameraBarcodeReaderView in xaml #164

Open RoyM33-T opened 5 months ago

RoyM33-T commented 5 months ago

I am using the CameraBarcodeReaderView in my xaml for barcodesDetected.

We also use NFC for scanning some card, when we open the barcode scanner it is disabling the NFC. I believe this is part of certain android devices to disable the NFC when the camera is opened. The NFC normaly gets enabled again when the camera is closed or released. However there is no way to release the camera when using the CameraBarcodeReaderView.

I was able to verify that releasing it fixes the problem through reflection but this causes other issues with the CameraPreview if you try to re-open the scanner it just stays black. Here is the reflection for what it is worth.

        private ICameraInternal GetCameraPreview(CameraBarcodeReaderView cameraBarcodeReaderView)
        {
            var bindingFlags = System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance;
            var strongHandlerProperty = typeof(CameraBarcodeReaderView).GetProperty("StrongHandler", bindingFlags);
            var cameraBarcodeReaderViewHandler = strongHandlerProperty.GetValue(cameraBarcodeReaderView) as CameraBarcodeReaderViewHandler;
            var manageField = typeof(CameraBarcodeReaderViewHandler).GetField("cameraManager", bindingFlags);
            var cameraManager = manageField.GetValue(cameraBarcodeReaderViewHandler);
            var cameraPreviewField = typeof(CameraBarcodeReaderViewHandler).Assembly.GetType("ZXing.Net.Maui.CameraManager").GetField("cameraPreview", bindingFlags);
            var preview = cameraPreviewField.GetValue(cameraManager) as Preview;
            return preview.Camera;
        }
cvaughn-bhs commented 5 months ago

Took me a while to get around this same issue. I found the problem to be that it isn't calling CameraBarcodeReaderViewHandler.DisconnectHandler() when you leave a page. Here is what I ended up with. I have been able to continuously reuse the camera but most of my testing with this was done on a Maui Community Toolkit Popup that uses the CameraBarcodeReaderView control.

        // Gets the CameraBarcodeReaderViewHandler.
    var cameraViewHandlerType = CameraView.Handler?.GetType();

    // Gets the CameraManager.
    var cameraManagerInfo = cameraViewHandlerType?.GetField("cameraManager", BindingFlags.NonPublic | BindingFlags.Instance);
    var cameraManagerValue = cameraManagerInfo?.GetValue(CameraView.Handler);

    // Gets the PreviewView that will be used as a parameter for calling DisconnectHandler.
    var cameraManagerType = cameraManagerValue?.GetType();
    FieldInfo? previewViewInfo = null;

    // The PreviewView is named differently on Android and iOS.
    if (OperatingSystem.IsAndroid())
    {
        previewViewInfo = cameraManagerType?.GetField("previewView", BindingFlags.NonPublic | BindingFlags.Instance);
    }
    else if (OperatingSystem.IsIOS())
    {
        previewViewInfo = cameraManagerType?.GetField("view", BindingFlags.NonPublic | BindingFlags.Instance);
    }

    var previewViewValue = previewViewInfo?.GetValue(cameraManagerValue);

    // Gets the DisconnectHandler method handle.
    var disconnectHandlerInfo = cameraViewHandlerType?.GetMethod("DisconnectHandler", BindingFlags.NonPublic | BindingFlags.Instance);

    if (previewViewValue != null)
    {
        var disconnectHandlerParams = new[] { previewViewValue };

        disconnectHandlerInfo?.Invoke(CameraView.Handler, disconnectHandlerParams);
    }
RoyM33-T commented 5 months ago

Unfortunately, this still doesn't release the camera and my NFC is not turning itself back on by disconnecting. I still have to call preview.Camera.Release()

Or else my NFC will not work again until the app is force closed.

RobFrancisAu commented 3 months ago

Had the same issue. Add this to the constructor of the page that is using the barcode reader.

Unloaded += (sender, e) => { cameraBarcodeReaderView?.Handler?.DisconnectHandler(); };

Source /Credit : https://github.com/Redth/ZXing.Net.Maui/issues/118#issuecomment-1540628217

figuerres commented 2 months ago

i think i am also seeing the same issue.... i just tried the last idea i added this line: Unloaded += (sender, e) => { cameraBarcodeReaderView?.Handler?.DisconnectHandler(); }; that did not seem to fix it in my case. will have to keep reading and try to see what is going on. i do not seem to get any error / execption just on second time to use the view its not showing the camera preview.

figuerres commented 2 months ago

Took me a while to get around this same issue. I found the problem to be that it isn't calling CameraBarcodeReaderViewHandler.DisconnectHandler() when you leave a page. Here is what I ended up with. I have been able to continuously reuse the camera but most of my testing with this was done on a Maui Community Toolkit Popup that uses the CameraBarcodeReaderView control.

        // Gets the CameraBarcodeReaderViewHandler.
  var cameraViewHandlerType = CameraView.Handler?.GetType();

  // Gets the CameraManager.
  var cameraManagerInfo = cameraViewHandlerType?.GetField("cameraManager", BindingFlags.NonPublic | BindingFlags.Instance);
  var cameraManagerValue = cameraManagerInfo?.GetValue(CameraView.Handler);

  // Gets the PreviewView that will be used as a parameter for calling DisconnectHandler.
  var cameraManagerType = cameraManagerValue?.GetType();
  FieldInfo? previewViewInfo = null;

  // The PreviewView is named differently on Android and iOS.
  if (OperatingSystem.IsAndroid())
  {
      previewViewInfo = cameraManagerType?.GetField("previewView", BindingFlags.NonPublic | BindingFlags.Instance);
  }
  else if (OperatingSystem.IsIOS())
  {
      previewViewInfo = cameraManagerType?.GetField("view", BindingFlags.NonPublic | BindingFlags.Instance);
  }

  var previewViewValue = previewViewInfo?.GetValue(cameraManagerValue);

  // Gets the DisconnectHandler method handle.
  var disconnectHandlerInfo = cameraViewHandlerType?.GetMethod("DisconnectHandler", BindingFlags.NonPublic | BindingFlags.Instance);

  if (previewViewValue != null)
  {
      var disconnectHandlerParams = new[] { previewViewValue };

      disconnectHandlerInfo?.Invoke(CameraView.Handler, disconnectHandlerParams);
  }

can you please show where you add this code to fix the control ? i am looking at the different posts here and trying to get this to work and i am still stuck. HELP! please.....

RoyM33-T commented 2 months ago

We use a simple Xaml page that is only for our barcode scanning. Once the Xaml is pushed to the navigation stack here is what we do.

        public async Task<BarcodeResult> WaitForScan()
        {
            _scanComplete = new TaskCompletionSource<BarcodeResult?>();
            var preview = await GetCameraPreview(barcodeView);
            preview?.Open();
            return await _scanComplete.Task;
        }

        protected void BarcodesDetected(object sender, BarcodeDetectionEventArgs e)
        {
            barcodeView.IsDetecting = false;
            foreach (var barcode in e.Results)
            {
                _scanComplete.SetResult(barcode);
                GetCameraPreview(barcodeView)?.Result?.Close();
                break;
            }
        }

        private async Task<ICameraInternal?> GetCameraPreview(CameraBarcodeReaderView cameraBarcodeReaderView)
        {

            var status = await Permissions.CheckStatusAsync<Permissions.Camera>();
            if (status == PermissionStatus.Denied)
                return null;

            //The 3rd party framework that we use doesn't close the camera which never turns the nfc back on
            //https://github.com/Redth/ZXing.Net.Maui/issues/164            
            var bindingFlags = System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance;
            var strongHandlerProperty = typeof(CameraBarcodeReaderView).GetProperty("StrongHandler", bindingFlags);
            var cameraBarcodeReaderViewHandler = strongHandlerProperty.GetValue(cameraBarcodeReaderView) as CameraBarcodeReaderViewHandler;
            var manageField = typeof(CameraBarcodeReaderViewHandler).GetField("cameraManager", bindingFlags);
            var cameraManager = manageField.GetValue(cameraBarcodeReaderViewHandler);
            var cameraPreviewField = typeof(CameraBarcodeReaderViewHandler).Assembly.GetType("ZXing.Net.Maui.CameraManager").GetField("cameraPreview", bindingFlags);
            var preview = cameraPreviewField.GetValue(cameraManager) as Preview;

            return preview.Camera;
        }

        protected override bool OnBackButtonPressed()
        {
            _scanComplete.SetResult(null);
            GetCameraPreview(barcodeView)?.Result?.Close();
            return true;
        }

The main thing is I had to manually open and close the camera preview. So the navigation.Push that opens then xaml waits for the WaitForScan method, and the BarcodesDetected method is mapped to the BarcodesDetected in the CameraBarcodeReaderView

We also only have to support Android so there is no IOS logic here that may or may not be necessary.

figuerres commented 2 months ago

i partly follow this but can you help me a bit more?

can i apply this code to a content page ? it looks like it will work for the most part. also i found that i need to add some using statements to get some of the classes and interfaces to work. it looks like some of them need to be in the android / native ?? or androidX ? spaces ? also what is this bound to: public async Task WaitForScan()

is _scanComplete a view model that you are injecting ???

sorry if i am slow here i have only recently gotten into maui and android so there is a lot i am trying to understand here....

perhaps a sample application that demos your basic setup might help ? i have a 2 page app that i am trying to use to test with.... just a home page and a scan page so that i can use the flyout to switch content pages.

RoyM33-T commented 2 months ago

That is almost the entire page but I can give you the whole page.

using AndroidX.Camera.Core.Impl;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Xaml;
using System.Threading.Tasks;
using ZXing.Net.Maui;
using ZXing.Net.Maui.Controls;
using Preview = AndroidX.Camera.Core.Preview;
using Microsoft.Maui.ApplicationModel;

namespace APPNAME.Scanning.QrCodeScanner
{

    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class QrCodeScanner : ContentPage
    {
        private TaskCompletionSource<BarcodeResult?> _scanComplete;

        public QrCodeScanner(QrCodeScannerViewModel viewModel)
        {
            InitializeComponent();
            BindingContext = viewModel;
        }

        protected override void OnAppearing()
        {
            base.OnAppearing();
        }

        public async Task<BarcodeResult> WaitForScan()
        {
            _scanComplete = new TaskCompletionSource<BarcodeResult?>();
            var preview = await GetCameraPreview(barcodeView);
            preview?.Open();
            return await _scanComplete.Task;
        }

        protected void BarcodesDetected(object sender, BarcodeDetectionEventArgs e)
        {
            barcodeView.IsDetecting = false;
            foreach (var barcode in e.Results)
            {
                _scanComplete.SetResult(barcode);
                GetCameraPreview(barcodeView)?.Result?.Close();
                break;
            }
        }

        private async Task<ICameraInternal?> GetCameraPreview(CameraBarcodeReaderView cameraBarcodeReaderView)
        {

            var status = await Permissions.CheckStatusAsync<Permissions.Camera>();
            if (status == PermissionStatus.Denied)
                return null;

            //The 3rd party framework that we use doesn't close the camera which never turns the nfc back on
            //https://github.com/Redth/ZXing.Net.Maui/issues/164            
            var bindingFlags = System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance;
            var strongHandlerProperty = typeof(CameraBarcodeReaderView).GetProperty("StrongHandler", bindingFlags);
            var cameraBarcodeReaderViewHandler = strongHandlerProperty.GetValue(cameraBarcodeReaderView) as CameraBarcodeReaderViewHandler;
            var manageField = typeof(CameraBarcodeReaderViewHandler).GetField("cameraManager", bindingFlags);
            var cameraManager = manageField.GetValue(cameraBarcodeReaderViewHandler);
            var cameraPreviewField = typeof(CameraBarcodeReaderViewHandler).Assembly.GetType("ZXing.Net.Maui.CameraManager").GetField("cameraPreview", bindingFlags);
            var preview = cameraPreviewField.GetValue(cameraManager) as Preview;

            return preview.Camera;
        }

        protected override bool OnBackButtonPressed()
        {
            _scanComplete.SetResult(null);
            GetCameraPreview(barcodeView)?.Result?.Close();
            return true;
        }
    }
}

here is the Xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:zxing="clr-namespace:ZXing.Net.Maui.Controls;assembly=ZXing.Net.MAUI.Controls"
             xmlns:scanning="clr-namespace:APPNAME.Scanning.QrCodeScanner;assembly=APPNAME"
             NavigationPage.HasNavigationBar="False"
             xmlns:data="clr-namespace:APPNAME.Common.Data;assembly=APPNAME"
             x:Class="APPNAME.Scanning.QrCodeScanner.QrCodeScanner"
             x:DataType="scanning:QrCodeScannerViewModel">
    <Grid RowDefinitions="*,*,*">
        <Label Grid.Row="0"
               ZIndex="2"
               Text="{Binding TopText}"
               TextColor="Black"
               Background="#33ffffff"
               FontSize="Subtitle"
               HorizontalTextAlignment="Center"
               VerticalTextAlignment="Center"/>
        <Label Grid.Row="2"
               ZIndex="2"
               Text="{Binding BottomText}"
               TextColor="Black"
               Background="#33ffffff"
               FontSize="Subtitle"
               HorizontalTextAlignment="Center"
               VerticalTextAlignment="Center"/>
        <zxing:CameraBarcodeReaderView
            Grid.Row="0"
            Grid.RowSpan="3"
            x:Name="barcodeView"
            BarcodesDetected="BarcodesDetected" />
    </Grid>
</ContentPage>

And here is how I display it

                    var qrCodeScanner = new QrCodeScanner(new QrCodeScannerViewModel
                    {
                        TopText = "Scan the Config QR Code"
                    });

                    //I was not able to move the push and pop inside the QrCodeScanner and get it to work consistently.
                    await Navigation.PushAsync(qrCodeScanner);
                    var scanResult = await qrCodeScanner.WaitForScan();
                    await Navigation.PopAsync();

The only things that the view model contains is the top and bottom text. At this point in the code the scan result should have what you want, the barcode result.

figuerres commented 2 months ago

when i try to add the using for AndroidX i get red squiggles and an error message. my content page is in the main application folder.

do i need to make this in the platform folder ??

or is there some other trick i am missing ??

RoyM33-T commented 2 months ago

I don't know of anything that would be causing this. Maybe something is not setup in the application correctly without being able to troubleshoot it I cannot be of that much assistance.

figuerres commented 2 months ago

is the code from your content page in the main folders or in the platform/android folder ? i just created a page under /platforms/android and that seems to make visual studio happy.... that may be the trick!

RoyM33-T commented 2 months ago

Nope, mine is in a scanning/qrcodescanner directly under the main project. I know putting some things in the platform android is how you solve certain problems. But glad you are able to get it working I hope it does what you want.

figuerres commented 2 months ago

well when i tried to put the code in the platform folder that created a new set of problems... not sure what the heck to try now..... i may just use an actual barcode scanner .... this is not working for me... and i know the answer is close,.....but just past my understanding of how to make it work!!!

figuerres commented 2 months ago

and i think i am close to having this working..... i have a bug or something .... i can get the camera back without closing the app. but i seem to be needing to use the default android camera to unlock it... but there may be a thing i am doing that plays a role in this.....

i created a new app, i pulled in the code

i did find that i had to put in #if ANDROID blocks in the code for the content page. but now it compiles and runs.

this seems like it is almost working now!

figuerres commented 2 months ago

nope, not working for me.... i keep getting the black preview box.

i have using the flyout menu to open the page and first time it works, second time not so much. if i minimize the app and go back then i get the control to work. i am sure that this can be solved but i am just lost as i do not know enough about how the android camera controls work. very frustrating !! so close but no joy.

figuerres commented 2 months ago

I think that i might now have the thing.... i was not trying the one thing that yo0u are doing different.... i hope this solves it! if it does i will have to post about this....

figuerres commented 2 months ago

so i can not use the view from a page that uses flyout navigation! that was the key here.... i have to use the push / read / pop that you used! along with the creating a local instance of the content page to force a 100% new instance every time.

THANK YOU!

kapparapukeerthana commented 2 months ago

I had similar issue and adding the above code to close the camera worked out really well for me. But then I had one more issue. We are using laser scan also in our app along with zxing. We tried to scan with laser and zxing which worked fine. When we do screen lock and try with laser it did not work until I trigger Zxing again. I used zxing then laser also started working fine. Please give me some suggestions or work around of what could be the issue here.

Thanks

t-macintosh commented 1 month ago

I have the same issue where if the camera view is in a ContentPage, it works the first time the page is navigated to but fails to reinitialize after navigating away and back again. It'd be nice if there was a Close() method to call on OnDisappearring and/or an Initialize() method to call on OnAppearing. Or if the library just handled this automatically :)

I've managed to work around the issue using the suggested solution above except that on navigating to the page in the OnApprearing method I call preview?.Open();

   protected async override void OnAppearing()
    {
        var preview = await GetCameraPreview(BarcodeReaderView);
        preview?.Open();

        BarcodeReaderView.IsDetecting = true;
figuerres commented 1 month ago

I have the same issue where if the camera view is in a ContentPage, it works the first time the page is navigated to but fails to reinitialize after navigating away and back again. It'd be nice if there was a Close() method to call on OnDisappearring and/or an Initialize() method to call on OnAppearing. Or if the library just handled this automatically :)

I've managed to work around the issue using the suggested solution above except that on navigating to the page in the OnApprearing method I call preview?.Open();

   protected async override void OnAppearing()
    {
        var preview = await GetCameraPreview(BarcodeReaderView);
        preview?.Open();

        BarcodeReaderView.IsDetecting = true;

in my app the solution was to use the Community toolkit popup control. with that i was able to create a new popup that is disposed of after use. that made sure that the camera and barcode are created new on each use.

yes an update to this package that made this automatic or un needed would be preferred.

kapparapukeerthana commented 1 week ago

I have the same issue where if the camera view is in a ContentPage, it works the first time the page is navigated to but fails to reinitialize after navigating away and back again. It'd be nice if there was a Close() method to call on OnDisappearring and/or an Initialize() method to call on OnAppearing. Or if the library just handled this automatically :)

I've managed to work around the issue using the suggested solution above except that on navigating to the page in the OnApprearing method I call preview?.Open();

   protected async override void OnAppearing()
    {
        var preview = await GetCameraPreview(BarcodeReaderView);
        preview?.Open();

        BarcodeReaderView.IsDetecting = true;

In my case, I am able to close the camera and reinitialize it. In all scenarios it is working fine. In our app we use laser scan. Laser scan is working fine in all scenarios. The issue is, when I scan with camera(Zxing) and lock the screen and unlock the screen, then tried with laser scan, but it did not trigger the laser. I have to kill the app or open the camera(Zxing) and then laser started working. I checked whether the camera instance is not closed after screen unlock, but it closed the camera. I checked if reinitialization of laser is not happening, but that is also working. I am unable to understand here what could be issue to think of solution. Please provide your ideas if anyone faced this kind of issue.

Thanks in advance.