Skip to content

Commit c3ecbf1

Browse files
Update ICameraProvider, Use SemaphoreSlim in CameraProvider to avoid async/await race conditions, Implement IDisplosable
1 parent 05ad3f5 commit c3ecbf1

File tree

7 files changed

+62
-43
lines changed

7 files changed

+62
-43
lines changed

samples/CommunityToolkit.Maui.Sample/ViewModels/Views/CameraView/CameraViewViewModel.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,15 @@ public partial class CameraViewViewModel(ICameraProvider cameraProvider) : BaseV
4242
[ObservableProperty]
4343
public partial string ResolutionText { get; set; } = string.Empty;
4444

45-
public async Task InitializeAsync()
45+
public async ValueTask InitializeAsync()
4646
{
47-
await cameraProvider.InitializeAsync(CancellationToken.None);
48-
foreach (var camera in cameraProvider.AvailableCameras ?? [])
47+
if (!cameraProvider.IsInitialized)
4948
{
50-
Cameras.Add(camera);
49+
await cameraProvider.InitializeAsync(CancellationToken.None);
50+
foreach (var camera in cameraProvider.AvailableCameras ?? [])
51+
{
52+
Cameras.Add(camera);
53+
}
5154
}
5255
}
5356

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,19 @@ public interface ICameraProvider
1414
/// List is initialized using <see cref="InitializeAsync"/>, and can be refreshed using <see cref="RefreshAvailableCameras(CancellationToken)"/>
1515
/// </remarks>
1616
IReadOnlyList<CameraInfo>? AvailableCameras { get; }
17+
18+
/// <summary>
19+
/// Gets a value indicating whether the camera provider has been successfully initialized.
20+
/// </summary>
21+
bool IsInitialized { get; }
1722

1823
/// <summary>
1924
/// Refreshes <see cref="AvailableCameras"/> with the cameras available on device.
2025
/// </summary>
2126
/// <param name="token"></param>
2227
/// <returns></returns>
2328
[MemberNotNull(nameof(AvailableCameras))]
24-
ValueTask RefreshAvailableCameras(CancellationToken token);
25-
26-
/// <summary>
27-
/// Gets a value indicating whether the camera provider has been successfully initialized.
28-
/// </summary>
29-
bool IsInitialized { get; }
29+
Task RefreshAvailableCameras(CancellationToken token);
3030

3131
/// <summary>
3232
/// Initialize the camera provider by refreshing the <see cref="AvailableCameras"/>.

src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.android.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ partial class CameraProvider
1616
{
1717
readonly Context context = Android.App.Application.Context;
1818

19-
internal async partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token)
19+
private async partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token)
2020
{
2121
var cameraProviderFuture = ProcessCameraProvider.GetInstance(context);
2222

src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.macios.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ partial class CameraProvider
99
{
1010
static readonly AVCaptureDeviceType[] captureDevices = InitializeCaptureDevices();
1111

12-
internal partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token)
12+
private partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token)
1313
{
1414
var discoverySession = AVCaptureDeviceDiscoverySession.Create(captureDevices, AVMediaTypes.Video, AVCaptureDevicePosition.Unspecified);
1515
var availableCameras = new List<CameraInfo>();

src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.net.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ namespace CommunityToolkit.Maui.Core;
22

33
partial class CameraProvider
44
{
5-
internal partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token) => throw new NotSupportedException();
5+
private partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token) => throw new NotSupportedException();
66
}

src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,50 +3,67 @@
33
/// <summary>
44
/// Implementation that provides the ability to discover cameras that are attached to the current device.
55
/// </summary>
6-
partial class CameraProvider : ICameraProvider
6+
partial class CameraProvider : ICameraProvider, IDisposable
77
{
8-
readonly Lock refreshLock = new();
9-
Task? refreshTask;
8+
readonly SemaphoreSlim refreshAvailableCamerasSemaphore = new(1, 1);
9+
Task? refreshAvailableCamerasTask;
1010

11-
internal partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token);
11+
~CameraProvider()
12+
{
13+
Dispose(false);
14+
}
1215

1316
/// <inheritdoc/>
14-
public IReadOnlyList<CameraInfo>? AvailableCameras { get; private set; }
17+
public bool IsInitialized => refreshAvailableCamerasTask?.IsCompletedSuccessfully is true;
1518

1619
/// <inheritdoc/>
17-
public bool IsInitialized => refreshTask?.IsCompletedSuccessfully is true;
20+
public IReadOnlyList<CameraInfo>? AvailableCameras { get; private set; }
1821

19-
ValueTask GetRefreshTask(CancellationToken token)
22+
public void Dispose()
23+
{
24+
Dispose(true);
25+
GC.SuppressFinalize(this);
26+
}
27+
28+
/// <inheritdoc/>
29+
public async ValueTask InitializeAsync(CancellationToken token)
2030
{
21-
if (refreshTask is null || refreshTask.IsCompleted)
31+
if (!IsInitialized)
2232
{
23-
refreshTask = PlatformRefreshAvailableCameras(token).AsTask();
33+
await RefreshAvailableCameras(token);
2434
}
25-
26-
return new ValueTask(refreshTask);
2735
}
2836

29-
/// <inheritdoc/>
30-
public ValueTask InitializeAsync(CancellationToken token)
37+
/// <inheritdoc/>
38+
public async Task RefreshAvailableCameras(CancellationToken token)
3139
{
32-
lock (refreshLock)
40+
await refreshAvailableCamerasSemaphore.WaitAsync(token);
41+
42+
try
3343
{
34-
if (IsInitialized)
44+
if (refreshAvailableCamerasTask is null || refreshAvailableCamerasTask.IsCompleted)
3545
{
36-
return ValueTask.CompletedTask;
46+
refreshAvailableCamerasTask = PlatformRefreshAvailableCameras(token).AsTask();
3747
}
3848

39-
return GetRefreshTask(token);
49+
await refreshAvailableCamerasTask;
50+
}
51+
finally
52+
{
53+
refreshAvailableCamerasSemaphore.Release();
4054
}
4155
}
4256

43-
/// <inheritdoc/>
44-
public ValueTask RefreshAvailableCameras(CancellationToken token)
57+
void Dispose(bool disposing)
4558
{
46-
lock (refreshLock)
59+
if (disposing)
4760
{
48-
return GetRefreshTask(token);
61+
refreshAvailableCamerasSemaphore.Dispose();
62+
63+
refreshAvailableCamerasTask?.Dispose();
64+
refreshAvailableCamerasTask = null;
4965
}
5066
}
51-
52-
}
67+
68+
private partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token);
69+
}

src/CommunityToolkit.Maui.UnitTests/Mocks/MockCameraProvider.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,16 @@ public class MockCameraProvider : ICameraProvider
88

99
public bool IsInitialized { get; private set; }
1010

11-
public ValueTask InitializeAsync(CancellationToken token)
11+
public async ValueTask InitializeAsync(CancellationToken token)
1212
{
13-
if (IsInitialized)
13+
if (!IsInitialized)
1414
{
15-
return ValueTask.CompletedTask;
15+
await RefreshAvailableCameras(token);
16+
IsInitialized = true;
1617
}
17-
IsInitialized = true;
18-
return RefreshAvailableCameras(token);
1918
}
2019

21-
public ValueTask RefreshAvailableCameras(CancellationToken token)
20+
public Task RefreshAvailableCameras(CancellationToken token)
2221
{
2322
AvailableCameras =
2423
[
@@ -33,6 +32,6 @@ public ValueTask RefreshAvailableCameras(CancellationToken token)
3332
])
3433
];
3534

36-
return ValueTask.CompletedTask;
35+
return Task.CompletedTask;
3736
}
3837
}

0 commit comments

Comments
 (0)