From 0c588b292533dc82523e418683cd7c693b511b94 Mon Sep 17 00:00:00 2001 From: Tim Nolte Date: Fri, 11 Dec 2020 08:56:16 +0100 Subject: [PATCH 01/43] Ported to camera2 --- .../Properties/AndroidManifest.xml | 2 +- .../Sample.Forms.Android.csproj | 12 +- .../CameraAccess/CameraAnalyzer.android.cs | 285 +++--- .../CameraAccess/CameraController.android.cs | 887 +++++++++--------- .../CameraEventsListener.android.cs | 69 +- .../Android/CameraAccess/Torch.android.cs | 157 ++-- .../Android/ZXingSurfaceView.android.cs | 309 +++--- 7 files changed, 893 insertions(+), 828 deletions(-) diff --git a/Samples/Sample.Forms/Sample.Forms.Android/Properties/AndroidManifest.xml b/Samples/Sample.Forms/Sample.Forms.Android/Properties/AndroidManifest.xml index 8599f2bbe..25ac59f65 100644 --- a/Samples/Sample.Forms/Sample.Forms.Android/Properties/AndroidManifest.xml +++ b/Samples/Sample.Forms/Sample.Forms.Android/Properties/AndroidManifest.xml @@ -1,6 +1,6 @@  - + diff --git a/Samples/Sample.Forms/Sample.Forms.Android/Sample.Forms.Android.csproj b/Samples/Sample.Forms/Sample.Forms.Android/Sample.Forms.Android.csproj index dcacc25c9..b3bd2595e 100644 --- a/Samples/Sample.Forms/Sample.Forms.Android/Sample.Forms.Android.csproj +++ b/Samples/Sample.Forms/Sample.Forms.Android/Sample.Forms.Android.csproj @@ -16,7 +16,7 @@ Resources Assets false - v10.0 + v9.0 true true Xamarin.Android.Net.AndroidClientHandler @@ -32,6 +32,11 @@ prompt 4 None + false + false + false + false + true true @@ -109,4 +114,9 @@ + + + + + \ No newline at end of file diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs index 50cabe36c..4e63473b5 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs @@ -1,155 +1,146 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using Android.Content; +using Android.Hardware.Camera2; +using Android.Hardware.Camera2.Params; using Android.Views; using ApxLabs.FastAndroidCamera; +using ZXing.Common; namespace ZXing.Mobile.CameraAccess { public class CameraAnalyzer - { - readonly CameraController cameraController; - readonly CameraEventsListener cameraEventListener; - Task processingTask; - DateTime lastPreviewAnalysis = DateTime.UtcNow; - bool wasScanned; - readonly IScannerSessionHost scannerHost; - BarcodeReader barcodeReader; - - public CameraAnalyzer(SurfaceView surfaceView, IScannerSessionHost scannerHost) - { - this.scannerHost = scannerHost; - cameraEventListener = new CameraEventsListener(); - cameraController = new CameraController(surfaceView, cameraEventListener, scannerHost); - Torch = new Torch(cameraController, surfaceView.Context); - } - - public Action BarcodeFound; - - public Torch Torch { get; } - - public bool IsAnalyzing { get; private set; } - - public void PauseAnalysis() - => IsAnalyzing = false; - - public void ResumeAnalysis() - => IsAnalyzing = true; - - public void ShutdownCamera() - { - IsAnalyzing = false; - cameraEventListener.OnPreviewFrameReady -= HandleOnPreviewFrameReady; - cameraController.ShutdownCamera(); - } - - public void SetupCamera() - { - cameraEventListener.OnPreviewFrameReady += HandleOnPreviewFrameReady; - cameraController.SetupCamera(); - barcodeReader = scannerHost.ScanningOptions.BuildBarcodeReader(); - } - - public void AutoFocus() - => cameraController.AutoFocus(); - - public void AutoFocus(int x, int y) - => cameraController.AutoFocus(x, y); - - public void RefreshCamera() - => cameraController.RefreshCamera(); - - bool CanAnalyzeFrame - { - get - { - if (!IsAnalyzing) - return false; - - //Check and see if we're still processing a previous frame - // todo: check if we can run as many as possible or mby run two analyzers at once (Vision + ZXing) - if (processingTask != null && !processingTask.IsCompleted) - return false; - - var elapsedTimeMs = (DateTime.UtcNow - lastPreviewAnalysis).TotalMilliseconds; - if (elapsedTimeMs < scannerHost.ScanningOptions.DelayBetweenAnalyzingFrames) - return false; - - // Delay a minimum between scans - if (wasScanned && elapsedTimeMs < scannerHost.ScanningOptions.DelayBetweenContinuousScans) - return false; - - return true; - } - } - - void HandleOnPreviewFrameReady(object sender, FastJavaByteArray fastArray) - { - if (!CanAnalyzeFrame) - return; - - wasScanned = false; - lastPreviewAnalysis = DateTime.UtcNow; - - processingTask = Task.Run(() => - { - try - { - DecodeFrame(fastArray); - } - catch (Exception ex) - { - Console.WriteLine(ex); - } - }).ContinueWith(task => - { - if (task.IsFaulted) - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "DecodeFrame exception occurs"); - }, TaskContinuationOptions.OnlyOnFaulted); - } - - void DecodeFrame(FastJavaByteArray fastArray) - { - var resolution = cameraController.CameraResolution; - var width = resolution.Width; - var height = resolution.Height; - - var rotate = false; - var newWidth = width; - var newHeight = height; - - // use last value for performance gain - var cDegrees = cameraController.LastCameraDisplayOrientationDegree; - - if (cDegrees == 90 || cDegrees == 270) - { - rotate = true; - newWidth = height; - newHeight = width; - } - - var start = PerformanceCounter.Start(); - - LuminanceSource fast = new FastJavaByteArrayYUVLuminanceSource(fastArray, width, height, 0, 0, width, height); // _area.Left, _area.Top, _area.Width, _area.Height); - if (rotate) - fast = fast.rotateCounterClockwise(); - - var result = barcodeReader.Decode(fast); - - fastArray.Dispose(); - fastArray = null; - - PerformanceCounter.Stop(start, - "Decode Time: {0} ms (width: " + width + ", height: " + height + ", degrees: " + cDegrees + ", rotate: " + - rotate + ")"); - - if (result != null) - { - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "Barcode Found"); - - wasScanned = true; - BarcodeFound?.Invoke(result); - return; - } - } - } -} \ No newline at end of file + { + readonly CameraController cameraController; + readonly CameraEventsListener cameraEventListener; + Task processingTask; + DateTime lastPreviewAnalysis = DateTime.UtcNow; + bool wasScanned; + IScannerSessionHost scannerHost; + CameraManager cameraManager; + + public CameraAnalyzer(SurfaceView surfaceView, IScannerSessionHost scannerHost) + { + this.scannerHost = scannerHost; + cameraEventListener = new CameraEventsListener(); + cameraController = new CameraController(surfaceView, cameraEventListener, scannerHost); + Torch = new Torch(cameraController, surfaceView.Context); + cameraManager = (CameraManager)surfaceView.Context.GetSystemService(Context.CameraService); + } + + public Action BarcodeFound; + + public Torch Torch { get; } + + public bool IsAnalyzing { get; private set; } + + public void PauseAnalysis() + => IsAnalyzing = false; + + public void ResumeAnalysis() + => IsAnalyzing = true; + + public void ShutdownCamera() + { + IsAnalyzing = false; + cameraEventListener.OnPreviewFrameReady -= HandleOnPreviewFrameReady; + cameraController.ShutdownCamera(); + } + + public void SetupCamera(int width, int height) + { + cameraEventListener.OnPreviewFrameReady += HandleOnPreviewFrameReady; + cameraController.SetupCamera(width, height); + } + + public void AutoFocus() + => cameraController.AutoFocus(); + + public void AutoFocus(int x, int y) + => cameraController.AutoFocus(x, y); + + public void RefreshCamera(int width, int height) + { + cameraController.RefreshCamera(width, height); + } + + bool CanAnalyzeFrame + { + get + { + if (!IsAnalyzing) + return false; + + //Check and see if we're still processing a previous frame + // todo: check if we can run as many as possible or mby run two analyzers at once (Vision + ZXing) + if (processingTask != null && !processingTask.IsCompleted) + return false; + + var elapsedTimeMs = (DateTime.UtcNow - lastPreviewAnalysis).TotalMilliseconds; + if (elapsedTimeMs < scannerHost.ScanningOptions.DelayBetweenAnalyzingFrames) + return false; + + // Delay a minimum between scans + if (wasScanned && elapsedTimeMs < scannerHost.ScanningOptions.DelayBetweenContinuousScans) + return false; + + return true; + } + } + + void HandleOnPreviewFrameReady(object sender, byte[] fastArray) + { + if (!CanAnalyzeFrame) + return; + + wasScanned = false; + lastPreviewAnalysis = DateTime.UtcNow; + + processingTask = Task.Run(() => + { + try + { + DecodeFrame(fastArray); + } + catch (Exception ex) + { + Console.WriteLine(ex); + } + }).ContinueWith(task => + { + if (task.IsFaulted) + Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "DecodeFrame exception occurs"); + }, TaskContinuationOptions.OnlyOnFaulted); + } + + void DecodeFrame(byte[] fastArray) + { + var previewSize = cameraController.IdealPhotoSize; + var width = previewSize.Width; + var height = previewSize.Height; + var barcodeReader = scannerHost.ScanningOptions.BuildBarcodeReader(); + + ZXing.Result result = null; + var start = PerformanceCounter.Start(); + + barcodeReader.AutoRotate = true; + + var source2 = new PlanarYUVLuminanceSource(fastArray, width, height, 0, 0, width, height, false); + + result = barcodeReader.Decode(source2); + PerformanceCounter.Stop(start, + "Decode Time: {0} ms (width: " + width + ", height: " + height + ")"); + + if (result != null) + { + Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "Barcode Found"); + + wasScanned = true; + BarcodeFound?.Invoke(result); + return; + } + } + } +} diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs index 71354dae5..0b20fd1b4 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs @@ -1,435 +1,478 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using Android.Content; using Android.Graphics; using Android.Hardware; +using Android.Hardware.Camera2; +using Android.Hardware.Camera2.Params; +using Android.Media; using Android.OS; using Android.Runtime; +using Android.Util; using Android.Views; using ApxLabs.FastAndroidCamera; -using Camera = Android.Hardware.Camera; +using Java.Lang; +using Java.Util.Concurrent; namespace ZXing.Mobile.CameraAccess { - public class CameraController - { - readonly Context context; - readonly ISurfaceHolder holder; - readonly SurfaceView surfaceView; - readonly CameraEventsListener cameraEventListener; - int cameraId; - IScannerSessionHost scannerHost; - - public CameraController(SurfaceView surfaceView, CameraEventsListener cameraEventListener, IScannerSessionHost scannerHost) - { - context = surfaceView.Context; - holder = surfaceView.Holder; - this.surfaceView = surfaceView; - this.cameraEventListener = cameraEventListener; - this.scannerHost = scannerHost; - } - - public Camera Camera { get; private set; } - - public CameraResolution CameraResolution { get; private set; } - - public int LastCameraDisplayOrientationDegree { get; private set; } - - public void RefreshCamera() - { - if (holder == null) return; - - ApplyCameraSettings(); - - try - { - Camera.SetPreviewDisplay(holder); - Camera.StartPreview(); - } - catch (Exception ex) - { - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, ex.ToString()); - } - } - - public void SetupCamera() - { - if (Camera != null) - return; - - var perf = PerformanceCounter.Start(); - OpenCamera(); - PerformanceCounter.Stop(perf, "Setup Camera took {0}ms"); - - if (Camera == null) return; - - perf = PerformanceCounter.Start(); - ApplyCameraSettings(); - - try - { - Camera.SetPreviewDisplay(holder); - - - var previewParameters = Camera.GetParameters(); - var previewSize = previewParameters.PreviewSize; - var bitsPerPixel = ImageFormat.GetBitsPerPixel(previewParameters.PreviewFormat); - - - var bufferSize = (previewSize.Width * previewSize.Height * bitsPerPixel) / 8; - const int NUM_PREVIEW_BUFFERS = 5; - for (uint i = 0; i < NUM_PREVIEW_BUFFERS; ++i) - { - using (var buffer = new FastJavaByteArray(bufferSize)) - Camera.AddCallbackBuffer(buffer); - } - - Camera.StartPreview(); - - Camera.SetNonMarshalingPreviewCallback(cameraEventListener); - } - catch (Exception ex) - { - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, ex.ToString()); - return; - } - finally - { - PerformanceCounter.Stop(perf, "Setup Camera Parameters took {0}ms"); - } - - // Docs suggest if Auto or Macro modes, we should invoke AutoFocus at least once - var currentFocusMode = Camera.GetParameters().FocusMode; - if (currentFocusMode == Camera.Parameters.FocusModeAuto - || currentFocusMode == Camera.Parameters.FocusModeMacro) - AutoFocus(); - } - - public void AutoFocus() - { - AutoFocus(0, 0, false); - } - - public void AutoFocus(int x, int y) - { - // The bounds for focus areas are actually -1000 to 1000 - // So we need to translate the touch coordinates to this scale - var focusX = x / surfaceView.Width * 2000 - 1000; - var focusY = y / surfaceView.Height * 2000 - 1000; - - // Call the autofocus with our coords - AutoFocus(focusX, focusY, true); - } - - public void ShutdownCamera() - { - if (Camera == null) return; - - // camera release logic takes about 0.005 sec so there is no need in async releasing - var perf = PerformanceCounter.Start(); - try - { - try - { - Camera.StopPreview(); - Camera.SetNonMarshalingPreviewCallback(null); - - //Camera.SetPreviewCallback(null); - - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, $"Calling SetPreviewDisplay: null"); - Camera.SetPreviewDisplay(null); - } - catch (Exception ex) - { - Android.Util.Log.Error(MobileBarcodeScanner.TAG, ex.ToString()); - } - Camera.Release(); - Camera = null; - } - catch (Exception e) - { - Android.Util.Log.Error(MobileBarcodeScanner.TAG, e.ToString()); - } - - PerformanceCounter.Stop(perf, "Shutdown camera took {0}ms"); - } - - void OpenCamera() - { - try - { - var version = Build.VERSION.SdkInt; - - if (version >= BuildVersionCodes.Gingerbread) - { - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "Checking Number of cameras..."); - - var numCameras = Camera.NumberOfCameras; - var camInfo = new Camera.CameraInfo(); - var found = false; - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "Found " + numCameras + " cameras..."); - - var whichCamera = CameraFacing.Back; - - if (scannerHost.ScanningOptions.UseFrontCameraIfAvailable.HasValue && - scannerHost.ScanningOptions.UseFrontCameraIfAvailable.Value) - whichCamera = CameraFacing.Front; - - for (var i = 0; i < numCameras; i++) - { - Camera.GetCameraInfo(i, camInfo); - if (camInfo.Facing == whichCamera) - { - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, - "Found " + whichCamera + " Camera, opening..."); - Camera = Camera.Open(i); - cameraId = i; - found = true; - break; - } - } - - if (!found) - { - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, - "Finding " + whichCamera + " camera failed, opening camera 0..."); - Camera = Camera.Open(0); - cameraId = 0; - } - } - else - { - Camera = Camera.Open(); - } - - //if (Camera != null) - // Camera.SetPreviewCallback(_cameraEventListener); - //else - // MobileBarcodeScanner.LogWarn(MobileBarcodeScanner.TAG, "Camera is null :("); - } - catch (Exception ex) - { - ShutdownCamera(); - MobileBarcodeScanner.LogError("Setup Error: {0}", ex); - } - } - - void ApplyCameraSettings() - { - if (Camera == null) - { - OpenCamera(); - } - - // do nothing if something wrong with camera - if (Camera == null) return; - - var parameters = Camera.GetParameters(); - parameters.PreviewFormat = ImageFormatType.Nv21; - - var supportedFocusModes = parameters.SupportedFocusModes; - if (scannerHost.ScanningOptions.DisableAutofocus) - parameters.FocusMode = Camera.Parameters.FocusModeFixed; - else if (Build.VERSION.SdkInt >= BuildVersionCodes.IceCreamSandwich && - supportedFocusModes.Contains(Camera.Parameters.FocusModeContinuousPicture)) - parameters.FocusMode = Camera.Parameters.FocusModeContinuousPicture; - else if (supportedFocusModes.Contains(Camera.Parameters.FocusModeContinuousVideo)) - parameters.FocusMode = Camera.Parameters.FocusModeContinuousVideo; - else if (supportedFocusModes.Contains(Camera.Parameters.FocusModeAuto)) - parameters.FocusMode = Camera.Parameters.FocusModeAuto; - else if (supportedFocusModes.Contains(Camera.Parameters.FocusModeFixed)) - parameters.FocusMode = Camera.Parameters.FocusModeFixed; - - var selectedFps = parameters.SupportedPreviewFpsRange.FirstOrDefault(); - if (selectedFps != null) - { - // This will make sure we select a range with the highest maximum fps - // which still has the lowest minimum fps (Widest Range) - foreach (var fpsRange in parameters.SupportedPreviewFpsRange) - { - if (fpsRange[1] > selectedFps[1] || fpsRange[1] == selectedFps[1] && fpsRange[0] < selectedFps[0]) - selectedFps = fpsRange; - } - parameters.SetPreviewFpsRange(selectedFps[0], selectedFps[1]); - } - - CameraResolution resolution = null; - var supportedPreviewSizes = parameters.SupportedPreviewSizes; - if (supportedPreviewSizes != null) - { - var availableResolutions = supportedPreviewSizes.Select(sps => new CameraResolution - { - Width = sps.Width, - Height = sps.Height - }); - - // Try and get a desired resolution from the options selector - resolution = scannerHost.ScanningOptions.GetResolution(availableResolutions.ToList()); - - // If the user did not specify a resolution, let's try and find a suitable one - if (resolution == null) - { - foreach (var sps in supportedPreviewSizes) - { - if (sps.Width >= 640 && sps.Width <= 1000 && sps.Height >= 360 && sps.Height <= 1000) - { - resolution = new CameraResolution - { - Width = sps.Width, - Height = sps.Height - }; - break; - } - } - } - } - - // Google Glass requires this fix to display the camera output correctly - if (Build.Model.Contains("Glass")) - { - resolution = new CameraResolution - { - Width = 640, - Height = 360 - }; - // Glass requires 30fps - parameters.SetPreviewFpsRange(30000, 30000); - } - - // Hopefully a resolution was selected at some point - if (resolution != null) - { - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, - "Selected Resolution: " + resolution.Width + "x" + resolution.Height); - - CameraResolution = resolution; - parameters.SetPreviewSize(resolution.Width, resolution.Height); - } - - Camera.SetParameters(parameters); - - SetCameraDisplayOrientation(); - } - - void AutoFocus(int x, int y, bool useCoordinates) - { - if (Camera == null) return; - - if (scannerHost.ScanningOptions.DisableAutofocus) - { - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "AutoFocus Disabled"); - return; - } - - var cameraParams = Camera.GetParameters(); - - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "AutoFocus Requested"); - - // Cancel any previous requests - Camera.CancelAutoFocus(); - - try - { - // If we want to use coordinates - // Also only if our camera supports Auto focus mode - // Since FocusAreas only really work with FocusModeAuto set - if (useCoordinates - && cameraParams.SupportedFocusModes.Contains(Camera.Parameters.FocusModeAuto)) - { - // Let's give the touched area a 20 x 20 minimum size rect to focus on - // So we'll offset -10 from the center of the touch and then - // make a rect of 20 to give an area to focus on based on the center of the touch - x = x - 10; - y = y - 10; - - // Ensure we don't go over the -1000 to 1000 limit of focus area - if (x >= 1000) - x = 980; - if (x < -1000) - x = -1000; - if (y >= 1000) - y = 980; - if (y < -1000) - y = -1000; - - // Explicitly set FocusModeAuto since Focus areas only work with this setting - cameraParams.FocusMode = Camera.Parameters.FocusModeAuto; - // Add our focus area - cameraParams.FocusAreas = new List - { - new Camera.Area(new Rect(x, y, x + 20, y + 20), 1000) - }; - Camera.SetParameters(cameraParams); - } - - // Finally autofocus (weather we used focus areas or not) - Camera.AutoFocus(cameraEventListener); - } - catch (Exception ex) - { - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "AutoFocus Failed: {0}", ex); - } - } - - void SetCameraDisplayOrientation() - { - var degrees = GetCameraDisplayOrientation(); - LastCameraDisplayOrientationDegree = degrees; - - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "Changing Camera Orientation to: " + degrees); - - try - { - Camera.SetDisplayOrientation(degrees); - } - catch (Exception ex) - { - Android.Util.Log.Error(MobileBarcodeScanner.TAG, ex.ToString()); - } - } - - int GetCameraDisplayOrientation() - { - int degrees; - var windowManager = context.GetSystemService(Context.WindowService).JavaCast(); - var display = windowManager.DefaultDisplay; - var rotation = display.Rotation; - - switch (rotation) - { - case SurfaceOrientation.Rotation0: - degrees = 0; - break; - case SurfaceOrientation.Rotation90: - degrees = 90; - break; - case SurfaceOrientation.Rotation180: - degrees = 180; - break; - case SurfaceOrientation.Rotation270: - degrees = 270; - break; - default: - throw new ArgumentOutOfRangeException(); - } - - var info = new Camera.CameraInfo(); - Camera.GetCameraInfo(cameraId, info); - - int correctedDegrees; - if (info.Facing == CameraFacing.Front) - { - correctedDegrees = (info.Orientation + degrees) % 360; - correctedDegrees = (360 - correctedDegrees) % 360; // compensate the mirror - } - else - { - // back-facing - correctedDegrees = (info.Orientation - degrees + 360) % 360; - } - - return correctedDegrees; - } - } -} \ No newline at end of file + public class CameraController + { + readonly Context context; + readonly ISurfaceHolder holder; + readonly SurfaceView surfaceView; + readonly CameraEventsListener cameraEventListener; + readonly IScannerSessionHost scannerHost; + readonly CameraStateCallback cameraStateCallback; + + CameraManager cameraManager; + private Size[] supportedJpegSizes; + private Size idealPhotoSize; + private ImageReader imageReader; + private bool flashSupported; + private Handler backgroundHandler; + private CaptureRequest.Builder previewBuilder; + private CameraCaptureSession previewSession; + private CaptureRequest previewRequest; + private HandlerThread backgroundThread; + private int? lastCameraDisplayOrientationDegree; + + public string CameraId { get; private set; } + + public bool OpeningCamera { get; private set; } + + public CameraDevice Camera { get; private set; } + + public Size PreviewSize { get; private set; } + + public Size IdealPhotoSize { get; private set; } + + public int LastCameraDisplayOrientationDegree + { + get + { + if (lastCameraDisplayOrientationDegree is null) + { + lastCameraDisplayOrientationDegree = GetCameraDisplayOrientation(); + } + + return lastCameraDisplayOrientationDegree.Value; + } + } + + public CameraController(SurfaceView surfaceView, CameraEventsListener cameraEventListener, IScannerSessionHost scannerHost) + { + context = surfaceView.Context; + holder = surfaceView.Holder; + this.surfaceView = surfaceView; + this.cameraEventListener = cameraEventListener; + this.scannerHost = scannerHost; + cameraStateCallback = new CameraStateCallback() + { + OnErrorAction = (camera, error) => + { + camera.Close(); + + Camera = null; + OpeningCamera = false; + + Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "Error on opening camera: " + error); + }, + OnOpenedAction = camera => + { + Camera = camera; + StartPreview(); + OpeningCamera = false; + }, + OnDisconnectedAction = camera => + { + camera.Close(); + Camera = null; + OpeningCamera = false; + } + }; + } + + public void RefreshCamera(int width, int height) + { + if (Camera is null || previewRequest is null || previewSession is null || previewBuilder is null) return; + SetUpCameraOutputs(width, height); + previewRequest.Dispose(); + previewSession.Dispose(); + previewBuilder.Dispose(); + StartPreview(); + } + + public void SetupCamera(int width, int height) + { + StartBackgroundThread(); + + OpenCamera(width, height); + } + + public void ShutdownCamera() + { + if (Camera != null) + Camera.Close(); + + StopBackgroundThread(); + } + + public void AutoFocus() + { + AutoFocus(0, 0, false); + } + + public void AutoFocus(int x, int y) + { + // The bounds for focus areas are actually -1000 to 1000 + // So we need to translate the touch coordinates to this scale + var focusX = x / surfaceView.Width * 2000 - 1000; + var focusY = y / surfaceView.Height * 2000 - 1000; + + // Call the autofocus with our coords + AutoFocus(focusX, focusY, true); + } + + int GetCameraDisplayOrientation() + { + int degrees; + var windowManager = context.GetSystemService(Context.WindowService).JavaCast(); + var display = windowManager.DefaultDisplay; + var rotation = display.Rotation; + + switch (rotation) + { + case SurfaceOrientation.Rotation0: + degrees = 0; + break; + case SurfaceOrientation.Rotation90: + degrees = 90; + break; + case SurfaceOrientation.Rotation180: + degrees = 180; + break; + case SurfaceOrientation.Rotation270: + degrees = 270; + break; + default: + throw new ArgumentOutOfRangeException(); + } + + var characteristics = cameraManager.GetCameraCharacteristics(CameraId); + var facing = (int)characteristics.Get(CameraCharacteristics.LensFacing); + var orientation = (int)characteristics.Get(CameraCharacteristics.SensorOrientation); + int correctedDegrees; + if (facing == (int)CameraFacing.Front) + { + correctedDegrees = (orientation + degrees) % 360; + correctedDegrees = (360 - correctedDegrees) % 360; // compensate the mirror + } + else + { + // back-facing + correctedDegrees = (orientation - degrees + 360) % 360; + } + + return correctedDegrees; + } + + void AutoFocus(int x, int y, bool useCoordinates) + { + if (Camera == null) return; + + if (scannerHost.ScanningOptions.DisableAutofocus) + { + Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "AutoFocus Disabled"); + return; + } + + var characteristics = cameraManager.GetCameraCharacteristics(CameraId.ToString()); + var map = (StreamConfigurationMap)characteristics.Get(CameraCharacteristics.ScalerStreamConfigurationMap); + var supportedFocusModes = ((int[])characteristics + .Get(CameraCharacteristics.ControlAfAvailableModes)) + .Select(x => (ControlAFMode)x); + + Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "AutoFocus Requested"); + + try + { + // If we want to use coordinates + // Also only if our camera supports Auto focus mode + // Since FocusAreas only really work with FocusModeAuto set + if (useCoordinates + && supportedFocusModes.Contains(ControlAFMode.Auto)) + { + // Let's give the touched area a 20 x 20 minimum size rect to focus on + // So we'll offset -10 from the center of the touch and then + // make a rect of 20 to give an area to focus on based on the center of the touch + x = x - 10; + y = y - 10; + + // Ensure we don't go over the -1000 to 1000 limit of focus area + if (x >= 1000) + x = 980; + if (x < -1000) + x = -1000; + if (y >= 1000) + y = 980; + if (y < -1000) + y = -1000; + + // Explicitly set FocusModeAuto since Focus areas only work with this setting + previewBuilder.Set(CaptureRequest.ControlAfMode, (int)ControlAFMode.Auto); + // Add our focus area + previewBuilder.Set(CaptureRequest.ControlAfRegions, new MeteringRectangle[] + { + new MeteringRectangle(x, y, x + 20, y + 20, 1000) + }); + + previewBuilder.Set(CaptureRequest.ControlAeRegions, new MeteringRectangle[] + { + new MeteringRectangle(x, y, x + 20, y + 20, 1000) + }); + } + + // Finally autofocus (weather we used focus areas or not) + previewBuilder.Set(CaptureRequest.ControlAfTrigger, (int)ControlAFTrigger.Start); + } + catch (System.Exception ex) + { + Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "AutoFocus Failed: {0}", ex); + } + } + + void SetUpCameraOutputs(int width, int height) + { + cameraManager = (CameraManager)context.GetSystemService(Context.CameraService); + + var cameraIds = cameraManager.GetCameraIdList(); + + CameraId = cameraIds[0]; + + for (var i = 0; i < cameraIds.Length; i++) + { + var cameraCharacteristics = cameraManager.GetCameraCharacteristics(cameraIds[i]); + + var facing = (Integer)cameraCharacteristics.Get(CameraCharacteristics.LensFacing); + if (facing != null && facing == (Integer.ValueOf((int)LensFacing.Back))) + { + CameraId = cameraIds[i]; + + //Phones like Galaxy S10 have 2 or 3 frontal cameras usually the one with flash is the one + //that should be chosen, if not It will select the first one and that can be the fish + //eye camera + if (HasFLash(cameraCharacteristics)) + break; + } + } + + var characteristics = cameraManager.GetCameraCharacteristics(CameraId); + var map = (StreamConfigurationMap)characteristics.Get(CameraCharacteristics.ScalerStreamConfigurationMap); + + if (supportedJpegSizes == null && characteristics != null) + { + supportedJpegSizes = ((StreamConfigurationMap)characteristics.Get(CameraCharacteristics.ScalerStreamConfigurationMap)).GetOutputSizes((int)ImageFormatType.Yuv420888); + } + + if (supportedJpegSizes != null && supportedJpegSizes.Length > 0) + { + idealPhotoSize = GetOptimalSize(supportedJpegSizes, 1050, 1400); //MAGIC NUMBER WHICH HAS PROVEN TO BE THE BEST + } + + imageReader = ImageReader.NewInstance(idealPhotoSize.Width, idealPhotoSize.Height, ImageFormatType.Yuv420888, 5); + + flashSupported = HasFLash(characteristics); + + imageReader.SetOnImageAvailableListener(cameraEventListener, backgroundHandler); + + IdealPhotoSize = idealPhotoSize; + + PreviewSize = GetOptimalSize(map.GetOutputSizes(Class.FromType(typeof(SurfaceTexture))), width, height); + + lastCameraDisplayOrientationDegree = null; + } + + bool HasFLash(CameraCharacteristics characteristics) + { + var available = (Java.Lang.Boolean)characteristics.Get(CameraCharacteristics.FlashInfoAvailable); + if (available == null) + { + return false; + } + else + { + return (bool)available; + } + } + + public void OpenCamera(int width, int height) + { + if (context == null || OpeningCamera) + { + return; + } + + OpeningCamera = true; + + SetUpCameraOutputs(width, height); + + cameraManager.OpenCamera(CameraId, cameraStateCallback, backgroundHandler); + } + + public void StartPreview() + { + if (Camera == null || PreviewSize == null) return; + + previewBuilder = Camera.CreateCaptureRequest(CameraTemplate.Preview); + previewBuilder.AddTarget(holder.Surface); + previewBuilder.AddTarget(imageReader.Surface); + + var surfaces = new List(); + surfaces.Add(holder.Surface); + surfaces.Add(imageReader.Surface); + + Camera.CreateCaptureSession(surfaces, + new CameraCaptureStateListener + { + OnConfigureFailedAction = session => + { + }, + OnConfiguredAction = session => + { + previewSession = session; + UpdatePreview(); + } + }, + backgroundHandler); + } + + void UpdatePreview() + { + if (Camera == null || previewSession == null) return; + + // Reset the auto-focus trigger + previewBuilder.Set(CaptureRequest.ControlAfMode, (int)ControlAFMode.ContinuousPicture); + //SetAutoFlash(previewBuilder); + + previewRequest = previewBuilder.Build(); + previewSession.SetRepeatingRequest(previewRequest, null, backgroundHandler); + } + + Size GetOptimalSize(IList sizes, int h, int w) + { + var AspectTolerance = 0.1; + var targetRatio = (double)w / h; + + if (sizes == null) + { + return null; + } + + Size optimalSize = null; + var minDiff = double.MaxValue; + var targetHeight = h; + + while (optimalSize == null) + { + foreach (var size in sizes) + { + var ratio = (double)size.Width / size.Height; + + if (System.Math.Abs(ratio - targetRatio) > AspectTolerance) + continue; + if (System.Math.Abs(size.Height - targetHeight) < minDiff) + { + optimalSize = size; + minDiff = System.Math.Abs(size.Height - targetHeight); + } + } + + if (optimalSize == null) + AspectTolerance += 0.1f; + } + + return optimalSize; + } + + public void SetAutoFlash(CaptureRequest.Builder requestBuilder) + { + if (flashSupported) + { + requestBuilder.Set(CaptureRequest.ControlAeMode, (int)ControlAEMode.OnAutoFlash); + } + } + + void StartBackgroundThread() + { + backgroundThread = new HandlerThread("CameraBackground"); + backgroundThread.Start(); + backgroundHandler = new Handler(backgroundThread.Looper); + } + + public void EnableTorch(bool state) + { + if (state) + { + previewBuilder.Set(CaptureRequest.ControlAeMode, (int)ControlAEMode.On); + previewBuilder.Set(CaptureRequest.FlashMode, (int)FlashMode.Torch); + } + else + { + previewBuilder.Set(CaptureRequest.FlashMode, (int)FlashMode.Off); + } + + UpdatePreview(); + } + + void StopBackgroundThread() + { + if (backgroundHandler == null || backgroundThread == null) return; + + backgroundThread.QuitSafely(); + try + { + backgroundThread.Join(); + backgroundThread = null; + backgroundHandler = null; + } + catch (InterruptedException e) + { + e.PrintStackTrace(); + } + } + } + + public class CameraCaptureStateListener : CameraCaptureSession.StateCallback + { + public Action OnConfigureFailedAction; + + public Action OnConfiguredAction; + + public override void OnConfigureFailed(CameraCaptureSession session) + { + OnConfigureFailedAction?.Invoke(session); + } + + public override void OnConfigured(CameraCaptureSession session) + { + OnConfiguredAction?.Invoke(session); + } + } + + public class CameraStateCallback : CameraDevice.StateCallback + { + public Action OnDisconnectedAction; + public Action OnErrorAction; + public Action OnOpenedAction; + + public override void OnDisconnected(CameraDevice camera) => OnDisconnectedAction?.Invoke(camera); + + public override void OnError(CameraDevice camera, [GeneratedEnum] Android.Hardware.Camera2.CameraError error) => OnErrorAction?.Invoke(camera, error); + + public override void OnOpened(CameraDevice camera) + => OnOpenedAction?.Invoke(camera); + } +} diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs index e42eaf560..b7ccbd847 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs @@ -1,29 +1,58 @@ using System; -using Android.Hardware; +using System.Runtime.InteropServices; +using Android.Content; +using Android.Graphics; +using Android.Media; +using Android.Renderscripts; using ApxLabs.FastAndroidCamera; +using Java.IO; +using Java.Nio; +using static Android.Media.ImageReader; namespace ZXing.Mobile.CameraAccess { - public class CameraEventsListener : Java.Lang.Object, INonMarshalingPreviewCallback, Camera.IAutoFocusCallback - { - public event EventHandler OnPreviewFrameReady; + public class CameraEventsListener : Java.Lang.Object, IOnImageAvailableListener + { + public event EventHandler OnPreviewFrameReady; - public void OnPreviewFrame(IntPtr data, Camera camera) - { - if (data != null && data != IntPtr.Zero) - { - using (var fastArray = new FastJavaByteArray(data)) - { - OnPreviewFrameReady?.Invoke(this, fastArray); + public void OnImageAvailable(ImageReader reader) + { + Image image = null; + try + { + image = reader.AcquireLatestImage(); + if (image is null) return; + var yuvBytes = ImageToByteArray(image); + OnPreviewFrameReady?.Invoke(this, yuvBytes); + } + catch (Exception ex) + { + Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "Could not start preview session"); + } + finally + { + image?.Close(); + } + } - camera.AddCallbackBuffer(fastArray); - } - } - } + byte[] ImageToByteArray(Image image) + { + byte[] result; + var yBuffer = image.GetPlanes()[0].Buffer; + var uBuffer = image.GetPlanes()[1].Buffer; + var vBuffer = image.GetPlanes()[2].Buffer; - public void OnAutoFocus(bool success, Camera camera) - { - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "AutoFocus {0}", success ? "Succeeded" : "Failed"); - } - } + var ySize = yBuffer.Remaining(); + var uSize = uBuffer.Remaining(); + var vSize = vBuffer.Remaining(); + + result = new byte[ySize + uSize + vSize]; + + yBuffer.Get(result, 0, ySize); + vBuffer.Get(result, ySize, vSize); + uBuffer.Get(result, ySize + vSize, uSize); + + return result; + } + } } diff --git a/ZXing.Net.Mobile/Android/CameraAccess/Torch.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/Torch.android.cs index faacb42f5..5f4b5c431 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/Torch.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/Torch.android.cs @@ -1,94 +1,75 @@ using Android.Content; using Android.Content.PM; using Android.Hardware; +using Android.Hardware.Camera2; +using Android.Hardware.Camera2.Params; namespace ZXing.Mobile.CameraAccess { - public class Torch - { - readonly CameraController cameraController; - readonly Context context; - bool? hasTorch; - - public Torch(CameraController cameraController, Context context) - { - this.cameraController = cameraController; - this.context = context; - } - - public bool IsSupported - { - get - { - if (hasTorch.HasValue) - return hasTorch.Value; - - if (!context.PackageManager.HasSystemFeature(PackageManager.FeatureCameraFlash)) - { - Android.Util.Log.Info(MobileBarcodeScanner.TAG, "Flash not supported on this device"); - return false; - } - - if (cameraController.Camera == null) - { - Android.Util.Log.Info(MobileBarcodeScanner.TAG, "Run camera first"); - return false; - } - - var p = cameraController.Camera.GetParameters(); - var supportedFlashModes = p.SupportedFlashModes; - - if ((supportedFlashModes != null) - && (supportedFlashModes.Contains(Camera.Parameters.FlashModeTorch) - || supportedFlashModes.Contains(Camera.Parameters.FlashModeOn))) - hasTorch = ZXing.Net.Mobile.Android.PermissionsHandler.IsTorchPermissionDeclared(); - - return hasTorch != null && hasTorch.Value; - } - } - - public bool IsEnabled { get; private set; } - - public void TurnOn() => Enable(true); - - public void TurnOff() => Enable(false); - - public void Toggle() => Enable(!IsEnabled); - - private void Enable(bool state) - { - if (!IsSupported || IsEnabled == state) - return; - - if (cameraController.Camera == null) - { - Android.Util.Log.Info(MobileBarcodeScanner.TAG, "NULL Camera, cannot toggle torch"); - return; - } - - var parameters = cameraController.Camera.GetParameters(); - var supportedFlashModes = parameters.SupportedFlashModes; - - var flashMode = string.Empty; - if (state) - { - if (supportedFlashModes.Contains(Camera.Parameters.FlashModeTorch)) - flashMode = Camera.Parameters.FlashModeTorch; - else if (supportedFlashModes.Contains(Camera.Parameters.FlashModeOn)) - flashMode = Camera.Parameters.FlashModeOn; - } - else - { - if (supportedFlashModes != null && supportedFlashModes.Contains(Camera.Parameters.FlashModeOff)) - flashMode = Camera.Parameters.FlashModeOff; - } - - if (!string.IsNullOrEmpty(flashMode)) - { - parameters.FlashMode = flashMode; - cameraController.Camera.SetParameters(parameters); - IsEnabled = state; - } - } - } -} \ No newline at end of file + public class Torch + { + readonly CameraController cameraController; + readonly Context context; + CameraManager cameraManager; + bool? hasTorch; + + public Torch(CameraController cameraController, Context context) + { + this.cameraController = cameraController; + this.context = context; + cameraManager = (CameraManager)context.GetSystemService(Context.CameraService); + } + + public bool IsSupported + { + get + { + if (hasTorch.HasValue) + return hasTorch.Value; + + if (!context.PackageManager.HasSystemFeature(PackageManager.FeatureCameraFlash)) + { + Android.Util.Log.Info(MobileBarcodeScanner.TAG, "Flash not supported on this device"); + return false; + } + + if (cameraController.Camera == null) + { + Android.Util.Log.Info(MobileBarcodeScanner.TAG, "Run camera first"); + return false; + } + + var characteristics = cameraManager.GetCameraCharacteristics(cameraController.CameraId.ToString()); + var cameraHasTorch = ((Java.Lang.Boolean)characteristics.Get(CameraCharacteristics.FlashInfoAvailable)).BooleanValue(); + + if (cameraHasTorch) + hasTorch = ZXing.Net.Mobile.Android.PermissionsHandler.IsTorchPermissionDeclared(); + + return hasTorch != null && hasTorch.Value; + } + } + + public bool IsEnabled { get; private set; } + + public void TurnOn() => Enable(true); + + public void TurnOff() => Enable(false); + + public void Toggle() => Enable(!IsEnabled); + + private void Enable(bool state) + { + if (!IsSupported || IsEnabled == state) + return; + + if (cameraController.Camera == null) + { + Android.Util.Log.Info(MobileBarcodeScanner.TAG, "NULL Camera, cannot toggle torch"); + return; + } + + cameraController.EnableTorch(state); + IsEnabled = state; + } + } +} diff --git a/ZXing.Net.Mobile/Android/ZXingSurfaceView.android.cs b/ZXing.Net.Mobile/Android/ZXingSurfaceView.android.cs index 0fe0c87f1..abd5876e0 100644 --- a/ZXing.Net.Mobile/Android/ZXingSurfaceView.android.cs +++ b/ZXing.Net.Mobile/Android/ZXingSurfaceView.android.cs @@ -5,156 +5,167 @@ using Android.Graphics; using ZXing.Mobile.CameraAccess; using ZXing.Net.Mobile.Android; +using System.Threading.Tasks; +using System.Threading; namespace ZXing.Mobile { - public class ZXingSurfaceView : SurfaceView, ISurfaceHolderCallback, IScannerView, IScannerSessionHost - { - public ZXingSurfaceView(Context context, MobileBarcodeScanningOptions options) - : base(context) - { - ScanningOptions = options ?? new MobileBarcodeScanningOptions(); - Init(); - } - - protected ZXingSurfaceView(IntPtr javaReference, JniHandleOwnership transfer) - : base(javaReference, transfer) => Init(); - - bool addedHolderCallback = false; - - void Init() - { - if (cameraAnalyzer == null) - cameraAnalyzer = new CameraAnalyzer(this, this); - - cameraAnalyzer.ResumeAnalysis(); - - if (!addedHolderCallback) - { - Holder.AddCallback(this); - Holder.SetType(SurfaceType.PushBuffers); - addedHolderCallback = true; - } - } - - public async void SurfaceCreated(ISurfaceHolder holder) - { - await PermissionsHandler.RequestPermissionsAsync(); - - cameraAnalyzer.SetupCamera(); - - surfaceCreated = true; - } - - public async void SurfaceChanged(ISurfaceHolder holder, Format format, int wx, int hx) - => cameraAnalyzer.RefreshCamera(); - - public async void SurfaceDestroyed(ISurfaceHolder holder) - { - try - { - if (addedHolderCallback) - { - Holder.RemoveCallback(this); - addedHolderCallback = false; - } - } - catch { } - - cameraAnalyzer.ShutdownCamera(); - } - - public override bool OnTouchEvent(MotionEvent e) - { - var r = base.OnTouchEvent(e); - - switch (e.Action) - { - case MotionEventActions.Down: - return true; - case MotionEventActions.Up: - var touchX = e.GetX(); - var touchY = e.GetY(); - AutoFocus((int)touchX, (int)touchY); - break; - } - - return r; - } - - public void AutoFocus() - => cameraAnalyzer.AutoFocus(); - - public void AutoFocus(int x, int y) - => cameraAnalyzer.AutoFocus(x, y); - - public void StartScanning(Action scanResultCallback, MobileBarcodeScanningOptions options = null) - { - cameraAnalyzer.SetupCamera(); - - ScanningOptions = options ?? MobileBarcodeScanningOptions.Default; - - cameraAnalyzer.BarcodeFound = (result) => - scanResultCallback?.Invoke(result); - cameraAnalyzer.ResumeAnalysis(); - } - - public void StopScanning() - => cameraAnalyzer.ShutdownCamera(); - - public void PauseAnalysis() - => cameraAnalyzer.PauseAnalysis(); - - public void ResumeAnalysis() - => cameraAnalyzer.ResumeAnalysis(); - - public void Torch(bool on) - { - if (on) - cameraAnalyzer.Torch.TurnOn(); - else - cameraAnalyzer.Torch.TurnOff(); - } - - public void ToggleTorch() - => cameraAnalyzer.Torch.Toggle(); - - public MobileBarcodeScanningOptions ScanningOptions { get; set; } - - public bool IsTorchOn => cameraAnalyzer.Torch.IsEnabled; - - public bool IsAnalyzing => cameraAnalyzer.IsAnalyzing; - - CameraAnalyzer cameraAnalyzer; - bool surfaceCreated; - - public bool HasTorch => cameraAnalyzer.Torch.IsSupported; - - protected override void OnAttachedToWindow() - { - base.OnAttachedToWindow(); - - // Reinit things - Init(); - } - - protected override void OnWindowVisibilityChanged(ViewStates visibility) - { - base.OnWindowVisibilityChanged(visibility); - if (visibility == ViewStates.Visible) - Init(); - } - - public override async void OnWindowFocusChanged(bool hasWindowFocus) - { - base.OnWindowFocusChanged(hasWindowFocus); - - if (!hasWindowFocus) - return; - - //only refresh the camera if the surface has already been created. Fixed #569 - if (surfaceCreated) - cameraAnalyzer.RefreshCamera(); - } - } + public class ZXingSurfaceView : SurfaceView, ISurfaceHolderCallback, IScannerView, IScannerSessionHost + { + public ZXingSurfaceView(Context context, MobileBarcodeScanningOptions options) + : base(context) + { + ScanningOptions = options ?? new MobileBarcodeScanningOptions(); + Init(); + } + + protected ZXingSurfaceView(IntPtr javaReference, JniHandleOwnership transfer) + : base(javaReference, transfer) => Init(); + + bool addedHolderCallback = false; + + void Init() + { + if (cameraAnalyzer == null) + cameraAnalyzer = new CameraAnalyzer(this, this); + + cameraAnalyzer.ResumeAnalysis(); + + if (!addedHolderCallback) + { + Holder.AddCallback(this); + Holder.SetType(SurfaceType.PushBuffers); + addedHolderCallback = true; + } + } + + public async void SurfaceCreated(ISurfaceHolder holder) + { + await PermissionsHandler.RequestPermissionsAsync(); + + cameraAnalyzer.SetupCamera(Width, Height); + + surfaceCreated = true; + surfaceCreatedResetEvent.Set(); + } + + public async void SurfaceChanged(ISurfaceHolder holder, Format format, int wx, int hx) + { + holder.SetFixedSize(wx, hx); + cameraAnalyzer.RefreshCamera(wx, hx); + } + + public async void SurfaceDestroyed(ISurfaceHolder holder) + { + try + { + if (addedHolderCallback) + { + Holder.RemoveCallback(this); + addedHolderCallback = false; + } + } + catch { } + + cameraAnalyzer.ShutdownCamera(); + } + + public override bool OnTouchEvent(MotionEvent e) + { + var r = base.OnTouchEvent(e); + + switch (e.Action) + { + case MotionEventActions.Down: + return true; + case MotionEventActions.Up: + var touchX = e.GetX(); + var touchY = e.GetY(); + AutoFocus((int)touchX, (int)touchY); + break; + } + + return r; + } + + public void AutoFocus() + => cameraAnalyzer.AutoFocus(); + + public void AutoFocus(int x, int y) + => cameraAnalyzer.AutoFocus(x, y); + + public void StartScanning(Action scanResultCallback, MobileBarcodeScanningOptions options = null) + { + Task.Run(() => + { + surfaceCreatedResetEvent.Wait(); + surfaceCreatedResetEvent.Reset(); + + ScanningOptions = options ?? MobileBarcodeScanningOptions.Default; + + cameraAnalyzer.BarcodeFound = (result) => + scanResultCallback?.Invoke(result); + cameraAnalyzer.ResumeAnalysis(); + }); + } + + public void StopScanning() + => cameraAnalyzer.ShutdownCamera(); + + public void PauseAnalysis() + => cameraAnalyzer.PauseAnalysis(); + + public void ResumeAnalysis() + => cameraAnalyzer.ResumeAnalysis(); + + public void Torch(bool on) + { + if (on) + cameraAnalyzer.Torch.TurnOn(); + else + cameraAnalyzer.Torch.TurnOff(); + } + + public void ToggleTorch() + => cameraAnalyzer.Torch.Toggle(); + + public MobileBarcodeScanningOptions ScanningOptions { get; set; } + + public bool IsTorchOn => cameraAnalyzer.Torch.IsEnabled; + + public bool IsAnalyzing => cameraAnalyzer.IsAnalyzing; + + CameraAnalyzer cameraAnalyzer; + bool surfaceCreated; + ManualResetEventSlim surfaceCreatedResetEvent = new ManualResetEventSlim(false); + + public bool HasTorch => cameraAnalyzer.Torch.IsSupported; + + protected override void OnAttachedToWindow() + { + base.OnAttachedToWindow(); + + // Reinit things + Init(); + } + + protected override void OnWindowVisibilityChanged(ViewStates visibility) + { + base.OnWindowVisibilityChanged(visibility); + if (visibility == ViewStates.Visible) + Init(); + } + + public override async void OnWindowFocusChanged(bool hasWindowFocus) + { + base.OnWindowFocusChanged(hasWindowFocus); + + if (!hasWindowFocus) + return; + + //only refresh the camera if the surface has already been created. Fixed #569 + if (surfaceCreated) + cameraAnalyzer.RefreshCamera(Width, Height); + } + } } From 9f6acb4ada0a3af716fdd5b6b0c1ec5747f8d0b3 Mon Sep 17 00:00:00 2001 From: Tim Nolte Date: Fri, 11 Dec 2020 15:10:05 +0100 Subject: [PATCH 02/43] Refactoring --- .../CameraAccess/CameraAnalyzer.android.cs | 8 +- .../CameraCaptureStateListener.android.cs | 22 ++ .../CameraAccess/CameraController.android.cs | 361 +++++++----------- .../CameraStateCallback.android.cs | 22 ++ .../Android/ZXingSurfaceView.android.cs | 6 +- 5 files changed, 194 insertions(+), 225 deletions(-) create mode 100644 ZXing.Net.Mobile/Android/CameraAccess/CameraCaptureStateListener.android.cs create mode 100644 ZXing.Net.Mobile/Android/CameraAccess/CameraStateCallback.android.cs diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs index 4e63473b5..1ec0c855b 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs @@ -49,10 +49,10 @@ public void ShutdownCamera() cameraController.ShutdownCamera(); } - public void SetupCamera(int width, int height) + public void SetupCamera() { cameraEventListener.OnPreviewFrameReady += HandleOnPreviewFrameReady; - cameraController.SetupCamera(width, height); + cameraController.SetupCamera(); } public void AutoFocus() @@ -61,9 +61,9 @@ public void AutoFocus() public void AutoFocus(int x, int y) => cameraController.AutoFocus(x, y); - public void RefreshCamera(int width, int height) + public void RefreshCamera() { - cameraController.RefreshCamera(width, height); + cameraController.RefreshCamera(); } bool CanAnalyzeFrame diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraCaptureStateListener.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraCaptureStateListener.android.cs new file mode 100644 index 000000000..5fb1b5b9d --- /dev/null +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraCaptureStateListener.android.cs @@ -0,0 +1,22 @@ +using System; +using Android.Hardware.Camera2; + +namespace ZXing.Mobile.CameraAccess +{ + public class CameraCaptureStateListener : CameraCaptureSession.StateCallback + { + public Action OnConfigureFailedAction; + + public Action OnConfiguredAction; + + public override void OnConfigureFailed(CameraCaptureSession session) + { + OnConfigureFailedAction?.Invoke(session); + } + + public override void OnConfigured(CameraCaptureSession session) + { + OnConfiguredAction?.Invoke(session); + } + } +} diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs index 0b20fd1b4..b9a99d17c 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs @@ -1,20 +1,15 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading; using Android.Content; using Android.Graphics; -using Android.Hardware; using Android.Hardware.Camera2; using Android.Hardware.Camera2.Params; using Android.Media; using Android.OS; -using Android.Runtime; using Android.Util; using Android.Views; -using ApxLabs.FastAndroidCamera; using Java.Lang; -using Java.Util.Concurrent; namespace ZXing.Mobile.CameraAccess { @@ -28,16 +23,13 @@ public class CameraController readonly CameraStateCallback cameraStateCallback; CameraManager cameraManager; - private Size[] supportedJpegSizes; - private Size idealPhotoSize; - private ImageReader imageReader; - private bool flashSupported; - private Handler backgroundHandler; - private CaptureRequest.Builder previewBuilder; - private CameraCaptureSession previewSession; - private CaptureRequest previewRequest; - private HandlerThread backgroundThread; - private int? lastCameraDisplayOrientationDegree; + ImageReader imageReader; + bool flashSupported; + Handler backgroundHandler; + CaptureRequest.Builder previewBuilder; + CameraCaptureSession previewSession; + CaptureRequest previewRequest; + HandlerThread backgroundThread; public string CameraId { get; private set; } @@ -45,23 +37,8 @@ public class CameraController public CameraDevice Camera { get; private set; } - public Size PreviewSize { get; private set; } - public Size IdealPhotoSize { get; private set; } - public int LastCameraDisplayOrientationDegree - { - get - { - if (lastCameraDisplayOrientationDegree is null) - { - lastCameraDisplayOrientationDegree = GetCameraDisplayOrientation(); - } - - return lastCameraDisplayOrientationDegree.Value; - } - } - public CameraController(SurfaceView surfaceView, CameraEventsListener cameraEventListener, IScannerSessionHost scannerHost) { context = surfaceView.Context; @@ -95,21 +72,22 @@ public CameraController(SurfaceView surfaceView, CameraEventsListener cameraEven }; } - public void RefreshCamera(int width, int height) + public void RefreshCamera() { if (Camera is null || previewRequest is null || previewSession is null || previewBuilder is null) return; - SetUpCameraOutputs(width, height); + + SetUpCameraOutputs(); previewRequest.Dispose(); previewSession.Dispose(); previewBuilder.Dispose(); StartPreview(); } - public void SetupCamera(int width, int height) + public void SetupCamera() { StartBackgroundThread(); - OpenCamera(width, height); + OpenCamera(); } public void ShutdownCamera() @@ -136,74 +114,30 @@ public void AutoFocus(int x, int y) AutoFocus(focusX, focusY, true); } - int GetCameraDisplayOrientation() - { - int degrees; - var windowManager = context.GetSystemService(Context.WindowService).JavaCast(); - var display = windowManager.DefaultDisplay; - var rotation = display.Rotation; - - switch (rotation) - { - case SurfaceOrientation.Rotation0: - degrees = 0; - break; - case SurfaceOrientation.Rotation90: - degrees = 90; - break; - case SurfaceOrientation.Rotation180: - degrees = 180; - break; - case SurfaceOrientation.Rotation270: - degrees = 270; - break; - default: - throw new ArgumentOutOfRangeException(); - } - - var characteristics = cameraManager.GetCameraCharacteristics(CameraId); - var facing = (int)characteristics.Get(CameraCharacteristics.LensFacing); - var orientation = (int)characteristics.Get(CameraCharacteristics.SensorOrientation); - int correctedDegrees; - if (facing == (int)CameraFacing.Front) - { - correctedDegrees = (orientation + degrees) % 360; - correctedDegrees = (360 - correctedDegrees) % 360; // compensate the mirror - } - else - { - // back-facing - correctedDegrees = (orientation - degrees + 360) % 360; - } - - return correctedDegrees; - } - void AutoFocus(int x, int y, bool useCoordinates) { if (Camera == null) return; - if (scannerHost.ScanningOptions.DisableAutofocus) + try { - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "AutoFocus Disabled"); - return; - } + if (scannerHost.ScanningOptions.DisableAutofocus) + { + Log.Debug(MobileBarcodeScanner.TAG, "AutoFocus Disabled"); + return; + } - var characteristics = cameraManager.GetCameraCharacteristics(CameraId.ToString()); - var map = (StreamConfigurationMap)characteristics.Get(CameraCharacteristics.ScalerStreamConfigurationMap); - var supportedFocusModes = ((int[])characteristics - .Get(CameraCharacteristics.ControlAfAvailableModes)) - .Select(x => (ControlAFMode)x); + var characteristics = cameraManager.GetCameraCharacteristics(CameraId.ToString()); + var map = (StreamConfigurationMap)characteristics.Get(CameraCharacteristics.ScalerStreamConfigurationMap); + var supportedFocusModes = ((int[])characteristics + .Get(CameraCharacteristics.ControlAfAvailableModes)) + .Select(x => (ControlAFMode)x); - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "AutoFocus Requested"); + Log.Debug(MobileBarcodeScanner.TAG, "AutoFocus Requested"); - try - { // If we want to use coordinates // Also only if our camera supports Auto focus mode // Since FocusAreas only really work with FocusModeAuto set - if (useCoordinates - && supportedFocusModes.Contains(ControlAFMode.Auto)) + if (useCoordinates && supportedFocusModes.Contains(ControlAFMode.Auto)) { // Let's give the touched area a 20 x 20 minimum size rect to focus on // So we'll offset -10 from the center of the touch and then @@ -237,62 +171,72 @@ void AutoFocus(int x, int y, bool useCoordinates) // Finally autofocus (weather we used focus areas or not) previewBuilder.Set(CaptureRequest.ControlAfTrigger, (int)ControlAFTrigger.Start); + + UpdatePreview(); } catch (System.Exception ex) { - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "AutoFocus Failed: {0}", ex); + Log.Debug(MobileBarcodeScanner.TAG, "AutoFocus Failed: {0}", ex); } } - void SetUpCameraOutputs(int width, int height) + void SetUpCameraOutputs() { - cameraManager = (CameraManager)context.GetSystemService(Context.CameraService); + try + { + cameraManager = (CameraManager)context.GetSystemService(Context.CameraService); - var cameraIds = cameraManager.GetCameraIdList(); + var cameraIds = cameraManager.GetCameraIdList(); - CameraId = cameraIds[0]; + CameraId = cameraIds[0]; - for (var i = 0; i < cameraIds.Length; i++) - { - var cameraCharacteristics = cameraManager.GetCameraCharacteristics(cameraIds[i]); + var whichCamera = LensFacing.Back; - var facing = (Integer)cameraCharacteristics.Get(CameraCharacteristics.LensFacing); - if (facing != null && facing == (Integer.ValueOf((int)LensFacing.Back))) + if (scannerHost.ScanningOptions.UseFrontCameraIfAvailable.HasValue && + scannerHost.ScanningOptions.UseFrontCameraIfAvailable.Value) + whichCamera = LensFacing.Front; + + for (var i = 0; i < cameraIds.Length; i++) { - CameraId = cameraIds[i]; + var cameraCharacteristics = cameraManager.GetCameraCharacteristics(cameraIds[i]); - //Phones like Galaxy S10 have 2 or 3 frontal cameras usually the one with flash is the one - //that should be chosen, if not It will select the first one and that can be the fish - //eye camera - if (HasFLash(cameraCharacteristics)) + var facing = (Integer)cameraCharacteristics.Get(CameraCharacteristics.LensFacing); + if (facing != null && facing.IntValue() == (int)whichCamera) + { + CameraId = cameraIds[i]; break; + } } - } - - var characteristics = cameraManager.GetCameraCharacteristics(CameraId); - var map = (StreamConfigurationMap)characteristics.Get(CameraCharacteristics.ScalerStreamConfigurationMap); - - if (supportedJpegSizes == null && characteristics != null) - { - supportedJpegSizes = ((StreamConfigurationMap)characteristics.Get(CameraCharacteristics.ScalerStreamConfigurationMap)).GetOutputSizes((int)ImageFormatType.Yuv420888); - } - if (supportedJpegSizes != null && supportedJpegSizes.Length > 0) - { - idealPhotoSize = GetOptimalSize(supportedJpegSizes, 1050, 1400); //MAGIC NUMBER WHICH HAS PROVEN TO BE THE BEST - } + var characteristics = cameraManager.GetCameraCharacteristics(CameraId); + var map = (StreamConfigurationMap)characteristics.Get(CameraCharacteristics.ScalerStreamConfigurationMap); + Size[] supportedSizes = null; - imageReader = ImageReader.NewInstance(idealPhotoSize.Width, idealPhotoSize.Height, ImageFormatType.Yuv420888, 5); + if (characteristics != null) + supportedSizes = ((StreamConfigurationMap)characteristics + .Get(CameraCharacteristics.ScalerStreamConfigurationMap)) + .GetOutputSizes((int)ImageFormatType.Yuv420888); - flashSupported = HasFLash(characteristics); + if (supportedSizes is null || supportedSizes.Length == 0) + { + Log.Debug(MobileBarcodeScanner.TAG, "Failed to get supported output sizes"); + return; + } - imageReader.SetOnImageAvailableListener(cameraEventListener, backgroundHandler); + // 1050 and 1400 are a random guess which work pretty good + var idealSize = GetOptimalSize(supportedSizes, 1050, 1400); + imageReader = ImageReader.NewInstance(idealSize.Width, idealSize.Height, ImageFormatType.Yuv420888, 5); - IdealPhotoSize = idealPhotoSize; + flashSupported = HasFLash(characteristics); - PreviewSize = GetOptimalSize(map.GetOutputSizes(Class.FromType(typeof(SurfaceTexture))), width, height); + imageReader.SetOnImageAvailableListener(cameraEventListener, backgroundHandler); - lastCameraDisplayOrientationDegree = null; + IdealPhotoSize = idealSize; + } + catch (System.Exception ex) + { + Log.Debug(MobileBarcodeScanner.TAG, "Could not setup camera outputs" + ex); + } } bool HasFLash(CameraCharacteristics characteristics) @@ -308,81 +252,96 @@ bool HasFLash(CameraCharacteristics characteristics) } } - public void OpenCamera(int width, int height) + public void OpenCamera() { if (context == null || OpeningCamera) { return; } - OpeningCamera = true; + try + { + OpeningCamera = true; - SetUpCameraOutputs(width, height); + SetUpCameraOutputs(); - cameraManager.OpenCamera(CameraId, cameraStateCallback, backgroundHandler); + cameraManager.OpenCamera(CameraId, cameraStateCallback, backgroundHandler); + } + catch (System.Exception ex) + { + Log.Debug(MobileBarcodeScanner.TAG, "Error on opening camera" + ex); + } } public void StartPreview() { - if (Camera == null || PreviewSize == null) return; + if (Camera is null || holder is null || imageReader is null || backgroundHandler is null) return; - previewBuilder = Camera.CreateCaptureRequest(CameraTemplate.Preview); - previewBuilder.AddTarget(holder.Surface); - previewBuilder.AddTarget(imageReader.Surface); + try + { + previewBuilder = Camera.CreateCaptureRequest(CameraTemplate.Preview); + previewBuilder.AddTarget(holder.Surface); + previewBuilder.AddTarget(imageReader.Surface); - var surfaces = new List(); - surfaces.Add(holder.Surface); - surfaces.Add(imageReader.Surface); + var surfaces = new List(); + surfaces.Add(holder.Surface); + surfaces.Add(imageReader.Surface); - Camera.CreateCaptureSession(surfaces, - new CameraCaptureStateListener - { - OnConfigureFailedAction = session => + Camera.CreateCaptureSession(surfaces, + new CameraCaptureStateListener { + OnConfigureFailedAction = session => + { + }, + OnConfiguredAction = session => + { + previewSession = session; + UpdatePreview(); + } }, - OnConfiguredAction = session => - { - previewSession = session; - UpdatePreview(); - } - }, - backgroundHandler); + backgroundHandler); + } + catch (System.Exception ex) + { + Log.Debug(MobileBarcodeScanner.TAG, "Error on starting preview" + ex); + } } void UpdatePreview() { - if (Camera == null || previewSession == null) return; - - // Reset the auto-focus trigger - previewBuilder.Set(CaptureRequest.ControlAfMode, (int)ControlAFMode.ContinuousPicture); - //SetAutoFlash(previewBuilder); + if (Camera is null || previewSession is null) return; - previewRequest = previewBuilder.Build(); - previewSession.SetRepeatingRequest(previewRequest, null, backgroundHandler); + try + { + previewRequest = previewBuilder.Build(); + previewSession.SetRepeatingRequest(previewRequest, null, backgroundHandler); + } + catch (System.Exception ex) + { + Log.Debug(MobileBarcodeScanner.TAG, "Error on updating preview" + ex); + } } - Size GetOptimalSize(IList sizes, int h, int w) + Size GetOptimalSize(IList sizes, int width, int height) { - var AspectTolerance = 0.1; - var targetRatio = (double)w / h; + if (sizes is null) return null; - if (sizes == null) - { - return null; - } + var aspectTolerance = 0.1; + var targetRatio = (double)width / height; Size optimalSize = null; var minDiff = double.MaxValue; - var targetHeight = h; + var targetHeight = height; - while (optimalSize == null) + while (optimalSize is null) { foreach (var size in sizes) { var ratio = (double)size.Width / size.Height; - if (System.Math.Abs(ratio - targetRatio) > AspectTolerance) + if (System.Math.Abs(ratio - targetRatio) > aspectTolerance) continue; + if (System.Math.Abs(size.Height - targetHeight) < minDiff) { optimalSize = size; @@ -391,88 +350,54 @@ Size GetOptimalSize(IList sizes, int h, int w) } if (optimalSize == null) - AspectTolerance += 0.1f; + aspectTolerance += 0.1f; } return optimalSize; } - public void SetAutoFlash(CaptureRequest.Builder requestBuilder) - { - if (flashSupported) - { - requestBuilder.Set(CaptureRequest.ControlAeMode, (int)ControlAEMode.OnAutoFlash); - } - } - void StartBackgroundThread() { - backgroundThread = new HandlerThread("CameraBackground"); + backgroundThread = new HandlerThread("CameraBackgroundThread"); backgroundThread.Start(); backgroundHandler = new Handler(backgroundThread.Looper); } public void EnableTorch(bool state) { - if (state) + try { - previewBuilder.Set(CaptureRequest.ControlAeMode, (int)ControlAEMode.On); - previewBuilder.Set(CaptureRequest.FlashMode, (int)FlashMode.Torch); + if (!flashSupported || previewBuilder is null) return; + + if (state) + { + previewBuilder.Set(CaptureRequest.ControlAeMode, (int)ControlAEMode.On); + previewBuilder.Set(CaptureRequest.FlashMode, (int)FlashMode.Torch); + } + else + previewBuilder.Set(CaptureRequest.FlashMode, (int)FlashMode.Off); + + UpdatePreview(); } - else + catch (System.Exception ex) { - previewBuilder.Set(CaptureRequest.FlashMode, (int)FlashMode.Off); + Log.Debug(MobileBarcodeScanner.TAG, "Error on enabling torch" + ex); } - - UpdatePreview(); } void StopBackgroundThread() { - if (backgroundHandler == null || backgroundThread == null) return; - - backgroundThread.QuitSafely(); try { - backgroundThread.Join(); + backgroundThread?.QuitSafely(); + backgroundThread?.Join(); backgroundThread = null; backgroundHandler = null; } - catch (InterruptedException e) + catch (InterruptedException ex) { - e.PrintStackTrace(); + Log.Debug(MobileBarcodeScanner.TAG, "Error stopping background threads: " + ex); } } } - - public class CameraCaptureStateListener : CameraCaptureSession.StateCallback - { - public Action OnConfigureFailedAction; - - public Action OnConfiguredAction; - - public override void OnConfigureFailed(CameraCaptureSession session) - { - OnConfigureFailedAction?.Invoke(session); - } - - public override void OnConfigured(CameraCaptureSession session) - { - OnConfiguredAction?.Invoke(session); - } - } - - public class CameraStateCallback : CameraDevice.StateCallback - { - public Action OnDisconnectedAction; - public Action OnErrorAction; - public Action OnOpenedAction; - - public override void OnDisconnected(CameraDevice camera) => OnDisconnectedAction?.Invoke(camera); - - public override void OnError(CameraDevice camera, [GeneratedEnum] Android.Hardware.Camera2.CameraError error) => OnErrorAction?.Invoke(camera, error); - - public override void OnOpened(CameraDevice camera) - => OnOpenedAction?.Invoke(camera); - } } diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraStateCallback.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraStateCallback.android.cs new file mode 100644 index 000000000..804553d2f --- /dev/null +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraStateCallback.android.cs @@ -0,0 +1,22 @@ +using System; +using Android.Hardware.Camera2; +using Android.Runtime; + +namespace ZXing.Mobile.CameraAccess +{ + public class CameraStateCallback : CameraDevice.StateCallback + { + public Action OnDisconnectedAction; + public Action OnErrorAction; + public Action OnOpenedAction; + + public override void OnDisconnected(CameraDevice camera) + => OnDisconnectedAction?.Invoke(camera); + + public override void OnError(CameraDevice camera, [GeneratedEnum] CameraError error) + => OnErrorAction?.Invoke(camera, error); + + public override void OnOpened(CameraDevice camera) + => OnOpenedAction?.Invoke(camera); + } +} diff --git a/ZXing.Net.Mobile/Android/ZXingSurfaceView.android.cs b/ZXing.Net.Mobile/Android/ZXingSurfaceView.android.cs index abd5876e0..5928234da 100644 --- a/ZXing.Net.Mobile/Android/ZXingSurfaceView.android.cs +++ b/ZXing.Net.Mobile/Android/ZXingSurfaceView.android.cs @@ -43,7 +43,7 @@ public async void SurfaceCreated(ISurfaceHolder holder) { await PermissionsHandler.RequestPermissionsAsync(); - cameraAnalyzer.SetupCamera(Width, Height); + cameraAnalyzer.SetupCamera(); surfaceCreated = true; surfaceCreatedResetEvent.Set(); @@ -52,7 +52,7 @@ public async void SurfaceCreated(ISurfaceHolder holder) public async void SurfaceChanged(ISurfaceHolder holder, Format format, int wx, int hx) { holder.SetFixedSize(wx, hx); - cameraAnalyzer.RefreshCamera(wx, hx); + cameraAnalyzer.RefreshCamera(); } public async void SurfaceDestroyed(ISurfaceHolder holder) @@ -165,7 +165,7 @@ public override async void OnWindowFocusChanged(bool hasWindowFocus) //only refresh the camera if the surface has already been created. Fixed #569 if (surfaceCreated) - cameraAnalyzer.RefreshCamera(Width, Height); + cameraAnalyzer.RefreshCamera(); } } } From a62bb1c5a2abed8dc822f8366e84070349403f24 Mon Sep 17 00:00:00 2001 From: Tim Nolte Date: Mon, 14 Dec 2020 12:41:38 +0100 Subject: [PATCH 03/43] Fixed distorted preview --- .../CameraAccess/CameraController.android.cs | 39 +++++++++++++++++++ .../Android/ZXingSurfaceView.android.cs | 1 - 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs index b9a99d17c..e3d88fa23 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using Android.Content; using Android.Graphics; using Android.Hardware.Camera2; @@ -273,12 +274,50 @@ public void OpenCamera() } } + private Size GetOptimalPreviewSize(SurfaceView surface) + { + var characteristics = cameraManager.GetCameraCharacteristics(CameraId); + var map = (StreamConfigurationMap)characteristics.Get(CameraCharacteristics.ScalerStreamConfigurationMap); + var availableSizes = ((StreamConfigurationMap)characteristics + .Get(CameraCharacteristics.ScalerStreamConfigurationMap)) + .GetOutputSizes(Class.FromType(typeof(ISurfaceHolder))); + + var aspectRatio = (double)surface.Height / (double)surface.Width; + var availableAspectRatios = availableSizes.Select(x => (x, (double)x.Width / (double)x.Height)); + + var differences = availableAspectRatios.Select(x => (x.x, System.Math.Abs(x.Item2 - aspectRatio))); + var bestMatches = differences.OrderBy(x => x.Item2).ThenBy(x => System.Math.Abs(x.x.Width - surface.Width)).ThenBy(x => System.Math.Abs(x.x.Height - surface.Height)).Take(5); + return bestMatches.OrderByDescending(x => x.x.Width).ThenByDescending(x => x.x.Height).First().x; + } + public void StartPreview() { if (Camera is null || holder is null || imageReader is null || backgroundHandler is null) return; try { + var optimalPreviewSize = GetOptimalPreviewSize(surfaceView); + + if (Looper.MyLooper() == Looper.MainLooper) + { + holder.SetFixedSize(optimalPreviewSize.Width, optimalPreviewSize.Height); + } + else + { + var sizeSetResetEvent = new ManualResetEventSlim(false); + using (var handler = new Handler(Looper.MainLooper)) + { + handler.Post(() => + { + holder.SetFixedSize(optimalPreviewSize.Width, optimalPreviewSize.Height); + sizeSetResetEvent.Set(); + }); + } + + sizeSetResetEvent.Wait(); + sizeSetResetEvent.Reset(); + } + previewBuilder = Camera.CreateCaptureRequest(CameraTemplate.Preview); previewBuilder.AddTarget(holder.Surface); previewBuilder.AddTarget(imageReader.Surface); diff --git a/ZXing.Net.Mobile/Android/ZXingSurfaceView.android.cs b/ZXing.Net.Mobile/Android/ZXingSurfaceView.android.cs index 5928234da..4c9049e49 100644 --- a/ZXing.Net.Mobile/Android/ZXingSurfaceView.android.cs +++ b/ZXing.Net.Mobile/Android/ZXingSurfaceView.android.cs @@ -51,7 +51,6 @@ public async void SurfaceCreated(ISurfaceHolder holder) public async void SurfaceChanged(ISurfaceHolder holder, Format format, int wx, int hx) { - holder.SetFixedSize(wx, hx); cameraAnalyzer.RefreshCamera(); } From 648d77257474e6439bbedebf35963e83b005c698 Mon Sep 17 00:00:00 2001 From: Tim Nolte Date: Mon, 21 Dec 2020 13:00:58 +0100 Subject: [PATCH 04/43] Cleanup --- .../Properties/AndroidManifest.xml | 2 +- .../Sample.Forms.Android/Sample.Forms.Android.csproj | 7 +------ .../Android/CameraAccess/CameraAnalyzer.android.cs | 10 +++++----- .../Android/CameraAccess/CameraController.android.cs | 3 ++- .../CameraAccess/CameraEventsListener.android.cs | 4 ---- 5 files changed, 9 insertions(+), 17 deletions(-) diff --git a/Samples/Sample.Forms/Sample.Forms.Android/Properties/AndroidManifest.xml b/Samples/Sample.Forms/Sample.Forms.Android/Properties/AndroidManifest.xml index 25ac59f65..8599f2bbe 100644 --- a/Samples/Sample.Forms/Sample.Forms.Android/Properties/AndroidManifest.xml +++ b/Samples/Sample.Forms/Sample.Forms.Android/Properties/AndroidManifest.xml @@ -1,6 +1,6 @@  - + diff --git a/Samples/Sample.Forms/Sample.Forms.Android/Sample.Forms.Android.csproj b/Samples/Sample.Forms/Sample.Forms.Android/Sample.Forms.Android.csproj index b3bd2595e..8d720a563 100644 --- a/Samples/Sample.Forms/Sample.Forms.Android/Sample.Forms.Android.csproj +++ b/Samples/Sample.Forms/Sample.Forms.Android/Sample.Forms.Android.csproj @@ -16,7 +16,7 @@ Resources Assets false - v9.0 + v10.0 true true Xamarin.Android.Net.AndroidClientHandler @@ -32,11 +32,6 @@ prompt 4 None - false - false - false - false - true true diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs index 1ec0c855b..d01c170f0 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs @@ -90,7 +90,7 @@ bool CanAnalyzeFrame } } - void HandleOnPreviewFrameReady(object sender, byte[] fastArray) + void HandleOnPreviewFrameReady(object sender, byte[] data) { if (!CanAnalyzeFrame) return; @@ -102,7 +102,7 @@ void HandleOnPreviewFrameReady(object sender, byte[] fastArray) { try { - DecodeFrame(fastArray); + DecodeFrame(data); } catch (Exception ex) { @@ -115,7 +115,7 @@ void HandleOnPreviewFrameReady(object sender, byte[] fastArray) }, TaskContinuationOptions.OnlyOnFaulted); } - void DecodeFrame(byte[] fastArray) + void DecodeFrame(byte[] data) { var previewSize = cameraController.IdealPhotoSize; var width = previewSize.Width; @@ -127,9 +127,9 @@ void DecodeFrame(byte[] fastArray) barcodeReader.AutoRotate = true; - var source2 = new PlanarYUVLuminanceSource(fastArray, width, height, 0, 0, width, height, false); + var source = new PlanarYUVLuminanceSource(data, width, height, 0, 0, width, height, false); - result = barcodeReader.Decode(source2); + result = barcodeReader.Decode(source); PerformanceCounter.Stop(start, "Decode Time: {0} ms (width: " + width + ", height: " + height + ")"); diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs index e3d88fa23..4109bf640 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs @@ -225,6 +225,7 @@ void SetUpCameraOutputs() } // 1050 and 1400 are a random guess which work pretty good + // inspired from https://github.com/vtserej/Camera2Forms/blob/master/Camera2Forms/Camera2Forms.Android/Camera2/CameraDroid.cs#L162 var idealSize = GetOptimalSize(supportedSizes, 1050, 1400); imageReader = ImageReader.NewInstance(idealSize.Width, idealSize.Height, ImageFormatType.Yuv420888, 5); @@ -274,7 +275,7 @@ public void OpenCamera() } } - private Size GetOptimalPreviewSize(SurfaceView surface) + Size GetOptimalPreviewSize(SurfaceView surface) { var characteristics = cameraManager.GetCameraCharacteristics(CameraId); var map = (StreamConfigurationMap)characteristics.Get(CameraCharacteristics.ScalerStreamConfigurationMap); diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs index b7ccbd847..0f89c0e6e 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs @@ -25,10 +25,6 @@ public void OnImageAvailable(ImageReader reader) var yuvBytes = ImageToByteArray(image); OnPreviewFrameReady?.Invoke(this, yuvBytes); } - catch (Exception ex) - { - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "Could not start preview session"); - } finally { image?.Close(); From fc35548ce4537c80d2b0fcb32d5cbdcf56c1f6f4 Mon Sep 17 00:00:00 2001 From: Denrage Date: Tue, 19 Jan 2021 10:05:39 +0100 Subject: [PATCH 05/43] Removed obsolet boundary check for auto focus --- .../CameraAccess/CameraController.android.cs | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs index 4109bf640..138099fd8 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs @@ -106,13 +106,8 @@ public void AutoFocus() public void AutoFocus(int x, int y) { - // The bounds for focus areas are actually -1000 to 1000 - // So we need to translate the touch coordinates to this scale - var focusX = x / surfaceView.Width * 2000 - 1000; - var focusY = y / surfaceView.Height * 2000 - 1000; - // Call the autofocus with our coords - AutoFocus(focusX, focusY, true); + AutoFocus(x, y, true); } void AutoFocus(int x, int y, bool useCoordinates) @@ -146,16 +141,6 @@ void AutoFocus(int x, int y, bool useCoordinates) x = x - 10; y = y - 10; - // Ensure we don't go over the -1000 to 1000 limit of focus area - if (x >= 1000) - x = 980; - if (x < -1000) - x = -1000; - if (y >= 1000) - y = 980; - if (y < -1000) - y = -1000; - // Explicitly set FocusModeAuto since Focus areas only work with this setting previewBuilder.Set(CaptureRequest.ControlAfMode, (int)ControlAFMode.Auto); // Add our focus area From 8850ea05e523619352941ec3f3f3b371b930d765 Mon Sep 17 00:00:00 2001 From: James Westgate Date: Thu, 4 Feb 2021 10:35:00 +0000 Subject: [PATCH 06/43] Add new Nv21 implementation --- .../CameraEventsListener.android.cs | 109 +++++++++++++++++- 1 file changed, 108 insertions(+), 1 deletion(-) diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs index 0f89c0e6e..8c739a050 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs @@ -1,10 +1,12 @@ using System; +using System.IO; using System.Runtime.InteropServices; using Android.Content; using Android.Graphics; using Android.Media; using Android.Renderscripts; using ApxLabs.FastAndroidCamera; + using Java.IO; using Java.Nio; using static Android.Media.ImageReader; @@ -20,10 +22,19 @@ public void OnImageAvailable(ImageReader reader) Image image = null; try { + //Returns a yuv420888 image image = reader.AcquireLatestImage(); + if (image is null) return; - var yuvBytes = ImageToByteArray(image); + + //On Pixel5, the original Yuv42088 to nv21 conversion fails + //var yuvBytes = ImageToByteArray(image); + var yuvBytes = Yuv420888toNv21(image); + OnPreviewFrameReady?.Invoke(this, yuvBytes); + + //To check that the yuv conversion is correct, you can save a jpg version + //SaveJpegBytes(yuvBytes, image.Width, image.Height); } finally { @@ -31,6 +42,102 @@ public void OnImageAvailable(ImageReader reader) } } + //Use this to save the converted yuv image to external storage + //You may need to include Permissions.StorageWrite in PermissionsHandler.android.cs and in the manifest + //void SaveJpegBytes(byte[] yuvBytes, int width, int height) + //{ + // var dcimDir = Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryDcim).AbsolutePath; + // var filename = System.IO.Path.Combine(dcimDir, $"nv21-output.jpg"); + + // //Convert the nv21 bytes to a jpeg + // var stream = new MemoryStream(); + // var yuvImage = new YuvImage(yuvBytes, ImageFormatType.Nv21, width, height, null); + // yuvImage.CompressToJpeg(new Rect(0, 0, width, height), 50, stream); + + // System.IO.File.WriteAllBytes(filename, stream.ToArray()); + //} + + //https://stackoverflow.com/questions/52726002/camera2-captured-picture-conversion-from-yuv-420-888-to-nv21 + byte[] Yuv420888toNv21(Image image) + { + var width = image.Width; + var height = image.Height; + var ySize = width * height; + var uvSize = width * height / 4; + + var nv21 = new byte[ySize + uvSize * 2]; + + var yBuffer = image.GetPlanes()[0].Buffer; // Y + var uBuffer = image.GetPlanes()[1].Buffer; // U + var vBuffer = image.GetPlanes()[2].Buffer; // V + + var rowStride = image.GetPlanes()[0].RowStride; + var pos = 0; + + if (rowStride == width) + { + // likely + yBuffer.Get(nv21, 0, ySize); + pos += ySize; + } + else + { + var yBufferPos = -rowStride; // not an actual position + for (; pos < ySize; pos += width) + { + yBufferPos += rowStride; + yBuffer.Position(yBufferPos); + yBuffer.Get(nv21, pos, width); + } + } + + rowStride = image.GetPlanes()[2].RowStride; + var pixelStride = image.GetPlanes()[2].PixelStride; + + if (pixelStride == 2 && rowStride == width && uBuffer.Get(0) == vBuffer.Get(1)) + { + // maybe V and U planes overlap as per NV21, which means vBuffer[1] is alias of uBuffer[0] + var savePixel = vBuffer.Get(1); + try + { + vBuffer.Put(1, (sbyte)~savePixel); + if (uBuffer.Get(0) == (sbyte)~savePixel) + { + vBuffer.Put(1, savePixel); + vBuffer.Position(0); + uBuffer.Position(0); + vBuffer.Get(nv21, ySize, 1); + uBuffer.Get(nv21, ySize + 1, uBuffer.Remaining()); + + return nv21; // shortcut + } + } + catch (ReadOnlyBufferException) + { + // unfortunately, we cannot check if vBuffer and uBuffer overlap + } + + // unfortunately, the check failed. We must save U and V pixel by pixel + vBuffer.Put(1, savePixel); + } + + // other optimizations could check if (pixelStride == 1) or (pixelStride == 2), + // but performance gain would be less significant + + for (var row = 0; row < height / 2; row++) + { + for (var col = 0; col < width / 2; col++) + { + var vuPos = col * pixelStride + row * rowStride; + nv21[pos++] = (byte)vBuffer.Get(vuPos); + nv21[pos++] = (byte)uBuffer.Get(vuPos); + } + } + + return nv21; + } + + //Convert to NV21 byte[] ImageToByteArray(Image image) { byte[] result; From 6d7930b03f0ef8626952e393a17931296b84fe59 Mon Sep 17 00:00:00 2001 From: James Westgate Date: Thu, 4 Feb 2021 10:41:15 +0000 Subject: [PATCH 07/43] Dont create new BarcodeReader on every decode --- .../Android/CameraAccess/CameraAnalyzer.android.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs index d01c170f0..25e27672f 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs @@ -20,6 +20,7 @@ public class CameraAnalyzer bool wasScanned; IScannerSessionHost scannerHost; CameraManager cameraManager; + BarcodeReader barcodeReader; public CameraAnalyzer(SurfaceView surfaceView, IScannerSessionHost scannerHost) { @@ -63,6 +64,7 @@ public void AutoFocus(int x, int y) public void RefreshCamera() { + barcodeReader = null; cameraController.RefreshCamera(); } @@ -120,8 +122,14 @@ void DecodeFrame(byte[] data) var previewSize = cameraController.IdealPhotoSize; var width = previewSize.Width; var height = previewSize.Height; - var barcodeReader = scannerHost.ScanningOptions.BuildBarcodeReader(); + if (barcodeReader == null) + { + barcodeReader = scannerHost.ScanningOptions.BuildBarcodeReader(); + + Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "Created Barcode Reader"); + } + ZXing.Result result = null; var start = PerformanceCounter.Start(); From 5b3b0db6f57e3bbe10886c82252908b2cc7b87a0 Mon Sep 17 00:00:00 2001 From: Denrage Date: Fri, 5 Feb 2021 11:05:15 +0100 Subject: [PATCH 08/43] removed obsolet code --- .../CameraEventsListener.android.cs | 42 ------------------- 1 file changed, 42 deletions(-) diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs index 8c739a050..6a44354d7 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs @@ -22,19 +22,13 @@ public void OnImageAvailable(ImageReader reader) Image image = null; try { - //Returns a yuv420888 image image = reader.AcquireLatestImage(); if (image is null) return; - //On Pixel5, the original Yuv42088 to nv21 conversion fails - //var yuvBytes = ImageToByteArray(image); var yuvBytes = Yuv420888toNv21(image); - OnPreviewFrameReady?.Invoke(this, yuvBytes); - //To check that the yuv conversion is correct, you can save a jpg version - //SaveJpegBytes(yuvBytes, image.Width, image.Height); } finally { @@ -42,21 +36,6 @@ public void OnImageAvailable(ImageReader reader) } } - //Use this to save the converted yuv image to external storage - //You may need to include Permissions.StorageWrite in PermissionsHandler.android.cs and in the manifest - //void SaveJpegBytes(byte[] yuvBytes, int width, int height) - //{ - // var dcimDir = Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryDcim).AbsolutePath; - // var filename = System.IO.Path.Combine(dcimDir, $"nv21-output.jpg"); - - // //Convert the nv21 bytes to a jpeg - // var stream = new MemoryStream(); - // var yuvImage = new YuvImage(yuvBytes, ImageFormatType.Nv21, width, height, null); - // yuvImage.CompressToJpeg(new Rect(0, 0, width, height), 50, stream); - - // System.IO.File.WriteAllBytes(filename, stream.ToArray()); - //} - //https://stackoverflow.com/questions/52726002/camera2-captured-picture-conversion-from-yuv-420-888-to-nv21 byte[] Yuv420888toNv21(Image image) { @@ -136,26 +115,5 @@ byte[] Yuv420888toNv21(Image image) return nv21; } - - //Convert to NV21 - byte[] ImageToByteArray(Image image) - { - byte[] result; - var yBuffer = image.GetPlanes()[0].Buffer; - var uBuffer = image.GetPlanes()[1].Buffer; - var vBuffer = image.GetPlanes()[2].Buffer; - - var ySize = yBuffer.Remaining(); - var uSize = uBuffer.Remaining(); - var vSize = vBuffer.Remaining(); - - result = new byte[ySize + uSize + vSize]; - - yBuffer.Get(result, 0, ySize); - vBuffer.Get(result, ySize, vSize); - uBuffer.Get(result, ySize + vSize, uSize); - - return result; - } } } From d38b362a7e567fd72598e6e49041118170e80904 Mon Sep 17 00:00:00 2001 From: Denrage Date: Fri, 5 Feb 2021 11:22:15 +0100 Subject: [PATCH 09/43] Removed FastAndroidCamera --- Samples/Sample.Android/Sample.Android.csproj | 1 - ZXing.Net.Mobile/ZXing.Net.Mobile.csproj | 1 - 2 files changed, 2 deletions(-) diff --git a/Samples/Sample.Android/Sample.Android.csproj b/Samples/Sample.Android/Sample.Android.csproj index b0c9c7d8b..8454a3062 100644 --- a/Samples/Sample.Android/Sample.Android.csproj +++ b/Samples/Sample.Android/Sample.Android.csproj @@ -89,7 +89,6 @@ - 1.3.0.4 diff --git a/ZXing.Net.Mobile/ZXing.Net.Mobile.csproj b/ZXing.Net.Mobile/ZXing.Net.Mobile.csproj index bfd587434..e4c239f4a 100644 --- a/ZXing.Net.Mobile/ZXing.Net.Mobile.csproj +++ b/ZXing.Net.Mobile/ZXing.Net.Mobile.csproj @@ -86,7 +86,6 @@ - From a50c5b74c59da99eda725c9be1d7d55e2c76fdea Mon Sep 17 00:00:00 2001 From: Denrage Date: Fri, 5 Feb 2021 16:39:15 +0100 Subject: [PATCH 10/43] Fixed Merge --- .../CameraAccess/CameraAnalyzer.android.cs | 22 +-- .../Android/FastJavaArrayEx.android.cs | 17 -- ...JavaByteArrayYUVLuminanceSource.android.cs | 173 ------------------ 3 files changed, 7 insertions(+), 205 deletions(-) delete mode 100644 ZXing.Net.Mobile/Android/FastJavaArrayEx.android.cs delete mode 100644 ZXing.Net.Mobile/Android/FastJavaByteArrayYUVLuminanceSource.android.cs diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs index 25e27672f..e9eb0cd0b 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs @@ -6,7 +6,6 @@ using Android.Hardware.Camera2; using Android.Hardware.Camera2.Params; using Android.Views; -using ApxLabs.FastAndroidCamera; using ZXing.Common; namespace ZXing.Mobile.CameraAccess @@ -18,9 +17,8 @@ public class CameraAnalyzer Task processingTask; DateTime lastPreviewAnalysis = DateTime.UtcNow; bool wasScanned; - IScannerSessionHost scannerHost; - CameraManager cameraManager; - BarcodeReader barcodeReader; + readonly IScannerSessionHost scannerHost; + BarcodeReaderGeneric barcodeReader; public CameraAnalyzer(SurfaceView surfaceView, IScannerSessionHost scannerHost) { @@ -28,7 +26,6 @@ public CameraAnalyzer(SurfaceView surfaceView, IScannerSessionHost scannerHost) cameraEventListener = new CameraEventsListener(); cameraController = new CameraController(surfaceView, cameraEventListener, scannerHost); Torch = new Torch(cameraController, surfaceView.Context); - cameraManager = (CameraManager)surfaceView.Context.GetSystemService(Context.CameraService); } public Action BarcodeFound; @@ -53,6 +50,10 @@ public void ShutdownCamera() public void SetupCamera() { cameraEventListener.OnPreviewFrameReady += HandleOnPreviewFrameReady; + + barcodeReader = scannerHost.ScanningOptions.BuildBarcodeReader(); + Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "Created Barcode Reader"); + cameraController.SetupCamera(); } @@ -64,7 +65,6 @@ public void AutoFocus(int x, int y) public void RefreshCamera() { - barcodeReader = null; cameraController.RefreshCamera(); } @@ -122,22 +122,14 @@ void DecodeFrame(byte[] data) var previewSize = cameraController.IdealPhotoSize; var width = previewSize.Width; var height = previewSize.Height; - - if (barcodeReader == null) - { - barcodeReader = scannerHost.ScanningOptions.BuildBarcodeReader(); - - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "Created Barcode Reader"); - } - ZXing.Result result = null; var start = PerformanceCounter.Start(); barcodeReader.AutoRotate = true; var source = new PlanarYUVLuminanceSource(data, width, height, 0, 0, width, height, false); - result = barcodeReader.Decode(source); + var result = barcodeReader.Decode(source); PerformanceCounter.Stop(start, "Decode Time: {0} ms (width: " + width + ", height: " + height + ")"); diff --git a/ZXing.Net.Mobile/Android/FastJavaArrayEx.android.cs b/ZXing.Net.Mobile/Android/FastJavaArrayEx.android.cs deleted file mode 100644 index 14584e001..000000000 --- a/ZXing.Net.Mobile/Android/FastJavaArrayEx.android.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using ApxLabs.FastAndroidCamera; - -namespace ZXing.Mobile -{ - public static class FastJavaArrayEx - { - public static void BlockCopyTo(this FastJavaByteArray self, int sourceIndex, byte[] array, int arrayIndex, int length) - { - unsafe - { - Marshal.Copy(new IntPtr(self.Raw + sourceIndex), array, arrayIndex, Math.Min(length, Math.Min(self.Count, array.Length - arrayIndex))); - } - } - } -} \ No newline at end of file diff --git a/ZXing.Net.Mobile/Android/FastJavaByteArrayYUVLuminanceSource.android.cs b/ZXing.Net.Mobile/Android/FastJavaByteArrayYUVLuminanceSource.android.cs deleted file mode 100644 index 1137377ac..000000000 --- a/ZXing.Net.Mobile/Android/FastJavaByteArrayYUVLuminanceSource.android.cs +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright 2009 ZXing authors - * Modifications copyright 2016 kasper@byolimit.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -using ApxLabs.FastAndroidCamera; - -using System; - -namespace ZXing.Mobile -{ - /// - /// This object extends LuminanceSource around an array of YUV data returned from the camera driver, - /// with the option to crop to a rectangle within the full data. This can be used to exclude - /// superfluous pixels around the perimeter and speed up decoding. - /// It works for any pixel format where the Y channel is planar and appears first, including - /// YCbCr_420_SP and YCbCr_422_SP. - /// - /// - /// Builds upon PlanarYUVLuminanceSource from ZXing.NET, which is a .Net port of ZXing. The original code - /// was authored by - /// @author dswitkin@google.com (Daniel Switkin) - /// - public sealed class FastJavaByteArrayYUVLuminanceSource : BaseLuminanceSource - { - private readonly FastJavaByteArray _yuv; - private readonly int _dataWidth; - private readonly int _dataHeight; - private readonly int _left; - private readonly int _top; - - /// - /// Initializes a new instance of the class. - /// - /// The yuv data. - /// Width of the data. - /// Height of the data. - /// The left. - /// The top. - /// The width. - /// The height. - /// if set to true [reverse horiz]. - public FastJavaByteArrayYUVLuminanceSource(FastJavaByteArray yuv, - int dataWidth, - int dataHeight, - int left, - int top, - int width, - int height) - : base(width, height) - { - if (left < 0) - throw new ArgumentException("Negative value", nameof(left)); - - if (top < 0) - throw new ArgumentException("Negative value", nameof(top)); - - if (width < 0) - throw new ArgumentException("Negative value", nameof(width)); - - if (height < 0) - throw new ArgumentException("Negative value", nameof(height)); - - if (left + width > dataWidth || top + height > dataHeight) - { - throw new ArgumentException("Crop rectangle does not fit within image data."); - } - - _yuv = yuv; - _dataWidth = dataWidth; - _dataHeight = dataHeight; - _left = left; - _top = top; - } - - /// - /// Fetches one row of luminance data from the underlying platform's bitmap. Values range from - /// 0 (black) to 255 (white). Because Java does not have an unsigned byte type, callers will have - /// to bitwise and with 0xff for each value. It is preferable for implementations of this method - /// to only fetch this row rather than the whole image, since no 2D Readers may be installed and - /// getMatrix() may never be called. - /// - /// The row to fetch, 0 <= y < Height. - /// An optional preallocated array. If null or too small, it will be ignored. - /// Always use the returned object, and ignore the .length of the array. - /// - /// An array containing the luminance data of the requested row. - /// - override public byte[] getRow(int y, byte[] row) - { - if (y < 0 || y >= Height) - throw new ArgumentException("Requested row is outside the image: " + y, nameof(y)); - - var width = Width; - if (row == null || row.Length < width) - row = new byte[width]; // ensure we have room for the row - - var offset = (y + _top) * _dataWidth + _left; - _yuv.BlockCopyTo(offset, row, 0, width); - return row; - } - - override public byte[] Matrix - { - get - { - var width = Width; - var height = Height; - - var area = width * height; - var matrix = new byte[area]; - var inputOffset = _top * _dataWidth + _left; - - // If the width matches the full width of the underlying data, perform a single copy. - if (width == _dataWidth) - { - _yuv.BlockCopyTo(inputOffset, matrix, 0, area); - return matrix; - } - - // Otherwise copy one cropped row at a time. - for (var y = 0; y < height; y++) - { - var outputOffset = y * width; - _yuv.BlockCopyTo(inputOffset, matrix, outputOffset, width); - inputOffset += _dataWidth; - } - return matrix; - } - } - - /// Whether this subclass supports cropping. - override public bool CropSupported - => true; - - /// - /// Returns a new object with cropped image data. Implementations may keep a reference to the - /// original data rather than a copy. Only callable if CropSupported is true. - /// - /// The left coordinate, 0 <= left < Width. - /// The top coordinate, 0 <= top <= Height. - /// The width of the rectangle to crop. - /// The height of the rectangle to crop. - /// - /// A cropped version of this object. - /// - override public LuminanceSource crop(int left, int top, int width, int height) - => new FastJavaByteArrayYUVLuminanceSource(_yuv, - _dataWidth, - _dataHeight, - _left + left, - _top + top, - width, - height); - - // Called when rotating. - // todo: This partially defeats the purpose as we traffic in byte[] luminances - protected override LuminanceSource CreateLuminanceSource(byte[] newLuminances, int width, int height) - => new PlanarYUVLuminanceSource(newLuminances, width, height, 0, 0, width, height, false); - } -} \ No newline at end of file From cbcabcd89e53448907f9a9c103c317f630ec1548 Mon Sep 17 00:00:00 2001 From: Denrage Date: Fri, 5 Feb 2021 16:39:30 +0100 Subject: [PATCH 11/43] rotate yuv output --- .../CameraEventsListener.android.cs | 58 ++++++++++++++++--- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs index 6a44354d7..2e9132d64 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs @@ -1,13 +1,6 @@ using System; -using System.IO; -using System.Runtime.InteropServices; -using Android.Content; -using Android.Graphics; using Android.Media; -using Android.Renderscripts; -using ApxLabs.FastAndroidCamera; - -using Java.IO; +using Java.Lang; using Java.Nio; using static Android.Media.ImageReader; @@ -15,6 +8,7 @@ namespace ZXing.Mobile.CameraAccess { public class CameraEventsListener : Java.Lang.Object, IOnImageAvailableListener { + public event EventHandler OnPreviewFrameReady; public void OnImageAvailable(ImageReader reader) @@ -27,6 +21,7 @@ public void OnImageAvailable(ImageReader reader) if (image is null) return; var yuvBytes = Yuv420888toNv21(image); + yuvBytes = rotateNV21(yuvBytes, image.Width, image.Height, 90); OnPreviewFrameReady?.Invoke(this, yuvBytes); } @@ -36,6 +31,53 @@ public void OnImageAvailable(ImageReader reader) } } + + // rotation from https://stackoverflow.com/questions/44994510/how-to-convert-rotate-raw-nv21-array-image-android-media-image-from-front-ca + public static byte[] rotateNV21(byte[] yuv, + int width, + int height, + int rotation) + { + if (rotation == 0) + return yuv; + if (rotation % 90 != 0 || rotation < 0 || rotation > 270) + { + throw new IllegalArgumentException("0 <= rotation < 360, rotation % 90 == 0"); + } + + var output = new byte[yuv.Length]; + var frameSize = width * height; + var swap = rotation % 180 != 0; + var xflip = rotation % 270 != 0; + var yflip = rotation >= 180; + + for (var j = 0; j < height; j++) + { + for (var i = 0; i < width; i++) + { + var yIn = j * width + i; + var uIn = frameSize + (j >> 1) * width + (i & ~1); + var vIn = uIn + 1; + + var wOut = swap ? height : width; + var hOut = swap ? width : height; + var iSwapped = swap ? j : i; + var jSwapped = swap ? i : j; + var iOut = xflip ? wOut - iSwapped - 1 : iSwapped; + var jOut = yflip ? hOut - jSwapped - 1 : jSwapped; + + var yOut = jOut * wOut + iOut; + var uOut = frameSize + (jOut >> 1) * wOut + (iOut & ~1); + var vOut = uOut + 1; + + output[yOut] = (byte)(0xff & yuv[yIn]); + output[uOut] = (byte)(0xff & yuv[uIn]); + output[vOut] = (byte)(0xff & yuv[vIn]); + } + } + return output; + } + //https://stackoverflow.com/questions/52726002/camera2-captured-picture-conversion-from-yuv-420-888-to-nv21 byte[] Yuv420888toNv21(Image image) { From 642de3c1daffd97c17cce3d1942275122703171b Mon Sep 17 00:00:00 2001 From: Denrage Date: Wed, 10 Mar 2021 08:55:37 +0100 Subject: [PATCH 12/43] Fixed scanning on tablet and sped up conversion --- .../CameraAccess/CameraAnalyzer.android.cs | 2 +- .../CameraAccess/CameraController.android.cs | 47 +++------ .../CameraEventsListener.android.cs | 98 ++++++------------- 3 files changed, 47 insertions(+), 100 deletions(-) diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs index e9eb0cd0b..f025c1b4a 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs @@ -122,7 +122,7 @@ void DecodeFrame(byte[] data) var previewSize = cameraController.IdealPhotoSize; var width = previewSize.Width; var height = previewSize.Height; - + var start = PerformanceCounter.Start(); barcodeReader.AutoRotate = true; diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs index 138099fd8..2bd5fef91 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs @@ -8,6 +8,7 @@ using Android.Hardware.Camera2.Params; using Android.Media; using Android.OS; +using Android.Runtime; using Android.Util; using Android.Views; using Java.Lang; @@ -209,9 +210,11 @@ void SetUpCameraOutputs() return; } - // 1050 and 1400 are a random guess which work pretty good - // inspired from https://github.com/vtserej/Camera2Forms/blob/master/Camera2Forms/Camera2Forms.Android/Camera2/CameraDroid.cs#L162 - var idealSize = GetOptimalSize(supportedSizes, 1050, 1400); + var wm = context.GetSystemService(Context.WindowService).JavaCast(); + var display = wm.DefaultDisplay; + var point = new Point(); + display.GetSize(point); + var idealSize = point.X > point.Y ? GetOptimalSize(supportedSizes, point.X, point.Y) : GetOptimalSize(supportedSizes, point.Y, point.X); imageReader = ImageReader.NewInstance(idealSize.Width, idealSize.Height, ImageFormatType.Yuv420888, 5); flashSupported = HasFLash(characteristics); @@ -262,17 +265,19 @@ public void OpenCamera() Size GetOptimalPreviewSize(SurfaceView surface) { + var width = surface.Width > surface.Height ? surface.Width : surface.Height; + var height = surface.Width > surface.Height ? surface.Height : surface.Width; var characteristics = cameraManager.GetCameraCharacteristics(CameraId); var map = (StreamConfigurationMap)characteristics.Get(CameraCharacteristics.ScalerStreamConfigurationMap); var availableSizes = ((StreamConfigurationMap)characteristics .Get(CameraCharacteristics.ScalerStreamConfigurationMap)) .GetOutputSizes(Class.FromType(typeof(ISurfaceHolder))); - var aspectRatio = (double)surface.Height / (double)surface.Width; + var aspectRatio = (double)width / (double)height; var availableAspectRatios = availableSizes.Select(x => (x, (double)x.Width / (double)x.Height)); var differences = availableAspectRatios.Select(x => (x.x, System.Math.Abs(x.Item2 - aspectRatio))); - var bestMatches = differences.OrderBy(x => x.Item2).ThenBy(x => System.Math.Abs(x.x.Width - surface.Width)).ThenBy(x => System.Math.Abs(x.x.Height - surface.Height)).Take(5); + var bestMatches = differences.OrderBy(x => x.Item2).ThenBy(x => System.Math.Abs(x.x.Width - width)).ThenBy(x => System.Math.Abs(x.x.Height - height)).Take(5); return bestMatches.OrderByDescending(x => x.x.Width).ThenByDescending(x => x.x.Height).First().x; } @@ -351,34 +356,12 @@ Size GetOptimalSize(IList sizes, int width, int height) { if (sizes is null) return null; - var aspectTolerance = 0.1; - var targetRatio = (double)width / height; + var aspectRatio = (double)width / (double)height; + var availableAspectRatios = sizes.Select(x => (x, (double)x.Width / (double)x.Height)); - Size optimalSize = null; - var minDiff = double.MaxValue; - var targetHeight = height; - - while (optimalSize is null) - { - foreach (var size in sizes) - { - var ratio = (double)size.Width / size.Height; - - if (System.Math.Abs(ratio - targetRatio) > aspectTolerance) - continue; - - if (System.Math.Abs(size.Height - targetHeight) < minDiff) - { - optimalSize = size; - minDiff = System.Math.Abs(size.Height - targetHeight); - } - } - - if (optimalSize == null) - aspectTolerance += 0.1f; - } - - return optimalSize; + var differences = availableAspectRatios.Select(x => (x.x, System.Math.Abs(x.Item2 - aspectRatio))); + var bestMatches = differences.OrderBy(x => x.Item2).ThenBy(x => System.Math.Abs(x.x.Width - width)).ThenBy(x => System.Math.Abs(x.x.Height - height)).Take(5); + return bestMatches.OrderByDescending(x => x.x.Width).ThenByDescending(x => x.x.Height).First().x; } void StartBackgroundThread() diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs index 2e9132d64..f31350dc1 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs @@ -1,4 +1,7 @@ using System; +using System.IO; +using Android.Content; +using Android.Graphics; using Android.Media; using Java.Lang; using Java.Nio; @@ -8,9 +11,12 @@ namespace ZXing.Mobile.CameraAccess { public class CameraEventsListener : Java.Lang.Object, IOnImageAvailableListener { - public event EventHandler OnPreviewFrameReady; + public CameraEventsListener() + { + } + public void OnImageAvailable(ImageReader reader) { Image image = null; @@ -21,9 +27,8 @@ public void OnImageAvailable(ImageReader reader) if (image is null) return; var yuvBytes = Yuv420888toNv21(image); - yuvBytes = rotateNV21(yuvBytes, image.Width, image.Height, 90); - OnPreviewFrameReady?.Invoke(this, yuvBytes); + OnPreviewFrameReady?.Invoke(this, yuvBytes); } finally { @@ -31,53 +36,6 @@ public void OnImageAvailable(ImageReader reader) } } - - // rotation from https://stackoverflow.com/questions/44994510/how-to-convert-rotate-raw-nv21-array-image-android-media-image-from-front-ca - public static byte[] rotateNV21(byte[] yuv, - int width, - int height, - int rotation) - { - if (rotation == 0) - return yuv; - if (rotation % 90 != 0 || rotation < 0 || rotation > 270) - { - throw new IllegalArgumentException("0 <= rotation < 360, rotation % 90 == 0"); - } - - var output = new byte[yuv.Length]; - var frameSize = width * height; - var swap = rotation % 180 != 0; - var xflip = rotation % 270 != 0; - var yflip = rotation >= 180; - - for (var j = 0; j < height; j++) - { - for (var i = 0; i < width; i++) - { - var yIn = j * width + i; - var uIn = frameSize + (j >> 1) * width + (i & ~1); - var vIn = uIn + 1; - - var wOut = swap ? height : width; - var hOut = swap ? width : height; - var iSwapped = swap ? j : i; - var jSwapped = swap ? i : j; - var iOut = xflip ? wOut - iSwapped - 1 : iSwapped; - var jOut = yflip ? hOut - jSwapped - 1 : jSwapped; - - var yOut = jOut * wOut + iOut; - var uOut = frameSize + (jOut >> 1) * wOut + (iOut & ~1); - var vOut = uOut + 1; - - output[yOut] = (byte)(0xff & yuv[yIn]); - output[uOut] = (byte)(0xff & yuv[uIn]); - output[vOut] = (byte)(0xff & yuv[vIn]); - } - } - return output; - } - //https://stackoverflow.com/questions/52726002/camera2-captured-picture-conversion-from-yuv-420-888-to-nv21 byte[] Yuv420888toNv21(Image image) { @@ -92,13 +50,22 @@ byte[] Yuv420888toNv21(Image image) var uBuffer = image.GetPlanes()[1].Buffer; // U var vBuffer = image.GetPlanes()[2].Buffer; // V + var yArray = new byte[yBuffer.Limit()]; + yBuffer.Get(yArray, 0, yArray.Length); + + var uArray = new byte[uBuffer.Limit()]; + uBuffer.Get(uArray, 0, uArray.Length); + + var vArray = new byte[vBuffer.Limit()]; + vBuffer.Get(vArray, 0, vArray.Length); + var rowStride = image.GetPlanes()[0].RowStride; var pos = 0; if (rowStride == width) { // likely - yBuffer.Get(nv21, 0, ySize); + Array.Copy(yArray, 0, nv21, 0, ySize); pos += ySize; } else @@ -107,28 +74,26 @@ byte[] Yuv420888toNv21(Image image) for (; pos < ySize; pos += width) { yBufferPos += rowStride; - yBuffer.Position(yBufferPos); - yBuffer.Get(nv21, pos, width); + + Array.Copy(yArray, yBufferPos, nv21, pos, width); } } rowStride = image.GetPlanes()[2].RowStride; var pixelStride = image.GetPlanes()[2].PixelStride; - if (pixelStride == 2 && rowStride == width && uBuffer.Get(0) == vBuffer.Get(1)) + if (pixelStride == 2 && rowStride == width && uArray[0] == vArray[1]) { // maybe V and U planes overlap as per NV21, which means vBuffer[1] is alias of uBuffer[0] - var savePixel = vBuffer.Get(1); + var savePixel = vArray[1]; try { - vBuffer.Put(1, (sbyte)~savePixel); - if (uBuffer.Get(0) == (sbyte)~savePixel) + vArray[1] = (byte)~savePixel; + if (uArray[0] == (sbyte)~savePixel) { - vBuffer.Put(1, savePixel); - vBuffer.Position(0); - uBuffer.Position(0); - vBuffer.Get(nv21, ySize, 1); - uBuffer.Get(nv21, ySize + 1, uBuffer.Remaining()); + vArray[1] = savePixel; + Array.Copy(vArray, 0, nv21, ySize, 1); + Array.Copy(vArray, 0, nv21, ySize + 1, uArray.Length); return nv21; // shortcut } @@ -139,19 +104,18 @@ byte[] Yuv420888toNv21(Image image) } // unfortunately, the check failed. We must save U and V pixel by pixel - vBuffer.Put(1, savePixel); + vArray[1] = savePixel; } - // other optimizations could check if (pixelStride == 1) or (pixelStride == 2), + // other optimizations could check if (pixelStride == 1) or (pixelStride == 2), // but performance gain would be less significant - for (var row = 0; row < height / 2; row++) { for (var col = 0; col < width / 2; col++) { var vuPos = col * pixelStride + row * rowStride; - nv21[pos++] = (byte)vBuffer.Get(vuPos); - nv21[pos++] = (byte)uBuffer.Get(vuPos); + nv21[pos++] = vArray[vuPos]; + nv21[pos++] = uArray[vuPos]; } } From 5d56763643944a55faca41c0bff1fbb454b7ef4c Mon Sep 17 00:00:00 2001 From: Denrage Date: Wed, 10 Mar 2021 08:56:06 +0100 Subject: [PATCH 13/43] Fixed distortion of camera preview --- .../Android/CameraAccess/CameraController.android.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs index 2bd5fef91..8389fc7dd 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs @@ -309,6 +309,9 @@ public void StartPreview() sizeSetResetEvent.Reset(); } + // This is needed bc otherwise the preview is sometimes distorted + System.Threading.Thread.Sleep(30); + previewBuilder = Camera.CreateCaptureRequest(CameraTemplate.Preview); previewBuilder.AddTarget(holder.Surface); previewBuilder.AddTarget(imageReader.Surface); From a8478b15e12e270350a59c4a0d20966b8bcbbc3f Mon Sep 17 00:00:00 2001 From: Tim Nolte Date: Wed, 9 Jun 2021 14:55:48 +0200 Subject: [PATCH 14/43] Select lowest resolution for faster analyze --- .../Android/CameraAccess/CameraController.android.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs index 8389fc7dd..ffe6d2e4d 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs @@ -364,7 +364,8 @@ Size GetOptimalSize(IList sizes, int width, int height) var differences = availableAspectRatios.Select(x => (x.x, System.Math.Abs(x.Item2 - aspectRatio))); var bestMatches = differences.OrderBy(x => x.Item2).ThenBy(x => System.Math.Abs(x.x.Width - width)).ThenBy(x => System.Math.Abs(x.x.Height - height)).Take(5); - return bestMatches.OrderByDescending(x => x.x.Width).ThenByDescending(x => x.x.Height).First().x; + var orderedMatches = bestMatches.OrderBy(x => x.x.Width).ThenBy(x => x.x.Height); + return orderedMatches.First().x; } void StartBackgroundThread() From 34e83d284fc0cb41828d77a430956bce22cd5c62 Mon Sep 17 00:00:00 2001 From: Tim Nolte Date: Thu, 1 Jul 2021 16:12:39 +0200 Subject: [PATCH 15/43] Use a minimum size to prevent using a resolution too small --- .../Android/CameraAccess/CameraController.android.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs index ffe6d2e4d..5119b22a5 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs @@ -357,6 +357,7 @@ void UpdatePreview() Size GetOptimalSize(IList sizes, int width, int height) { + const int minimumSize = 1000; if (sizes is null) return null; var aspectRatio = (double)width / (double)height; @@ -365,7 +366,7 @@ Size GetOptimalSize(IList sizes, int width, int height) var differences = availableAspectRatios.Select(x => (x.x, System.Math.Abs(x.Item2 - aspectRatio))); var bestMatches = differences.OrderBy(x => x.Item2).ThenBy(x => System.Math.Abs(x.x.Width - width)).ThenBy(x => System.Math.Abs(x.x.Height - height)).Take(5); var orderedMatches = bestMatches.OrderBy(x => x.x.Width).ThenBy(x => x.x.Height); - return orderedMatches.First().x; + return orderedMatches.Where(x => x.x.Height > minimumSize || x.x.Width > minimumSize).First().x; } void StartBackgroundThread() From b9b99f31818bed5e53c47139d692943c08ed8995 Mon Sep 17 00:00:00 2001 From: DarkIrata Date: Mon, 18 Jul 2022 15:45:47 +0200 Subject: [PATCH 16/43] Updated ZXing Lib --- Samples/Sample.Forms/Sample.Forms/Sample.Forms.csproj | 2 +- ZXing.Net.Mobile.Forms/ZXing.Net.Mobile.Forms.csproj | 2 +- ZXing.Net.Mobile/ZXing.Net.Mobile.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Samples/Sample.Forms/Sample.Forms/Sample.Forms.csproj b/Samples/Sample.Forms/Sample.Forms/Sample.Forms.csproj index 8eaf1848a..19de08fda 100644 --- a/Samples/Sample.Forms/Sample.Forms/Sample.Forms.csproj +++ b/Samples/Sample.Forms/Sample.Forms/Sample.Forms.csproj @@ -13,7 +13,7 @@ - + diff --git a/ZXing.Net.Mobile.Forms/ZXing.Net.Mobile.Forms.csproj b/ZXing.Net.Mobile.Forms/ZXing.Net.Mobile.Forms.csproj index 95095e523..374cb7d3e 100644 --- a/ZXing.Net.Mobile.Forms/ZXing.Net.Mobile.Forms.csproj +++ b/ZXing.Net.Mobile.Forms/ZXing.Net.Mobile.Forms.csproj @@ -89,7 +89,7 @@ - + diff --git a/ZXing.Net.Mobile/ZXing.Net.Mobile.csproj b/ZXing.Net.Mobile/ZXing.Net.Mobile.csproj index bf2ea8819..5b9e6215d 100644 --- a/ZXing.Net.Mobile/ZXing.Net.Mobile.csproj +++ b/ZXing.Net.Mobile/ZXing.Net.Mobile.csproj @@ -105,6 +105,6 @@ - + \ No newline at end of file From af54b7f9462dc376ac016b0feb6eac9b456dd410 Mon Sep 17 00:00:00 2001 From: DarkIrata Date: Mon, 18 Jul 2022 16:03:16 +0200 Subject: [PATCH 17/43] Add DebugHelper.android.cs class --- .../Android/DebugHelper.android.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 ZXing.Net.Mobile/Android/DebugHelper.android.cs diff --git a/ZXing.Net.Mobile/Android/DebugHelper.android.cs b/ZXing.Net.Mobile/Android/DebugHelper.android.cs new file mode 100644 index 000000000..d4b8117e3 --- /dev/null +++ b/ZXing.Net.Mobile/Android/DebugHelper.android.cs @@ -0,0 +1,48 @@ +using System.IO; +using System.Net.Http; +using Android.Graphics; + +namespace ZXing.Net.Mobile.Android +{ +#if DEBUG + // Do not use this in production. + // This is a simple helping class to use with the ImageReceiver Tool + public static class DebugHelper + { + // Used to check NV21 Output + static byte[] NV21toJPEG(byte[] nv21, int width, int height) + { + using (var mems = new MemoryStream()) + { + var yuv = new YuvImage(nv21, ImageFormatType.Nv21, width, height, null); + yuv.CompressToJpeg(new Rect(0, 0, width, height), 100, mems); + return mems.ToArray(); + } + } + + public static void SendBytesToEndpoint(byte[] data, string endpoint) + { + var httpClient = GetHttpClient(); + var request = new HttpRequestMessage(HttpMethod.Post, endpoint); + request.Content = new ByteArrayContent(data); + httpClient.SendAsync(request); + } + + public static void SendNV21toJPEGToEndpoint(byte[] data, int width, int height, string endpoint) + { + var jpeg = NV21toJPEG(data, width, height); + SendBytesToEndpoint(jpeg, endpoint); + } + + static HttpClient GetHttpClient() + { + var handler = new HttpClientHandler(); + // we dont need to validate certificates + handler.ClientCertificateOptions = ClientCertificateOption.Manual; + handler.ServerCertificateCustomValidationCallback = (_, __, ___, ____) => true; + + return new HttpClient(handler); + } + } +#endif +} From 21e40080cc92afa27d19ba2e8f942c015c7decdb Mon Sep 17 00:00:00 2001 From: DarkIrata Date: Mon, 18 Jul 2022 16:04:26 +0200 Subject: [PATCH 18/43] Improved read speed a little bit and better recognition for bar codes --- .../CameraAccess/CameraAnalyzer.android.cs | 46 +++++++++++++------ .../CameraAccess/CameraController.android.cs | 25 ++++++++-- .../CameraEventsListener.android.cs | 10 ++-- 3 files changed, 55 insertions(+), 26 deletions(-) diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs index f025c1b4a..a5e87bec4 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs @@ -1,17 +1,13 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Android.Content; -using Android.Hardware.Camera2; -using Android.Hardware.Camera2.Params; using Android.Views; -using ZXing.Common; namespace ZXing.Mobile.CameraAccess { public class CameraAnalyzer { + readonly Context context; readonly CameraController cameraController; readonly CameraEventsListener cameraEventListener; Task processingTask; @@ -22,6 +18,7 @@ public class CameraAnalyzer public CameraAnalyzer(SurfaceView surfaceView, IScannerSessionHost scannerHost) { + context = surfaceView.Context; this.scannerHost = scannerHost; cameraEventListener = new CameraEventsListener(); cameraController = new CameraController(surfaceView, cameraEventListener, scannerHost); @@ -92,7 +89,7 @@ bool CanAnalyzeFrame } } - void HandleOnPreviewFrameReady(object sender, byte[] data) + void HandleOnPreviewFrameReady(object sender, (byte[] Nv21, int Width, int Height) data) { if (!CanAnalyzeFrame) return; @@ -117,21 +114,40 @@ void HandleOnPreviewFrameReady(object sender, byte[] data) }, TaskContinuationOptions.OnlyOnFaulted); } - void DecodeFrame(byte[] data) + void DecodeFrame((byte[] Nv21, int Width, int Height) data) { - var previewSize = cameraController.IdealPhotoSize; - var width = previewSize.Width; - var height = previewSize.Height; + var sensorRotation = cameraController.SensorRotation; var start = PerformanceCounter.Start(); + LuminanceSource source = new PlanarYUVLuminanceSource(data.Nv21, data.Width, data.Height, 0, 0, data.Width, data.Height, false); - barcodeReader.AutoRotate = true; - - var source = new PlanarYUVLuminanceSource(data, width, height, 0, 0, width, height, false); + if (!barcodeReader.AutoRotate && context.Resources.Configuration.Orientation == Android.Content.Res.Orientation.Portrait && sensorRotation != 0) + { + switch (sensorRotation) + { + case 90: + source = source.rotateCounterClockwise().rotateCounterClockwise().rotateCounterClockwise(); + break; + case 180: + source = source.rotateCounterClockwise().rotateCounterClockwise(); + break; + case 270: + source = source.rotateCounterClockwise(); + break; + } + } + var initPerformance = PerformanceCounter.Stop(start); + start = PerformanceCounter.Start(); var result = barcodeReader.Decode(source); - PerformanceCounter.Stop(start, - "Decode Time: {0} ms (width: " + width + ", height: " + height + ")"); + Android.Util.Log.Debug( + MobileBarcodeScanner.TAG, + "Decode Time: {0} ms (width: {1}, height: {2}, AutoRotation: {3}), Source setup: {4} ms", + PerformanceCounter.Stop(start).Milliseconds, + data.Width, + data.Height, + barcodeReader.AutoRotate, + initPerformance.Milliseconds); if (result != null) { diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs index 5119b22a5..1ba717205 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs @@ -39,7 +39,9 @@ public class CameraController public CameraDevice Camera { get; private set; } - public Size IdealPhotoSize { get; private set; } + public Size DisplaySize { get; private set; } + + public int SensorRotation { get; private set; } public CameraController(SurfaceView surfaceView, CameraEventsListener cameraEventListener, IScannerSessionHost scannerHost) { @@ -214,14 +216,15 @@ void SetUpCameraOutputs() var display = wm.DefaultDisplay; var point = new Point(); display.GetSize(point); - var idealSize = point.X > point.Y ? GetOptimalSize(supportedSizes, point.X, point.Y) : GetOptimalSize(supportedSizes, point.Y, point.X); - imageReader = ImageReader.NewInstance(idealSize.Width, idealSize.Height, ImageFormatType.Yuv420888, 5); + DisplaySize = new Size(point.X, point.Y); + + imageReader = ImageReader.NewInstance(DisplaySize.Width, DisplaySize.Height, ImageFormatType.Yuv420888, 5); flashSupported = HasFLash(characteristics); + SensorRotation = GetSensorRotation(characteristics); imageReader.SetOnImageAvailableListener(cameraEventListener, backgroundHandler); - IdealPhotoSize = idealSize; } catch (System.Exception ex) { @@ -242,6 +245,20 @@ bool HasFLash(CameraCharacteristics characteristics) } } + int GetSensorRotation(CameraCharacteristics characteristics) + { + var rotation = (int?)characteristics.Get(CameraCharacteristics.SensorOrientation); + if (rotation == null) + { + return 0; + } + else + { + + return rotation.Value; + } + } + public void OpenCamera() { if (context == null || OpeningCamera) diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs index f31350dc1..f75e8c0c2 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs @@ -1,9 +1,5 @@ using System; -using System.IO; -using Android.Content; -using Android.Graphics; using Android.Media; -using Java.Lang; using Java.Nio; using static Android.Media.ImageReader; @@ -11,7 +7,7 @@ namespace ZXing.Mobile.CameraAccess { public class CameraEventsListener : Java.Lang.Object, IOnImageAvailableListener { - public event EventHandler OnPreviewFrameReady; + public event EventHandler<(byte[] Nv21, int Width, int Height)> OnPreviewFrameReady; public CameraEventsListener() { @@ -25,10 +21,10 @@ public void OnImageAvailable(ImageReader reader) image = reader.AcquireLatestImage(); if (image is null) return; - var yuvBytes = Yuv420888toNv21(image); - OnPreviewFrameReady?.Invoke(this, yuvBytes); + + OnPreviewFrameReady?.Invoke(this, (yuvBytes, image.Width, image.Height)); } finally { From 4c86173ab60941e578086c45626c307822ac0ec2 Mon Sep 17 00:00:00 2001 From: DarkIrata Date: Mon, 18 Jul 2022 16:04:41 +0200 Subject: [PATCH 19/43] Spacings --- .../MobileBarcodeScanningOptions.shared.cs | 165 +++++++++--------- 1 file changed, 81 insertions(+), 84 deletions(-) diff --git a/ZXing.Net.Mobile/MobileBarcodeScanningOptions.shared.cs b/ZXing.Net.Mobile/MobileBarcodeScanningOptions.shared.cs index 2ab08614a..14f0f69ee 100644 --- a/ZXing.Net.Mobile/MobileBarcodeScanningOptions.shared.cs +++ b/ZXing.Net.Mobile/MobileBarcodeScanningOptions.shared.cs @@ -1,121 +1,118 @@ -using System; -using System.Collections; using System.Collections.Generic; using System.Linq; -using ZXing; namespace ZXing.Mobile { - public class MobileBarcodeScanningOptions - { - /// - /// Camera resolution selector delegate, must return the selected Resolution from the list of available resolutions - /// - public delegate CameraResolution CameraResolutionSelectorDelegate(List availableResolutions); + public class MobileBarcodeScanningOptions + { + /// + /// Camera resolution selector delegate, must return the selected Resolution from the list of available resolutions + /// + public delegate CameraResolution CameraResolutionSelectorDelegate(List availableResolutions); - public MobileBarcodeScanningOptions() - { - PossibleFormats = new List(); - //this.AutoRotate = true; - DelayBetweenAnalyzingFrames = 150; - InitialDelayBeforeAnalyzingFrames = 300; - DelayBetweenContinuousScans = 1000; - UseNativeScanning = false; - } + public MobileBarcodeScanningOptions() + { + PossibleFormats = new List(); + //this.AutoRotate = true; + DelayBetweenAnalyzingFrames = 150; + InitialDelayBeforeAnalyzingFrames = 300; + DelayBetweenContinuousScans = 1000; + UseNativeScanning = false; + } - public CameraResolutionSelectorDelegate CameraResolutionSelector { get; set; } + public CameraResolutionSelectorDelegate CameraResolutionSelector { get; set; } - public IEnumerable PossibleFormats { get; set; } + public IEnumerable PossibleFormats { get; set; } - public bool? TryHarder { get; set; } + public bool? TryHarder { get; set; } - public bool? PureBarcode { get; set; } + public bool? PureBarcode { get; set; } - public bool? AutoRotate { get; set; } + public bool? AutoRotate { get; set; } - public bool? UseCode39ExtendedMode { get; set; } + public bool? UseCode39ExtendedMode { get; set; } - public string CharacterSet { get; set; } + public string CharacterSet { get; set; } - public bool? TryInverted { get; set; } + public bool? TryInverted { get; set; } - public bool? UseFrontCameraIfAvailable { get; set; } + public bool? UseFrontCameraIfAvailable { get; set; } - public bool? AssumeGS1 { get; set; } + public bool? AssumeGS1 { get; set; } - public bool DisableAutofocus { get; set; } + public bool DisableAutofocus { get; set; } - public bool UseNativeScanning { get; set; } + public bool UseNativeScanning { get; set; } - public int DelayBetweenContinuousScans { get; set; } + public int DelayBetweenContinuousScans { get; set; } - public int DelayBetweenAnalyzingFrames { get; set; } - public int InitialDelayBeforeAnalyzingFrames { get; set; } + public int DelayBetweenAnalyzingFrames { get; set; } + public int InitialDelayBeforeAnalyzingFrames { get; set; } - public static MobileBarcodeScanningOptions Default - { - get { return new MobileBarcodeScanningOptions(); } - } + public static MobileBarcodeScanningOptions Default + { + get { return new MobileBarcodeScanningOptions(); } + } - public BarcodeReaderGeneric BuildBarcodeReader() - { - var reader = new BarcodeReaderGeneric(); - if (TryHarder.HasValue) - reader.Options.TryHarder = TryHarder.Value; - if (PureBarcode.HasValue) - reader.Options.PureBarcode = PureBarcode.Value; - if (AutoRotate.HasValue) - reader.AutoRotate = AutoRotate.Value; - if (UseCode39ExtendedMode.HasValue) - reader.Options.UseCode39ExtendedMode = UseCode39ExtendedMode.Value; - if (!string.IsNullOrEmpty(CharacterSet)) - reader.Options.CharacterSet = CharacterSet; - if (TryInverted.HasValue) - reader.TryInverted = TryInverted.Value; - if (AssumeGS1.HasValue) - reader.Options.AssumeGS1 = AssumeGS1.Value; + public BarcodeReaderGeneric BuildBarcodeReader() + { + var reader = new BarcodeReaderGeneric(); + if (TryHarder.HasValue) + reader.Options.TryHarder = TryHarder.Value; + if (PureBarcode.HasValue) + reader.Options.PureBarcode = PureBarcode.Value; + if (AutoRotate.HasValue) + reader.AutoRotate = AutoRotate.Value; + if (UseCode39ExtendedMode.HasValue) + reader.Options.UseCode39ExtendedMode = UseCode39ExtendedMode.Value; + if (!string.IsNullOrEmpty(CharacterSet)) + reader.Options.CharacterSet = CharacterSet; + if (TryInverted.HasValue) + reader.Options.TryInverted = TryInverted.Value; + if (AssumeGS1.HasValue) + reader.Options.AssumeGS1 = AssumeGS1.Value; - if (PossibleFormats?.Any() ?? false) - { - reader.Options.PossibleFormats = new List(); + if (PossibleFormats?.Any() ?? false) + { + reader.Options.PossibleFormats = new List(); - foreach (var pf in PossibleFormats) - reader.Options.PossibleFormats.Add(pf); - } + foreach (var pf in PossibleFormats) + reader.Options.PossibleFormats.Add(pf); + } - return reader; - } + return reader; + } - public MultiFormatReader BuildMultiFormatReader() - { - var reader = new MultiFormatReader(); + public MultiFormatReader BuildMultiFormatReader() + { + var reader = new MultiFormatReader(); - var hints = new Dictionary(); + var hints = new Dictionary(); - if (TryHarder.HasValue && TryHarder.Value) - hints.Add(DecodeHintType.TRY_HARDER, TryHarder.Value); - if (PureBarcode.HasValue && PureBarcode.Value) - hints.Add(DecodeHintType.PURE_BARCODE, PureBarcode.Value); + if (TryHarder.HasValue && TryHarder.Value) + hints.Add(DecodeHintType.TRY_HARDER, TryHarder.Value); + if (PureBarcode.HasValue && PureBarcode.Value) + hints.Add(DecodeHintType.PURE_BARCODE, PureBarcode.Value); - if (PossibleFormats?.Any() ?? false) - hints.Add(DecodeHintType.POSSIBLE_FORMATS, PossibleFormats); + if (PossibleFormats?.Any() ?? false) + hints.Add(DecodeHintType.POSSIBLE_FORMATS, PossibleFormats); - reader.Hints = hints; + reader.Hints = hints; - return reader; - } + return reader; + } - public CameraResolution GetResolution(List availableResolutions) - { - CameraResolution r = null; + public CameraResolution GetResolution(List availableResolutions) + { + CameraResolution r = null; - var dg = CameraResolutionSelector; + var dg = CameraResolutionSelector; - if (dg != null) - r = dg(availableResolutions); + if (dg != null) + r = dg(availableResolutions); - return r; - } - } + return r; + } + } } From 3c120fac2787a33af9b69b3e0d59684e4942d5d9 Mon Sep 17 00:00:00 2001 From: DarkIrata Date: Mon, 18 Jul 2022 16:09:10 +0200 Subject: [PATCH 20/43] Allow tools folder --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 740321b79..6572e8060 100644 --- a/.gitignore +++ b/.gitignore @@ -39,7 +39,6 @@ TestCloud/ .vs/ project.lock.json -tools/ .nugetapikey .mygetapikey *.xam From 9ad00627a327d93d0a2c09b8a4be5875cae89982 Mon Sep 17 00:00:00 2001 From: DarkIrata Date: Mon, 18 Jul 2022 16:09:28 +0200 Subject: [PATCH 21/43] Added ImageReceiver Tool --- Tools/ImageReceiver/ImageReceiver.csproj | 9 +++ Tools/ImageReceiver/Program.cs | 31 +++++++++ .../Properties/launchSettings.json | 27 ++++++++ .../appsettings.Development.json | 8 +++ Tools/ImageReceiver/appsettings.json | 9 +++ ZXing.Net.Mobile.sln | 65 ++++++++++++++++++- 6 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 Tools/ImageReceiver/ImageReceiver.csproj create mode 100644 Tools/ImageReceiver/Program.cs create mode 100644 Tools/ImageReceiver/Properties/launchSettings.json create mode 100644 Tools/ImageReceiver/appsettings.Development.json create mode 100644 Tools/ImageReceiver/appsettings.json diff --git a/Tools/ImageReceiver/ImageReceiver.csproj b/Tools/ImageReceiver/ImageReceiver.csproj new file mode 100644 index 000000000..c78c9c7e8 --- /dev/null +++ b/Tools/ImageReceiver/ImageReceiver.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + + diff --git a/Tools/ImageReceiver/Program.cs b/Tools/ImageReceiver/Program.cs new file mode 100644 index 000000000..14f13620e --- /dev/null +++ b/Tools/ImageReceiver/Program.cs @@ -0,0 +1,31 @@ +var builder = WebApplication.CreateBuilder(args); + +var app = builder.Build(); + +var picNum = 0; +var ignoreCount = 0; +var saveAfterAmount = 20; + +var imagePath = Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location)!, "Images"); +Directory.CreateDirectory(imagePath); +Console.WriteLine($"Saving images to: {imagePath}"); + + +app.MapPost("/", async context => +{ + if (ignoreCount >= saveAfterAmount) + { + using (var fs = File.Create(Path.Combine(imagePath, $"File_{picNum}.bmp"))) + { + picNum++; + await context.Request.Body.CopyToAsync(fs); + } + ignoreCount = 0; + } + + ignoreCount++; + + context.Response.StatusCode = 200; +}); + +app.Run(); \ No newline at end of file diff --git a/Tools/ImageReceiver/Properties/launchSettings.json b/Tools/ImageReceiver/Properties/launchSettings.json new file mode 100644 index 000000000..76b7d4176 --- /dev/null +++ b/Tools/ImageReceiver/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:63208", + "sslPort": 0 + } + }, + "profiles": { + "ImageReceiver": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:5390", + "dotnetRunMessages": true + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/Tools/ImageReceiver/appsettings.Development.json b/Tools/ImageReceiver/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/Tools/ImageReceiver/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Tools/ImageReceiver/appsettings.json b/Tools/ImageReceiver/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/Tools/ImageReceiver/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/ZXing.Net.Mobile.sln b/ZXing.Net.Mobile.sln index b33050cd7..c4914e227 100644 --- a/ZXing.Net.Mobile.sln +++ b/ZXing.Net.Mobile.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29806.167 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32616.157 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ZXing.Net.Mobile", "ZXing.Net.Mobile\ZXing.Net.Mobile.csproj", "{8B7A8AB6-35A4-4C9C-83E1-96BA8BE3C941}" EndProject @@ -32,6 +32,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.Forms.UWP", "Samples EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Forms.Tizen", "Samples\Sample.Forms\Sample.Forms.Tizen\Sample.Forms.Tizen.csproj", "{9CBD2F34-9649-48DE-9B35-0D1291F4E714}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{09527748-5F83-45A9-B1A4-DB3C85D136FB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageReceiver", "Tools\ImageReceiver\ImageReceiver.csproj", "{47387C60-8776-44DE-A73E-8D6C72D9BC43}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Ad-Hoc|Any CPU = Ad-Hoc|Any CPU @@ -686,6 +690,62 @@ Global {9CBD2F34-9649-48DE-9B35-0D1291F4E714}.Release|x64.Build.0 = Release|Any CPU {9CBD2F34-9649-48DE-9B35-0D1291F4E714}.Release|x86.ActiveCfg = Release|Any CPU {9CBD2F34-9649-48DE-9B35-0D1291F4E714}.Release|x86.Build.0 = Release|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Ad-Hoc|ARM64.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Ad-Hoc|ARM64.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Ad-Hoc|x64.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Ad-Hoc|x86.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.AppStore|ARM.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.AppStore|ARM.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.AppStore|ARM64.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.AppStore|ARM64.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.AppStore|iPhone.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.AppStore|x64.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.AppStore|x64.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.AppStore|x86.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.AppStore|x86.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Debug|Any CPU.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Debug|ARM.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Debug|ARM.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Debug|ARM64.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Debug|iPhone.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Debug|x64.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Debug|x64.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Debug|x86.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Debug|x86.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Release|Any CPU.ActiveCfg = Release|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Release|Any CPU.Build.0 = Release|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Release|ARM.ActiveCfg = Release|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Release|ARM.Build.0 = Release|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Release|ARM64.ActiveCfg = Release|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Release|ARM64.Build.0 = Release|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Release|iPhone.ActiveCfg = Release|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Release|iPhone.Build.0 = Release|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Release|x64.ActiveCfg = Release|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Release|x64.Build.0 = Release|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Release|x86.ActiveCfg = Release|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -699,6 +759,7 @@ Global {CFF9673E-1188-4646-BCC7-F7601F3A1D1A} = {E0DF8E5D-AF49-43C9-9921-D67268E75964} {96FBFDD1-F91A-44F6-962A-51E1AC7AAC63} = {E0DF8E5D-AF49-43C9-9921-D67268E75964} {9CBD2F34-9649-48DE-9B35-0D1291F4E714} = {E0DF8E5D-AF49-43C9-9921-D67268E75964} + {47387C60-8776-44DE-A73E-8D6C72D9BC43} = {09527748-5F83-45A9-B1A4-DB3C85D136FB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {887F72FF-99D0-4882-A73A-A913A0534F0C} From f9789651e8468c069657265e3be11bbe81d666fd Mon Sep 17 00:00:00 2001 From: DarkIrata Date: Mon, 18 Jul 2022 16:32:35 +0200 Subject: [PATCH 22/43] typo fixed --- .../Android/CameraAccess/CameraController.android.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs index 1ba717205..4ccec4f97 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs @@ -220,7 +220,7 @@ void SetUpCameraOutputs() imageReader = ImageReader.NewInstance(DisplaySize.Width, DisplaySize.Height, ImageFormatType.Yuv420888, 5); - flashSupported = HasFLash(characteristics); + flashSupported = HasFlash(characteristics); SensorRotation = GetSensorRotation(characteristics); imageReader.SetOnImageAvailableListener(cameraEventListener, backgroundHandler); @@ -232,7 +232,7 @@ void SetUpCameraOutputs() } } - bool HasFLash(CameraCharacteristics characteristics) + bool HasFlash(CameraCharacteristics characteristics) { var available = (Java.Lang.Boolean)characteristics.Get(CameraCharacteristics.FlashInfoAvailable); if (available == null) From 6b0c54de036f98a6df3fe3ee720777ce5c27dc5d Mon Sep 17 00:00:00 2001 From: DarkIrata Date: Mon, 18 Jul 2022 17:17:51 +0200 Subject: [PATCH 23/43] Prefere slight distortion but see whats on screen --- .../CameraAccess/CameraController.android.cs | 38 +++++-------------- 1 file changed, 10 insertions(+), 28 deletions(-) diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs index 4ccec4f97..b1b34c5a6 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs @@ -304,38 +304,20 @@ public void StartPreview() try { - var optimalPreviewSize = GetOptimalPreviewSize(surfaceView); - - if (Looper.MyLooper() == Looper.MainLooper) - { - holder.SetFixedSize(optimalPreviewSize.Width, optimalPreviewSize.Height); - } - else - { - var sizeSetResetEvent = new ManualResetEventSlim(false); - using (var handler = new Handler(Looper.MainLooper)) - { - handler.Post(() => - { - holder.SetFixedSize(optimalPreviewSize.Width, optimalPreviewSize.Height); - sizeSetResetEvent.Set(); - }); - } - - sizeSetResetEvent.Wait(); - sizeSetResetEvent.Reset(); - } - // This is needed bc otherwise the preview is sometimes distorted System.Threading.Thread.Sleep(30); - previewBuilder = Camera.CreateCaptureRequest(CameraTemplate.Preview); - previewBuilder.AddTarget(holder.Surface); - previewBuilder.AddTarget(imageReader.Surface); + var surfaces = new List + { + holder.Surface, + imageReader.Surface + }; - var surfaces = new List(); - surfaces.Add(holder.Surface); - surfaces.Add(imageReader.Surface); + previewBuilder = Camera.CreateCaptureRequest(CameraTemplate.Preview); + foreach (var surface in surfaces) + { + previewBuilder.AddTarget(surface); + } Camera.CreateCaptureSession(surfaces, new CameraCaptureStateListener From dd2e2592dc08662c0721bc9745f31af0b9b1a5c3 Mon Sep 17 00:00:00 2001 From: DarkIrata Date: Tue, 2 Aug 2022 10:16:00 +0200 Subject: [PATCH 24/43] Use helper class CapturedImageData --- .../CameraAccess/CameraAnalyzer.android.cs | 15 +++++++-------- .../CameraEventsListener.android.cs | 8 +++----- .../CameraAccess/CapturedImageData.android.cs | 18 ++++++++++++++++++ .../Android/DebugHelper.android.cs | 4 ++++ 4 files changed, 32 insertions(+), 13 deletions(-) create mode 100644 ZXing.Net.Mobile/Android/CameraAccess/CapturedImageData.android.cs diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs index a5e87bec4..627c21d05 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs @@ -89,7 +89,7 @@ bool CanAnalyzeFrame } } - void HandleOnPreviewFrameReady(object sender, (byte[] Nv21, int Width, int Height) data) + void HandleOnPreviewFrameReady(object sender, CapturedImageData data) { if (!CanAnalyzeFrame) return; @@ -114,13 +114,9 @@ void HandleOnPreviewFrameReady(object sender, (byte[] Nv21, int Width, int Heigh }, TaskContinuationOptions.OnlyOnFaulted); } - void DecodeFrame((byte[] Nv21, int Width, int Height) data) + void DecodeFrame(CapturedImageData data) { var sensorRotation = cameraController.SensorRotation; - - var start = PerformanceCounter.Start(); - LuminanceSource source = new PlanarYUVLuminanceSource(data.Nv21, data.Width, data.Height, 0, 0, data.Width, data.Height, false); - if (!barcodeReader.AutoRotate && context.Resources.Configuration.Orientation == Android.Content.Res.Orientation.Portrait && sensorRotation != 0) { switch (sensorRotation) @@ -136,17 +132,20 @@ void DecodeFrame((byte[] Nv21, int Width, int Height) data) break; } } - var initPerformance = PerformanceCounter.Stop(start); + var start = PerformanceCounter.Start(); + LuminanceSource source = new PlanarYUVLuminanceSource(data.Matrix, data.Width, data.Height, 0, 0, data.Width, data.Height, false); + var initPerformance = PerformanceCounter.Stop(start); start = PerformanceCounter.Start(); var result = barcodeReader.Decode(source); Android.Util.Log.Debug( MobileBarcodeScanner.TAG, - "Decode Time: {0} ms (width: {1}, height: {2}, AutoRotation: {3}), Source setup: {4} ms", + "Decode Time: {0} ms (width: {1}, height: {2}, AutoRotation: {3}, SensorRotation: {4}), Source setup: {5} ms", PerformanceCounter.Stop(start).Milliseconds, data.Width, data.Height, barcodeReader.AutoRotate, + sensorRotation, initPerformance.Milliseconds); if (result != null) diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs index f75e8c0c2..04ce4e635 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs @@ -7,7 +7,7 @@ namespace ZXing.Mobile.CameraAccess { public class CameraEventsListener : Java.Lang.Object, IOnImageAvailableListener { - public event EventHandler<(byte[] Nv21, int Width, int Height)> OnPreviewFrameReady; + public event EventHandler OnPreviewFrameReady; public CameraEventsListener() { @@ -21,10 +21,9 @@ public void OnImageAvailable(ImageReader reader) image = reader.AcquireLatestImage(); if (image is null) return; - var yuvBytes = Yuv420888toNv21(image); - - OnPreviewFrameReady?.Invoke(this, (yuvBytes, image.Width, image.Height)); + var bytes = Yuv420888toNv21(image); + OnPreviewFrameReady?.Invoke(null, new CapturedImageData(bytes, image.Width, image.Height)); } finally { @@ -70,7 +69,6 @@ byte[] Yuv420888toNv21(Image image) for (; pos < ySize; pos += width) { yBufferPos += rowStride; - Array.Copy(yArray, yBufferPos, nv21, pos, width); } } diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CapturedImageData.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CapturedImageData.android.cs new file mode 100644 index 000000000..b7ac0e3f8 --- /dev/null +++ b/ZXing.Net.Mobile/Android/CameraAccess/CapturedImageData.android.cs @@ -0,0 +1,18 @@ +namespace ZXing.Mobile.CameraAccess +{ + public class CapturedImageData + { + public byte[] Matrix { get; private set; } + + public int Width { get; private set; } + + public int Height { get; private set; } + + public CapturedImageData(byte[] matrix, int width, int height) + { + Matrix = matrix; + Width = width; + Height = height; + } + } +} diff --git a/ZXing.Net.Mobile/Android/DebugHelper.android.cs b/ZXing.Net.Mobile/Android/DebugHelper.android.cs index d4b8117e3..317eddfad 100644 --- a/ZXing.Net.Mobile/Android/DebugHelper.android.cs +++ b/ZXing.Net.Mobile/Android/DebugHelper.android.cs @@ -1,6 +1,7 @@ using System.IO; using System.Net.Http; using Android.Graphics; +using ZXing.Mobile.CameraAccess; namespace ZXing.Net.Mobile.Android { @@ -34,6 +35,9 @@ public static void SendNV21toJPEGToEndpoint(byte[] data, int width, int height, SendBytesToEndpoint(jpeg, endpoint); } + public static void SendNV21toJPEGToEndpoint(CapturedImageData data, string endpoint) + => SendNV21toJPEGToEndpoint(data.Matrix, data.Width, data.Height, endpoint); + static HttpClient GetHttpClient() { var handler = new HttpClientHandler(); From c5d86315d26794affa75246a31db681e3cbac12e Mon Sep 17 00:00:00 2001 From: DarkIrata Date: Tue, 2 Aug 2022 10:29:37 +0200 Subject: [PATCH 25/43] Call AutoFocus at least once --- .../Android/CameraAccess/CameraController.android.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs index b1b34c5a6..5302f7ece 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs @@ -65,6 +65,7 @@ public CameraController(SurfaceView surfaceView, CameraEventsListener cameraEven { Camera = camera; StartPreview(); + AutoFocus(); OpeningCamera = false; }, OnDisconnectedAction = camera => From 7af01bfcf020f2d74c9a1336eeb3259edadc8abe Mon Sep 17 00:00:00 2001 From: DarkIrata Date: Tue, 2 Aug 2022 10:30:41 +0200 Subject: [PATCH 26/43] =?UTF-8?q?if=20available=20and=20not=20other=20give?= =?UTF-8?q?n,=20use=20continousvideo=20mode=20f=C3=BCr=20auto=20focus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Android/CameraAccess/CameraController.android.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs index 5302f7ece..c2285c160 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs @@ -137,7 +137,11 @@ void AutoFocus(int x, int y, bool useCoordinates) // If we want to use coordinates // Also only if our camera supports Auto focus mode // Since FocusAreas only really work with FocusModeAuto set - if (useCoordinates && supportedFocusModes.Contains(ControlAFMode.Auto)) + if (supportedFocusModes.Contains(ControlAFMode.ContinuousVideo)) + { + previewBuilder.Set(CaptureRequest.ControlAfMode, (int)ControlAFMode.ContinuousVideo); + } + else if (useCoordinates && supportedFocusModes.Contains(ControlAFMode.Auto)) { // Let's give the touched area a 20 x 20 minimum size rect to focus on // So we'll offset -10 from the center of the touch and then From 7248a2615d61fe8bdb275cb863d26214bed63b34 Mon Sep 17 00:00:00 2001 From: DarkIrata Date: Tue, 2 Aug 2022 12:03:46 +0200 Subject: [PATCH 27/43] Use ideal sizes and update accordingly --- .../CameraAccess/CameraController.android.cs | 60 +++++++++++++++++-- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs index c2285c160..1b2f3d50e 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs @@ -32,6 +32,7 @@ public class CameraController CameraCaptureSession previewSession; CaptureRequest previewRequest; HandlerThread backgroundThread; + Size[] supportedSizes; public string CameraId { get; private set; } @@ -41,6 +42,8 @@ public class CameraController public Size DisplaySize { get; private set; } + public Size IdealPhotoSize { get; private set; } + public int SensorRotation { get; private set; } public CameraController(SurfaceView surfaceView, CameraEventsListener cameraEventListener, IScannerSessionHost scannerHost) @@ -139,6 +142,7 @@ void AutoFocus(int x, int y, bool useCoordinates) // Since FocusAreas only really work with FocusModeAuto set if (supportedFocusModes.Contains(ControlAFMode.ContinuousVideo)) { + previewBuilder.Set(CaptureRequest.ControlAfTrigger, (int)ControlAFTrigger.Cancel); previewBuilder.Set(CaptureRequest.ControlAfMode, (int)ControlAFMode.ContinuousVideo); } else if (useCoordinates && supportedFocusModes.Contains(ControlAFMode.Auto)) @@ -161,10 +165,10 @@ void AutoFocus(int x, int y, bool useCoordinates) { new MeteringRectangle(x, y, x + 20, y + 20, 1000) }); - } - // Finally autofocus (weather we used focus areas or not) - previewBuilder.Set(CaptureRequest.ControlAfTrigger, (int)ControlAFTrigger.Start); + // Finally autofocus (weather we used focus areas or not) + previewBuilder.Set(CaptureRequest.ControlAfTrigger, (int)ControlAFTrigger.Start); + } UpdatePreview(); } @@ -204,9 +208,8 @@ void SetUpCameraOutputs() var characteristics = cameraManager.GetCameraCharacteristics(CameraId); var map = (StreamConfigurationMap)characteristics.Get(CameraCharacteristics.ScalerStreamConfigurationMap); - Size[] supportedSizes = null; - if (characteristics != null) + if (characteristics != null && supportedSizes == null) supportedSizes = ((StreamConfigurationMap)characteristics .Get(CameraCharacteristics.ScalerStreamConfigurationMap)) .GetOutputSizes((int)ImageFormatType.Yuv420888); @@ -223,7 +226,9 @@ void SetUpCameraOutputs() display.GetSize(point); DisplaySize = new Size(point.X, point.Y); - imageReader = ImageReader.NewInstance(DisplaySize.Width, DisplaySize.Height, ImageFormatType.Yuv420888, 5); + IdealPhotoSize = DisplaySize.Width > DisplaySize.Height ? GetOptimalSize(supportedSizes, DisplaySize) : GetOptimalSize(supportedSizes, DisplaySize, true); + + imageReader = ImageReader.NewInstance(IdealPhotoSize.Width, IdealPhotoSize.Height, ImageFormatType.Yuv420888, 5); flashSupported = HasFlash(characteristics); SensorRotation = GetSensorRotation(characteristics); @@ -287,6 +292,11 @@ public void OpenCamera() Size GetOptimalPreviewSize(SurfaceView surface) { + if (IdealPhotoSize != null) + { + return IdealPhotoSize; + } + var width = surface.Width > surface.Height ? surface.Width : surface.Height; var height = surface.Width > surface.Height ? surface.Height : surface.Width; var characteristics = cameraManager.GetCameraCharacteristics(CameraId); @@ -303,12 +313,40 @@ Size GetOptimalPreviewSize(SurfaceView surface) return bestMatches.OrderByDescending(x => x.x.Width).ThenByDescending(x => x.x.Height).First().x; } + void SetupHolderSize() + { + if (Camera is null || holder is null || imageReader is null || backgroundHandler is null) return; + + var optimalPreviewSize = GetOptimalPreviewSize(surfaceView); + if (Looper.MyLooper() == Looper.MainLooper) + { + holder.SetFixedSize(optimalPreviewSize.Width, optimalPreviewSize.Height); + } + else + { + var sizeSetResetEvent = new ManualResetEventSlim(false); + using (var handler = new Handler(Looper.MainLooper)) + { + handler.Post(() => + { + holder.SetFixedSize(optimalPreviewSize.Width, optimalPreviewSize.Height); + sizeSetResetEvent.Set(); + }); + } + + sizeSetResetEvent.Wait(); + sizeSetResetEvent.Reset(); + } + } + public void StartPreview() { if (Camera is null || holder is null || imageReader is null || backgroundHandler is null) return; try { + SetupHolderSize(); + // This is needed bc otherwise the preview is sometimes distorted System.Threading.Thread.Sleep(30); @@ -332,6 +370,9 @@ public void StartPreview() }, OnConfiguredAction = session => { + if (previewSession != null) + previewSession.Dispose(); + previewSession = session; UpdatePreview(); } @@ -350,6 +391,11 @@ void UpdatePreview() try { + SetupHolderSize(); + + if (previewRequest != null) + previewRequest.Dispose(); + previewRequest = previewBuilder.Build(); previewSession.SetRepeatingRequest(previewRequest, null, backgroundHandler); } @@ -359,6 +405,8 @@ void UpdatePreview() } } + Size GetOptimalSize(IList sizes, Size displaySize, bool flipWidthWithHeight = false) => flipWidthWithHeight ? GetOptimalSize(sizes, displaySize.Height, displaySize.Width) : GetOptimalSize(sizes, displaySize.Width, displaySize.Height); + Size GetOptimalSize(IList sizes, int width, int height) { const int minimumSize = 1000; From 7507b08fb836298d80a76aa010908d0ae8cb16bd Mon Sep 17 00:00:00 2001 From: DarkIrata Date: Wed, 3 Aug 2022 16:54:22 +0200 Subject: [PATCH 28/43] Better LuminanceSource for nv21 --- .../PlanarNV21LuminanceSource.android.cs | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 ZXing.Net.Mobile/Android/PlanarNV21LuminanceSource.android.cs diff --git a/ZXing.Net.Mobile/Android/PlanarNV21LuminanceSource.android.cs b/ZXing.Net.Mobile/Android/PlanarNV21LuminanceSource.android.cs new file mode 100644 index 000000000..90028f1fc --- /dev/null +++ b/ZXing.Net.Mobile/Android/PlanarNV21LuminanceSource.android.cs @@ -0,0 +1,105 @@ +using System; + +namespace ZXing.Net.Mobile.Android +{ + public class PlanarNV21LuminanceSource : BaseLuminanceSource + { + int sensorRotation = 0; + + public override bool CropSupported => false; + + public override bool RotateSupported => true; + + public int CurrentRotation { get; private set; } + + public PlanarNV21LuminanceSource(int sensorRotation, byte[] nv21Data, int width, int height, bool correctToSensorOrientation = true) + : base(width, height) + { + this.sensorRotation = sensorRotation; + base.luminances = nv21Data; + Width = width; + Height = height; + + if (correctToSensorOrientation) + ValidateRotation(); + } + + void ValidateRotation() + { + if (sensorRotation % 90 != 0) // we don't support weird sensor orientations + { + return; + } + + if (sensorRotation != CurrentRotation) + { + var rotateResult = RotateNV21(luminances, Width, Height, sensorRotation); + luminances = rotateResult.NV21; + Width = rotateResult.Width; + Height = rotateResult.Height; + + CurrentRotation = sensorRotation; + } + } + + protected override LuminanceSource CreateLuminanceSource(byte[] newLuminances, int width, int height) + => new PlanarNV21LuminanceSource(0, newLuminances, width, height); + + public override LuminanceSource rotateCounterClockwise() => GetRotatedLuminanceSource(270); + + public LuminanceSource RotateClockwise() => GetRotatedLuminanceSource(90); + + public LuminanceSource Mirror() => GetRotatedLuminanceSource(180); + + LuminanceSource GetRotatedLuminanceSource(int rotation) + { + var rotateResult = RotateNV21(luminances, Width, Height, rotation); + return CreateLuminanceSource(rotateResult.NV21, rotateResult.Width, rotateResult.Height); + } + + // https://stackoverflow.com/questions/6853401/camera-pixels-rotated/31425229#31425229 + public static (byte[] NV21, int Width, int Height) RotateNV21(byte[] nv21, int width, int height, int rotation) + { + if (rotation == 0) + return (nv21, width, height); + + if (rotation % 90 != 0 || rotation < 0 || rotation > 270) + { + throw new ArgumentException("0 <= rotation < 360, rotation % 90 == 0"); + } + + var output = new byte[nv21.Length]; + var frameSize = width * height; + var swap = rotation % 180 != 0; + var xflip = rotation % 270 != 0; + var yflip = rotation > 180; + + for (var row = 0; row < height; row++) + { + for (var col = 0; col < width; col++) + { + var yInPos = row * width + col; + var uInPos = frameSize + (row >> 1) * width + (col & ~1); + var vInPos = uInPos + 1; + + var widthOut = swap ? height : width; + var heightOut = swap ? width : height; + var colSwapped = swap ? row : col; + var rowSwapped = swap ? col : row; + var colOut = xflip ? widthOut - colSwapped - 1 : colSwapped; + var rowOut = yflip ? heightOut - rowSwapped - 1 : rowSwapped; + + var yOutPos = rowOut * widthOut + colOut; + var uOutPos = frameSize + (rowOut >> 1) * widthOut + (colOut & ~1); + var vOutPos = uOutPos + 1; + + output[yOutPos] = (byte)(0xff & nv21[yInPos]); + output[uOutPos] = (byte)(0xff & nv21[uInPos]); + output[vOutPos] = (byte)(0xff & nv21[vInPos]); + } + } + + return (output, swap ? height : width, swap ? width : height); + } + } +} From 124104c4de4d1c3a0ffcf9ac084eea03db3c2a9d Mon Sep 17 00:00:00 2001 From: DarkIrata Date: Wed, 3 Aug 2022 16:54:51 +0200 Subject: [PATCH 29/43] Small adjustment in yuv420 to nv21 converter --- .../CameraAccess/CameraEventsListener.android.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs index 04ce4e635..d32e58db8 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs @@ -40,10 +40,11 @@ byte[] Yuv420888toNv21(Image image) var uvSize = width * height / 4; var nv21 = new byte[ySize + uvSize * 2]; + var planes = image.GetPlanes(); - var yBuffer = image.GetPlanes()[0].Buffer; // Y - var uBuffer = image.GetPlanes()[1].Buffer; // U - var vBuffer = image.GetPlanes()[2].Buffer; // V + var yBuffer = planes[0].Buffer; // Y + var uBuffer = planes[1].Buffer; // U + var vBuffer = planes[2].Buffer; // V var yArray = new byte[yBuffer.Limit()]; yBuffer.Get(yArray, 0, yArray.Length); @@ -54,7 +55,7 @@ byte[] Yuv420888toNv21(Image image) var vArray = new byte[vBuffer.Limit()]; vBuffer.Get(vArray, 0, vArray.Length); - var rowStride = image.GetPlanes()[0].RowStride; + var rowStride = planes[0].RowStride; var pos = 0; if (rowStride == width) @@ -73,8 +74,8 @@ byte[] Yuv420888toNv21(Image image) } } - rowStride = image.GetPlanes()[2].RowStride; - var pixelStride = image.GetPlanes()[2].PixelStride; + rowStride = planes[2].RowStride; + var pixelStride = planes[2].PixelStride; if (pixelStride == 2 && rowStride == width && uArray[0] == vArray[1]) { From 485a0942b630926bff0f88a7d7ca268910cd14c5 Mon Sep 17 00:00:00 2001 From: DarkIrata Date: Wed, 3 Aug 2022 16:55:36 +0200 Subject: [PATCH 30/43] Use the improved LuminanceSource with correct rotation --- .../CameraAccess/CameraAnalyzer.android.cs | 22 ++++--------------- 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs index 627c21d05..28bb41b57 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Android.Content; using Android.Views; +using ZXing.Net.Mobile.Android; namespace ZXing.Mobile.CameraAccess { @@ -117,30 +118,15 @@ void HandleOnPreviewFrameReady(object sender, CapturedImageData data) void DecodeFrame(CapturedImageData data) { var sensorRotation = cameraController.SensorRotation; - if (!barcodeReader.AutoRotate && context.Resources.Configuration.Orientation == Android.Content.Res.Orientation.Portrait && sensorRotation != 0) - { - switch (sensorRotation) - { - case 90: - source = source.rotateCounterClockwise().rotateCounterClockwise().rotateCounterClockwise(); - break; - case 180: - source = source.rotateCounterClockwise().rotateCounterClockwise(); - break; - case 270: - source = source.rotateCounterClockwise(); - break; - } - } - var start = PerformanceCounter.Start(); - LuminanceSource source = new PlanarYUVLuminanceSource(data.Matrix, data.Width, data.Height, 0, 0, data.Width, data.Height, false); + var source = new PlanarNV21LuminanceSource(sensorRotation, data.Matrix, data.Width, data.Height, true); + var initPerformance = PerformanceCounter.Stop(start); start = PerformanceCounter.Start(); var result = barcodeReader.Decode(source); Android.Util.Log.Debug( MobileBarcodeScanner.TAG, - "Decode Time: {0} ms (width: {1}, height: {2}, AutoRotation: {3}, SensorRotation: {4}), Source setup: {5} ms", + "Decode Time: {0} ms (Width: {1}, Height: {2}, AutoRotation: {3}, SensorRotation: {4}), Source setup: {5} ms", PerformanceCounter.Stop(start).Milliseconds, data.Width, data.Height, From c6115987a5d912c40fb5592f503e951d25da6459 Mon Sep 17 00:00:00 2001 From: DarkIrata Date: Wed, 3 Aug 2022 17:09:04 +0200 Subject: [PATCH 31/43] Changed ImageReader instances to 3 --- .../Android/CameraAccess/CameraController.android.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs index 1b2f3d50e..7575fc95f 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs @@ -228,7 +228,7 @@ void SetUpCameraOutputs() IdealPhotoSize = DisplaySize.Width > DisplaySize.Height ? GetOptimalSize(supportedSizes, DisplaySize) : GetOptimalSize(supportedSizes, DisplaySize, true); - imageReader = ImageReader.NewInstance(IdealPhotoSize.Width, IdealPhotoSize.Height, ImageFormatType.Yuv420888, 5); + imageReader = ImageReader.NewInstance(IdealPhotoSize.Width, IdealPhotoSize.Height, ImageFormatType.Yuv420888, 3); flashSupported = HasFlash(characteristics); SensorRotation = GetSensorRotation(characteristics); From b5476486a671b6cf062bfc87e866e07a923f75d5 Mon Sep 17 00:00:00 2001 From: DarkIrata Date: Thu, 4 Aug 2022 14:06:41 +0200 Subject: [PATCH 32/43] Some Debug Helper improvements --- Tools/ImageReceiver/Program.cs | 22 +++++++++----- .../Android/DebugHelper.android.cs | 30 +++++++++++++++---- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/Tools/ImageReceiver/Program.cs b/Tools/ImageReceiver/Program.cs index 14f13620e..896d25c19 100644 --- a/Tools/ImageReceiver/Program.cs +++ b/Tools/ImageReceiver/Program.cs @@ -1,10 +1,8 @@ -var builder = WebApplication.CreateBuilder(args); +var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); var picNum = 0; -var ignoreCount = 0; -var saveAfterAmount = 20; var imagePath = Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location)!, "Images"); Directory.CreateDirectory(imagePath); @@ -13,17 +11,25 @@ app.MapPost("/", async context => { - if (ignoreCount >= saveAfterAmount) + try { - using (var fs = File.Create(Path.Combine(imagePath, $"File_{picNum}.bmp"))) + var filePrefix = "File"; + if (context.Request.Headers.TryGetValue("FileName", out var newFilePrefix)) + { + filePrefix = newFilePrefix; + } + + using (var fs = File.Create(Path.Combine(imagePath, $"{filePrefix}_{picNum}.bmp"))) { picNum++; await context.Request.Body.CopyToAsync(fs); } - ignoreCount = 0; } - - ignoreCount++; + catch (Exception ex) + { + Console.Error.WriteLine($"Failed to save File_{picNum - 1}.bmp"); + Console.Error.WriteLine(ex.Message); + } context.Response.StatusCode = 200; }); diff --git a/ZXing.Net.Mobile/Android/DebugHelper.android.cs b/ZXing.Net.Mobile/Android/DebugHelper.android.cs index 317eddfad..13a7763d6 100644 --- a/ZXing.Net.Mobile/Android/DebugHelper.android.cs +++ b/ZXing.Net.Mobile/Android/DebugHelper.android.cs @@ -10,6 +10,9 @@ namespace ZXing.Net.Mobile.Android // This is a simple helping class to use with the ImageReceiver Tool public static class DebugHelper { + static int sendThreshold = 18; + static int sendCount; + // Used to check NV21 Output static byte[] NV21toJPEG(byte[] nv21, int width, int height) { @@ -21,22 +24,39 @@ static byte[] NV21toJPEG(byte[] nv21, int width, int height) } } - public static void SendBytesToEndpoint(byte[] data, string endpoint) + public static void SendBytesToEndpoint(byte[] data, string endpoint, bool ignoreThreshold = false, params (string Key, string Value)[] headers) { + if (!ignoreThreshold) + { + if (sendCount < sendThreshold) + { + sendCount++; + return; + } + + sendCount = 0; + } + var httpClient = GetHttpClient(); var request = new HttpRequestMessage(HttpMethod.Post, endpoint); request.Content = new ByteArrayContent(data); + + foreach (var header in headers) + { + httpClient.DefaultRequestHeaders.Add(header.Key, header.Value); + } + httpClient.SendAsync(request); } - public static void SendNV21toJPEGToEndpoint(byte[] data, int width, int height, string endpoint) + public static void SendNV21toJPEGToEndpoint(byte[] data, int width, int height, string endpoint, bool ignoreThreshold = false, params (string Key, string Value)[] headers) { var jpeg = NV21toJPEG(data, width, height); - SendBytesToEndpoint(jpeg, endpoint); + SendBytesToEndpoint(jpeg, endpoint, ignoreThreshold, headers); } - public static void SendNV21toJPEGToEndpoint(CapturedImageData data, string endpoint) - => SendNV21toJPEGToEndpoint(data.Matrix, data.Width, data.Height, endpoint); + public static void SendNV21toJPEGToEndpoint(CapturedImageData data, string endpoint, bool ignoreThreshold = false, params (string Key, string Value)[] headers) + => SendNV21toJPEGToEndpoint(data.Matrix, data.Width, data.Height, endpoint, ignoreThreshold, headers); static HttpClient GetHttpClient() { From 6cc85dc9c8839c7482294b63b791b43dca05ee88 Mon Sep 17 00:00:00 2001 From: DarkIrata Date: Thu, 4 Aug 2022 14:07:27 +0200 Subject: [PATCH 33/43] Better initial image rotation --- .../CameraAccess/CameraAnalyzer.android.cs | 12 +++- .../Android/DeviceOrientationData.android.cs | 20 +++++++ ...DeviceOrientationEventListener.android..cs | 42 ++++++++++++++ .../PlanarNV21LuminanceSource.android.cs | 56 ++++++++++++++----- 4 files changed, 112 insertions(+), 18 deletions(-) create mode 100644 ZXing.Net.Mobile/Android/DeviceOrientationData.android.cs create mode 100644 ZXing.Net.Mobile/Android/DeviceOrientationEventListener.android..cs diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs index 28bb41b57..ff8fe2f8b 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs @@ -11,6 +11,7 @@ public class CameraAnalyzer readonly Context context; readonly CameraController cameraController; readonly CameraEventsListener cameraEventListener; + readonly DeviceOrientationEventListener orientationEventListener; Task processingTask; DateTime lastPreviewAnalysis = DateTime.UtcNow; bool wasScanned; @@ -22,6 +23,7 @@ public CameraAnalyzer(SurfaceView surfaceView, IScannerSessionHost scannerHost) context = surfaceView.Context; this.scannerHost = scannerHost; cameraEventListener = new CameraEventsListener(); + orientationEventListener = new DeviceOrientationEventListener(context, Android.Hardware.SensorDelay.Normal); cameraController = new CameraController(surfaceView, cameraEventListener, scannerHost); Torch = new Torch(cameraController, surfaceView.Context); } @@ -42,12 +44,17 @@ public void ShutdownCamera() { IsAnalyzing = false; cameraEventListener.OnPreviewFrameReady -= HandleOnPreviewFrameReady; + orientationEventListener.Disable(); cameraController.ShutdownCamera(); } public void SetupCamera() { cameraEventListener.OnPreviewFrameReady += HandleOnPreviewFrameReady; + if (orientationEventListener.CanDetectOrientation()) + { + orientationEventListener.Enable(); + } barcodeReader = scannerHost.ScanningOptions.BuildBarcodeReader(); Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "Created Barcode Reader"); @@ -117,10 +124,9 @@ void HandleOnPreviewFrameReady(object sender, CapturedImageData data) void DecodeFrame(CapturedImageData data) { - var sensorRotation = cameraController.SensorRotation; + var orientationData = new DeviceOrientationData(context.Resources.Configuration.Orientation, orientationEventListener.Orientation, cameraController.SensorRotation); var start = PerformanceCounter.Start(); - var source = new PlanarNV21LuminanceSource(sensorRotation, data.Matrix, data.Width, data.Height, true); - + var source = new PlanarNV21LuminanceSource(data.Matrix, data.Width, data.Height, orientationData, (!barcodeReader.AutoRotate && orientationEventListener.IsEnabled)); var initPerformance = PerformanceCounter.Stop(start); start = PerformanceCounter.Start(); var result = barcodeReader.Decode(source); diff --git a/ZXing.Net.Mobile/Android/DeviceOrientationData.android.cs b/ZXing.Net.Mobile/Android/DeviceOrientationData.android.cs new file mode 100644 index 000000000..cf72ecb83 --- /dev/null +++ b/ZXing.Net.Mobile/Android/DeviceOrientationData.android.cs @@ -0,0 +1,20 @@ +using Android.Content.Res; + +namespace ZXing.Net.Mobile.Android +{ + public class DeviceOrientationData + { + public Orientation OrientationMode { get; } + + public int DeviceOrientation { get; } + + public int SensorRotation { get; } + + public DeviceOrientationData(Orientation orientationMode, int orientation, int sensorRotation) + { + OrientationMode = orientationMode; + DeviceOrientation = orientation; + SensorRotation = sensorRotation; + } + } +} diff --git a/ZXing.Net.Mobile/Android/DeviceOrientationEventListener.android..cs b/ZXing.Net.Mobile/Android/DeviceOrientationEventListener.android..cs new file mode 100644 index 000000000..e03175646 --- /dev/null +++ b/ZXing.Net.Mobile/Android/DeviceOrientationEventListener.android..cs @@ -0,0 +1,42 @@ +using Android.Content; +using Android.Hardware; +using Android.Runtime; +using Android.Views; + +namespace ZXing.Net.Mobile.Android +{ + public class DeviceOrientationEventListener : OrientationEventListener + { + public int Orientation { get; private set; } + + public bool IsEnabled { get; private set; } = false; + + public DeviceOrientationEventListener(Context context) + : base(context) + { + } + + public DeviceOrientationEventListener(Context context, [GeneratedEnum] SensorDelay rate) + : base(context, rate) + { + } + + public override void OnOrientationChanged(int orientation) + { + if (orientation != OrientationUnknown) + Orientation = orientation; + } + + public override void Enable() + { + base.Enable(); + IsEnabled = true; + } + + public override void Disable() + { + base.Disable(); + IsEnabled = false; + } + } +} diff --git a/ZXing.Net.Mobile/Android/PlanarNV21LuminanceSource.android.cs b/ZXing.Net.Mobile/Android/PlanarNV21LuminanceSource.android.cs index 90028f1fc..58feea0fc 100644 --- a/ZXing.Net.Mobile/Android/PlanarNV21LuminanceSource.android.cs +++ b/ZXing.Net.Mobile/Android/PlanarNV21LuminanceSource.android.cs @@ -1,49 +1,75 @@ using System; +using Android.Content.Res; namespace ZXing.Net.Mobile.Android { public class PlanarNV21LuminanceSource : BaseLuminanceSource { - int sensorRotation = 0; + DeviceOrientationData orientationData = null; public override bool CropSupported => false; public override bool RotateSupported => true; - public int CurrentRotation { get; private set; } - public PlanarNV21LuminanceSource(int sensorRotation, byte[] nv21Data, int width, int height, bool correctToSensorOrientation = true) + public PlanarNV21LuminanceSource(byte[] nv21Data, int width, int height, DeviceOrientationData orientationData, bool useOrientationDataToCorrect) : base(width, height) { - this.sensorRotation = sensorRotation; + this.orientationData = orientationData; base.luminances = nv21Data; Width = width; Height = height; - if (correctToSensorOrientation) + if (useOrientationDataToCorrect && orientationData == null) + throw new ArgumentNullException($"{orientationData} can't be null when correction is requested"); + + if (orientationData != null && useOrientationDataToCorrect) ValidateRotation(); } + public PlanarNV21LuminanceSource(byte[] nv21Data, int width, int height) + : this(nv21Data, width, height, null, false) + { + } + void ValidateRotation() { - if (sensorRotation % 90 != 0) // we don't support weird sensor orientations + if (orientationData.SensorRotation % 90 != 0) // we don't support weird sensor orientations { return; } - if (sensorRotation != CurrentRotation) + var rotateBy = 0; + if (orientationData.OrientationMode == Orientation.Landscape) { - var rotateResult = RotateNV21(luminances, Width, Height, sensorRotation); - luminances = rotateResult.NV21; - Width = rotateResult.Width; - Height = rotateResult.Height; - - CurrentRotation = sensorRotation; + if (orientationData.DeviceOrientation >= 180) // Navigation on the left, Header on the left (270°) + { + rotateBy = 270; // 270 + 90 = 360 || 360 zeros out so nothing to do + } + else if (orientationData.DeviceOrientation < 180) // Navigation on the left, Header on the Right (90°) + { + rotateBy = 90; + } } + else + { + if (orientationData.DeviceOrientation > 90) // Upside down and not landscape mode + { + rotateBy = 180; + } + } + + rotateBy += orientationData.SensorRotation; + rotateBy %= 360; + + var rotateResult = RotateNV21(luminances, Width, Height, rotateBy); + luminances = rotateResult.NV21; + Width = rotateResult.Width; + Height = rotateResult.Height; } protected override LuminanceSource CreateLuminanceSource(byte[] newLuminances, int width, int height) - => new PlanarNV21LuminanceSource(0, newLuminances, width, height); + => new PlanarNV21LuminanceSource(newLuminances, width, height, orientationData, false); public override LuminanceSource rotateCounterClockwise() => GetRotatedLuminanceSource(270); @@ -72,7 +98,7 @@ public static (byte[] NV21, int Width, int Height) RotateNV21(byte[] nv21, int w var frameSize = width * height; var swap = rotation % 180 != 0; var xflip = rotation % 270 != 0; - var yflip = rotation > 180; + var yflip = rotation >= 180; for (var row = 0; row < height; row++) { From d0cfe36842bf8eca08afcf93d8bbb6595beb3b8f Mon Sep 17 00:00:00 2001 From: DarkIrata Date: Thu, 4 Aug 2022 15:34:48 +0200 Subject: [PATCH 34/43] Adjusted Log Message --- .../Android/CameraAccess/CameraAnalyzer.android.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs index ff8fe2f8b..0e1c267d5 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs @@ -132,12 +132,12 @@ void DecodeFrame(CapturedImageData data) var result = barcodeReader.Decode(source); Android.Util.Log.Debug( MobileBarcodeScanner.TAG, - "Decode Time: {0} ms (Width: {1}, Height: {2}, AutoRotation: {3}, SensorRotation: {4}), Source setup: {5} ms", + "Decode Time: {0} ms (Width: {1}, Height: {2}, Rotations (S/D): {3} / {4}), Source setup: {5} ms", PerformanceCounter.Stop(start).Milliseconds, data.Width, data.Height, - barcodeReader.AutoRotate, - sensorRotation, + orientationData.SensorRotation, + orientationData.DeviceOrientation, initPerformance.Milliseconds); if (result != null) From 8eb531c61feec8698468cb6636c4ef57ebbfafc6 Mon Sep 17 00:00:00 2001 From: DarkIrata Date: Thu, 4 Aug 2022 15:35:20 +0200 Subject: [PATCH 35/43] Fixed Bug where potrait images gets rotated because of device orientation --- ZXing.Net.Mobile/Android/PlanarNV21LuminanceSource.android.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ZXing.Net.Mobile/Android/PlanarNV21LuminanceSource.android.cs b/ZXing.Net.Mobile/Android/PlanarNV21LuminanceSource.android.cs index 58feea0fc..0ac6936e0 100644 --- a/ZXing.Net.Mobile/Android/PlanarNV21LuminanceSource.android.cs +++ b/ZXing.Net.Mobile/Android/PlanarNV21LuminanceSource.android.cs @@ -53,14 +53,14 @@ void ValidateRotation() } else { - if (orientationData.DeviceOrientation > 90) // Upside down and not landscape mode + if (orientationData.DeviceOrientation > 175 && orientationData.DeviceOrientation < 185) // Upside down and not landscape mode { rotateBy = 180; } } rotateBy += orientationData.SensorRotation; - rotateBy %= 360; + rotateBy %= 360; // Normalize var rotateResult = RotateNV21(luminances, Width, Height, rotateBy); luminances = rotateResult.NV21; From e77093c91d23c61c8de35167e416dba3ffdbd32f Mon Sep 17 00:00:00 2001 From: DarkIrata Date: Thu, 4 Aug 2022 15:35:55 +0200 Subject: [PATCH 36/43] Moved initializing orientationdata for logging --- ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs index 0e1c267d5..2a0f2b725 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs @@ -124,8 +124,8 @@ void HandleOnPreviewFrameReady(object sender, CapturedImageData data) void DecodeFrame(CapturedImageData data) { - var orientationData = new DeviceOrientationData(context.Resources.Configuration.Orientation, orientationEventListener.Orientation, cameraController.SensorRotation); var start = PerformanceCounter.Start(); + var orientationData = new DeviceOrientationData(context.Resources.Configuration.Orientation, orientationEventListener.Orientation, cameraController.SensorRotation); var source = new PlanarNV21LuminanceSource(data.Matrix, data.Width, data.Height, orientationData, (!barcodeReader.AutoRotate && orientationEventListener.IsEnabled)); var initPerformance = PerformanceCounter.Stop(start); start = PerformanceCounter.Start(); From 4edae28bfaf4357d6a11ee5ab4656938b4087c2b Mon Sep 17 00:00:00 2001 From: DarkIrata Date: Thu, 4 Aug 2022 15:36:44 +0200 Subject: [PATCH 37/43] Adjusted Optimal Sizes for quality and performance --- .../CameraAccess/CameraController.android.cs | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs index 7575fc95f..64ccc4245 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs @@ -292,20 +292,20 @@ public void OpenCamera() Size GetOptimalPreviewSize(SurfaceView surface) { + var width = surface.Width > surface.Height ? surface.Width : surface.Height; + var height = surface.Width > surface.Height ? surface.Height : surface.Width; + var aspectRatio = (double)width / (double)height; + if (IdealPhotoSize != null) { - return IdealPhotoSize; + aspectRatio = (double)IdealPhotoSize.Width / (double)IdealPhotoSize.Height; } - var width = surface.Width > surface.Height ? surface.Width : surface.Height; - var height = surface.Width > surface.Height ? surface.Height : surface.Width; var characteristics = cameraManager.GetCameraCharacteristics(CameraId); var map = (StreamConfigurationMap)characteristics.Get(CameraCharacteristics.ScalerStreamConfigurationMap); var availableSizes = ((StreamConfigurationMap)characteristics .Get(CameraCharacteristics.ScalerStreamConfigurationMap)) .GetOutputSizes(Class.FromType(typeof(ISurfaceHolder))); - - var aspectRatio = (double)width / (double)height; var availableAspectRatios = availableSizes.Select(x => (x, (double)x.Width / (double)x.Height)); var differences = availableAspectRatios.Select(x => (x.x, System.Math.Abs(x.Item2 - aspectRatio))); @@ -405,20 +405,22 @@ void UpdatePreview() } } - Size GetOptimalSize(IList sizes, Size displaySize, bool flipWidthWithHeight = false) => flipWidthWithHeight ? GetOptimalSize(sizes, displaySize.Height, displaySize.Width) : GetOptimalSize(sizes, displaySize.Width, displaySize.Height); + Size GetOptimalSize(IList sizes, Size referenceSize, bool flipWidthWithHeight = false) => flipWidthWithHeight ? GetOptimalSize(sizes, referenceSize.Height, referenceSize.Width) : GetOptimalSize(sizes, referenceSize.Width, referenceSize.Height); Size GetOptimalSize(IList sizes, int width, int height) { - const int minimumSize = 1000; + const int minimumSize = 550; + const int maximumSize = 1200; if (sizes is null) return null; var aspectRatio = (double)width / (double)height; var availableAspectRatios = sizes.Select(x => (x, (double)x.Width / (double)x.Height)); var differences = availableAspectRatios.Select(x => (x.x, System.Math.Abs(x.Item2 - aspectRatio))); - var bestMatches = differences.OrderBy(x => x.Item2).ThenBy(x => System.Math.Abs(x.x.Width - width)).ThenBy(x => System.Math.Abs(x.x.Height - height)).Take(5); + var bestMatches = differences.OrderBy(x => x.Item2).ThenBy(x => System.Math.Abs(x.x.Width - width)).ThenBy(x => System.Math.Abs(x.x.Height - height)).Take(10); var orderedMatches = bestMatches.OrderBy(x => x.x.Width).ThenBy(x => x.x.Height); - return orderedMatches.Where(x => x.x.Height > minimumSize || x.x.Width > minimumSize).First().x; + var matches = orderedMatches.Where(x => (x.x.Height >= minimumSize || x.x.Width >= minimumSize) && (x.x.Height <= maximumSize || x.x.Width <= maximumSize)); + return matches.First().x; } void StartBackgroundThread() From 485c8095e707b9cf01cc6f367154687d2f402f49 Mon Sep 17 00:00:00 2001 From: DarkIrata Date: Thu, 4 Aug 2022 15:40:11 +0200 Subject: [PATCH 38/43] More samples since we got lower decode times --- .../Android/CameraAccess/CameraController.android.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs index 64ccc4245..597879ffe 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs @@ -228,7 +228,7 @@ void SetUpCameraOutputs() IdealPhotoSize = DisplaySize.Width > DisplaySize.Height ? GetOptimalSize(supportedSizes, DisplaySize) : GetOptimalSize(supportedSizes, DisplaySize, true); - imageReader = ImageReader.NewInstance(IdealPhotoSize.Width, IdealPhotoSize.Height, ImageFormatType.Yuv420888, 3); + imageReader = ImageReader.NewInstance(IdealPhotoSize.Width, IdealPhotoSize.Height, ImageFormatType.Yuv420888, 8); flashSupported = HasFlash(characteristics); SensorRotation = GetSensorRotation(characteristics); From d55753711ca6ecab822fb73e460c735ebf398dd8 Mon Sep 17 00:00:00 2001 From: DarkIrata Date: Fri, 5 Aug 2022 10:28:06 +0200 Subject: [PATCH 39/43] Debug call example --- .../Android/CameraAccess/CameraAnalyzer.android.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs index 2a0f2b725..75a40dc68 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs @@ -128,6 +128,9 @@ void DecodeFrame(CapturedImageData data) var orientationData = new DeviceOrientationData(context.Resources.Configuration.Orientation, orientationEventListener.Orientation, cameraController.SensorRotation); var source = new PlanarNV21LuminanceSource(data.Matrix, data.Width, data.Height, orientationData, (!barcodeReader.AutoRotate && orientationEventListener.IsEnabled)); var initPerformance = PerformanceCounter.Stop(start); + + //DebugHelper.SendNV21toJPEGToEndpoint(source.Matrix, source.Width, source.Height, "https://local.imagereceiver.ip:5390"); + start = PerformanceCounter.Start(); var result = barcodeReader.Decode(source); Android.Util.Log.Debug( From 67a8abc3148282539ba821f08e636ae1ccf0c46c Mon Sep 17 00:00:00 2001 From: DarkIrata Date: Fri, 5 Aug 2022 10:29:41 +0200 Subject: [PATCH 40/43] adjusted how the optimal sizes get determined --- .../Android/CameraAccess/CameraController.android.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs index 597879ffe..cd67d4cfb 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs @@ -409,7 +409,7 @@ void UpdatePreview() Size GetOptimalSize(IList sizes, int width, int height) { - const int minimumSize = 550; + const int minimumSize = 500; const int maximumSize = 1200; if (sizes is null) return null; @@ -419,7 +419,7 @@ Size GetOptimalSize(IList sizes, int width, int height) var differences = availableAspectRatios.Select(x => (x.x, System.Math.Abs(x.Item2 - aspectRatio))); var bestMatches = differences.OrderBy(x => x.Item2).ThenBy(x => System.Math.Abs(x.x.Width - width)).ThenBy(x => System.Math.Abs(x.x.Height - height)).Take(10); var orderedMatches = bestMatches.OrderBy(x => x.x.Width).ThenBy(x => x.x.Height); - var matches = orderedMatches.Where(x => (x.x.Height >= minimumSize || x.x.Width >= minimumSize) && (x.x.Height <= maximumSize || x.x.Width <= maximumSize)); + var matches = orderedMatches.Where(x => (x.x.Height >= minimumSize && x.x.Width >= minimumSize) && (x.x.Height <= maximumSize && x.x.Width <= maximumSize)); return matches.First().x; } From 8b6cc79e5172e1b175cc34817aba1cd93c4a89bb Mon Sep 17 00:00:00 2001 From: DarkIrata Date: Fri, 5 Aug 2022 11:27:18 +0200 Subject: [PATCH 41/43] Fallback for OptimalSize --- .../Android/CameraAccess/CameraController.android.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs index cd67d4cfb..b4c84d730 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs @@ -417,9 +417,15 @@ Size GetOptimalSize(IList sizes, int width, int height) var availableAspectRatios = sizes.Select(x => (x, (double)x.Width / (double)x.Height)); var differences = availableAspectRatios.Select(x => (x.x, System.Math.Abs(x.Item2 - aspectRatio))); - var bestMatches = differences.OrderBy(x => x.Item2).ThenBy(x => System.Math.Abs(x.x.Width - width)).ThenBy(x => System.Math.Abs(x.x.Height - height)).Take(10); + var bestMatches = differences.OrderBy(x => x.Item2).ThenBy(x => System.Math.Abs(x.x.Width - width)).ThenBy(x => System.Math.Abs(x.x.Height - height)); var orderedMatches = bestMatches.OrderBy(x => x.x.Width).ThenBy(x => x.x.Height); var matches = orderedMatches.Where(x => (x.x.Height >= minimumSize && x.x.Width >= minimumSize) && (x.x.Height <= maximumSize && x.x.Width <= maximumSize)); + + if (matches.Count() == 0) + { + return orderedMatches.First().x; + } + return matches.First().x; } From 8fe06c1195e52a8a6312fd491aba5add8b2a00fe Mon Sep 17 00:00:00 2001 From: DarkIrata Date: Fri, 5 Aug 2022 13:26:45 +0200 Subject: [PATCH 42/43] More OptimalSize adjustments --- .../Android/CameraAccess/CameraController.android.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs index b4c84d730..3f82576b0 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs @@ -410,16 +410,16 @@ void UpdatePreview() Size GetOptimalSize(IList sizes, int width, int height) { const int minimumSize = 500; - const int maximumSize = 1200; + const int maximumSize = 1280; if (sizes is null) return null; var aspectRatio = (double)width / (double)height; var availableAspectRatios = sizes.Select(x => (x, (double)x.Width / (double)x.Height)); var differences = availableAspectRatios.Select(x => (x.x, System.Math.Abs(x.Item2 - aspectRatio))); - var bestMatches = differences.OrderBy(x => x.Item2).ThenBy(x => System.Math.Abs(x.x.Width - width)).ThenBy(x => System.Math.Abs(x.x.Height - height)); + var bestMatches = differences.OrderBy(x => x.Item2).ThenBy(x => System.Math.Abs(x.x.Width - width)).ThenBy(x => System.Math.Abs(x.x.Height - height)).Take(sizes.Count / 2); var orderedMatches = bestMatches.OrderBy(x => x.x.Width).ThenBy(x => x.x.Height); - var matches = orderedMatches.Where(x => (x.x.Height >= minimumSize && x.x.Width >= minimumSize) && (x.x.Height <= maximumSize && x.x.Width <= maximumSize)); + var matches = orderedMatches.Where(x => (x.x.Height >= minimumSize || x.x.Width >= minimumSize) && (x.x.Height <= maximumSize || x.x.Width <= maximumSize)); if (matches.Count() == 0) { From 9f2319583ef64289936681e63cd4d3c729ec58a1 Mon Sep 17 00:00:00 2001 From: DarkIrata Date: Fri, 5 Aug 2022 13:46:00 +0200 Subject: [PATCH 43/43] Fixed image bending to a certain degree --- .../CameraAccess/CameraController.android.cs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs index 3f82576b0..c35b0cce9 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs @@ -296,11 +296,6 @@ Size GetOptimalPreviewSize(SurfaceView surface) var height = surface.Width > surface.Height ? surface.Height : surface.Width; var aspectRatio = (double)width / (double)height; - if (IdealPhotoSize != null) - { - aspectRatio = (double)IdealPhotoSize.Width / (double)IdealPhotoSize.Height; - } - var characteristics = cameraManager.GetCameraCharacteristics(CameraId); var map = (StreamConfigurationMap)characteristics.Get(CameraCharacteristics.ScalerStreamConfigurationMap); var availableSizes = ((StreamConfigurationMap)characteristics @@ -309,8 +304,9 @@ Size GetOptimalPreviewSize(SurfaceView surface) var availableAspectRatios = availableSizes.Select(x => (x, (double)x.Width / (double)x.Height)); var differences = availableAspectRatios.Select(x => (x.x, System.Math.Abs(x.Item2 - aspectRatio))); - var bestMatches = differences.OrderBy(x => x.Item2).ThenBy(x => System.Math.Abs(x.x.Width - width)).ThenBy(x => System.Math.Abs(x.x.Height - height)).Take(5); - return bestMatches.OrderByDescending(x => x.x.Width).ThenByDescending(x => x.x.Height).First().x; + var bestMatches = differences.OrderBy(x => x.Item2).ThenBy(x => System.Math.Abs(x.x.Width - width)).ThenBy(x => System.Math.Abs(x.x.Height - height)).Take(availableSizes.Count() / 2); + var matches = bestMatches.OrderBy(m => m.Item2).ThenByDescending(x => x.x.Width).ThenByDescending(x => x.x.Height); + return matches.First().x; } void SetupHolderSize() @@ -419,11 +415,11 @@ Size GetOptimalSize(IList sizes, int width, int height) var differences = availableAspectRatios.Select(x => (x.x, System.Math.Abs(x.Item2 - aspectRatio))); var bestMatches = differences.OrderBy(x => x.Item2).ThenBy(x => System.Math.Abs(x.x.Width - width)).ThenBy(x => System.Math.Abs(x.x.Height - height)).Take(sizes.Count / 2); var orderedMatches = bestMatches.OrderBy(x => x.x.Width).ThenBy(x => x.x.Height); - var matches = orderedMatches.Where(x => (x.x.Height >= minimumSize || x.x.Width >= minimumSize) && (x.x.Height <= maximumSize || x.x.Width <= maximumSize)); + var matches = orderedMatches.Where(m => (m.x.Height >= minimumSize || m.x.Width >= minimumSize) && (m.x.Height <= maximumSize || m.x.Width <= maximumSize)); if (matches.Count() == 0) { - return orderedMatches.First().x; + matches = orderedMatches; } return matches.First().x;