Skip to content

Commit ecc51ed

Browse files
Return Camera Image (Stream) from ICameraView.CaptureImage() (#2695)
* Update API * Support MediaCaptureFailed * Use try/finally block to unsubscribe event handlers * Add SemaphoreSlim to ensure correct Stream is returned * Add `IDisposable` * Update src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent a438997 commit ecc51ed

File tree

2 files changed

+62
-8
lines changed

2 files changed

+62
-8
lines changed

src/CommunityToolkit.Maui.Camera/Interfaces/ICameraView.shared.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public interface ICameraView : IView
5555
/// To customize the behavior of the camera when capturing an image, consider overriding the behavior through
5656
/// <c>CameraViewHandler.CommandMapper.ReplaceMapping(nameof(ICameraView.CaptureImage), ADD YOUR METHOD);</c>.
5757
/// </remarks>
58-
ValueTask CaptureImage(CancellationToken token);
58+
Task<Stream> CaptureImage(CancellationToken token);
5959

6060
/// <summary>
6161
/// Starts the camera preview.

src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ namespace CommunityToolkit.Maui.Views;
1313
[SupportedOSPlatform("android21.0")]
1414
[SupportedOSPlatform("ios")]
1515
[SupportedOSPlatform("maccatalyst")]
16-
public partial class CameraView : View, ICameraView
16+
public partial class CameraView : View, ICameraView, IDisposable
1717
{
1818
static readonly BindablePropertyKey isAvailablePropertyKey =
1919
BindableProperty.CreateReadOnly(nameof(IsAvailable), typeof(bool), typeof(CameraView), CameraViewDefaults.IsAvailable);
@@ -65,22 +65,26 @@ public partial class CameraView : View, ICameraView
6565
/// Backing BindableProperty for the <see cref="CaptureImageCommand"/> property.
6666
/// </summary>
6767
public static readonly BindableProperty CaptureImageCommandProperty =
68-
BindableProperty.CreateReadOnly(nameof(CaptureImageCommand), typeof(Command<CancellationToken>), typeof(CameraView), default, BindingMode.OneWayToSource, defaultValueCreator: CameraViewDefaults.CreateCaptureImageCommand).BindableProperty;
68+
BindableProperty.CreateReadOnly(nameof(CaptureImageCommand), typeof(Command<CancellationToken>), typeof(CameraView), null, BindingMode.OneWayToSource, defaultValueCreator: CameraViewDefaults.CreateCaptureImageCommand).BindableProperty;
6969

7070
/// <summary>
7171
/// Backing BindableProperty for the <see cref="StartCameraPreviewCommand"/> property.
7272
/// </summary>
7373
public static readonly BindableProperty StartCameraPreviewCommandProperty =
74-
BindableProperty.CreateReadOnly(nameof(StartCameraPreviewCommand), typeof(Command<CancellationToken>), typeof(CameraView), default, BindingMode.OneWayToSource, defaultValueCreator: CameraViewDefaults.CreateStartCameraPreviewCommand).BindableProperty;
74+
BindableProperty.CreateReadOnly(nameof(StartCameraPreviewCommand), typeof(Command<CancellationToken>), typeof(CameraView), null, BindingMode.OneWayToSource, defaultValueCreator: CameraViewDefaults.CreateStartCameraPreviewCommand).BindableProperty;
7575

7676
/// <summary>
7777
/// Backing BindableProperty for the <see cref="StopCameraPreviewCommand"/> property.
7878
/// </summary>
7979
public static readonly BindableProperty StopCameraPreviewCommandProperty =
80-
BindableProperty.CreateReadOnly(nameof(StopCameraPreviewCommand), typeof(ICommand), typeof(CameraView), default, BindingMode.OneWayToSource, defaultValueCreator: CameraViewDefaults.CreateStopCameraPreviewCommand).BindableProperty;
80+
BindableProperty.CreateReadOnly(nameof(StopCameraPreviewCommand), typeof(ICommand), typeof(CameraView), null, BindingMode.OneWayToSource, defaultValueCreator: CameraViewDefaults.CreateStopCameraPreviewCommand).BindableProperty;
8181

82+
83+
readonly SemaphoreSlim captureImageSemaphoreSlim = new(1, 1);
8284
readonly WeakEventManager weakEventManager = new();
8385

86+
bool isDisposed;
87+
8488
/// <summary>
8589
/// Event that is raised when the camera capture fails.
8690
/// </summary>
@@ -188,7 +192,14 @@ bool ICameraView.IsBusy
188192
set => SetValue(isCameraBusyPropertyKey, value);
189193
}
190194

191-
private protected new CameraViewHandler Handler => (CameraViewHandler)(base.Handler ?? throw new InvalidOperationException("Unable to retrieve Handler"));
195+
new CameraViewHandler Handler => (CameraViewHandler)(base.Handler ?? throw new InvalidOperationException("Unable to retrieve Handler"));
196+
197+
/// <inheritdoc/>
198+
public void Dispose()
199+
{
200+
Dispose(disposing: true);
201+
GC.SuppressFinalize(this);
202+
}
192203

193204
/// <inheritdoc cref="ICameraView.GetAvailableCameras"/>
194205
public async ValueTask<IReadOnlyList<CameraInfo>> GetAvailableCameras(CancellationToken token)
@@ -207,8 +218,37 @@ public async ValueTask<IReadOnlyList<CameraInfo>> GetAvailableCameras(Cancellati
207218
}
208219

209220
/// <inheritdoc cref="ICameraView.CaptureImage"/>
210-
public ValueTask CaptureImage(CancellationToken token) =>
211-
Handler.CameraManager.TakePicture(token);
221+
public async Task<Stream> CaptureImage(CancellationToken token)
222+
{
223+
// Use SemaphoreSlim to ensure `MediaCaptured` and `MediaCaptureFailed` events are unsubscribed before calling `TakePicture` again
224+
// 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
225+
await captureImageSemaphoreSlim.WaitAsync(token);
226+
227+
var mediaStreamTCS = new TaskCompletionSource<Stream>(TaskCreationOptions.RunContinuationsAsynchronously);
228+
229+
MediaCaptured += HandleMediaCaptured;
230+
MediaCaptureFailed += HandleMediaCapturedFailed;
231+
232+
try
233+
{
234+
await Handler.CameraManager.TakePicture(token);
235+
236+
var stream = await mediaStreamTCS.Task.WaitAsync(token);
237+
return stream;
238+
}
239+
finally
240+
{
241+
MediaCaptured -= HandleMediaCaptured;
242+
MediaCaptureFailed -= HandleMediaCapturedFailed;
243+
244+
// Release SemaphoreSlim after `MediaCaptured` and `MediaCaptureFailed` events are unsubscribed
245+
captureImageSemaphoreSlim.Release();
246+
}
247+
248+
void HandleMediaCaptured(object? sender, MediaCapturedEventArgs e) => mediaStreamTCS.SetResult(e.Media);
249+
250+
void HandleMediaCapturedFailed(object? sender, MediaCaptureFailedEventArgs e) => mediaStreamTCS.SetException(new CameraException(e.FailureReason));
251+
}
212252

213253
/// <inheritdoc cref="ICameraView.StartCameraPreview"/>
214254
public Task StartCameraPreview(CancellationToken token) =>
@@ -218,6 +258,20 @@ public Task StartCameraPreview(CancellationToken token) =>
218258
public void StopCameraPreview() =>
219259
Handler.CameraManager.StopCameraPreview();
220260

261+
/// <inheritdoc/>
262+
protected virtual void Dispose(bool disposing)
263+
{
264+
if (!isDisposed)
265+
{
266+
if (disposing)
267+
{
268+
captureImageSemaphoreSlim.Dispose();
269+
}
270+
271+
isDisposed = true;
272+
}
273+
}
274+
221275
void ICameraView.OnMediaCaptured(Stream imageData)
222276
{
223277
weakEventManager.HandleEvent(this, new MediaCapturedEventArgs(imageData), nameof(MediaCaptured));

0 commit comments

Comments
 (0)