Blazor WASM File Upload/IBrowserFile randomly fails on mobile devices

48 Views Asked by At

I am running a client-side .Net 7 Blazor WASM app.

The FileUpload component (IBrowserFile) randomly fails to return the selected photo/camera input.

This code works fine for most people, most of the time. Desktop browsers Chrome/Firefox/Edge, iPad, iPhone, and a Samsung Galaxy Tablet. However in the field, on some devices when the user takes a photo, or selects an image file, randomly, the app will just crash. It is either force restarted by the browser, or I get the error:

" Microsoft.JSInterop.JSException: An exception occurred executing JS interop: JSON serialization is attempting to deserialize an unexpected byte array.. See InnerException for more details. System.Text.Json.JsonException: JSON serialization is attempting to deserialize an unexpected byte array. at System.Text.Json.Serialization.JsonConverter1[[System.Byte[], System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].ReadCore(Utf8JsonReader& , JsonSerializerOptions , ReadStack& ) at System.Text.Json.JsonSerializer.Read[Object](Utf8JsonReader& , JsonTypeInfo ) at Microsoft.JSInterop.JSRuntime.EndInvokeJS(Int64 , Boolean , Utf8JsonReader& ) Exception_EndOfInnerExceptionStack at Microsoft.JSInterop.JSRuntime.<InvokeAsync>d__161[[System.Byte[], System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].MoveNext() at Microsoft.AspNetCore.Components.PullFromJSDataStream.RequestDataFromJSAsync(Int32 ) at Microsoft.AspNetCore.Components.PullFromJSDataStream.ReadAsync(Memory1 , CancellationToken ) at Microsoft.AspNetCore.Components.Forms.BrowserFileStream.CopyFileDataIntoBuffer(Memory1 , CancellationToken ) at Microsoft.AspNetCore.Components.Forms.BrowserFileStream.ReadAsync(Memory`1 , CancellationToken ) "

The component:

<MudFileUpload T="IBrowserFile" OnFilesChanged="e => OpenAddPhotoDialog(e, location.Id, PM_Mobile.Shared.DataConstants.PhotoLinkObject.Location)" Accept="image/*" MaximumFileCount="1" SuppressOnChangeWhenInvalid=true>
    <ButtonTemplate>
        <MudIconButton HtmlTag="label"
                Color="Color.Default"
                Size="MudBlazor.Size.Large"
                Icon="@Icons.Material.Outlined.PhotoCamera"
                for="@context.Id">
        </MudIconButton>
    </ButtonTemplate>
</MudFileUpload>

The event handler:

private async Task OpenAddPhotoDialog(InputFileChangeEventArgs e, int id, PM_Mobile.Shared.DataConstants.PhotoLinkObject objectType)
    {
        var photo = new PM_Mobile.Shared.Models.Photo();
        var photoData = new PM_Mobile.Shared.Models.PhotoData();
        var photoThumb = new PM_Mobile.Shared.Models.PhotoThumbnail
        
            try
            {
                var buffer = new byte[0];

                using (var source = e.File.OpenReadStream(Constants.Image_MaxSize))
                {
                    buffer = new byte[source.Length];
                    await source.ReadAsync(buffer, 0, (int)source.Length);
                }

                var images = await JS.InvokeAsync<List<byte[]>>("resizeImage", buffer);

                photoData.Data = images[0];
                photoThumb.Data = images[1];

                photo.FileSize = photoData.Data.Length;

                await IndexedDB.PutAsync(StoreName.PhotoData, photoData);
                await IndexedDB.PutAsync(StoreName.PhotoThumb, photoThumb);
                await IndexedDB.PutAsync(StoreName.Photo, photo);
                
                images.Clear();
                images = null;
                photoData = null;
                photoThumb = null;
                buffer = null;
            }
            catch (Exception ex)
            {
                throw new Exception("Failed to resize and store image", ex);
            }
    }

The javascript:

window.resizeImage = (buffer) => {
    return new Promise((resolve) => {

        const img = new Image();

        img.onload = () => {
            const canvas = document.createElement('canvas');
            const ctx = canvas.getContext('2d');

            var width = 1920;
            var height = 1920;

            var widthTh = 350;
            var heightTh = 200;

            var newHeight = img.naturalHeight * width / img.naturalWidth;
            var newHeightTh = img.naturalHeight * widthTh / img.naturalWidth;

            if (newHeight > height)
            {
                // Resize with height instead
                width = img.naturalWidth * height / img.naturalHeight;
                widthTh = img.naturalWidth * heightTh / img.naturalHeight;
            }
            else
            {
                height = newHeight;
                heightTh = newHeightTh;
            }

            canvas.width = width;
            canvas.height = height;

            ctx.drawImage(img, 0, 0, width, height);

            var images = [];

            var buffer = base64ToArrayBuffer(canvas.toDataURL('image/jpeg').split(';base64,')[1]);
            images.push(buffer);
            
            canvas.width = widthTh;
            canvas.height = heightTh;

            ctx.drawImage(img, 0, 0, widthTh, heightTh);

            buffer = base64ToArrayBuffer(canvas.toDataURL('image/jpeg').split(';base64,')[1]);
            images.push(buffer);

            img.src = null;
            
            URL.revokeObjectURL(imageBlob);

            resolve(images);
        };

        const imageFile = new File([buffer], "photo", { type: 'image/jpeg' });
        const imageBlob = URL.createObjectURL(imageFile);

        img.src = imageBlob;
    });
};

function base64ToArrayBuffer(base64) {
    var binaryString = atob(base64);
    var bytes = new Uint8Array(binaryString.length);

    for (var i = 0; i < binaryString.length; i++) {
        bytes[i] = binaryString.charCodeAt(i);
    }

    return bytes;
}

Very occasionally I also see the error:

"Cannot read property '_blazorFilesById' of null error"

I was originally passing Base64 strings between .Net and JS interop, but I then found that you can actually pass byte arrays. So I now do that instead, which is obviously much better but the problem remains.

I have removed all of the JavaScript resizing logic and used the framework provided e.File.RequestImageFileAsync("image/jpeg",1920,1920) method instead. Which made no difference.

I read some mention that the "InputFile element has to stay present, otherwise the browser cleans up any resources associated with it." I am not using the InputFile component directly, rather I am using the MudBlazor 'MudFileUpload' component, which I presume is essentially just a UI wrapper.

I am at my wits end with this problem, as it only presents in production, on mobile devices, and (possibly) only when on a 4g/5g connection.

Any help, or suggestions would be most welcome. Thanks

0

There are 0 best solutions below