Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public interface ICameraView : IView
/// To customize the behavior of the camera when capturing an image, consider overriding the behavior through
/// <c>CameraViewHandler.CommandMapper.ReplaceMapping(nameof(ICameraView.CaptureImage), ADD YOUR METHOD);</c>.
/// </remarks>
ValueTask CaptureImage(CancellationToken token);
Task<Stream> CaptureImage(CancellationToken token);

/// <summary>
/// Starts the camera preview.
Expand Down
68 changes: 61 additions & 7 deletions src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -65,22 +65,26 @@ public partial class CameraView : View, ICameraView
/// Backing BindableProperty for the <see cref="CaptureImageCommand"/> property.
/// </summary>
public static readonly BindableProperty CaptureImageCommandProperty =
BindableProperty.CreateReadOnly(nameof(CaptureImageCommand), typeof(Command<CancellationToken>), typeof(CameraView), default, BindingMode.OneWayToSource, defaultValueCreator: CameraViewDefaults.CreateCaptureImageCommand).BindableProperty;
BindableProperty.CreateReadOnly(nameof(CaptureImageCommand), typeof(Command<CancellationToken>), typeof(CameraView), null, BindingMode.OneWayToSource, defaultValueCreator: CameraViewDefaults.CreateCaptureImageCommand).BindableProperty;

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

/// <summary>
/// Backing BindableProperty for the <see cref="StopCameraPreviewCommand"/> property.
/// </summary>
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;

/// <summary>
/// Event that is raised when the camera capture fails.
/// </summary>
Expand Down Expand Up @@ -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"));

/// <inheritdoc/>
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}

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

/// <inheritdoc cref="ICameraView.CaptureImage"/>
public ValueTask CaptureImage(CancellationToken token) =>
Handler.CameraManager.TakePicture(token);
public async Task<Stream> 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<Stream>();

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

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

/// <inheritdoc/>
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));
Expand Down
Loading