@@ -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