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;