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));