diff --git a/src/CommunityToolkit.Maui.Camera/Interfaces/ICameraView.shared.cs b/src/CommunityToolkit.Maui.Camera/Interfaces/ICameraView.shared.cs
index 625aa51294..739617d0be 100644
--- a/src/CommunityToolkit.Maui.Camera/Interfaces/ICameraView.shared.cs
+++ b/src/CommunityToolkit.Maui.Camera/Interfaces/ICameraView.shared.cs
@@ -55,7 +55,7 @@ public interface ICameraView : IView
/// To customize the behavior of the camera when capturing an image, consider overriding the behavior through
/// CameraViewHandler.CommandMapper.ReplaceMapping(nameof(ICameraView.CaptureImage), ADD YOUR METHOD);.
///
- ValueTask CaptureImage(CancellationToken token);
+ Task CaptureImage(CancellationToken token);
///
/// Starts the camera preview.
diff --git a/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs b/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs
index cdfbcacc5f..edbd26460d 100644
--- a/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs
+++ b/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs
@@ -13,7 +13,7 @@ namespace CommunityToolkit.Maui.Views;
[SupportedOSPlatform("android21.0")]
[SupportedOSPlatform("ios")]
[SupportedOSPlatform("maccatalyst")]
-public partial class CameraView : View, ICameraView
+public partial class CameraView : View, ICameraView, IDisposable
{
static readonly BindablePropertyKey isAvailablePropertyKey =
BindableProperty.CreateReadOnly(nameof(IsAvailable), typeof(bool), typeof(CameraView), CameraViewDefaults.IsAvailable);
@@ -65,22 +65,26 @@ public partial class CameraView : View, ICameraView
/// Backing BindableProperty for the property.
///
public static readonly BindableProperty CaptureImageCommandProperty =
- BindableProperty.CreateReadOnly(nameof(CaptureImageCommand), typeof(Command), typeof(CameraView), default, BindingMode.OneWayToSource, defaultValueCreator: CameraViewDefaults.CreateCaptureImageCommand).BindableProperty;
+ BindableProperty.CreateReadOnly(nameof(CaptureImageCommand), typeof(Command), typeof(CameraView), null, BindingMode.OneWayToSource, defaultValueCreator: CameraViewDefaults.CreateCaptureImageCommand).BindableProperty;
///
/// Backing BindableProperty for the property.
///
public static readonly BindableProperty StartCameraPreviewCommandProperty =
- BindableProperty.CreateReadOnly(nameof(StartCameraPreviewCommand), typeof(Command), typeof(CameraView), default, BindingMode.OneWayToSource, defaultValueCreator: CameraViewDefaults.CreateStartCameraPreviewCommand).BindableProperty;
+ BindableProperty.CreateReadOnly(nameof(StartCameraPreviewCommand), typeof(Command), typeof(CameraView), null, BindingMode.OneWayToSource, defaultValueCreator: CameraViewDefaults.CreateStartCameraPreviewCommand).BindableProperty;
///
/// Backing BindableProperty for the property.
///
public static readonly BindableProperty StopCameraPreviewCommandProperty =
- BindableProperty.CreateReadOnly(nameof(StopCameraPreviewCommand), typeof(ICommand), typeof(CameraView), default, BindingMode.OneWayToSource, defaultValueCreator: CameraViewDefaults.CreateStopCameraPreviewCommand).BindableProperty;
+ BindableProperty.CreateReadOnly(nameof(StopCameraPreviewCommand), typeof(ICommand), typeof(CameraView), null, BindingMode.OneWayToSource, defaultValueCreator: CameraViewDefaults.CreateStopCameraPreviewCommand).BindableProperty;
+
+ readonly SemaphoreSlim captureImageSemaphoreSlim = new(1, 1);
readonly WeakEventManager weakEventManager = new();
+ bool isDisposed;
+
///
/// Event that is raised when the camera capture fails.
///
@@ -188,7 +192,14 @@ bool ICameraView.IsBusy
set => SetValue(isCameraBusyPropertyKey, value);
}
- private protected new CameraViewHandler Handler => (CameraViewHandler)(base.Handler ?? throw new InvalidOperationException("Unable to retrieve Handler"));
+ new CameraViewHandler Handler => (CameraViewHandler)(base.Handler ?? throw new InvalidOperationException("Unable to retrieve Handler"));
+
+ ///
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
///
public async ValueTask> GetAvailableCameras(CancellationToken token)
@@ -207,8 +218,37 @@ public async ValueTask> GetAvailableCameras(Cancellati
}
///
- public ValueTask CaptureImage(CancellationToken token) =>
- Handler.CameraManager.TakePicture(token);
+ public async Task CaptureImage(CancellationToken token)
+ {
+ // Use SemaphoreSlim to ensure `MediaCaptured` and `MediaCaptureFailed` events are unsubscribed before calling `TakePicture` again
+ // Without this SemaphoreSlim, previous calls to this method will fire `MediaCaptured` and/or `MediaCaptureFailed` events causing this method to return the wrong Stream or throw the wrong Exception
+ await captureImageSemaphoreSlim.WaitAsync(token);
+
+ var mediaStreamTCS = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ MediaCaptured += HandleMediaCaptured;
+ MediaCaptureFailed += HandleMediaCapturedFailed;
+
+ try
+ {
+ await Handler.CameraManager.TakePicture(token);
+
+ var stream = await mediaStreamTCS.Task.WaitAsync(token);
+ return stream;
+ }
+ finally
+ {
+ MediaCaptured -= HandleMediaCaptured;
+ MediaCaptureFailed -= HandleMediaCapturedFailed;
+
+ // Release SemaphoreSlim after `MediaCaptured` and `MediaCaptureFailed` events are unsubscribed
+ captureImageSemaphoreSlim.Release();
+ }
+
+ void HandleMediaCaptured(object? sender, MediaCapturedEventArgs e) => mediaStreamTCS.SetResult(e.Media);
+
+ void HandleMediaCapturedFailed(object? sender, MediaCaptureFailedEventArgs e) => mediaStreamTCS.SetException(new CameraException(e.FailureReason));
+ }
///
public Task StartCameraPreview(CancellationToken token) =>
@@ -218,6 +258,20 @@ public Task StartCameraPreview(CancellationToken token) =>
public void StopCameraPreview() =>
Handler.CameraManager.StopCameraPreview();
+ ///
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!isDisposed)
+ {
+ if (disposing)
+ {
+ captureImageSemaphoreSlim.Dispose();
+ }
+
+ isDisposed = true;
+ }
+ }
+
void ICameraView.OnMediaCaptured(Stream imageData)
{
weakEventManager.HandleEvent(this, new MediaCapturedEventArgs(imageData), nameof(MediaCaptured));