Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Windows unexpected exceptions in multi-page applications. #146

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 110 additions & 47 deletions ZXing.Net.MAUI/Platforms/Windows/CameraManager.windows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,11 @@ internal partial class CameraManager
//Active camera properties
string _currentMediaFrameSourceGroupId;
string _currentMediaFrameSourceInfoId;
string _lastUnloadedMediaFrameSourceGroupId = null;
private SoftwareBitmap _backBuffer;
private bool _taskRunning = false;
private volatile bool _taskRunning = false;
private volatile bool _colorFrameReaderHandlerRunning = false;
private volatile bool _colorFrameReaderHandlerAvailable = false;
private readonly DispatcherQueue _dispatcherQueueUI = DispatcherQueue.GetForCurrentThread();

//MediaCapture
Expand Down Expand Up @@ -163,6 +166,7 @@ public NativePlatformCameraPreviewView CreateNativeView()
//See https://learn.microsoft.com/en-us/dotnet/maui/user-interface/handlers/create#native-view-cleanup
//for an explanation of DisconnectHandler() behaviour.
_cameraPreview.Unloaded += CameraPreview_Unloaded;
_cameraPreview.Loaded += CameraPreview_Loaded;
}
return _cameraPreview;
}
Expand Down Expand Up @@ -214,6 +218,9 @@ private async Task ConnectAsync()
{
await ExecuteLockedAsync(async () =>
{
//Connecting: forget any previously unloaded camera.
_lastUnloadedMediaFrameSourceGroupId = null;

await InitCameraUnlockedAsync();

#if ENABLE_DEVICE_WATCHER
Expand All @@ -226,6 +233,9 @@ private async Task DisconnectAsync()
{
await ExecuteLockedAsync(async () =>
{
//Disconnecting: forget any previously unloaded camera.
_lastUnloadedMediaFrameSourceGroupId = null;

#if ENABLE_DEVICE_WATCHER
UnregisterWatcher(this);
#endif
Expand All @@ -239,6 +249,12 @@ private async Task UpdateCameraAsync(string sourceGroupId = null)
{
await ExecuteLockedAsync(async () =>
{
if (!string.IsNullOrEmpty(_lastUnloadedMediaFrameSourceGroupId) && _lastUnloadedMediaFrameSourceGroupId.Equals(sourceGroupId))
{
//This cameraManager will be initialized in CameraPreview_Loaded handler.
return;
}

await InitCameraUnlockedAsync(sourceGroupId);
});
}
Expand Down Expand Up @@ -354,6 +370,7 @@ private async Task InitCameraUnlockedAsync(string sourceGroupId = null)
#else
_mediaFrameReader = await _mediaCapture.CreateFrameReaderAsync(selectedMediaFrameSource, MediaEncodingSubtypes.Bgra8);
#endif
_colorFrameReaderHandlerAvailable = true;
_mediaFrameReader.FrameArrived += ColorFrameReader_FrameArrived;
await _mediaFrameReader.StartAsync();

Expand All @@ -375,6 +392,7 @@ private async Task UninitCameraUnlockedAsync(string sourceGroupId = null)
{
if (_mediaFrameReader != null)
{
_colorFrameReaderHandlerAvailable = false;
await _mediaFrameReader.StopAsync();
_mediaFrameReader.FrameArrived -= ColorFrameReader_FrameArrived;
_mediaFrameReader.Dispose();
Expand All @@ -388,6 +406,20 @@ private async Task UninitCameraUnlockedAsync(string sourceGroupId = null)
}
_currentMediaFrameSourceInfoId = null;
_currentMediaFrameSourceGroupId = null;
//Wait for and flush any pending 'ColorFrameReader_FrameArrived' event.
while (_colorFrameReaderHandlerRunning || _taskRunning)
{
await Task.Delay(1);
}
//Detach any valid SoftwareBitmap from imageSource, to avoid random and unexpected win32 exceptions
//(such as 0xC000027B) when recreating the camera manager, after unloading and reloading the _cameraPreview control.
//The crash may happen in multipage applications, such as those based upon Shell layout.
_backBuffer?.Dispose();
_backBuffer = null;
{
var imageSource = _imageElement?.Source as SoftwareBitmapSource;
await imageSource?.SetBitmapAsync(null);
}
HidePreviewBitmap();
}
}
Expand Down Expand Up @@ -624,14 +656,31 @@ private void ShowPreviewBitmap()
#endif
_imageElement.Visibility = Microsoft.UI.Xaml.Visibility.Visible;
}
#endregion
#endregion

#region Event handlers
//Hack to call Disconnect() when closing the CameraManager owner, because,
//currently, DisconnectHandler() is not automatically called by Maui framework.
#region Event handlers
//Cleanup all camera resources when unloading the _cameraPreview element,
//otherwise the camera stream continuously generates frames also when this camera
//is temporarily hidden, for example when switching pages in Shell-based applications.
//Note: this handler is needed since DisconnectHandler() is not automatically called by
//Maui framework when closing the page owning this camera.
//See https://learn.microsoft.com/en-us/dotnet/maui/user-interface/handlers/create#native-view-cleanup
private void CameraPreview_Unloaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
Disconnect();
//Use CleanupCameraAsync instead of Disconnect to keep the DeviceWatcher alive. Only DisconnectHandler()
//will call Disconnect() to also detach the DeviceWatcher.
_lastUnloadedMediaFrameSourceGroupId = _currentMediaFrameSourceGroupId;
TryEnqueueUI(async () => await CleanupCameraAsync());
}

private void CameraPreview_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
if (!string.IsNullOrEmpty(_lastUnloadedMediaFrameSourceGroupId))
{
string sourceGroupId = _lastUnloadedMediaFrameSourceGroupId;
_lastUnloadedMediaFrameSourceGroupId = null;
TryEnqueueUI(async () => await UpdateCameraAsync(sourceGroupId));
}
}

//Reset the camera in case of errors
Expand All @@ -645,56 +694,70 @@ private void MediaCapture_Failed(MediaCapture sender, MediaCaptureFailedEventArg
//Display the captured frame and send it to the registered FrameReady owner.
private void ColorFrameReader_FrameArrived(MediaFrameReader sender, MediaFrameArrivedEventArgs args)
{
var mediaFrameReference = sender.TryAcquireLatestFrame();
var videoMediaFrame = mediaFrameReference?.VideoMediaFrame;
var softwareBitmap = videoMediaFrame?.SoftwareBitmap;

if (softwareBitmap != null)
if (_colorFrameReaderHandlerAvailable)
{
//Convert to Bgra8 Premultiplied softwareBitmap.
if (softwareBitmap.BitmapPixelFormat != Windows.Graphics.Imaging.BitmapPixelFormat.Bgra8 ||
softwareBitmap.BitmapAlphaMode != Windows.Graphics.Imaging.BitmapAlphaMode.Premultiplied)
{
softwareBitmap = SoftwareBitmap.Convert(softwareBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
}
_colorFrameReaderHandlerRunning = true;

//Send bitmap to BarCodeReaderView/CameraView
FrameReady?.Invoke(this, new CameraFrameBufferEventArgs(
new Readers.PixelBufferHolder
{
Data = softwareBitmap,
Size = new Microsoft.Maui.Graphics.Size(softwareBitmap.PixelWidth, softwareBitmap.PixelHeight)
}));

// Swap the processed frame to _backBuffer and dispose of the unused image.
softwareBitmap = Interlocked.Exchange(ref _backBuffer, softwareBitmap);
softwareBitmap?.Dispose();
try
{
var mediaFrameReference = sender.TryAcquireLatestFrame();
var videoMediaFrame = mediaFrameReference?.VideoMediaFrame;
var softwareBitmap = videoMediaFrame?.SoftwareBitmap;

// Changes to XAML ImageElement must happen on UI thread through Dispatcher
TryEnqueueUI(
async () =>
if (softwareBitmap != null)
{
// Don't let two copies of this task run at the same time.
if (_taskRunning)
//Convert to Bgra8 Premultiplied softwareBitmap.
if (softwareBitmap.BitmapPixelFormat != Windows.Graphics.Imaging.BitmapPixelFormat.Bgra8 ||
softwareBitmap.BitmapAlphaMode != Windows.Graphics.Imaging.BitmapAlphaMode.Premultiplied)
{
return;
softwareBitmap = SoftwareBitmap.Convert(softwareBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
}
_taskRunning = true;

// Keep draining frames from the backbuffer until the backbuffer is empty.
SoftwareBitmap latestBitmap;
while ((latestBitmap = Interlocked.Exchange(ref _backBuffer, null)) != null)
{
var imageSource = (SoftwareBitmapSource)_imageElement.Source;
await imageSource.SetBitmapAsync(latestBitmap);
latestBitmap.Dispose();
}
//Send bitmap to BarCodeReaderView/CameraView
FrameReady?.Invoke(this, new CameraFrameBufferEventArgs(
new Readers.PixelBufferHolder
{
Data = softwareBitmap,
Size = new Microsoft.Maui.Graphics.Size(softwareBitmap.PixelWidth, softwareBitmap.PixelHeight)
}));

_taskRunning = false;
});
}
// Swap the processed frame to _backBuffer and dispose of the unused image.
softwareBitmap = Interlocked.Exchange(ref _backBuffer, softwareBitmap);
softwareBitmap?.Dispose();

mediaFrameReference?.Dispose();
// Changes to XAML ImageElement must happen on UI thread through Dispatcher
TryEnqueueUI(
async () =>
{
// Don't let two copies of this task run at the same time.
if (_taskRunning || !_colorFrameReaderHandlerAvailable)
{
return;
}
_taskRunning = true;

// Keep draining frames from the backbuffer until the backbuffer is empty.
SoftwareBitmap latestBitmap;
while ((latestBitmap = Interlocked.Exchange(ref _backBuffer, null)) != null)
{
var imageSource = (SoftwareBitmapSource)_imageElement.Source;
await imageSource.SetBitmapAsync(latestBitmap);
latestBitmap.Dispose();
}

_taskRunning = false;
});
}

mediaFrameReference?.Dispose();
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}

_colorFrameReaderHandlerRunning = false;
}
}
#endregion

Expand Down
Loading