Taking photos on Android with Blazor Webassembly "Out Of Memory" Exception

331 Views Asked by At

I am new to Blazor Webassembly. I am trying to take a simple photo on my Android device (Samsung Galaxy Tab S7 FE).

Here is the code

<div>
    <InputFile OnChange="@OnFileSelection" id="ImageInput"></InputFile>
    <img src="@imgUrl" style="max-width: 500px; max-height: 500px;">
</div>

@code {

  private async Task OnFileSelection(InputFileChangeEventArgs e)
    {
        IBrowserFile imgFile = e.File;
        var currentImageType = imgFile.ContentType;

        var resizedImageFile = await imgFile.RequestImageFileAsync(currentImageType, 1000, 1000);
        var imageBytes = new byte[resizedImageFile.Size];
        await resizedImageFile.OpenReadStream().ReadAsync(imageBytes);
        imgUrl = $"data:{currentImageType};base64,{Convert.ToBase64String(imageBytes)}";

        StateHasChanged();
    }

}

On Windows in Edge, the InputFile works as Upload, because I have no camera installed i guess. On my Android Device I got the Popup (just once) to choose between Camera or Upload (FileExplorer).

So I am choosing Camera, the camera opens and after taking the photo and confirm the photo with "OK", the app crashes and restarts with the Message "Unable to complete previous operation due to low memory".

enter image description here

At first, I tried using await imgFile.OpenReadStream().ReadAsync(CurrentImageBytes); on the original image, without scaling the image down with await imgFile.RequestImageFileAsync(currentImageType, 1000, 1000);. But in this case, it worked never.

With scaling the image to 1000x1000 it crashes only lets say every 5th time. (but sometimes it still crashes at the first time)

Is there any best practise example for taking images with Blazor Webassemblys or can i make other improvments on the memory usage?

EDIT:

As mentioned, the app does not crash every time, so I am able to take some photos and save them to my DB (I am using SqliteWasmHelper Nuget).

The app does not crash, display more than 10 images at the same time, using the base64 transformation for each of them.

    <div style="display: flex; flex-direction: row; gap: 10px; flex-wrap: wrap;">
        @foreach (var bitimage in myImages)
        {
            <img src="@bitimage.ImageByteSource" style="max-width: 200px; max-height: 200px; object-fit:contain;">
        }
    </div>
        public string ImageByteSource
        {
            get
            {
                var imagesrc = Convert.ToBase64String(ImageData);
                return string.Format("data:image/jpeg;base64,{0}", imagesrc);
            }
        }

EDIT 2:

Even if I reduce my method to a minimum and just display to a label the size of the image result:

  private async Task OnFileSelection(InputFileChangeEventArgs e)
  {
      IBrowserFile imgFile = e.File;
      CurrentImageType = imgFile.ContentType;
      ImageBytesLabel = imgFile.Size.ToString();
}

I get the memory error. Is there a way to set the maximum resolution of the taken image BEFORE taking the image.

EDIT 3: I DO NOT crash on my Google Pixel 7, old HUAWEI Tab or on my iPAD. Meanwhile i think, the problem is the build in camera app from Samsung. I had problems with Xamarin.Android an images on this tablet too. The app uses all of the memory (or releases all memory) for the camera app.

2

There are 2 best solutions below

1
jokuskay On

Given you must load all bytes into memory in order to convert it to a Base64 string, consider increasing maxAllowedSize which is 500 KB by default.

await resizedImageFile.OpenReadStream(maxAllowedSize: 1000000).ReadAsync(imageBytes);
2
Fco P. On

The problem here is you are doing two things that can deplete your RAM: Loading an entire file in memory, and convert it to Base64. Each one of those can lead to OOMs by themselves. Loading the file is bad because it can be too big, and passing a file to Base64 will make it about a 40% bigger, which is obviously worse. So avoid it.

Instead of trying to pass the file contents, allow your webview to read local files, and then, after taking the picture, pass the file path to the img tag.

So instead of

var resizedImageFile = await imgFile.RequestImageFileAsync(currentImageType, 1000, 1000);
var imageBytes = new byte[resizedImageFile.Size];
await resizedImageFile.OpenReadStream().ReadAsync(imageBytes);
imgUrl = $"data:{currentImageType};base64,{Convert.ToBase64String(imageBytes)}";

you need to do something like

var resizedImageFile = await imgFile.RequestImageFileAsync(currentImageType, 1000, 1000);
imgUrl = "$resizedImageFile.path";

If StateHasChanged works as I suppose and triggers a reloading, then your img tag will display the image. As an alternative, you could return a fake URL "pointing" to the image, and set up interception at the webview level to provide the file's content from there. In any case, base 64 will not be needed.