From dd28128ac2821f830e38621b11967e3e4224f71e Mon Sep 17 00:00:00 2001 From: Zhitao Pan Date: Thu, 17 Apr 2025 13:40:42 +1000 Subject: [PATCH 01/12] Fix CameraView crash when switching camera on Windows * Add automatic initialization in CameraProvider * Remove unnecessary refresh cameras calls in CameraManager * Add camera switcher in camera sample page --- .../Views/CameraView/CameraViewPage.xaml | 12 +++++-- .../Views/CameraView/CameraViewPage.xaml.cs | 5 ++- .../Views/CameraView/CameraViewViewModel.cs | 25 +++++++++++++- .../CameraManager.android.cs | 25 +++----------- .../CameraManager.macios.cs | 31 ++++------------- .../CameraManager.windows.cs | 33 +++++-------------- .../Handlers/CameraViewHandler.shared.cs | 17 ++++++++-- .../Interfaces/ICameraProvider.shared.cs | 13 ++++++-- .../Providers/CameraProvider.shared.cs | 11 ++++++- .../Providers/CameraProvider.windows.cs | 4 ++- .../Views/CameraView.shared.cs | 13 ++------ .../Mocks/MockCameraProvider.cs | 2 ++ 12 files changed, 96 insertions(+), 95 deletions(-) diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml b/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml index e517746091..8810b78a81 100644 --- a/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml @@ -10,7 +10,7 @@ x:TypeArguments="viewModels:CameraViewViewModel" x:DataType="viewModels:CameraViewViewModel"> - + - + - + @@ -61,6 +61,12 @@ + + File.OpenRead(imagePath)); + image.Source = ImageSource.FromStream(() => File.OpenRead(imagePath)); #else image.Source = ImageSource.FromFile(imagePath); #endif diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/CameraView/CameraViewViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/CameraView/CameraViewViewModel.cs index 055637f821..3a3f8f2991 100644 --- a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/CameraView/CameraViewViewModel.cs +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/CameraView/CameraViewViewModel.cs @@ -9,7 +9,8 @@ public partial class CameraViewViewModel(ICameraProvider cameraProvider) : BaseV { readonly ICameraProvider cameraProvider = cameraProvider; - public IReadOnlyList Cameras => cameraProvider.AvailableCameras ?? []; + [ObservableProperty] + public partial IReadOnlyList Cameras { get; set; } public CancellationToken Token => CancellationToken.None; @@ -42,6 +43,12 @@ public partial class CameraViewViewModel(ICameraProvider cameraProvider) : BaseV [ObservableProperty] public partial string ResolutionText { get; set; } = string.Empty; + public async Task InitializeAsync() + { + await cameraProvider.InitializeAsync; + Cameras = cameraProvider.AvailableCameras ?? []; + } + [RelayCommand] async Task RefreshCameras(CancellationToken token) => await cameraProvider.RefreshAvailableCameras(token); @@ -60,6 +67,22 @@ partial void OnSelectedResolutionChanged(Size value) UpdateResolutionText(); } + partial void OnSelectedCameraChanged(CameraInfo? oldValue, CameraInfo? newValue) + { + UpdateCameraInfoText(); + } + + void UpdateCameraInfoText() + { + if (SelectedCamera is null) + { + return; + } + CameraNameText = $"{SelectedCamera.Name}"; + ZoomRangeText = $"Min Zoom: {SelectedCamera.MinimumZoomFactor}, Max Zoom: {SelectedCamera.MaximumZoomFactor}"; + UpdateFlashModeText(); + } + void UpdateFlashModeText() { if (SelectedCamera is null) diff --git a/src/CommunityToolkit.Maui.Camera/CameraManager.android.cs b/src/CommunityToolkit.Maui.Camera/CameraManager.android.cs index edf800459c..3b1c544f8b 100644 --- a/src/CommunityToolkit.Maui.Camera/CameraManager.android.cs +++ b/src/CommunityToolkit.Maui.Camera/CameraManager.android.cs @@ -159,16 +159,6 @@ protected virtual async partial Task PlatformConnectCamera(CancellationToken tok { processCameraProvider = (ProcessCameraProvider)(cameraProviderFuture.Get() ?? throw new CameraException($"Unable to retrieve {nameof(ProcessCameraProvider)}")); - if (cameraProvider.AvailableCameras is null) - { - await cameraProvider.RefreshAvailableCameras(token); - - if (cameraProvider.AvailableCameras is null) - { - throw new CameraException("Unable to refresh available cameras"); - } - } - await StartUseCase(token); cameraProviderTCS.SetResult(); @@ -201,22 +191,14 @@ protected async Task StartUseCase(CancellationToken token) await StartCameraPreview(token); } - protected virtual async partial Task PlatformStartCameraPreview(CancellationToken token) + protected virtual partial Task PlatformStartCameraPreview(CancellationToken token) { if (previewView is null || processCameraProvider is null || cameraPreview is null || imageCapture is null) { - return; + return Task.CompletedTask; } - if (cameraView.SelectedCamera is null) - { - if (cameraProvider.AvailableCameras is null) - { - await cameraProvider.RefreshAvailableCameras(token); - } - - cameraView.SelectedCamera = cameraProvider.AvailableCameras?.FirstOrDefault() ?? throw new CameraException("No camera available on device"); - } + cameraView.SelectedCamera ??= cameraProvider.AvailableCameras?.FirstOrDefault() ?? throw new CameraException("No camera available on device"); var cameraSelector = cameraView.SelectedCamera.CameraSelector ?? throw new CameraException($"Unable to retrieve {nameof(CameraSelector)}"); @@ -232,6 +214,7 @@ protected virtual async partial Task PlatformStartCameraPreview(CancellationToke IsInitialized = true; OnLoaded.Invoke(); + return Task.CompletedTask; } protected virtual partial void PlatformStopCameraPreview() diff --git a/src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs b/src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs index 9c905f3285..e9683c5781 100644 --- a/src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs +++ b/src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs @@ -78,24 +78,18 @@ public partial void UpdateZoom(float zoomLevel) captureDevice.UnlockForConfiguration(); } - public async partial ValueTask UpdateCaptureResolution(Size resolution, CancellationToken token) + public partial ValueTask UpdateCaptureResolution(Size resolution, CancellationToken token) { - if (captureDevice is null) + if (captureDevice is null || cameraView.SelectedCamera is null) { - return; + return ValueTask.CompletedTask; } captureDevice.LockForConfiguration(out NSError? error); if (error is not null) { Trace.WriteLine(error); - return; - } - - if (cameraView.SelectedCamera is null) - { - await cameraProvider.RefreshAvailableCameras(token); - cameraView.SelectedCamera = cameraProvider.AvailableCameras?.FirstOrDefault() ?? throw new CameraException("No camera available on device"); + return ValueTask.CompletedTask; } var filteredFormatList = cameraView.SelectedCamera.SupportedFormats.Where(f => @@ -117,20 +111,11 @@ public async partial ValueTask UpdateCaptureResolution(Size resolution, Cancella } captureDevice.UnlockForConfiguration(); + return ValueTask.CompletedTask; } protected virtual async partial Task PlatformConnectCamera(CancellationToken token) { - if (cameraProvider.AvailableCameras is null) - { - await cameraProvider.RefreshAvailableCameras(token); - - if (cameraProvider.AvailableCameras is null) - { - throw new CameraException("Unable to refresh cameras"); - } - } - await PlatformStartCameraPreview(token); } @@ -149,11 +134,7 @@ protected virtual async partial Task PlatformStartCameraPreview(CancellationToke input.Dispose(); } - if (cameraView.SelectedCamera is null) - { - await cameraProvider.RefreshAvailableCameras(token); - cameraView.SelectedCamera = cameraProvider.AvailableCameras?.FirstOrDefault() ?? throw new CameraException("No camera available on device"); - } + cameraView.SelectedCamera ??= cameraProvider.AvailableCameras?.FirstOrDefault() ?? throw new CameraException("No camera available on device"); captureDevice = cameraView.SelectedCamera.CaptureDevice ?? throw new CameraException($"No Camera found"); captureInput = new AVCaptureDeviceInput(captureDevice, out _); diff --git a/src/CommunityToolkit.Maui.Camera/CameraManager.windows.cs b/src/CommunityToolkit.Maui.Camera/CameraManager.windows.cs index 3c4718b0e5..b48569dad4 100644 --- a/src/CommunityToolkit.Maui.Camera/CameraManager.windows.cs +++ b/src/CommunityToolkit.Maui.Camera/CameraManager.windows.cs @@ -1,7 +1,6 @@ using System.Runtime.Versioning; using CommunityToolkit.Maui.Core.Primitives; using CommunityToolkit.Maui.Extensions; -using Microsoft.Maui.Controls.PlatformConfiguration; using Microsoft.UI.Xaml.Controls; using Windows.Media.Capture; using Windows.Media.Capture.Frames; @@ -121,16 +120,6 @@ protected virtual void Dispose(bool disposing) protected virtual async partial Task PlatformConnectCamera(CancellationToken token) { - if (cameraProvider.AvailableCameras is null) - { - await cameraProvider.RefreshAvailableCameras(token); - - if (cameraProvider.AvailableCameras is null) - { - throw new CameraException("Unable to refresh cameras"); - } - } - await StartCameraPreview(token); } @@ -141,13 +130,9 @@ protected virtual async partial Task PlatformStartCameraPreview(CancellationToke return; } - mediaCapture = new MediaCapture(); + cameraView.SelectedCamera ??= cameraProvider.AvailableCameras?.FirstOrDefault() ?? throw new CameraException("No camera available on device"); - if (cameraView.SelectedCamera is null) - { - await cameraProvider.RefreshAvailableCameras(token); - cameraView.SelectedCamera = cameraProvider.AvailableCameras?.FirstOrDefault() ?? throw new CameraException("No camera available on device"); - } + mediaCapture = new MediaCapture(); await mediaCapture.InitializeCameraForCameraView(cameraView.SelectedCamera.DeviceId, token); @@ -182,22 +167,22 @@ protected virtual partial void PlatformStopCameraPreview() protected async Task PlatformUpdateResolution(Size resolution, CancellationToken token) { - if (!IsInitialized || mediaCapture is null) + if (!IsInitialized || mediaCapture is null || cameraView.SelectedCamera is null) { return; } - if (cameraView.SelectedCamera is null) + if (mediaCapture.VideoDeviceController.Id != cameraView.SelectedCamera.DeviceId) { - await cameraProvider.RefreshAvailableCameras(token); - cameraView.SelectedCamera = cameraProvider.AvailableCameras?.FirstOrDefault() ?? throw new CameraException("No camera available on device"); + return; } var filteredPropertiesList = cameraView.SelectedCamera.ImageEncodingProperties.Where(p => p.Width <= resolution.Width && p.Height <= resolution.Height).ToList(); - filteredPropertiesList = filteredPropertiesList.Count is not 0 - ? filteredPropertiesList - : [.. cameraView.SelectedCamera.ImageEncodingProperties.OrderByDescending(p => p.Width * p.Height)]; + if (filteredPropertiesList.Count is 0) + { + filteredPropertiesList = [.. cameraView.SelectedCamera.ImageEncodingProperties.OrderByDescending(p => p.Width * p.Height)]; + } if (filteredPropertiesList.Count is not 0) { diff --git a/src/CommunityToolkit.Maui.Camera/Handlers/CameraViewHandler.shared.cs b/src/CommunityToolkit.Maui.Camera/Handlers/CameraViewHandler.shared.cs index 7a345bc61a..464ac1501f 100644 --- a/src/CommunityToolkit.Maui.Camera/Handlers/CameraViewHandler.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Handlers/CameraViewHandler.shared.cs @@ -90,9 +90,20 @@ protected override async void ConnectHandler(NativePlatformCameraPreviewView pla { base.ConnectHandler(platformView); - await (cameraManager?.ArePermissionsGranted() ?? Task.CompletedTask); - await (cameraManager?.ConnectCamera(CancellationToken.None) ?? Task.CompletedTask); - await cameraProvider.RefreshAvailableCameras(CancellationToken.None); + if (cameraManager is null) + { + throw new CameraException("CameraManager is null"); + } + + if (await cameraManager.ArePermissionsGranted() is false) + { + throw new PermissionException("Camera permissions not granted"); + } + + await cameraProvider.InitializeAsync; + VirtualView.SelectedCamera ??= cameraProvider.AvailableCameras?.FirstOrDefault() ?? throw new CameraException("No camera available on device"); + + await cameraManager.ConnectCamera(CancellationToken.None); } /// diff --git a/src/CommunityToolkit.Maui.Camera/Interfaces/ICameraProvider.shared.cs b/src/CommunityToolkit.Maui.Camera/Interfaces/ICameraProvider.shared.cs index 3f0090e2ec..1f1f014037 100644 --- a/src/CommunityToolkit.Maui.Camera/Interfaces/ICameraProvider.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Interfaces/ICameraProvider.shared.cs @@ -11,15 +11,24 @@ public interface ICameraProvider /// Cameras available on device /// /// - /// List is initialized using + /// List is initialized using , and can be refreshed using /// IReadOnlyList? AvailableCameras { get; } /// - /// Assigns with the cameras available on device + /// Refreshes with the cameras available on device /// + /// + /// Only call this method when the available cameras on device has been updated, as the list is automatically initialized with + /// /// /// [MemberNotNull(nameof(AvailableCameras))] ValueTask RefreshAvailableCameras(CancellationToken token); + + /// + /// The to initialize the available cameras list. This task is automatically started when the is created. + /// + Task InitializeAsync { get; } + } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs index adf24f9846..fa4552f020 100644 --- a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs @@ -10,4 +10,13 @@ partial class CameraProvider : ICameraProvider /// public partial ValueTask RefreshAvailableCameras(CancellationToken token); -} \ No newline at end of file + + public CameraProvider() + { + InitializeAsync = RefreshAvailableCameras(CancellationToken.None).AsTask(); + } + + /// + public Task InitializeAsync { get; init; } + +} diff --git a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.windows.cs b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.windows.cs index b07ecbcddc..3de845e5fa 100644 --- a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.windows.cs +++ b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.windows.cs @@ -14,12 +14,12 @@ public async partial ValueTask RefreshAvailableCameras(CancellationToken token) var deviceInfoCollection = await DeviceInformation.FindAllAsync(DeviceClass.VideoCapture).AsTask(token); var mediaFrameSourceGroup = await MediaFrameSourceGroup.FindAllAsync().AsTask(token); var videoCaptureSourceGroup = mediaFrameSourceGroup.Where(sourceGroup => deviceInfoCollection.Any(deviceInfo => deviceInfo.Id == sourceGroup.Id)).ToList(); - var mediaCapture = new MediaCapture(); var availableCameras = new List(); foreach (var sourceGroup in videoCaptureSourceGroup) { + var mediaCapture = new MediaCapture(); await mediaCapture.InitializeCameraForCameraView(sourceGroup.Id, token); CameraPosition position = CameraPosition.Unknown; @@ -63,6 +63,8 @@ public async partial ValueTask RefreshAvailableCameras(CancellationToken token) imageEncodingPropertiesList); availableCameras.Add(cameraInfo); + + mediaCapture.Dispose(); } AvailableCameras = availableCameras; diff --git a/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs b/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs index d2b010cac9..29d6a43b75 100644 --- a/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs @@ -210,17 +210,8 @@ public void OnMediaCapturedFailed(string failureReason) /// public async ValueTask> GetAvailableCameras(CancellationToken token) { - if (CameraProvider.AvailableCameras is null) - { - await CameraProvider.RefreshAvailableCameras(token); - - if (CameraProvider.AvailableCameras is null) - { - throw new CameraException("Unable to refresh available cameras"); - } - } - - return CameraProvider.AvailableCameras; + await CameraProvider.InitializeAsync; + return CameraProvider.AvailableCameras ?? throw new CameraException("No camera available on device"); } /// diff --git a/src/CommunityToolkit.Maui.UnitTests/Mocks/MockCameraProvider.cs b/src/CommunityToolkit.Maui.UnitTests/Mocks/MockCameraProvider.cs index aa42ebeabd..3c0c0fa1a2 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Mocks/MockCameraProvider.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Mocks/MockCameraProvider.cs @@ -7,6 +7,8 @@ public class MockCameraProvider : ICameraProvider { public IReadOnlyList? AvailableCameras { get; private set; } + public Task InitializeAsync { get; } = Task.CompletedTask; + public ValueTask RefreshAvailableCameras(CancellationToken token) { AvailableCameras = From bc419011cfa4d8707db9ef919666a9ebb6c6e6c9 Mon Sep 17 00:00:00 2001 From: Zhitao Pan Date: Mon, 26 May 2025 14:18:00 +1000 Subject: [PATCH 02/12] Fix torch still in use after image capture on Android * Unbind ProcessCameraProvider when dispose --- src/CommunityToolkit.Maui.Camera/CameraManager.android.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CommunityToolkit.Maui.Camera/CameraManager.android.cs b/src/CommunityToolkit.Maui.Camera/CameraManager.android.cs index 3b1c544f8b..dc63452afa 100644 --- a/src/CommunityToolkit.Maui.Camera/CameraManager.android.cs +++ b/src/CommunityToolkit.Maui.Camera/CameraManager.android.cs @@ -130,6 +130,7 @@ protected virtual void Dispose(bool disposing) previewView?.Dispose(); previewView = null; + processCameraProvider?.UnbindAll(); processCameraProvider?.Dispose(); processCameraProvider = null; From 4476ad6f0dd415d7e82b854e52ef8c42586a872f Mon Sep 17 00:00:00 2001 From: Zhitao Pan Date: Thu, 26 Jun 2025 20:30:02 +1000 Subject: [PATCH 03/12] Move Permission and SelectedCamera to ConnectCamera --- .../CameraManager.shared.cs | 13 ++++++++++++- .../Handlers/CameraViewHandler.shared.cs | 9 --------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs b/src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs index d7703d2572..dd2adaf983 100644 --- a/src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs @@ -33,7 +33,18 @@ public async Task ArePermissionsGranted() /// Connects to the camera. /// /// A that can be awaited. - public Task ConnectCamera(CancellationToken token) => PlatformConnectCamera(token); + public async Task ConnectCamera(CancellationToken token) + { + if (await ArePermissionsGranted() is false) + { + throw new PermissionException("Camera permissions not granted"); + } + + await cameraProvider.InitializeAsync; + cameraView.SelectedCamera ??= cameraProvider.AvailableCameras?.FirstOrDefault() ?? throw new CameraException("No camera available on device"); + + await PlatformConnectCamera(token); + } /// /// Disconnects from the camera. diff --git a/src/CommunityToolkit.Maui.Camera/Handlers/CameraViewHandler.shared.cs b/src/CommunityToolkit.Maui.Camera/Handlers/CameraViewHandler.shared.cs index 5b626ae6cc..646731179b 100644 --- a/src/CommunityToolkit.Maui.Camera/Handlers/CameraViewHandler.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Handlers/CameraViewHandler.shared.cs @@ -87,15 +87,6 @@ void Init(ICameraView view) protected override async void ConnectHandler(NativePlatformCameraPreviewView platformView) { base.ConnectHandler(platformView); - - if (await CameraManager.ArePermissionsGranted() is false) - { - throw new PermissionException("Camera permissions not granted"); - } - - await cameraProvider.InitializeAsync; - VirtualView.SelectedCamera ??= cameraProvider.AvailableCameras?.FirstOrDefault() ?? throw new CameraException("No camera available on device"); - await CameraManager.ConnectCamera(CancellationToken.None); } From 35d859fe41515d324d2ebbdb3b8d4636d3cf0ded Mon Sep 17 00:00:00 2001 From: Zhitao Pan Date: Sat, 28 Jun 2025 00:57:23 +1000 Subject: [PATCH 04/12] Refactor CameraProvider initialization and refresh logic, improve thread safety --- .../Views/CameraView/CameraViewViewModel.cs | 3 +- .../CameraManager.shared.cs | 2 +- .../Interfaces/ICameraProvider.shared.cs | 18 ++++++--- .../Providers/CameraProvider.android.cs | 2 +- .../Providers/CameraProvider.macios.cs | 2 +- .../Providers/CameraProvider.net.cs | 2 +- .../Providers/CameraProvider.shared.cs | 40 +++++++++++++++++-- .../Providers/CameraProvider.tizen.cs | 2 +- .../Providers/CameraProvider.windows.cs | 2 +- .../Views/CameraView.shared.cs | 2 +- .../Mocks/MockCameraProvider.cs | 13 +++++- 11 files changed, 67 insertions(+), 21 deletions(-) diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/CameraView/CameraViewViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/CameraView/CameraViewViewModel.cs index 3a3f8f2991..a395045ea2 100644 --- a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/CameraView/CameraViewViewModel.cs +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/CameraView/CameraViewViewModel.cs @@ -1,5 +1,4 @@ using CommunityToolkit.Maui.Core; -using CommunityToolkit.Maui.Core.Primitives; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -45,7 +44,7 @@ public partial class CameraViewViewModel(ICameraProvider cameraProvider) : BaseV public async Task InitializeAsync() { - await cameraProvider.InitializeAsync; + await cameraProvider.InitializeAsync(CancellationToken.None); Cameras = cameraProvider.AvailableCameras ?? []; } diff --git a/src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs b/src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs index dd2adaf983..4678183bcb 100644 --- a/src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs @@ -40,7 +40,7 @@ public async Task ConnectCamera(CancellationToken token) throw new PermissionException("Camera permissions not granted"); } - await cameraProvider.InitializeAsync; + await cameraProvider.InitializeAsync(token); cameraView.SelectedCamera ??= cameraProvider.AvailableCameras?.FirstOrDefault() ?? throw new CameraException("No camera available on device"); await PlatformConnectCamera(token); diff --git a/src/CommunityToolkit.Maui.Camera/Interfaces/ICameraProvider.shared.cs b/src/CommunityToolkit.Maui.Camera/Interfaces/ICameraProvider.shared.cs index 1f1f014037..afc7cd6345 100644 --- a/src/CommunityToolkit.Maui.Camera/Interfaces/ICameraProvider.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Interfaces/ICameraProvider.shared.cs @@ -16,19 +16,25 @@ public interface ICameraProvider IReadOnlyList? AvailableCameras { get; } /// - /// Refreshes with the cameras available on device + /// Refreshes with the cameras available on device. /// - /// - /// Only call this method when the available cameras on device has been updated, as the list is automatically initialized with - /// /// /// [MemberNotNull(nameof(AvailableCameras))] ValueTask RefreshAvailableCameras(CancellationToken token); /// - /// The to initialize the available cameras list. This task is automatically started when the is created. + /// Gets a value indicating whether the camera provider has been successfully initialized. /// - Task InitializeAsync { get; } + bool IsInitialized { get; } + + /// + /// Initialize the camera provider by refreshing the . + /// + /// + /// If the provider is already initialized, the will not be refreshed again until is called, + /// and this method will return a . + /// + ValueTask InitializeAsync(CancellationToken token); } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.android.cs b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.android.cs index f908040cd0..3112dcb543 100644 --- a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.android.cs +++ b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.android.cs @@ -16,7 +16,7 @@ partial class CameraProvider { readonly Context context = Android.App.Application.Context; - public async partial ValueTask RefreshAvailableCameras(CancellationToken token) + internal async partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token) { var cameraProviderFuture = ProcessCameraProvider.GetInstance(context); diff --git a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.macios.cs b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.macios.cs index 1a93631309..115c74579f 100644 --- a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.macios.cs +++ b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.macios.cs @@ -9,7 +9,7 @@ partial class CameraProvider { static readonly AVCaptureDeviceType[] captureDevices = InitializeCaptureDevices(); - public partial ValueTask RefreshAvailableCameras(CancellationToken token) + internal partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token) { var discoverySession = AVCaptureDeviceDiscoverySession.Create(captureDevices, AVMediaTypes.Video, AVCaptureDevicePosition.Unspecified); var availableCameras = new List(); diff --git a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.net.cs b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.net.cs index d4c7daf353..d46bfa94ee 100644 --- a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.net.cs +++ b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.net.cs @@ -2,5 +2,5 @@ namespace CommunityToolkit.Maui.Core; partial class CameraProvider { - public partial ValueTask RefreshAvailableCameras(CancellationToken token) => throw new NotSupportedException(); + internal partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token) => throw new NotSupportedException(); } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs index fa4552f020..5ed09f799a 100644 --- a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs @@ -5,18 +5,50 @@ /// partial class CameraProvider : ICameraProvider { + readonly Lock initializationLock = new(); + Task? initializationTask; + + internal partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token); + /// public IReadOnlyList? AvailableCameras { get; private set; } /// - public partial ValueTask RefreshAvailableCameras(CancellationToken token); + public bool IsInitialized => initializationTask?.IsCompletedSuccessfully is true; - public CameraProvider() + bool IsNotRefreshing => initializationTask is null || initializationTask.IsCompleted; + + /// + public ValueTask InitializeAsync(CancellationToken token) { - InitializeAsync = RefreshAvailableCameras(CancellationToken.None).AsTask(); + lock (initializationLock) + { + if (IsInitialized) + { + return ValueTask.CompletedTask; + } + + if (IsNotRefreshing) + { + initializationTask = PlatformRefreshAvailableCameras(token).AsTask(); + } + + return new ValueTask(initializationTask!); + } } /// - public Task InitializeAsync { get; init; } + public ValueTask RefreshAvailableCameras(CancellationToken token) + { + lock (initializationLock) + { + if (IsNotRefreshing) + { + initializationTask = PlatformRefreshAvailableCameras(token).AsTask(); + } + + return new ValueTask(initializationTask!); + } + } } diff --git a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.tizen.cs b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.tizen.cs index 03140011e6..b72ed1924a 100644 --- a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.tizen.cs +++ b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.tizen.cs @@ -2,5 +2,5 @@ partial class CameraProvider { - public partial ValueTask RefreshAvailableCameras(CancellationToken token) => throw new NotSupportedException(); + internal partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token) => throw new NotSupportedException(); } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.windows.cs b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.windows.cs index 0abda918a6..7e73df5f21 100644 --- a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.windows.cs +++ b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.windows.cs @@ -9,7 +9,7 @@ namespace CommunityToolkit.Maui.Core; partial class CameraProvider { - public async partial ValueTask RefreshAvailableCameras(CancellationToken token) + internal async partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token) { var deviceInfoCollection = await DeviceInformation.FindAllAsync(DeviceClass.VideoCapture).AsTask(token); var mediaFrameSourceGroup = await MediaFrameSourceGroup.FindAllAsync().AsTask(token); diff --git a/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs b/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs index ce6bd328d3..2a4735dcce 100644 --- a/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs @@ -204,7 +204,7 @@ public void Dispose() /// public async ValueTask> GetAvailableCameras(CancellationToken token) { - await CameraProvider.InitializeAsync; + await CameraProvider.InitializeAsync(token); return CameraProvider.AvailableCameras ?? throw new CameraException("No camera available on device"); } diff --git a/src/CommunityToolkit.Maui.UnitTests/Mocks/MockCameraProvider.cs b/src/CommunityToolkit.Maui.UnitTests/Mocks/MockCameraProvider.cs index 3c0c0fa1a2..3997bdb855 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Mocks/MockCameraProvider.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Mocks/MockCameraProvider.cs @@ -1,5 +1,4 @@ using CommunityToolkit.Maui.Core; -using CommunityToolkit.Maui.Core.Primitives; namespace CommunityToolkit.Maui.UnitTests.Mocks; @@ -7,7 +6,17 @@ public class MockCameraProvider : ICameraProvider { public IReadOnlyList? AvailableCameras { get; private set; } - public Task InitializeAsync { get; } = Task.CompletedTask; + public bool IsInitialized { get; private set; } + + public ValueTask InitializeAsync(CancellationToken token) + { + if (IsInitialized) + { + return ValueTask.CompletedTask; + } + IsInitialized = true; + return RefreshAvailableCameras(token); + } public ValueTask RefreshAvailableCameras(CancellationToken token) { From 79da15b480d8066a7f394db4c0002270bfc79e55 Mon Sep 17 00:00:00 2001 From: Zhitao Pan Date: Wed, 16 Jul 2025 12:04:16 +1000 Subject: [PATCH 05/12] Rename refreshLock and refreshTask --- .../Providers/CameraProvider.shared.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs index 5ed09f799a..d3f3b6cd9c 100644 --- a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs @@ -5,8 +5,8 @@ /// partial class CameraProvider : ICameraProvider { - readonly Lock initializationLock = new(); - Task? initializationTask; + readonly Lock refreshLock = new(); + Task? refreshTask; internal partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token); @@ -14,14 +14,14 @@ partial class CameraProvider : ICameraProvider public IReadOnlyList? AvailableCameras { get; private set; } /// - public bool IsInitialized => initializationTask?.IsCompletedSuccessfully is true; + public bool IsInitialized => refreshTask?.IsCompletedSuccessfully is true; - bool IsNotRefreshing => initializationTask is null || initializationTask.IsCompleted; + bool IsNotRefreshing => refreshTask is null || refreshTask.IsCompleted; /// public ValueTask InitializeAsync(CancellationToken token) { - lock (initializationLock) + lock (refreshLock) { if (IsInitialized) { @@ -30,24 +30,24 @@ public ValueTask InitializeAsync(CancellationToken token) if (IsNotRefreshing) { - initializationTask = PlatformRefreshAvailableCameras(token).AsTask(); + refreshTask = PlatformRefreshAvailableCameras(token).AsTask(); } - return new ValueTask(initializationTask!); + return new ValueTask(refreshTask!); } } /// public ValueTask RefreshAvailableCameras(CancellationToken token) { - lock (initializationLock) + lock (refreshLock) { if (IsNotRefreshing) { - initializationTask = PlatformRefreshAvailableCameras(token).AsTask(); + refreshTask = PlatformRefreshAvailableCameras(token).AsTask(); } - return new ValueTask(initializationTask!); + return new ValueTask(refreshTask!); } } From 04d09d21081fcfc5e6b64282f086dda1d1373efe Mon Sep 17 00:00:00 2001 From: Zhitao Pan Date: Wed, 16 Jul 2025 20:19:18 +1000 Subject: [PATCH 06/12] Simplify camera refresh task --- .../Providers/CameraProvider.shared.cs | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs index d3f3b6cd9c..ef08211e5c 100644 --- a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs @@ -16,7 +16,15 @@ partial class CameraProvider : ICameraProvider /// public bool IsInitialized => refreshTask?.IsCompletedSuccessfully is true; - bool IsNotRefreshing => refreshTask is null || refreshTask.IsCompleted; + ValueTask GetRefreshTask(CancellationToken token) + { + if (refreshTask is null || refreshTask.IsCompleted) + { + refreshTask = PlatformRefreshAvailableCameras(token).AsTask(); + } + + return new ValueTask(refreshTask); + } /// public ValueTask InitializeAsync(CancellationToken token) @@ -28,12 +36,7 @@ public ValueTask InitializeAsync(CancellationToken token) return ValueTask.CompletedTask; } - if (IsNotRefreshing) - { - refreshTask = PlatformRefreshAvailableCameras(token).AsTask(); - } - - return new ValueTask(refreshTask!); + return GetRefreshTask(token); } } @@ -42,12 +45,7 @@ public ValueTask RefreshAvailableCameras(CancellationToken token) { lock (refreshLock) { - if (IsNotRefreshing) - { - refreshTask = PlatformRefreshAvailableCameras(token).AsTask(); - } - - return new ValueTask(refreshTask!); + return GetRefreshTask(token); } } From 05ad3f50d71c26248093c8b3944caf0be44c3129 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Thu, 28 Aug 2025 13:07:22 -0700 Subject: [PATCH 07/12] Use `ObservableCollection` --- .../Views/CameraView/CameraViewViewModel.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/CameraView/CameraViewViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/CameraView/CameraViewViewModel.cs index a395045ea2..be80a1ab66 100644 --- a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/CameraView/CameraViewViewModel.cs +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/CameraView/CameraViewViewModel.cs @@ -1,4 +1,5 @@ -using CommunityToolkit.Maui.Core; +using System.Collections.ObjectModel; +using CommunityToolkit.Maui.Core; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -8,10 +9,9 @@ public partial class CameraViewViewModel(ICameraProvider cameraProvider) : BaseV { readonly ICameraProvider cameraProvider = cameraProvider; - [ObservableProperty] - public partial IReadOnlyList Cameras { get; set; } - public CancellationToken Token => CancellationToken.None; + + public ObservableCollection Cameras { get; } = []; public ICollection FlashModes { get; } = Enum.GetValues(); @@ -45,7 +45,10 @@ public partial class CameraViewViewModel(ICameraProvider cameraProvider) : BaseV public async Task InitializeAsync() { await cameraProvider.InitializeAsync(CancellationToken.None); - Cameras = cameraProvider.AvailableCameras ?? []; + foreach (var camera in cameraProvider.AvailableCameras ?? []) + { + Cameras.Add(camera); + } } [RelayCommand] From c3ecbf11b0b8449500b68165feed2c11567816dc Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Thu, 28 Aug 2025 13:56:50 -0700 Subject: [PATCH 08/12] Update `ICameraProvider`, Use `SemaphoreSlim` in `CameraProvider` to avoid async/await race conditions, Implement `IDisplosable` --- .../Views/CameraView/CameraViewViewModel.cs | 11 ++-- .../Interfaces/ICameraProvider.shared.cs | 12 ++-- .../Providers/CameraProvider.android.cs | 2 +- .../Providers/CameraProvider.macios.cs | 2 +- .../Providers/CameraProvider.net.cs | 2 +- .../Providers/CameraProvider.shared.cs | 63 ++++++++++++------- .../Mocks/MockCameraProvider.cs | 13 ++-- 7 files changed, 62 insertions(+), 43 deletions(-) diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/CameraView/CameraViewViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/CameraView/CameraViewViewModel.cs index be80a1ab66..996a535b4d 100644 --- a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/CameraView/CameraViewViewModel.cs +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/CameraView/CameraViewViewModel.cs @@ -42,12 +42,15 @@ public partial class CameraViewViewModel(ICameraProvider cameraProvider) : BaseV [ObservableProperty] public partial string ResolutionText { get; set; } = string.Empty; - public async Task InitializeAsync() + public async ValueTask InitializeAsync() { - await cameraProvider.InitializeAsync(CancellationToken.None); - foreach (var camera in cameraProvider.AvailableCameras ?? []) + if (!cameraProvider.IsInitialized) { - Cameras.Add(camera); + await cameraProvider.InitializeAsync(CancellationToken.None); + foreach (var camera in cameraProvider.AvailableCameras ?? []) + { + Cameras.Add(camera); + } } } diff --git a/src/CommunityToolkit.Maui.Camera/Interfaces/ICameraProvider.shared.cs b/src/CommunityToolkit.Maui.Camera/Interfaces/ICameraProvider.shared.cs index afc7cd6345..80636137ed 100644 --- a/src/CommunityToolkit.Maui.Camera/Interfaces/ICameraProvider.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Interfaces/ICameraProvider.shared.cs @@ -14,6 +14,11 @@ public interface ICameraProvider /// List is initialized using , and can be refreshed using /// IReadOnlyList? AvailableCameras { get; } + + /// + /// Gets a value indicating whether the camera provider has been successfully initialized. + /// + bool IsInitialized { get; } /// /// Refreshes with the cameras available on device. @@ -21,12 +26,7 @@ public interface ICameraProvider /// /// [MemberNotNull(nameof(AvailableCameras))] - ValueTask RefreshAvailableCameras(CancellationToken token); - - /// - /// Gets a value indicating whether the camera provider has been successfully initialized. - /// - bool IsInitialized { get; } + Task RefreshAvailableCameras(CancellationToken token); /// /// Initialize the camera provider by refreshing the . diff --git a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.android.cs b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.android.cs index 3112dcb543..ad25030de9 100644 --- a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.android.cs +++ b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.android.cs @@ -16,7 +16,7 @@ partial class CameraProvider { readonly Context context = Android.App.Application.Context; - internal async partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token) + private async partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token) { var cameraProviderFuture = ProcessCameraProvider.GetInstance(context); diff --git a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.macios.cs b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.macios.cs index 115c74579f..5a9a9bad6b 100644 --- a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.macios.cs +++ b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.macios.cs @@ -9,7 +9,7 @@ partial class CameraProvider { static readonly AVCaptureDeviceType[] captureDevices = InitializeCaptureDevices(); - internal partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token) + private partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token) { var discoverySession = AVCaptureDeviceDiscoverySession.Create(captureDevices, AVMediaTypes.Video, AVCaptureDevicePosition.Unspecified); var availableCameras = new List(); diff --git a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.net.cs b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.net.cs index d46bfa94ee..57a58ae47d 100644 --- a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.net.cs +++ b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.net.cs @@ -2,5 +2,5 @@ namespace CommunityToolkit.Maui.Core; partial class CameraProvider { - internal partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token) => throw new NotSupportedException(); + private partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token) => throw new NotSupportedException(); } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs index ef08211e5c..b07937ec2f 100644 --- a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs @@ -3,50 +3,67 @@ /// /// Implementation that provides the ability to discover cameras that are attached to the current device. /// -partial class CameraProvider : ICameraProvider +partial class CameraProvider : ICameraProvider, IDisposable { - readonly Lock refreshLock = new(); - Task? refreshTask; + readonly SemaphoreSlim refreshAvailableCamerasSemaphore = new(1, 1); + Task? refreshAvailableCamerasTask; - internal partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token); + ~CameraProvider() + { + Dispose(false); + } /// - public IReadOnlyList? AvailableCameras { get; private set; } + public bool IsInitialized => refreshAvailableCamerasTask?.IsCompletedSuccessfully is true; /// - public bool IsInitialized => refreshTask?.IsCompletedSuccessfully is true; + public IReadOnlyList? AvailableCameras { get; private set; } - ValueTask GetRefreshTask(CancellationToken token) + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + public async ValueTask InitializeAsync(CancellationToken token) { - if (refreshTask is null || refreshTask.IsCompleted) + if (!IsInitialized) { - refreshTask = PlatformRefreshAvailableCameras(token).AsTask(); + await RefreshAvailableCameras(token); } - - return new ValueTask(refreshTask); } - /// - public ValueTask InitializeAsync(CancellationToken token) + /// + public async Task RefreshAvailableCameras(CancellationToken token) { - lock (refreshLock) + await refreshAvailableCamerasSemaphore.WaitAsync(token); + + try { - if (IsInitialized) + if (refreshAvailableCamerasTask is null || refreshAvailableCamerasTask.IsCompleted) { - return ValueTask.CompletedTask; + refreshAvailableCamerasTask = PlatformRefreshAvailableCameras(token).AsTask(); } - return GetRefreshTask(token); + await refreshAvailableCamerasTask; + } + finally + { + refreshAvailableCamerasSemaphore.Release(); } } - /// - public ValueTask RefreshAvailableCameras(CancellationToken token) + void Dispose(bool disposing) { - lock (refreshLock) + if (disposing) { - return GetRefreshTask(token); + refreshAvailableCamerasSemaphore.Dispose(); + + refreshAvailableCamerasTask?.Dispose(); + refreshAvailableCamerasTask = null; } } - -} + + private partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token); +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.UnitTests/Mocks/MockCameraProvider.cs b/src/CommunityToolkit.Maui.UnitTests/Mocks/MockCameraProvider.cs index 3997bdb855..8ef976513d 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Mocks/MockCameraProvider.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Mocks/MockCameraProvider.cs @@ -8,17 +8,16 @@ public class MockCameraProvider : ICameraProvider public bool IsInitialized { get; private set; } - public ValueTask InitializeAsync(CancellationToken token) + public async ValueTask InitializeAsync(CancellationToken token) { - if (IsInitialized) + if (!IsInitialized) { - return ValueTask.CompletedTask; + await RefreshAvailableCameras(token); + IsInitialized = true; } - IsInitialized = true; - return RefreshAvailableCameras(token); } - public ValueTask RefreshAvailableCameras(CancellationToken token) + public Task RefreshAvailableCameras(CancellationToken token) { AvailableCameras = [ @@ -33,6 +32,6 @@ public ValueTask RefreshAvailableCameras(CancellationToken token) ]) ]; - return ValueTask.CompletedTask; + return Task.CompletedTask; } } \ No newline at end of file From 61bd8ac330503d3e9a9e54adae0737635064c75e Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Thu, 28 Aug 2025 14:14:44 -0700 Subject: [PATCH 09/12] Change method access modifier to private --- .../Providers/CameraProvider.windows.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.windows.cs b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.windows.cs index 7e73df5f21..f7c24cf785 100644 --- a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.windows.cs +++ b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.windows.cs @@ -9,7 +9,7 @@ namespace CommunityToolkit.Maui.Core; partial class CameraProvider { - internal async partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token) + private async partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token) { var deviceInfoCollection = await DeviceInformation.FindAllAsync(DeviceClass.VideoCapture).AsTask(token); var mediaFrameSourceGroup = await MediaFrameSourceGroup.FindAllAsync().AsTask(token); @@ -70,4 +70,4 @@ internal async partial ValueTask PlatformRefreshAvailableCameras(CancellationTok AvailableCameras = availableCameras; } -} \ No newline at end of file +} From de245ce3e85d235c7f946eeac6a4d2ce280f4d59 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Thu, 28 Aug 2025 14:28:46 -0700 Subject: [PATCH 10/12] Update CameraProvider.tizen.cs --- .../Providers/CameraProvider.tizen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.tizen.cs b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.tizen.cs index b72ed1924a..5ae4c99da8 100644 --- a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.tizen.cs +++ b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.tizen.cs @@ -2,5 +2,5 @@ partial class CameraProvider { - internal partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token) => throw new NotSupportedException(); + private partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token) => throw new NotSupportedException(); } \ No newline at end of file From 8953ce9b39ca6b4e031b61485851380432a6f210 Mon Sep 17 00:00:00 2001 From: Zhitao Pan Date: Mon, 1 Sep 2025 21:35:44 +1000 Subject: [PATCH 11/12] Refactor CameraViewPage and ViewModel: Remove unused event handlers and improve initialization logic --- .../Views/CameraView/CameraViewPage.xaml | 1 - .../Views/CameraView/CameraViewPage.xaml.cs | 19 +----------------- .../Views/CameraView/CameraViewViewModel.cs | 20 ++++++++++++------- .../Interfaces/ICameraProvider.shared.cs | 9 ++++++--- 4 files changed, 20 insertions(+), 29 deletions(-) diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml b/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml index 8810b78a81..7dacdbf27e 100644 --- a/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml @@ -5,7 +5,6 @@ xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit" xmlns:viewModels="clr-namespace:CommunityToolkit.Maui.Sample.ViewModels.Views" Title="CameraView" - Unloaded="OnUnloaded" x:Class="CommunityToolkit.Maui.Sample.Pages.Views.CameraViewPage" x:TypeArguments="viewModels:CameraViewViewModel" x:DataType="viewModels:CameraViewViewModel"> diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml.cs b/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml.cs index 7f807c290d..5016b65ef0 100644 --- a/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml.cs +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using CommunityToolkit.Maui.Core; using CommunityToolkit.Maui.Sample.ViewModels.Views; @@ -7,7 +6,6 @@ namespace CommunityToolkit.Maui.Sample.Pages.Views; public partial class CameraViewPage : BasePage { readonly string imagePath; - int pageCount; public CameraViewPage(CameraViewViewModel viewModel, IFileSystem fileSystem) : base(viewModel) { @@ -16,11 +14,6 @@ public CameraViewPage(CameraViewViewModel viewModel, IFileSystem fileSystem) : b imagePath = Path.Combine(fileSystem.CacheDirectory, "camera-view-image.jpg"); Camera.MediaCaptured += OnMediaCaptured; - - Loaded += (s, e) => - { - pageCount = Navigation.NavigationStack.Count; - }; } protected override async void OnAppearing() @@ -30,15 +23,11 @@ protected override async void OnAppearing() await BindingContext.InitializeAsync(); } - // https://github.com/dotnet/maui/issues/16697 - // https://github.com/dotnet/maui/issues/15833 protected override void OnNavigatedFrom(NavigatedFromEventArgs args) { base.OnNavigatedFrom(args); - Debug.WriteLine($"< < OnNavigatedFrom {pageCount} {Navigation.NavigationStack.Count}"); - - if (Navigation.NavigationStack.Count < pageCount) + if (!Shell.Current.Navigation.NavigationStack.Contains(this)) { Cleanup(); } @@ -56,12 +45,6 @@ async void OnImageTapped(object? sender, TappedEventArgs args) void Cleanup() { Camera.MediaCaptured -= OnMediaCaptured; - Camera.Handler?.DisconnectHandler(); - } - - void OnUnloaded(object? sender, EventArgs e) - { - //Cleanup(); } void OnMediaCaptured(object? sender, MediaCapturedEventArgs e) diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/CameraView/CameraViewViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/CameraView/CameraViewViewModel.cs index 996a535b4d..bdd6854357 100644 --- a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/CameraView/CameraViewViewModel.cs +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/CameraView/CameraViewViewModel.cs @@ -9,8 +9,10 @@ public partial class CameraViewViewModel(ICameraProvider cameraProvider) : BaseV { readonly ICameraProvider cameraProvider = cameraProvider; + bool isInitialized = false; + public CancellationToken Token => CancellationToken.None; - + public ObservableCollection Cameras { get; } = []; public ICollection FlashModes { get; } = Enum.GetValues(); @@ -44,14 +46,18 @@ public partial class CameraViewViewModel(ICameraProvider cameraProvider) : BaseV public async ValueTask InitializeAsync() { - if (!cameraProvider.IsInitialized) + if (isInitialized) { - await cameraProvider.InitializeAsync(CancellationToken.None); - foreach (var camera in cameraProvider.AvailableCameras ?? []) - { - Cameras.Add(camera); - } + return; } + + await cameraProvider.InitializeAsync(CancellationToken.None); + foreach (var camera in cameraProvider.AvailableCameras ?? []) + { + Cameras.Add(camera); + } + + isInitialized = true; } [RelayCommand] diff --git a/src/CommunityToolkit.Maui.Camera/Interfaces/ICameraProvider.shared.cs b/src/CommunityToolkit.Maui.Camera/Interfaces/ICameraProvider.shared.cs index 80636137ed..e44e0bbe44 100644 --- a/src/CommunityToolkit.Maui.Camera/Interfaces/ICameraProvider.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Interfaces/ICameraProvider.shared.cs @@ -21,8 +21,11 @@ public interface ICameraProvider bool IsInitialized { get; } /// - /// Refreshes with the cameras available on device. + /// Refreshes the on device, regardless of the current initialization state. /// + /// + /// This ensures the list is up to date if available cameras have changed after initialization. + /// /// /// [MemberNotNull(nameof(AvailableCameras))] @@ -30,10 +33,10 @@ public interface ICameraProvider /// /// Initialize the camera provider by refreshing the . + /// This performs a one-time discovery of available cameras. Subsequent calls are no-ops unless initialization failed previously. /// /// - /// If the provider is already initialized, the will not be refreshed again until is called, - /// and this method will return a . + /// To force a refresh after the provider is initialized, call . /// ValueTask InitializeAsync(CancellationToken token); From c0d810bdd128fd233f63300b964d3d214de9296f Mon Sep 17 00:00:00 2001 From: Zhitao Pan Date: Mon, 1 Sep 2025 23:45:44 +1000 Subject: [PATCH 12/12] Refactor CameraProvider: Improve camera refresh logic and ensure proper semaphore usage --- .../Interfaces/ICameraProvider.shared.cs | 2 +- .../Providers/CameraProvider.shared.cs | 34 +++++++++++++------ 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/CommunityToolkit.Maui.Camera/Interfaces/ICameraProvider.shared.cs b/src/CommunityToolkit.Maui.Camera/Interfaces/ICameraProvider.shared.cs index e44e0bbe44..dff17dd2aa 100644 --- a/src/CommunityToolkit.Maui.Camera/Interfaces/ICameraProvider.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Interfaces/ICameraProvider.shared.cs @@ -14,7 +14,7 @@ public interface ICameraProvider /// List is initialized using , and can be refreshed using /// IReadOnlyList? AvailableCameras { get; } - + /// /// Gets a value indicating whether the camera provider has been successfully initialized. /// diff --git a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs index b07937ec2f..f1dc9da8c6 100644 --- a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs @@ -25,12 +25,31 @@ public void Dispose() GC.SuppressFinalize(this); } + Task GetRefreshTask(CancellationToken token) + { + if (refreshAvailableCamerasTask is null || refreshAvailableCamerasTask.IsCompleted) + { + refreshAvailableCamerasTask = PlatformRefreshAvailableCameras(token).AsTask(); + } + + return refreshAvailableCamerasTask; + } + /// public async ValueTask InitializeAsync(CancellationToken token) { - if (!IsInitialized) + await refreshAvailableCamerasSemaphore.WaitAsync(token); + + try + { + if (!IsInitialized) + { + await GetRefreshTask(token); + } + } + finally { - await RefreshAvailableCameras(token); + refreshAvailableCamerasSemaphore.Release(); } } @@ -38,15 +57,10 @@ public async ValueTask InitializeAsync(CancellationToken token) public async Task RefreshAvailableCameras(CancellationToken token) { await refreshAvailableCamerasSemaphore.WaitAsync(token); - + try { - if (refreshAvailableCamerasTask is null || refreshAvailableCamerasTask.IsCompleted) - { - refreshAvailableCamerasTask = PlatformRefreshAvailableCameras(token).AsTask(); - } - - await refreshAvailableCamerasTask; + await GetRefreshTask(token); } finally { @@ -64,6 +78,6 @@ void Dispose(bool disposing) refreshAvailableCamerasTask = null; } } - + private partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token); } \ No newline at end of file