From c7c8099cf9350a6b96e66fd5f417ec7b12d8651e Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Tue, 10 Jun 2025 11:06:41 -0700 Subject: [PATCH 1/6] Update API --- .../Interfaces/ICameraView.shared.cs | 2 +- .../Views/CameraView.shared.cs | 26 ++++++++++++++----- 2 files changed, 21 insertions(+), 7 deletions(-) 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..addaf334bf 100644 --- a/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs @@ -65,19 +65,19 @@ 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 WeakEventManager weakEventManager = new(); @@ -188,7 +188,7 @@ 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 async ValueTask> GetAvailableCameras(CancellationToken token) @@ -207,8 +207,22 @@ public async ValueTask> GetAvailableCameras(Cancellati } /// - public ValueTask CaptureImage(CancellationToken token) => - Handler.CameraManager.TakePicture(token); + public async Task CaptureImage(CancellationToken token) + { + var mediaStream = new TaskCompletionSource(); + + MediaCaptured += HandleMediaCaptured; + await Handler.CameraManager.TakePicture(token); + + var stream = await mediaStream.Task.WaitAsync(token); + return stream; + + void HandleMediaCaptured(object? sender, MediaCapturedEventArgs e) + { + MediaCaptured -= HandleMediaCaptured; + mediaStream.SetResult(e.Media); + } + } /// public Task StartCameraPreview(CancellationToken token) => From 18717b51e1cc3b5272042ab5388e625cbed83e43 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Tue, 10 Jun 2025 12:01:09 -0700 Subject: [PATCH 2/6] Support MediaCaptureFailed --- .../Views/CameraView.shared.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs b/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs index addaf334bf..bfb3a71d3f 100644 --- a/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs @@ -209,18 +209,30 @@ public async ValueTask> GetAvailableCameras(Cancellati /// public async Task CaptureImage(CancellationToken token) { - var mediaStream = new TaskCompletionSource(); + var mediaStreamTCS = new TaskCompletionSource(); MediaCaptured += HandleMediaCaptured; + MediaCaptureFailed += HandleMediaCapturedFailed; + await Handler.CameraManager.TakePicture(token); - var stream = await mediaStream.Task.WaitAsync(token); + var stream = await mediaStreamTCS.Task.WaitAsync(token); return stream; void HandleMediaCaptured(object? sender, MediaCapturedEventArgs e) { MediaCaptured -= HandleMediaCaptured; - mediaStream.SetResult(e.Media); + MediaCaptureFailed -= HandleMediaCapturedFailed; + + mediaStreamTCS.SetResult(e.Media); + } + + void HandleMediaCapturedFailed(object? sender, MediaCaptureFailedEventArgs e) + { + MediaCaptured -= HandleMediaCaptured; + MediaCaptureFailed -= HandleMediaCapturedFailed; + + mediaStreamTCS.SetException(new CameraException(e.FailureReason)); } } From 2f864fb8c049f80349071c0f8eb20e7a0e7b67ef Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Tue, 10 Jun 2025 12:14:08 -0700 Subject: [PATCH 3/6] Use try/finally block to unsubscribe event handlers --- .../Views/CameraView.shared.cs | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs b/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs index bfb3a71d3f..c6cc598170 100644 --- a/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs @@ -214,26 +214,22 @@ public async Task CaptureImage(CancellationToken token) MediaCaptured += HandleMediaCaptured; MediaCaptureFailed += HandleMediaCapturedFailed; - await Handler.CameraManager.TakePicture(token); - - var stream = await mediaStreamTCS.Task.WaitAsync(token); - return stream; - - void HandleMediaCaptured(object? sender, MediaCapturedEventArgs e) + try { - MediaCaptured -= HandleMediaCaptured; - MediaCaptureFailed -= HandleMediaCapturedFailed; + await Handler.CameraManager.TakePicture(token); - mediaStreamTCS.SetResult(e.Media); + var stream = await mediaStreamTCS.Task.WaitAsync(token); + return stream; } - - void HandleMediaCapturedFailed(object? sender, MediaCaptureFailedEventArgs e) + finally { MediaCaptured -= HandleMediaCaptured; MediaCaptureFailed -= HandleMediaCapturedFailed; - - mediaStreamTCS.SetException(new CameraException(e.FailureReason)); } + + void HandleMediaCaptured(object? sender, MediaCapturedEventArgs e) => mediaStreamTCS.SetResult(e.Media); + + void HandleMediaCapturedFailed(object? sender, MediaCaptureFailedEventArgs e) => mediaStreamTCS.SetException(new CameraException(e.FailureReason)); } /// From d5861f349725f5b986b8d677a1f3cf4e82f84d68 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Tue, 10 Jun 2025 12:28:11 -0700 Subject: [PATCH 4/6] Add SemaphoreSlim to ensure correct Stream is returned --- .../Views/CameraView.shared.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs b/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs index c6cc598170..649692b252 100644 --- a/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs @@ -15,6 +15,8 @@ namespace CommunityToolkit.Maui.Views; [SupportedOSPlatform("maccatalyst")] public partial class CameraView : View, ICameraView { + readonly SemaphoreSlim captureImageSemaphoreSlim = new(1,1); + static readonly BindablePropertyKey isAvailablePropertyKey = BindableProperty.CreateReadOnly(nameof(IsAvailable), typeof(bool), typeof(CameraView), CameraViewDefaults.IsAvailable); @@ -209,6 +211,10 @@ public async ValueTask> GetAvailableCameras(Cancellati /// 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(); MediaCaptured += HandleMediaCaptured; @@ -225,6 +231,9 @@ public async Task CaptureImage(CancellationToken token) { 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); From 7034ff3bbc098d299c6260ef3c3d9fa9e3c1aacb Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Tue, 10 Jun 2025 12:39:02 -0700 Subject: [PATCH 5/6] Add `IDisposable` --- .../Views/CameraView.shared.cs | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs b/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs index 649692b252..77d0a46bfb 100644 --- a/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs @@ -13,10 +13,8 @@ namespace CommunityToolkit.Maui.Views; [SupportedOSPlatform("android21.0")] [SupportedOSPlatform("ios")] [SupportedOSPlatform("maccatalyst")] -public partial class CameraView : View, ICameraView +public partial class CameraView : View, ICameraView, IDisposable { - readonly SemaphoreSlim captureImageSemaphoreSlim = new(1,1); - static readonly BindablePropertyKey isAvailablePropertyKey = BindableProperty.CreateReadOnly(nameof(IsAvailable), typeof(bool), typeof(CameraView), CameraViewDefaults.IsAvailable); @@ -81,8 +79,12 @@ public partial class CameraView : View, ICameraView public static readonly BindableProperty StopCameraPreviewCommandProperty = 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. /// @@ -192,6 +194,13 @@ bool ICameraView.IsBusy 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) { @@ -249,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)); From 3a1abfc446528db6b99313d00503e6e4111773ac Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Tue, 10 Jun 2025 12:42:49 -0700 Subject: [PATCH 6/6] Update src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs b/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs index 77d0a46bfb..edbd26460d 100644 --- a/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs @@ -224,7 +224,7 @@ public async Task CaptureImage(CancellationToken token) // 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(); + var mediaStreamTCS = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); MediaCaptured += HandleMediaCaptured; MediaCaptureFailed += HandleMediaCapturedFailed;