diff --git a/BarcodeScanning.Native.Maui/BarcodeScanning.Native.Maui.csproj b/BarcodeScanning.Native.Maui/BarcodeScanning.Native.Maui.csproj index cd22414..115769a 100644 --- a/BarcodeScanning.Native.Maui/BarcodeScanning.Native.Maui.csproj +++ b/BarcodeScanning.Native.Maui/BarcodeScanning.Native.Maui.csproj @@ -8,7 +8,7 @@ true enable true - 1.5.7 + 1.5.8 Alen Friščić MIT README.md @@ -49,17 +49,12 @@ - - - + + + - - - - - - - + + diff --git a/BarcodeScanning.Native.Maui/CameraView.cs b/BarcodeScanning.Native.Maui/CameraView.cs index 3313d21..38e2571 100644 --- a/BarcodeScanning.Native.Maui/CameraView.cs +++ b/BarcodeScanning.Native.Maui/CameraView.cs @@ -32,7 +32,7 @@ public ICommand OnImageCapturedCommand public static readonly BindableProperty VibrationOnDetectedProperty = BindableProperty.Create(nameof(VibrationOnDetected) , typeof(bool) , typeof(CameraView) - , true + , false , BindingMode.TwoWay , propertyChanged: (bindable, value, newValue) => ((CameraView)bindable).VibrationOnDetected = (bool)newValue); /// diff --git a/BarcodeScanning.Native.Maui/Platform/Android/BarcodeAnalyzer.cs b/BarcodeScanning.Native.Maui/Platform/Android/BarcodeAnalyzer.cs index abb372a..0103b75 100644 --- a/BarcodeScanning.Native.Maui/Platform/Android/BarcodeAnalyzer.cs +++ b/BarcodeScanning.Native.Maui/Platform/Android/BarcodeAnalyzer.cs @@ -1,43 +1,139 @@ -using AndroidX.Camera.Core; +using Android.Gms.Tasks; +using AndroidX.Camera.Core; +using AndroidX.Camera.View.Transform; +using Microsoft.Maui.Graphics.Platform; using System.Diagnostics; +using Xamarin.Google.MLKit.Vision.Common; using Size = Android.Util.Size; namespace BarcodeScanning; -internal class BarcodeAnalyzer : Java.Lang.Object, ImageAnalysis.IAnalyzer +internal class BarcodeAnalyzer : Java.Lang.Object, ImageAnalysis.IAnalyzer, IOnSuccessListener, IOnCompleteListener { public Size DefaultTargetResolution => Methods.TargetResolution(CaptureQuality.Medium); public int TargetCoordinateSystem => ImageAnalysis.CoordinateSystemOriginal; + private CoordinateTransform _coordinateTransform; + private bool _processInverted; + private IImageProxy _proxy; + + private readonly HashSet _barcodeResults; private readonly CameraManager _cameraManager; internal BarcodeAnalyzer(CameraManager cameraManager) { + _barcodeResults = []; _cameraManager = cameraManager; + _processInverted = false; } public void Analyze(IImageProxy proxy) { try { - _cameraManager?.AnalyzeFrame(proxy); + _proxy = proxy; + _barcodeResults.Clear(); + + if (_cameraManager.CameraView.CaptureNextFrame) + { + _cameraManager.CameraView.CaptureNextFrame = false; + var image = new PlatformImage(_proxy.ToBitmap()); + _cameraManager.CameraView.TriggerOnImageCaptured(image); + } + + if (_cameraManager.RecalculateCoordinateTransform || _coordinateTransform is null) + _coordinateTransform = _cameraManager.GetCoordinateTransform(_proxy); + + _processInverted = _cameraManager.CameraView.ForceInverted; + using var inputImage = InputImage.FromMediaImage(_proxy.Image, _proxy.ImageInfo.RotationDegrees); + _cameraManager.BarcodeScanner.Process(inputImage).AddOnSuccessListener(this).AddOnCompleteListener(this); } - catch (Exception ex) + catch (Exception) { - Debug.WriteLine(ex); + CloseProxy(); + } + } + + public void OnSuccess(Java.Lang.Object result) + { + try + { + Methods.ProcessBarcodeResult(result, _barcodeResults, _coordinateTransform); + + if (!_processInverted) + { + if (_cameraManager.CameraView.AimMode) + { + var previewCenter = new Point(_cameraManager.PreviewView.Width / 2, _cameraManager.PreviewView.Height / 2); + + foreach (var barcode in _barcodeResults) + { + if (!barcode.PreviewBoundingBox.Contains(previewCenter)) + _barcodeResults.Remove(barcode); + } + } + + if (_cameraManager.CameraView.ViewfinderMode) + { + var previewRect = new RectF(0, 0, _cameraManager.PreviewView.Width, _cameraManager.PreviewView.Height); + + foreach (var barcode in _barcodeResults) + { + if (!previewRect.Contains(barcode.PreviewBoundingBox)) + _barcodeResults.Remove(barcode); + } + } + + _cameraManager?.CameraView?.DetectionFinished(_barcodeResults); + } } - finally + catch (Exception) + { + } + } + + public void OnComplete(Android.Gms.Tasks.Task task) + { + if (_processInverted) { try { - proxy?.Close(); + Methods.InvertLuminance(_proxy.Image); + + _processInverted = false; + using var inputImage = InputImage.FromMediaImage(_proxy.Image, _proxy.ImageInfo.RotationDegrees); + _cameraManager.BarcodeScanner.Process(inputImage).AddOnSuccessListener(this).AddOnCompleteListener(this); } - catch (Exception ex) + catch (Exception) { - Debug.WriteLine(ex); - MainThread.BeginInvokeOnMainThread(() => _cameraManager?.Start()); + CloseProxy(); } } + else + { + CloseProxy(); + } + + } + + private void CloseProxy() + { + try + { + _proxy?.Close(); + } + catch (Exception ex) + { + Debug.WriteLine(ex); + MainThread.BeginInvokeOnMainThread(() => _cameraManager?.Start()); + } + } + + protected override void Dispose(bool disposing) + { + _coordinateTransform?.Dispose(); + + base.Dispose(disposing); } } \ No newline at end of file diff --git a/BarcodeScanning.Native.Maui/Platform/Android/CameraManager.cs b/BarcodeScanning.Native.Maui/Platform/Android/CameraManager.cs index faebe46..93dc2d3 100644 --- a/BarcodeScanning.Native.Maui/Platform/Android/CameraManager.cs +++ b/BarcodeScanning.Native.Maui/Platform/Android/CameraManager.cs @@ -1,63 +1,84 @@ using Android.Content; -using Android.Gms.Extensions; using Android.Graphics; using Android.Widget; using AndroidX.Camera.Core; using AndroidX.Camera.View; using AndroidX.Camera.View.Transform; +using AndroidX.Core.Content; using AndroidX.Lifecycle; using Java.Util.Concurrent; -using Microsoft.Maui.Graphics.Platform; using Microsoft.Maui.Platform; using Xamarin.Google.MLKit.Vision.BarCode; -using Xamarin.Google.MLKit.Vision.Common; using static Android.Views.ViewGroup; using Color = Android.Graphics.Color; using MLKitBarcodeScanning = Xamarin.Google.MLKit.Vision.BarCode.BarcodeScanning; using Paint = Android.Graphics.Paint; -using Point = Microsoft.Maui.Graphics.Point; -using RectF = Microsoft.Maui.Graphics.RectF; namespace BarcodeScanning; internal class CameraManager : IDisposable { internal BarcodeView BarcodeView { get => _barcodeView; } + internal IBarcodeScanner BarcodeScanner { get => _barcodeScanner; } + internal CameraView CameraView { get => _cameraView; } + internal PreviewView PreviewView { get => _previewView; } + + internal CameraState OpenedCameraState { get; set; } + internal bool RecalculateCoordinateTransform { get; set; } - private BarcodeAnalyzer _barcodeAnalyzer; private IBarcodeScanner _barcodeScanner; + private ICameraInfo _currentCameraInfo; + private readonly BarcodeAnalyzer _barcodeAnalyzer; private readonly BarcodeView _barcodeView; private readonly CameraView _cameraView; private readonly Context _context; private readonly IExecutorService _cameraExecutor; private readonly ImageView _imageView; private readonly LifecycleCameraController _cameraController; + private readonly ILifecycleOwner _lifecycleOwner; private readonly PreviewView _previewView; + private readonly PreviewViewOnLayoutChangeListener _previewViewOnLayoutChangeListener; private readonly RelativeLayout _relativeLayout; - private readonly ZoomStateObserver _zoomStateObserver; + private readonly CameraStateObserver _cameraStateObserver; - private readonly HashSet _barcodeResults = []; private const int aimRadius = 25; - private bool _cameraRunning = false; internal CameraManager(CameraView cameraView, Context context) { _context = context; _cameraView = cameraView; + if (_context is ILifecycleOwner) + _lifecycleOwner = _context as ILifecycleOwner; + else if ((_context as ContextWrapper)?.BaseContext is ILifecycleOwner) + _lifecycleOwner = (_context as ContextWrapper)?.BaseContext as ILifecycleOwner; + else if (Platform.CurrentActivity is ILifecycleOwner) + _lifecycleOwner = Platform.CurrentActivity as ILifecycleOwner; + else + _lifecycleOwner = null; + _cameraExecutor = Executors.NewSingleThreadExecutor(); - _zoomStateObserver = new ZoomStateObserver(this, _cameraView); + _barcodeAnalyzer = new BarcodeAnalyzer(this); + + _cameraStateObserver = new CameraStateObserver(this, _cameraView); _cameraController = new LifecycleCameraController(_context) { TapToFocusEnabled = _cameraView?.TapToFocusEnabled ?? false, ImageAnalysisBackpressureStrategy = ImageAnalysis.StrategyKeepOnlyLatest }; _cameraController.SetEnabledUseCases(CameraController.ImageAnalysis); - _cameraController.ZoomState.ObserveForever(_zoomStateObserver); - + _cameraController.ZoomState.ObserveForever(_cameraStateObserver); + _cameraController.InitializationFuture.AddListener(new Java.Lang.Runnable(() => + { + _currentCameraInfo?.CameraState.RemoveObserver(_cameraStateObserver); + _currentCameraInfo = _cameraController.CameraInfo; + _currentCameraInfo?.CameraState.ObserveForever(_cameraStateObserver); + }), ContextCompat.GetMainExecutor(_context)); + + _previewViewOnLayoutChangeListener = new PreviewViewOnLayoutChangeListener(this); _previewView = new PreviewView(_context) { Controller = _cameraController, @@ -66,6 +87,7 @@ internal CameraManager(CameraView cameraView, Context context) _previewView.SetBackgroundColor(_cameraView?.BackgroundColor?.ToPlatform() ?? Color.Transparent); _previewView.SetImplementationMode(PreviewView.ImplementationMode.Compatible); _previewView.SetScaleType(PreviewView.ScaleType.FillCenter); + _previewView.AddOnLayoutChangeListener(_previewViewOnLayoutChangeListener); var layoutParams = new RelativeLayout.LayoutParams(LayoutParams.WrapContent, LayoutParams.WrapContent); layoutParams.AddRule(LayoutRules.CenterInParent); @@ -97,46 +119,26 @@ internal CameraManager(CameraView cameraView, Context context) //TODO Implement camera-mlkit-vision //https://developer.android.com/reference/androidx/camera/mlkit/vision/MlKitAnalyzer - internal void Start() + internal void Start(bool skipResolution = false) { if (_cameraController is not null) { - if (_cameraRunning) - { + if (OpenedCameraState?.GetType() != CameraState.Type.Closed) _cameraController.Unbind(); - _cameraRunning = false; - } - - ILifecycleOwner lifecycleOwner = null; - if (_context is ILifecycleOwner) - lifecycleOwner = _context as ILifecycleOwner; - else if ((_context as ContextWrapper)?.BaseContext is ILifecycleOwner) - lifecycleOwner = (_context as ContextWrapper)?.BaseContext as ILifecycleOwner; - else if (Platform.CurrentActivity is ILifecycleOwner) - lifecycleOwner = Platform.CurrentActivity as ILifecycleOwner; - - if (lifecycleOwner is null) - return; if (_cameraController.CameraSelector is null) UpdateCamera(); - if (_cameraController.ImageAnalysisTargetSize is null) + if (_cameraController.ImageAnalysisTargetSize is null && !skipResolution) UpdateResolution(); - - if (_cameraController is not null && _cameraExecutor is not null) - { - _cameraController.ClearImageAnalysisAnalyzer(); - _barcodeAnalyzer?.Dispose(); - _barcodeAnalyzer = new BarcodeAnalyzer(this); + if (_barcodeAnalyzer is not null && _cameraExecutor is not null) _cameraController.SetImageAnalysisAnalyzer(_cameraExecutor, _barcodeAnalyzer); - } UpdateSymbologies(); UpdateTorch(); - _cameraController.BindToLifecycle(lifecycleOwner); - _cameraRunning = true; - } + if (_lifecycleOwner is not null) + _cameraController.BindToLifecycle(_lifecycleOwner); + } } internal void Stop() @@ -151,10 +153,7 @@ internal void Stop() _cameraView.TorchOn = false; } - if (_cameraRunning) - _cameraController.Unbind(); - - _cameraRunning = false; + _cameraController.Unbind(); } } @@ -197,8 +196,8 @@ internal void UpdateResolution() if (_cameraController is not null) _cameraController.ImageAnalysisTargetSize = new CameraController.OutputSize(Methods.TargetResolution(_cameraView?.CaptureQuality)); - if (_cameraRunning) - Start(); + if (OpenedCameraState?.GetType() == CameraState.Type.Open || OpenedCameraState?.GetType() == CameraState.Type.Opening || OpenedCameraState?.GetType() == CameraState.Type.PendingOpen) + Start(true); } internal void UpdateSymbologies() @@ -239,72 +238,19 @@ internal void UpdateZoomFactor() } } - internal void AnalyzeFrame(IImageProxy proxy) - { - if (proxy is not null && _cameraView is not null && _previewView is not null && _barcodeResults is not null && _barcodeScanner is not null && !_cameraView.PauseScanning) - { - DetectBarcode(proxy).Wait(2000); - - if (_cameraView.CaptureNextFrame) - CaptureImage(proxy); - } - } - - private void CaptureImage(IImageProxy proxy) - { - _cameraView.CaptureNextFrame = false; - var image = new PlatformImage(proxy.ToBitmap()); - _cameraView.TriggerOnImageCaptured(image); - } - - private async Task DetectBarcode(IImageProxy proxy) + internal CoordinateTransform GetCoordinateTransform(IImageProxy proxy) { - _barcodeResults.Clear(); - using var target = await MainThread.InvokeOnMainThreadAsync(() => _previewView.OutputTransform).ConfigureAwait(false); - using var source = new ImageProxyTransformFactory + var imageOutputTransform = new ImageProxyTransformFactory { UsingRotationDegrees = true } .GetOutputTransform(proxy); - using var coordinateTransform = new CoordinateTransform(source, target); - - using var image = InputImage.FromMediaImage(proxy.Image, proxy.ImageInfo.RotationDegrees); - using var results = await _barcodeScanner.Process(image).AsAsync().ConfigureAwait(false); - - Methods.ProcessBarcodeResult(results, _barcodeResults, coordinateTransform); - - if (_cameraView.ForceInverted) - { - Methods.InvertLuminance(proxy.Image); - using var invertedimage = InputImage.FromMediaImage(proxy.Image, proxy.ImageInfo.RotationDegrees); - using var invertedresults = await _barcodeScanner.Process(invertedimage).AsAsync().ConfigureAwait(false); - - Methods.ProcessBarcodeResult(invertedresults, _barcodeResults, coordinateTransform); - } - - if (_cameraView.AimMode) - { - var previewCenter = new Point(_previewView.Width / 2, _previewView.Height / 2); - - foreach (var barcode in _barcodeResults) - { - if (!barcode.PreviewBoundingBox.Contains(previewCenter)) - _barcodeResults.Remove(barcode); - } - } - - if (_cameraView.ViewfinderMode) - { - var previewRect = new RectF(0, 0, _previewView.Width, _previewView.Height); - - foreach (var barcode in _barcodeResults) - { - if (!previewRect.Contains(barcode.PreviewBoundingBox)) - _barcodeResults.Remove(barcode); - } - } + var previewOutputTransform = MainThread.InvokeOnMainThreadAsync(() => _previewView?.OutputTransform).Result; - _cameraView.DetectionFinished(_barcodeResults); + if (imageOutputTransform is not null && previewOutputTransform is not null) + return new CoordinateTransform(imageOutputTransform, previewOutputTransform); + else + return null; } private void MainDisplayInfoChanged(object sender, DisplayInfoChangedEventArgs e) @@ -316,7 +262,6 @@ private void MainDisplayInfoChanged(object sender, DisplayInfoChangedEventArgs e { try { - if (_cameraRunning && _cameraView.CameraEnabled) UpdateResolution(); } catch (Exception) @@ -337,17 +282,13 @@ protected virtual void Dispose(bool disposing) { if (disposing) { - try - { - DeviceDisplay.Current.MainDisplayInfoChanged -= MainDisplayInfoChanged; - } - catch (Exception) - { - } + DeviceDisplay.Current.MainDisplayInfoChanged -= MainDisplayInfoChanged; Stop(); - _cameraController?.ZoomState.RemoveObserver(_zoomStateObserver); + _cameraController?.ZoomState.RemoveObserver(_cameraStateObserver); + _currentCameraInfo?.CameraState.RemoveObserver(_cameraStateObserver); + _previewView?.RemoveOnLayoutChangeListener(_previewViewOnLayoutChangeListener); _barcodeView?.RemoveAllViews(); _relativeLayout?.RemoveAllViews(); @@ -355,8 +296,10 @@ protected virtual void Dispose(bool disposing) _relativeLayout?.Dispose(); _imageView?.Dispose(); _previewView?.Dispose(); + _previewViewOnLayoutChangeListener?.Dispose(); _cameraController?.Dispose(); - _zoomStateObserver?.Dispose(); + _currentCameraInfo?.Dispose(); + _cameraStateObserver?.Dispose(); _barcodeAnalyzer?.Dispose(); _barcodeScanner?.Dispose(); _cameraExecutor?.Dispose(); diff --git a/BarcodeScanning.Native.Maui/Platform/Android/CameraStateObserver.cs b/BarcodeScanning.Native.Maui/Platform/Android/CameraStateObserver.cs new file mode 100644 index 0000000..5bcc920 --- /dev/null +++ b/BarcodeScanning.Native.Maui/Platform/Android/CameraStateObserver.cs @@ -0,0 +1,38 @@ +using AndroidX.Camera.Core; +using AndroidX.Lifecycle; + +namespace BarcodeScanning; + +internal class CameraStateObserver : Java.Lang.Object, IObserver +{ + private readonly CameraManager _cameraManager; + private readonly CameraView _cameraView; + + internal CameraStateObserver(CameraManager cameraManager, CameraView cameraView) + { + _cameraManager = cameraManager; + _cameraView = cameraView; + } + + public void OnChanged(Java.Lang.Object value) + { + if (value is not null && _cameraView is not null && _cameraManager is not null) + { + if (value is IZoomState zoomState) + { + _cameraView.CurrentZoomFactor = zoomState.ZoomRatio; + _cameraView.MinZoomFactor = zoomState.MinZoomRatio; + _cameraView.MaxZoomFactor = zoomState.MaxZoomRatio; + + _cameraManager.UpdateZoomFactor(); + } + + if (value is CameraState cameraState) + { + _cameraManager.OpenedCameraState = cameraState; + } + + _cameraManager.RecalculateCoordinateTransform = true; + } + } +} \ No newline at end of file diff --git a/BarcodeScanning.Native.Maui/Platform/Android/Methods.cs b/BarcodeScanning.Native.Maui/Platform/Android/Methods.cs index 3c36c7f..da54264 100644 --- a/BarcodeScanning.Native.Maui/Platform/Android/Methods.cs +++ b/BarcodeScanning.Native.Maui/Platform/Android/Methods.cs @@ -35,16 +35,17 @@ public static async Task> ScanFromImageAsync(Stream strea => await ProcessBitmapAsync(await BitmapFactory.DecodeStreamAsync(stream)); private static async Task> ProcessBitmapAsync(Bitmap bitmap) { + var barcodeResults = new HashSet(); + if (bitmap is null) - return null; + return barcodeResults; - var barcodeResults = new HashSet(); using var scanner = Xamarin.Google.MLKit.Vision.BarCode.BarcodeScanning.GetClient(new BarcodeScannerOptions.Builder() .SetBarcodeFormats(Barcode.FormatAllFormats) .Build()); using var image = InputImage.FromBitmap(bitmap, 0); - using var results = await scanner.Process(image).AsAsync(); + using var results = await scanner.Process(image); ProcessBarcodeResult(results, barcodeResults); using var invertedBitmap = Bitmap.CreateBitmap(bitmap.Height, bitmap.Width, Bitmap.Config.Argb8888); @@ -65,7 +66,7 @@ private static async Task> ProcessBitmapAsync(Bitmap bitm canvas.DrawBitmap(bitmap, 0, 0, paint); using var invertedImage = InputImage.FromBitmap(invertedBitmap, 0); - using var invertedResults = await scanner.Process(invertedImage).AsAsync(); + using var invertedResults = await scanner.Process(invertedImage); ProcessBarcodeResult(invertedResults, barcodeResults); return barcodeResults; diff --git a/BarcodeScanning.Native.Maui/Platform/Android/PreviewViewOnLayoutChangeListener.cs b/BarcodeScanning.Native.Maui/Platform/Android/PreviewViewOnLayoutChangeListener.cs new file mode 100644 index 0000000..7be8cea --- /dev/null +++ b/BarcodeScanning.Native.Maui/Platform/Android/PreviewViewOnLayoutChangeListener.cs @@ -0,0 +1,19 @@ +using static Android.Views.View; + +namespace BarcodeScanning; + +internal class PreviewViewOnLayoutChangeListener : Java.Lang.Object, IOnLayoutChangeListener +{ + private readonly CameraManager _cameraManager; + + internal PreviewViewOnLayoutChangeListener(CameraManager cameraManager) + { + _cameraManager = cameraManager; + } + + public void OnLayoutChange(Android.Views.View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) + { + if (_cameraManager is not null) + _cameraManager.RecalculateCoordinateTransform = true; + } +} \ No newline at end of file diff --git a/BarcodeScanning.Native.Maui/Platform/Android/ZoomStateObserver.cs b/BarcodeScanning.Native.Maui/Platform/Android/ZoomStateObserver.cs deleted file mode 100644 index e26803f..0000000 --- a/BarcodeScanning.Native.Maui/Platform/Android/ZoomStateObserver.cs +++ /dev/null @@ -1,28 +0,0 @@ -using AndroidX.Camera.Core; -using AndroidX.Lifecycle; - -namespace BarcodeScanning; - -internal class ZoomStateObserver : Java.Lang.Object, IObserver -{ - private readonly CameraManager _cameraManager; - private readonly CameraView _cameraView; - - internal ZoomStateObserver(CameraManager cameraManager, CameraView cameraView) - { - _cameraManager = cameraManager; - _cameraView = cameraView; - } - - public void OnChanged(Java.Lang.Object value) - { - if (value is not null && _cameraView is not null && value is IZoomState state) - { - _cameraView.CurrentZoomFactor = state.ZoomRatio; - _cameraView.MinZoomFactor = state.MinZoomRatio; - _cameraView.MaxZoomFactor = state.MaxZoomRatio; - - _cameraManager?.UpdateZoomFactor(); - } - } -} \ No newline at end of file diff --git a/BarcodeScanning.Native.Maui/Platform/MaciOS/BarcodeAnalyzer.cs b/BarcodeScanning.Native.Maui/Platform/MaciOS/BarcodeAnalyzer.cs index 087eec0..23fd8a9 100644 --- a/BarcodeScanning.Native.Maui/Platform/MaciOS/BarcodeAnalyzer.cs +++ b/BarcodeScanning.Native.Maui/Platform/MaciOS/BarcodeAnalyzer.cs @@ -1,23 +1,78 @@ using AVFoundation; +using CoreImage; using CoreMedia; +using Microsoft.Maui.Graphics.Platform; using System.Diagnostics; +using UIKit; +using Vision; namespace BarcodeScanning; internal class BarcodeAnalyzer : AVCaptureVideoDataOutputSampleBufferDelegate { + private readonly HashSet _barcodeResults; private readonly CameraManager _cameraManager; + private readonly VNDetectBarcodesRequest _detectBarcodesRequest; + private readonly VNSequenceRequestHandler _sequenceRequestHandler; internal BarcodeAnalyzer(CameraManager cameraManager) { + _barcodeResults = []; _cameraManager = cameraManager; + _detectBarcodesRequest = new VNDetectBarcodesRequest((request, error) => + { + if (error is null) + Methods.ProcessBarcodeResult(request.GetResults(), _barcodeResults, _cameraManager.PreviewLayer); + }); + _sequenceRequestHandler = new VNSequenceRequestHandler(); + } + + internal void UpdateSymbologies(VNBarcodeSymbology[] symbologies) + { + _detectBarcodesRequest.Symbologies = symbologies; } public override void DidOutputSampleBuffer(AVCaptureOutput captureOutput, CMSampleBuffer sampleBuffer, AVCaptureConnection connection) { try { - _cameraManager?.AnalyzeFrame(sampleBuffer); + if (_cameraManager.CameraView.CaptureNextFrame) + { + _cameraManager.CameraView.CaptureNextFrame = false; + using var imageBuffer = sampleBuffer.GetImageBuffer(); + using var cIImage = new CIImage(imageBuffer); + using var cIContext = new CIContext(); + using var cGImage = cIContext.CreateCGImage(cIImage, cIImage.Extent); + var image = new PlatformImage(new UIImage(cGImage)); + _cameraManager.CameraView.TriggerOnImageCaptured(image); + } + + _barcodeResults.Clear(); + _sequenceRequestHandler?.Perform([_detectBarcodesRequest], sampleBuffer, out _); + + if (_cameraManager.CameraView.AimMode) + { + var previewCenter = new Point(_cameraManager.PreviewLayer.Bounds.Width / 2, _cameraManager.PreviewLayer.Bounds.Height / 2); + + foreach (var barcode in _barcodeResults) + { + if (!barcode.PreviewBoundingBox.Contains(previewCenter)) + _barcodeResults.Remove(barcode); + } + } + + if (_cameraManager.CameraView.ViewfinderMode) + { + var previewRect = new RectF(0, 0, (float)_cameraManager.PreviewLayer.Bounds.Width, (float)_cameraManager.PreviewLayer.Bounds.Height); + + foreach (var barcode in _barcodeResults) + { + if (!previewRect.Contains(barcode.PreviewBoundingBox)) + _barcodeResults.Remove(barcode); + } + } + + _cameraManager.CameraView.DetectionFinished(_barcodeResults); } catch (Exception ex) { @@ -36,4 +91,12 @@ public override void DidOutputSampleBuffer(AVCaptureOutput captureOutput, CMSamp } } } + + protected override void Dispose(bool disposing) + { + _sequenceRequestHandler?.Dispose(); + _detectBarcodesRequest?.Dispose(); + + base.Dispose(disposing); + } } \ No newline at end of file diff --git a/BarcodeScanning.Native.Maui/Platform/MaciOS/CameraManager.cs b/BarcodeScanning.Native.Maui/Platform/MaciOS/CameraManager.cs index 340747f..469be65 100644 --- a/BarcodeScanning.Native.Maui/Platform/MaciOS/CameraManager.cs +++ b/BarcodeScanning.Native.Maui/Platform/MaciOS/CameraManager.cs @@ -2,38 +2,33 @@ using CoreAnimation; using CoreFoundation; using CoreGraphics; -using CoreImage; -using CoreMedia; using Foundation; -using Microsoft.Maui.Graphics.Platform; using Microsoft.Maui.Platform; using System.Diagnostics; using UIKit; -using Vision; namespace BarcodeScanning; internal class CameraManager : IDisposable { + internal AVCaptureVideoPreviewLayer PreviewLayer { get => _previewLayer; } internal BarcodeView BarcodeView { get => _barcodeView; } + internal CameraView CameraView { get => _cameraView; } private AVCaptureDevice _captureDevice; private AVCaptureInput _captureInput; - private BarcodeAnalyzer _barcodeAnalyzer; private readonly AVCaptureVideoDataOutput _videoDataOutput; private readonly AVCaptureVideoPreviewLayer _previewLayer; private readonly AVCaptureSession _captureSession; + private readonly BarcodeAnalyzer _barcodeAnalyzer; private readonly BarcodeView _barcodeView; private readonly CameraView _cameraView; private readonly CAShapeLayer _shapeLayer; private readonly DispatchQueue _dispatchQueue; private readonly NSObject _subjectAreaChangedNotificaion; - private readonly VNDetectBarcodesRequest _detectBarcodesRequest; - private readonly VNSequenceRequestHandler _sequenceRequestHandler; private readonly UITapGestureRecognizer _uITapGestureRecognizer; - private readonly HashSet _barcodeResults = []; private const int aimRadius = 8; internal CameraManager(CameraView cameraView) @@ -41,7 +36,7 @@ internal CameraManager(CameraView cameraView) _cameraView = cameraView; _captureSession = new AVCaptureSession(); - _sequenceRequestHandler = new VNSequenceRequestHandler(); + _barcodeAnalyzer = new BarcodeAnalyzer(this); _dispatchQueue = new DispatchQueue("com.barcodescanning.maui.sessionQueue", new DispatchQueue.Attributes() { QualityOfService = DispatchQualityOfService.UserInitiated @@ -50,11 +45,6 @@ internal CameraManager(CameraView cameraView) { AlwaysDiscardsLateVideoFrames = true }; - _detectBarcodesRequest = new VNDetectBarcodesRequest((request, error) => - { - if (error is null) - Methods.ProcessBarcodeResult(request.GetResults(), _barcodeResults, _previewLayer); - }); _uITapGestureRecognizer = new UITapGestureRecognizer(FocusOnTap); _subjectAreaChangedNotificaion = NSNotificationCenter.DefaultCenter.AddObserver(AVCaptureDevice.SubjectAreaDidChangeNotification, (n) => @@ -109,8 +99,6 @@ internal void Start() if (_videoDataOutput is not null) { _videoDataOutput.SetSampleBufferDelegate(null, null); - _barcodeAnalyzer?.Dispose(); - _barcodeAnalyzer = new BarcodeAnalyzer(this); _videoDataOutput.SetSampleBufferDelegate(_barcodeAnalyzer, DispatchQueue.DefaultGlobalQueue); } @@ -222,8 +210,8 @@ internal void UpdateResolution() internal void UpdateSymbologies() { - if (_detectBarcodesRequest is not null && _cameraView is not null) - _detectBarcodesRequest.Symbologies = Methods.SelectedSymbologies(_cameraView.BarcodeSymbologies); + if (_barcodeAnalyzer is not null && _cameraView is not null) + _barcodeAnalyzer.UpdateSymbologies(Methods.SelectedSymbologies(_cameraView.BarcodeSymbologies)); } internal void UpdateTapToFocus() {} @@ -271,58 +259,6 @@ internal void UpdateZoomFactor() } } - internal void AnalyzeFrame(CMSampleBuffer sampleBuffer) - { - if (sampleBuffer is not null && _cameraView is not null && _previewLayer is not null && _barcodeResults is not null && _detectBarcodesRequest is not null && !_cameraView.PauseScanning) - { - DetectBarcode(sampleBuffer); - - if (_cameraView.CaptureNextFrame) - CaptureImage(sampleBuffer); - } - } - - private void CaptureImage(CMSampleBuffer sampleBuffer) - { - _cameraView.CaptureNextFrame = false; - using var imageBuffer = sampleBuffer.GetImageBuffer(); - using var cIImage = new CIImage(imageBuffer); - using var cIContext = new CIContext(); - using var cGImage = cIContext.CreateCGImage(cIImage, cIImage.Extent); - var image = new PlatformImage(new UIImage(cGImage)); - _cameraView.TriggerOnImageCaptured(image); - } - - private void DetectBarcode(CMSampleBuffer sampleBuffer) - { - _barcodeResults.Clear(); - _sequenceRequestHandler?.Perform([_detectBarcodesRequest], sampleBuffer, out _); - - if (_cameraView.AimMode) - { - var previewCenter = new Point(_previewLayer.Bounds.Width / 2, _previewLayer.Bounds.Height / 2); - - foreach (var barcode in _barcodeResults) - { - if (!barcode.PreviewBoundingBox.Contains(previewCenter)) - _barcodeResults.Remove(barcode); - } - } - - if (_cameraView.ViewfinderMode) - { - var previewRect = new RectF(0, 0, (float)_previewLayer.Bounds.Width, (float)_previewLayer.Bounds.Height); - - foreach (var barcode in _barcodeResults) - { - if (!previewRect.Contains(barcode.PreviewBoundingBox)) - _barcodeResults.Remove(barcode); - } - } - - _cameraView.DetectionFinished(_barcodeResults); - } - private void DeviceLock(Action action) { DispatchQueue.MainQueue.DispatchAsync(() => @@ -406,8 +342,6 @@ protected virtual void Dispose(bool disposing) _barcodeAnalyzer?.Dispose(); _captureDevice?.Dispose(); - _sequenceRequestHandler?.Dispose(); - _detectBarcodesRequest?.Dispose(); _uITapGestureRecognizer?.Dispose(); _subjectAreaChangedNotificaion?.Dispose(); _dispatchQueue?.Dispose(); diff --git a/BarcodeScanning.Native.Maui/Platform/MaciOS/Methods.cs b/BarcodeScanning.Native.Maui/Platform/MaciOS/Methods.cs index 9094f7a..bc8ec42 100644 --- a/BarcodeScanning.Native.Maui/Platform/MaciOS/Methods.cs +++ b/BarcodeScanning.Native.Maui/Platform/MaciOS/Methods.cs @@ -1,6 +1,5 @@ using AVFoundation; using CoreGraphics; -using CoreImage; using Foundation; using Microsoft.Maui.Graphics.Platform; using System.Text; @@ -21,8 +20,10 @@ public static async Task> ScanFromImageAsync(Stream strea => await ProcessBitmapAsync(UIImage.LoadFromData(NSData.FromStream(stream))); private static async Task> ProcessBitmapAsync(UIImage image) { + var barcodeResults = new HashSet(); + if (image is null) - return null; + return barcodeResults; VNBarcodeObservation[] observations = null; using var barcodeRequest = new VNDetectBarcodesRequest((request, error) => { @@ -31,7 +32,6 @@ private static async Task> ProcessBitmapAsync(UIImage ima }); using var handler = new VNImageRequestHandler(image.CGImage, new NSDictionary()); await Task.Run(() => handler.Perform([barcodeRequest], out _)); - var barcodeResults = new HashSet(); ProcessBarcodeResult(observations, barcodeResults); return barcodeResults; } @@ -43,7 +43,6 @@ internal static void ProcessBarcodeResult(VNBarcodeObservation[] inputResults, H lock (outputResults) { - //TODO NSProcessInfo.ProcessInfo.OperatingSystemVersion.Major > 17 and add payloadData foreach (var barcode in inputResults) { outputResults.Add(new BarcodeResult() @@ -52,7 +51,7 @@ internal static void ProcessBarcodeResult(VNBarcodeObservation[] inputResults, H BarcodeFormat = ConvertFromIOSFormats(barcode.Symbology), DisplayValue = barcode.PayloadStringValue, RawValue = barcode.PayloadStringValue, - RawBytes = GetRawBytes(barcode) ?? Encoding.ASCII.GetBytes(barcode.PayloadStringValue), + RawBytes = OperatingSystem.IsIOSVersionAtLeast(17) ? [.. barcode.PayloadData] : Encoding.ASCII.GetBytes(barcode.PayloadStringValue), PreviewBoundingBox = previewLayer?.MapToLayerCoordinates(InvertY(barcode.BoundingBox)).AsRectangleF() ?? RectF.Zero, ImageBoundingBox = barcode.BoundingBox.AsRectangleF() }); @@ -168,18 +167,6 @@ private static CGRect InvertY(CGRect rect) return new CGRect(rect.X, 1 - rect.Y - rect.Height, rect.Width, rect.Height); } - private static byte[] GetRawBytes(VNBarcodeObservation barcodeObservation) - { - return barcodeObservation.Symbology switch - { - VNBarcodeSymbology.QR => ((CIQRCodeDescriptor)barcodeObservation.BarcodeDescriptor)?.ErrorCorrectedPayload?.ToArray(), - VNBarcodeSymbology.Aztec => ((CIAztecCodeDescriptor)barcodeObservation.BarcodeDescriptor)?.ErrorCorrectedPayload?.ToArray(), - VNBarcodeSymbology.Pdf417 => ((CIPdf417CodeDescriptor)barcodeObservation.BarcodeDescriptor)?.ErrorCorrectedPayload?.ToArray(), - VNBarcodeSymbology.DataMatrix => ((CIDataMatrixCodeDescriptor)barcodeObservation.BarcodeDescriptor)?.ErrorCorrectedPayload?.ToArray(), - _ => null - }; - } - private static NSString SessionPresetTranslator(CaptureQuality quality) { return quality switch diff --git a/BarcodeScanning.Native.Maui/Shared/BarcodeResult.cs b/BarcodeScanning.Native.Maui/Shared/BarcodeResult.cs index b59450a..1b0aa4b 100644 --- a/BarcodeScanning.Native.Maui/Shared/BarcodeResult.cs +++ b/BarcodeScanning.Native.Maui/Shared/BarcodeResult.cs @@ -15,7 +15,7 @@ public bool Equals(BarcodeResult other) if (other is null) return false; - if (this.RawValue == other.RawValue && this.ImageBoundingBox.IntersectsWith(other.ImageBoundingBox)) + if (this.DisplayValue == other.DisplayValue && this.ImageBoundingBox.IntersectsWith(other.ImageBoundingBox)) return true; else return false; @@ -31,6 +31,6 @@ public override bool Equals(object obj) } public override int GetHashCode() { - return this.RawValue.GetHashCode(); + return this.DisplayValue.GetHashCode(); } } \ No newline at end of file