1

I'm developing a Xamarin Forms application in the netstandard2.0 framework to deploy both on Android and iOS. The purpose of the program is to create a custom camera to capture and handle high resolution pictures from the back camera that I will send later on to a computer to be analysed.

I have managed to take photos at full resolution on iOS, but I struggle for Android on newer phones. On the first phone (Samsung SM-A510F, Android 7.0 API 24) I was able to take 12 MP pictures, which is the same as the native Camera app, but on the second phone (Samsung SM-A415F, Android 10.0 API 29) which has 3 back cameras (5MP, 8MP, 48MP) the resolution is limited to 3.5MP(2560*1440).

I have created a custom CameraPreview that I bind to my CameraPreviewRenderer and allows me to have one View linked to either native renderers.

CameraPreview in the shared code

using System;
using System.Collections.Generic;
using System.Text;
using Xamarin.Forms;

namespace XamPOC.CustomRenderer
{
    public enum CameraOptions
    {
        Rear,
        Front
    }
    public class CameraPreview : View
    {
        public static readonly BindableProperty CameraProperty = BindableProperty.Create(propertyName: "Camera", returnType: typeof(CameraOptions), declaringType: typeof(CameraPreview), defaultValue: CameraOptions.Rear);
        public CameraOptions Camera
        {
            get
            {
                return (CameraOptions)GetValue(CameraProperty);
            }
            set
            {
                SetValue(CameraProperty, value);
            }
        }

        public void CapturePhoto()
        {
            MessagingCenter.Send(this, "CapturePhoto");
        }
    }
}

CameraPreviewRenderer for Android

using Android.Content;
using Android.Hardware.Camera2;
using Android.Util;
using AndroidX.Camera.Core;
using AndroidX.Camera.Lifecycle;
using AndroidX.Core.Content;
using Java.Util.Concurrent;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using XamPOC;
using XamPOC.Droid;
using XamPOC.CustomRenderer;
using System;
using Android.App;
using Android.Graphics;
using Android.Hardware.Camera2.Params;
using AndroidX.Camera.Camera2.InterOp;
using AndroidX.Lifecycle;
using Java.Lang;
using XamPOC.Services;
using XamPOC.CustomRenderer.Droid;
using AndroidX.Camera.Core.Impl;
using AndroidX.Camera.Camera2.Internal.Compat;
using Android.Net.Wifi.Aware;

[assembly: ExportRenderer(typeof(CameraPreview), typeof(CameraPreviewRenderer))]
namespace XamPOC.Droid
{
    public class CameraPreviewRenderer : ViewRenderer<CameraPreview, UICameraPreview>
    {
        private UICameraPreview _uiCameraPreview;
        private IExecutorService _cameraExecutor;
        private ImageCapture _imageCapture;
        private CameraManager _cameraManager;

        public CameraPreviewRenderer(Context context) : base(context)
        {
            _cameraManager = (CameraManager) Context.GetSystemService(Context.CameraService);
            _cameraManager.GetCameraIdList();
        }

        protected override void OnElementChanged(ElementChangedEventArgs<CameraPreview> e)
        {
            base.OnElementChanged(e);
            if (e.OldElement != null)
            {
                MessagingCenter.Unsubscribe<CameraPreview>(this, "CapturePhoto");
            }
            if (e.NewElement != null)
            {
                if (Control == null)
                {
                    _uiCameraPreview = new UICameraPreview(Context);
                    SetNativeControl(_uiCameraPreview);

                    _cameraExecutor = Executors.NewSingleThreadExecutor();
                    StartCamera();
                }
                MessagingCenter.Subscribe<CameraPreview>(this, "CapturePhoto", async (sender) => TakePhoto());
            }
        }

        private void StartCamera()
        {
            var cameraProviderFuture = ProcessCameraProvider.GetInstance(Context);
            cameraProviderFuture.AddListener(new Runnable(() =>
            {
                var cameraProvider = (ProcessCameraProvider)cameraProviderFuture.Get();

                var preview = new Preview.Builder().Build();
                preview.SetSurfaceProvider(_uiCameraPreview.previewView.SurfaceProvider);
                var cameraSelector = new CameraSelector.Builder()
                    .RequireLensFacing(CameraSelector.LensFacingBack)
                    .Build();

                _imageCapture = new ImageCapture.Builder()
                    .SetCaptureMode(ImageCapture.CaptureModeMaximizeQuality)
                    .Build();

                try
                {
                    cameraProvider.UnbindAll();
                    var list = cameraProvider.AvailableCameraInfos;
                    foreach (var cameraInfo in list)
                    {
                        var c = cameraInfo;
                    }
                    var camera = cameraProvider.BindToLifecycle(GetLifecycleOwner(), cameraSelector, preview, _imageCapture);
                }
                catch (System.Exception ex)
                {
                    Log.Error("CameraPreviewRenderer", "Use case binding failed", ex);
                }
            }), ContextCompat.GetMainExecutor(Context));
        }

        private ILifecycleOwner GetLifecycleOwner()
        {
            if (Context is ILifecycleOwner lifecycleOwner)
            {
                return lifecycleOwner;
            }

            if (Context is Activity activity)
            {
                var lifecycleRegistry = new LifecycleRegistry((ILifecycleOwner)activity);
                lifecycleRegistry.SetCurrentState(Lifecycle.State.Started);
                return (ILifecycleOwner)lifecycleRegistry;
            }

            throw new InvalidOperationException("Context does not implement ILifecycleOwner");
        }

        public void TakePhoto()
        {
            if (_imageCapture == null)
                return;
            var fileService = DependencyService.Get<IFileService>();
            var folder = fileService.GetPicturesFolder();
            var photoFile = new Java.IO.File(System.IO.Path.Combine(folder, $"CCube{DateTime.Now:yyyyMMdd_HHmmss}.jpg"));
            var outputOptions = new ImageCapture.OutputFileOptions.Builder(photoFile)
                .Build();

            _imageCapture.TakePicture(outputOptions, _cameraExecutor, new ImageCaptureCallback(photoFile));
        }

        private class ImageCaptureCallback : Java.Lang.Object, ImageCapture.IOnImageSavedCallback
        {
            private Java.IO.File _photoFile;

            public ImageCaptureCallback(Java.IO.File photoFile)
            {
                _photoFile = photoFile;
            }

            public void OnError(ImageCaptureException exception)
            {
                Log.Error("CameraPreviewRenderer", "Photo capture failed: " + exception.Message);
            }

            public void OnImageSaved(ImageCapture.OutputFileResults outputFileResults)
            {
                Log.Debug("CameraPreviewRenderer", "Photo capture succeeded: " + _photoFile.AbsolutePath);
                byte[] ImageBytes = System.IO.File.ReadAllBytes(_photoFile.AbsolutePath);
                MessagingCenter.Send(this, "PhotoArray", ImageBytes);
                
            }
        }

        protected override void Dispose(bool disposing)
        {
            base.Dispose(disposing);

            if (disposing)
            {
                _cameraExecutor?.Shutdown();
            }
        }
    }
}

CameraPage that displays the camera

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
    x:Class="XamPOC.Views.CameraPage"
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:XamPOC.CustomRenderer"
    xmlns:vm="clr-namespace:XamPOC.ViewModels"
    Title="CameraPage"
    x:DataType="vm:CameraViewModel"
    NavigationPage.HasNavigationBar="true">
    <Grid Padding="0" BackgroundColor="Black">
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition x:Name="ImageRow" Height="10*" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <local:CameraPreview
            x:Name="ImageView"
            Grid.Row="1"
            Grid.RowSpan="11"
            Grid.Column="0"
            Grid.ColumnSpan="12"
            BindingContext="{Binding CameraPreview}" />
        <ImageButton
            Grid.Row="8"
            Grid.RowSpan="3"
            Grid.Column="4"
            Grid.ColumnSpan="4"
            Margin="10"
            BackgroundColor="Transparent"
            Clicked="Button_Clicked"
            Source="Camera.png" />
        <FlexLayout
            Grid.ColumnSpan="12"
            AlignContent="Center"
            AlignItems="Center"
            Direction="Row"
            JustifyContent="SpaceEvenly">
            <Label
                HorizontalTextAlignment="Center"
                Text="CLI"
                TextColor="White"
                WidthRequest="85" />
            <Switch Toggled="Switch_Toggled" />
            <Label
                HorizontalTextAlignment="Center"
                Text="DERM"
                TextColor="White"
                WidthRequest="85" />
        </FlexLayout>
    </Grid>
</ContentPage>

I also tried using the CameraXBasics repository, but I encounter the same issue (Resolution is not good enough), so I was wondering if that was the limit to a custom camera on Xamarin Forms.

Thank you for reading this far !

0