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.JsonConverter
1[[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