From 4713b19e9529b49fd4a8a8a9e18c0b84f1c49cf7 Mon Sep 17 00:00:00 2001 From: Zhitao Pan Date: Wed, 15 Oct 2025 16:33:33 +1100 Subject: [PATCH 01/16] Refactor camera refresh logic * Add PlatformRefreshAvailableCameras * Don't invoke refresh camera task when one is in progress * Remove redundant refresh camera calls in CameraManager --- .../Views/CameraView/CameraViewPage.xaml | 3 +- .../Views/CameraView/CameraViewPage.xaml.cs | 28 +++++----------- .../CameraManager.android.cs | 26 +++------------ .../CameraManager.macios.cs | 31 ++++------------- .../CameraManager.shared.cs | 10 +++++- .../CameraManager.windows.cs | 33 ++++--------------- .../Handlers/CameraViewHandler.shared.cs | 1 - .../Providers/CameraProvider.android.cs | 2 +- .../Providers/CameraProvider.macios.cs | 2 +- .../Providers/CameraProvider.net.cs | 2 +- .../Providers/CameraProvider.shared.cs | 19 ++++++++--- .../Providers/CameraProvider.tizen.cs | 2 +- .../Providers/CameraProvider.windows.cs | 4 +-- .../Views/CameraView.shared.cs | 8 +---- 14 files changed, 59 insertions(+), 112 deletions(-) diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml b/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml index f34ac7d3a7..a77b635c7e 100644 --- a/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml @@ -5,12 +5,11 @@ 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"> - + { readonly string imagePath; - int pageCount; + bool isInitialized = false; public CameraViewPage(CameraViewViewModel viewModel, IFileSystem fileSystem) : base(viewModel) { @@ -16,30 +15,27 @@ 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() { base.OnAppearing(); + if (isInitialized) + { + return; + } + var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(3)); await BindingContext.RefreshCamerasCommand.ExecuteAsync(cancellationTokenSource.Token); + isInitialized = true; } - // 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(); } @@ -57,12 +53,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) @@ -75,7 +65,7 @@ void OnMediaCaptured(object? sender, MediaCapturedEventArgs e) { // workaround for https://github.com/dotnet/maui/issues/13858 #if ANDROID - image.Source = ImageSource.FromStream(() => File.OpenRead(imagePath)); + image.Source = ImageSource.FromStream(() => File.OpenRead(imagePath)); #else image.Source = ImageSource.FromFile(imagePath); #endif diff --git a/src/CommunityToolkit.Maui.Camera/CameraManager.android.cs b/src/CommunityToolkit.Maui.Camera/CameraManager.android.cs index c642ca4218..4f7eaddf48 100644 --- a/src/CommunityToolkit.Maui.Camera/CameraManager.android.cs +++ b/src/CommunityToolkit.Maui.Camera/CameraManager.android.cs @@ -129,6 +129,7 @@ protected virtual void Dispose(bool disposing) previewView?.Dispose(); previewView = null; + processCameraProvider?.UnbindAll(); processCameraProvider?.Dispose(); processCameraProvider = null; @@ -158,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(); @@ -200,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)}"); @@ -231,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 17941d0cf7..178e0c621d 100644 --- a/src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs +++ b/src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs @@ -77,24 +77,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 => @@ -116,20 +110,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); } @@ -148,11 +133,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.shared.cs b/src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs index d7703d2572..7869d07c98 100644 --- a/src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs @@ -33,7 +33,15 @@ 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 (cameraProvider.AvailableCameras is null) + { + await cameraProvider.RefreshAvailableCameras(token); + } + 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/CameraManager.windows.cs b/src/CommunityToolkit.Maui.Camera/CameraManager.windows.cs index 5edf33fc77..49565f1d11 100644 --- a/src/CommunityToolkit.Maui.Camera/CameraManager.windows.cs +++ b/src/CommunityToolkit.Maui.Camera/CameraManager.windows.cs @@ -119,16 +119,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); } @@ -139,13 +129,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); @@ -180,22 +166,17 @@ 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) - { - await cameraProvider.RefreshAvailableCameras(token); - cameraView.SelectedCamera = cameraProvider.AvailableCameras?.FirstOrDefault() ?? throw new CameraException("No camera available on device"); - } - 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 9f4b12f36c..2b56a50015 100644 --- a/src/CommunityToolkit.Maui.Camera/Handlers/CameraViewHandler.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Handlers/CameraViewHandler.shared.cs @@ -90,7 +90,6 @@ protected override async void ConnectHandler(NativePlatformCameraPreviewView pla await CameraManager.ArePermissionsGranted(); await CameraManager.ConnectCamera(CancellationToken.None); - await cameraProvider.RefreshAvailableCameras(CancellationToken.None); } /// diff --git a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.android.cs b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.android.cs index f908040cd0..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; - public async partial ValueTask RefreshAvailableCameras(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 1a93631309..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(); - public partial ValueTask RefreshAvailableCameras(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 d4c7daf353..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 { - public partial ValueTask RefreshAvailableCameras(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 7e7298461d..0e2b6de76b 100644 --- a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs @@ -6,7 +6,8 @@ partial class CameraProvider : ICameraProvider { readonly WeakEventManager availableCamerasChangedEventManager = new(); - + Task? refreshAvailableCamerasTask; + public event EventHandler?> AvailableCamerasChanged { add => availableCamerasChangedEventManager.AddEventHandler(value); @@ -22,13 +23,23 @@ private set if (!AreCameraInfoListsEqual(field, value)) { field = value; - availableCamerasChangedEventManager.HandleEvent(this, value, nameof(AvailableCamerasChanged)); + availableCamerasChangedEventManager.HandleEvent(this, value, nameof(AvailableCamerasChanged)); } } } + private partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token); + /// - public partial ValueTask RefreshAvailableCameras(CancellationToken token); + public async ValueTask RefreshAvailableCameras(CancellationToken token) + { + if (refreshAvailableCamerasTask is null || refreshAvailableCamerasTask.IsCompleted) + { + refreshAvailableCamerasTask = PlatformRefreshAvailableCameras(token).AsTask(); + } + + await refreshAvailableCamerasTask; + } internal static bool AreCameraInfoListsEqual(in IReadOnlyList? cameraInfoList1, in IReadOnlyList? cameraInfoList2) { @@ -47,4 +58,4 @@ internal static bool AreCameraInfoListsEqual(in IReadOnlyList? camer return cameraInfosInList1ButNotInList2.Count is 0 && cameraInfosInList2ButNotInList1.Count is 0; } -} \ No newline at end of file +} diff --git a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.tizen.cs b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.tizen.cs index 03140011e6..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 { - public partial ValueTask RefreshAvailableCameras(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.windows.cs b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.windows.cs index c8064b0b49..40b48e6609 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) + private async partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token) { var deviceInfoCollection = await DeviceInformation.FindAllAsync(DeviceClass.VideoCapture).AsTask(token); var mediaFrameSourceGroup = await MediaFrameSourceGroup.FindAllAsync().AsTask(token); @@ -68,4 +68,4 @@ public async partial ValueTask RefreshAvailableCameras(CancellationToken token) AvailableCameras = availableCameras; } -} \ No newline at end of file +} diff --git a/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs b/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs index edbd26460d..6115a9b0b9 100644 --- a/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs @@ -207,14 +207,8 @@ public async ValueTask> GetAvailableCameras(Cancellati 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; + return CameraProvider.AvailableCameras ?? throw new CameraException("No camera available on device"); } /// From b9e9345d7d53fbd749c1621d516c719451a45835 Mon Sep 17 00:00:00 2001 From: Zhitao Pan Date: Fri, 24 Oct 2025 13:28:15 +1100 Subject: [PATCH 02/16] Remove Refresh in StartVideoRecording --- .../CameraManager.android.cs | 48 ++++++++----------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/src/CommunityToolkit.Maui.Camera/CameraManager.android.cs b/src/CommunityToolkit.Maui.Camera/CameraManager.android.cs index d525e9d291..c759d78aea 100644 --- a/src/CommunityToolkit.Maui.Camera/CameraManager.android.cs +++ b/src/CommunityToolkit.Maui.Camera/CameraManager.android.cs @@ -1,6 +1,5 @@ using System.Runtime.Versioning; using Android.Content; -using Android.Provider; using Android.Runtime; using Android.Views; using AndroidX.Camera.Core; @@ -48,10 +47,10 @@ public async Task SetExtensionMode(int mode, CancellationToken token) { extensionMode = mode; if (cameraView.SelectedCamera is null - || processCameraProvider is null - || cameraPreview is null - || imageCapture is null - || videoCapture is null) + || processCameraProvider is null + || cameraPreview is null + || imageCapture is null + || videoCapture is null) { return; } @@ -104,7 +103,7 @@ public async partial ValueTask UpdateCaptureResolution(Size resolution, Cancella if (resolutionFilter is not null) { if (Math.Abs(resolutionFilter.TargetSize.Width - resolution.Width) < double.Epsilon && - Math.Abs(resolutionFilter.TargetSize.Height - resolution.Height) < double.Epsilon) + Math.Abs(resolutionFilter.TargetSize.Height - resolution.Height) < double.Epsilon) { return; } @@ -242,11 +241,11 @@ protected async Task StartUseCase(CancellationToken token) await StartCameraPreview(token); } - protected virtual partial Task PlatformStartCameraPreview(CancellationToken token) + protected virtual async partial Task PlatformStartCameraPreview(CancellationToken token) { if (previewView is null || processCameraProvider is null || cameraPreview is null || imageCapture is null || videoCapture is null) { - return Task.CompletedTask; + return; } cameraView.SelectedCamera ??= cameraProvider.AvailableCameras?.FirstOrDefault() ?? throw new CameraException("No camera available on device"); @@ -260,7 +259,6 @@ protected virtual partial Task PlatformStartCameraPreview(CancellationToken toke IsInitialized = true; OnLoaded.Invoke(); - return Task.CompletedTask; } protected virtual partial void PlatformStopCameraPreview() @@ -290,27 +288,19 @@ protected virtual partial ValueTask PlatformTakePicture(CancellationToken token) protected virtual async partial Task PlatformStartVideoRecording(Stream stream, CancellationToken token) { if (previewView is null - || processCameraProvider is null - || cameraPreview is null - || imageCapture is null - || videoCapture is null - || videoRecorder is null - || videoRecordingFile is not null) + || processCameraProvider is null + || cameraPreview is null + || imageCapture is null + || videoCapture is null + || videoRecorder is null + || videoRecordingFile is not null) { return; } videoRecordingStream = stream; - 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"); if (camera is null || !IsVideoCaptureAlreadyBound()) { @@ -336,9 +326,9 @@ protected virtual async partial Task PlatformStopVideoRecording(Cancella { ArgumentNullException.ThrowIfNull(cameraExecutor); if (videoRecording is null - || videoRecordingFile is null - || videoRecordingFinalizeTcs is null - || videoRecordingStream is null) + || videoRecordingFile is null + || videoRecordingFinalizeTcs is null + || videoRecordingStream is null) { return Stream.Null; } @@ -357,8 +347,8 @@ protected virtual async partial Task PlatformStopVideoRecording(Cancella bool IsVideoCaptureAlreadyBound() { return processCameraProvider is not null - && videoCapture is not null - && processCameraProvider.IsBound(videoCapture); + && videoCapture is not null + && processCameraProvider.IsBound(videoCapture); } void CleanupVideoRecordingResources() From 9cf7487e89090fe66d23a27d7048744d2b6f34a6 Mon Sep 17 00:00:00 2001 From: Zhitao Pan Date: Sun, 26 Oct 2025 14:41:02 +1100 Subject: [PATCH 03/16] Change from private to internal --- .../Providers/CameraProvider.android.cs | 2 +- .../Providers/CameraProvider.macios.cs | 2 +- .../Providers/CameraProvider.net.cs | 2 +- .../Providers/CameraProvider.shared.cs | 2 +- .../Providers/CameraProvider.tizen.cs | 2 +- .../Providers/CameraProvider.windows.cs | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.android.cs b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.android.cs index ad25030de9..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; - private async partial ValueTask PlatformRefreshAvailableCameras(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 5a9a9bad6b..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(); - private partial ValueTask PlatformRefreshAvailableCameras(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 57a58ae47d..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 { - private partial ValueTask PlatformRefreshAvailableCameras(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 0e2b6de76b..7c2cfd127b 100644 --- a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs @@ -28,7 +28,7 @@ private set } } - private partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token); + internal partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token); /// public async ValueTask RefreshAvailableCameras(CancellationToken token) diff --git a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.tizen.cs b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.tizen.cs index 5ae4c99da8..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 { - private partial ValueTask PlatformRefreshAvailableCameras(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 40b48e6609..d89398f5ba 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 { - private async partial ValueTask PlatformRefreshAvailableCameras(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); From 5996869fc5757cf77193904aa6225c16c193db86 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Tue, 18 Nov 2025 10:41:34 -0800 Subject: [PATCH 04/16] Add `CameraManager.Disconnect()` in `protected override void DisconnectHandler(NativePlatformCameraPreviewView)` --- .../Handlers/CameraViewHandler.shared.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/CommunityToolkit.Maui.Camera/Handlers/CameraViewHandler.shared.cs b/src/CommunityToolkit.Maui.Camera/Handlers/CameraViewHandler.shared.cs index dff22edcdc..9f413d43bc 100644 --- a/src/CommunityToolkit.Maui.Camera/Handlers/CameraViewHandler.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Handlers/CameraViewHandler.shared.cs @@ -91,6 +91,8 @@ protected override async void ConnectHandler(NativePlatformCameraPreviewView pla protected override void DisconnectHandler(NativePlatformCameraPreviewView platformView) { base.DisconnectHandler(platformView); + + CameraManager.Disconnect(); Dispose(); } From 9b344fbb4e4e25ac83043793281ff7a9b4155d56 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Tue, 18 Nov 2025 10:58:20 -0800 Subject: [PATCH 05/16] Refactor CameraManager and CameraProvider for improved disposal and async handling Refactored CameraManager to use sealed classes and private partial platform methods, and improved resource disposal logic across all platforms. CameraProvider is now sealed, implements IDisposable, and uses a semaphore to synchronize RefreshAvailableCameras, which now returns Task instead of ValueTask. Updated ICameraProvider interface and related mocks to match the new async signature. --- .../AppBuilderExtensions.shared.cs | 4 +- .../CameraManager.android.cs | 109 ++++++++---------- .../CameraManager.macios.cs | 90 +++++++-------- .../CameraManager.net.cs | 16 +-- .../CameraManager.shared.cs | 22 ++-- .../CameraManager.windows.cs | 20 ++-- .../Handlers/CameraViewHandler.shared.cs | 5 +- .../Interfaces/ICameraProvider.shared.cs | 4 +- .../Primitives/CameraViewDefaults.shared.cs | 1 - .../Providers/CameraProvider.android.cs | 2 +- .../Providers/CameraProvider.macios.cs | 66 +++++------ .../Providers/CameraProvider.net.cs | 2 +- .../Providers/CameraProvider.shared.cs | 32 +++-- .../Providers/CameraProvider.tizen.cs | 2 +- .../Providers/CameraProvider.windows.cs | 3 +- .../Mocks/MockCameraProvider.cs | 4 +- 16 files changed, 189 insertions(+), 193 deletions(-) diff --git a/src/CommunityToolkit.Maui.Camera/AppBuilderExtensions.shared.cs b/src/CommunityToolkit.Maui.Camera/AppBuilderExtensions.shared.cs index 3961e7fe52..6e09e1151d 100644 --- a/src/CommunityToolkit.Maui.Camera/AppBuilderExtensions.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/AppBuilderExtensions.shared.cs @@ -12,7 +12,7 @@ namespace CommunityToolkit.Maui; [SupportedOSPlatform("android21.0")] [SupportedOSPlatform("ios15.0")] [SupportedOSPlatform("maccatalyst15.0")] -[SupportedOSPlatform("tizen6.5")] +[UnsupportedOSPlatform("tizen")] public static class AppBuilderExtensions { /// @@ -23,7 +23,7 @@ public static class AppBuilderExtensions public static MauiAppBuilder UseMauiCommunityToolkitCamera(this MauiAppBuilder builder) { builder.Services.AddSingleton(); - builder.ConfigureMauiHandlers(h => h.AddHandler()); + builder.ConfigureMauiHandlers(static h => h.AddHandler()); return builder; } diff --git a/src/CommunityToolkit.Maui.Camera/CameraManager.android.cs b/src/CommunityToolkit.Maui.Camera/CameraManager.android.cs index 602d769b21..f3af41e441 100644 --- a/src/CommunityToolkit.Maui.Camera/CameraManager.android.cs +++ b/src/CommunityToolkit.Maui.Camera/CameraManager.android.cs @@ -62,8 +62,48 @@ public async Task SetExtensionMode(int mode, CancellationToken token) public void Dispose() { - Dispose(true); - GC.SuppressFinalize(this); + CleanupVideoRecordingResources(); + + camera?.Dispose(); + camera = null; + + cameraControl?.Dispose(); + cameraControl = null; + + cameraPreview?.Dispose(); + cameraPreview = null; + + cameraExecutor?.Dispose(); + cameraExecutor = null; + + imageCapture?.Dispose(); + imageCapture = null; + + videoCapture?.Dispose(); + videoCapture = null; + + imageCallback?.Dispose(); + imageCallback = null; + + previewView?.Dispose(); + previewView = null; + + processCameraProvider?.UnbindAll(); + processCameraProvider?.Dispose(); + processCameraProvider = null; + + resolutionSelector?.Dispose(); + resolutionSelector = null; + + resolutionFilter?.Dispose(); + resolutionFilter = null; + + orientationListener?.Disable(); + orientationListener?.Dispose(); + orientationListener = null; + + videoRecordingStream?.Dispose(); + videoRecordingStream = null; } // IN the future change the return type to be an alias @@ -137,56 +177,7 @@ public async partial ValueTask UpdateCaptureResolution(Size resolution, Cancella } } - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - CleanupVideoRecordingResources(); - - camera?.Dispose(); - camera = null; - - cameraControl?.Dispose(); - cameraControl = null; - - cameraPreview?.Dispose(); - cameraPreview = null; - - cameraExecutor?.Dispose(); - cameraExecutor = null; - - imageCapture?.Dispose(); - imageCapture = null; - - videoCapture?.Dispose(); - videoCapture = null; - - imageCallback?.Dispose(); - imageCallback = null; - - previewView?.Dispose(); - previewView = null; - - processCameraProvider?.UnbindAll(); - processCameraProvider?.Dispose(); - processCameraProvider = null; - - resolutionSelector?.Dispose(); - resolutionSelector = null; - - resolutionFilter?.Dispose(); - resolutionFilter = null; - - orientationListener?.Disable(); - orientationListener?.Dispose(); - orientationListener = null; - - videoRecordingStream?.Dispose(); - videoRecordingStream = null; - } - } - - protected virtual async partial Task PlatformConnectCamera(CancellationToken token) + private async partial Task PlatformConnectCamera(CancellationToken token) { var cameraProviderFuture = ProcessCameraProvider.GetInstance(context); if (previewView is null) @@ -208,7 +199,7 @@ protected virtual async partial Task PlatformConnectCamera(CancellationToken tok await cameraProviderTCS.Task.WaitAsync(token); } - protected async Task StartUseCase(CancellationToken token) + async Task StartUseCase(CancellationToken token) { if (resolutionSelector is null || cameraExecutor is null) { @@ -255,7 +246,7 @@ protected async Task StartUseCase(CancellationToken token) await StartCameraPreview(token); } - protected virtual async partial Task PlatformStartCameraPreview(CancellationToken token) + private async partial Task PlatformStartCameraPreview(CancellationToken token) { if (previewView is null || processCameraProvider is null || cameraPreview is null || imageCapture is null || videoCapture is null) { @@ -275,7 +266,7 @@ protected virtual async partial Task PlatformStartCameraPreview(CancellationToke OnLoaded.Invoke(); } - protected virtual partial void PlatformStopCameraPreview() + private partial void PlatformStopCameraPreview() { if (processCameraProvider is null) { @@ -286,11 +277,11 @@ protected virtual partial void PlatformStopCameraPreview() IsInitialized = false; } - protected virtual partial void PlatformDisconnect() + private partial void PlatformDisconnect() { } - protected virtual partial ValueTask PlatformTakePicture(CancellationToken token) + private partial ValueTask PlatformTakePicture(CancellationToken token) { ArgumentNullException.ThrowIfNull(cameraExecutor); ArgumentNullException.ThrowIfNull(imageCallback); @@ -299,7 +290,7 @@ protected virtual partial ValueTask PlatformTakePicture(CancellationToken token) return ValueTask.CompletedTask; } - protected virtual async partial Task PlatformStartVideoRecording(Stream stream, CancellationToken token) + private async partial Task PlatformStartVideoRecording(Stream stream, CancellationToken token) { if (previewView is null || processCameraProvider is null @@ -341,7 +332,7 @@ protected virtual async partial Task PlatformStartVideoRecording(Stream stream, // https://developer.android.com/reference/androidx/camera/video/Recorder#prepareRecording(android.content.Context,androidx.camera.video.MediaStoreOutputOptions) } - protected virtual async partial Task PlatformStopVideoRecording(CancellationToken token) + private async partial Task PlatformStopVideoRecording(CancellationToken token) { ArgumentNullException.ThrowIfNull(cameraExecutor); if (videoRecording is null diff --git a/src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs b/src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs index 19a0ff8509..ff78ff7490 100644 --- a/src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs +++ b/src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs @@ -34,8 +34,28 @@ partial class CameraManager /// public void Dispose() { - Dispose(true); - GC.SuppressFinalize(this); + CleanupVideoRecordingResources(); + + captureSession?.StopRunning(); + captureSession?.Dispose(); + captureSession = null; + + captureInput?.Dispose(); + captureInput = null; + + captureDevice = null; + + orientationDidChangeObserver?.Dispose(); + orientationDidChangeObserver = null; + + photoOutput?.Dispose(); + photoOutput = null; + + previewView?.Dispose(); + previewView = null; + + videoRecordingStream?.Dispose(); + videoRecordingStream = null; } public NativePlatformCameraPreviewView CreatePlatformView() @@ -86,7 +106,12 @@ public partial void UpdateZoom(float zoomLevel) public partial ValueTask UpdateCaptureResolution(Size resolution, CancellationToken token) { - if (captureDevice is null || cameraView.SelectedCamera is null) + if (cameraView.SelectedCamera is null) + { + throw new CameraException($"Unable to update Capture Resolution because {nameof(ICameraView)}.{nameof(ICameraView.SelectedCamera)} is null."); + } + + if (captureDevice is null) { return ValueTask.CompletedTask; } @@ -119,12 +144,12 @@ public partial ValueTask UpdateCaptureResolution(Size resolution, CancellationTo return ValueTask.CompletedTask; } - protected virtual async partial Task PlatformConnectCamera(CancellationToken token) + private async partial Task PlatformConnectCamera(CancellationToken token) { await PlatformStartCameraPreview(token); } - protected virtual async partial Task PlatformStartCameraPreview(CancellationToken token) + private async partial Task PlatformStartCameraPreview(CancellationToken token) { if (captureSession is null) { @@ -159,7 +184,7 @@ protected virtual async partial Task PlatformStartCameraPreview(CancellationToke OnLoaded.Invoke(); } - protected virtual partial void PlatformStopCameraPreview() + private partial void PlatformStopCameraPreview() { if (captureSession is null) { @@ -174,11 +199,11 @@ protected virtual partial void PlatformStopCameraPreview() IsInitialized = false; } - protected virtual partial void PlatformDisconnect() + private partial void PlatformDisconnect() { } - protected virtual async partial Task PlatformStartVideoRecording(Stream stream, CancellationToken token) + private async partial Task PlatformStartVideoRecording(Stream stream, CancellationToken token) { var isPermissionGranted = await AVCaptureDevice.RequestAccessForMediaTypeAsync(AVAuthorizationMediaType.Video).WaitAsync(token); if (!isPermissionGranted) @@ -261,14 +286,14 @@ protected virtual async partial Task PlatformStartVideoRecording(Stream stream, videoOutput.StartRecordingToOutputFile(outputUrl, new AVCaptureMovieFileOutputRecordingDelegate(videoRecordingFinalizeTcs)); } - protected virtual async partial Task PlatformStopVideoRecording(CancellationToken token) + private async partial Task PlatformStopVideoRecording(CancellationToken token) { if (captureSession is null - || videoRecordingFileName is null - || videoInput is null - || videoOutput is null - || videoRecordingStream is null - || videoRecordingFinalizeTcs is null) + || videoRecordingFileName is null + || videoInput is null + || videoOutput is null + || videoRecordingStream is null + || videoRecordingFinalizeTcs is null) { return Stream.Null; } @@ -333,7 +358,7 @@ void CleanupVideoRecordingResources() videoRecordingFinalizeTcs = null; } - protected virtual async partial ValueTask PlatformTakePicture(CancellationToken token) + private async partial ValueTask PlatformTakePicture(CancellationToken token) { ArgumentNullException.ThrowIfNull(photoOutput); @@ -388,35 +413,6 @@ protected virtual async partial ValueTask PlatformTakePicture(CancellationToken } } - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - CleanupVideoRecordingResources(); - - captureSession?.StopRunning(); - captureSession?.Dispose(); - captureSession = null; - - captureInput?.Dispose(); - captureInput = null; - - captureDevice = null; - - orientationDidChangeObserver?.Dispose(); - orientationDidChangeObserver = null; - - photoOutput?.Dispose(); - photoOutput = null; - - previewView?.Dispose(); - previewView = null; - - videoRecordingStream?.Dispose(); - videoRecordingStream = null; - } - } - static AVCaptureVideoOrientation GetVideoOrientation() { IEnumerable scenes = UIApplication.SharedApplication.ConnectedScenes; @@ -446,7 +442,7 @@ IEnumerable GetPhotoCompatibleFormats(IEnumerable photoPixelFormats.Contains((NSNumber)format.FormatDescription.MediaSubType)); - } + } return formats; } @@ -454,8 +450,8 @@ IEnumerable GetPhotoCompatibleFormats(IEnumerable throw new NotSupportedException(notSupportedMessage); - protected virtual partial Task PlatformStartVideoRecording(Stream stream, CancellationToken token) => throw new NotSupportedException(notSupportedMessage); - protected virtual partial Task PlatformStopVideoRecording(CancellationToken token) => throw new NotSupportedException(notSupportedMessage); - protected virtual partial Task PlatformStartCameraPreview(CancellationToken token) => throw new NotSupportedException(notSupportedMessage); + private partial Task PlatformStartVideoRecording(Stream stream, CancellationToken token) => throw new NotSupportedException(notSupportedMessage); + private partial Task PlatformStopVideoRecording(CancellationToken token) => throw new NotSupportedException(notSupportedMessage); + private partial Task PlatformStartCameraPreview(CancellationToken token) => throw new NotSupportedException(notSupportedMessage); - protected virtual partial void PlatformStopCameraPreview() => throw new NotSupportedException(notSupportedMessage); + private partial void PlatformStopCameraPreview() => throw new NotSupportedException(notSupportedMessage); - protected virtual partial Task PlatformConnectCamera(CancellationToken token) => throw new NotSupportedException(notSupportedMessage); + private partial Task PlatformConnectCamera(CancellationToken token) => throw new NotSupportedException(notSupportedMessage); - protected virtual partial void PlatformDisconnect() => throw new NotSupportedException(notSupportedMessage); + private partial void PlatformDisconnect() => throw new NotSupportedException(notSupportedMessage); - protected virtual partial ValueTask PlatformTakePicture(CancellationToken token) => throw new NotSupportedException(notSupportedMessage); + private partial ValueTask PlatformTakePicture(CancellationToken token) => throw new NotSupportedException(notSupportedMessage); } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs b/src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs index 5408870685..f86754ed82 100644 --- a/src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs @@ -1,6 +1,4 @@ -using System.Diagnostics; - -namespace CommunityToolkit.Maui.Core; +namespace CommunityToolkit.Maui.Core; /// /// A class that manages the camera functionality. @@ -14,7 +12,7 @@ namespace CommunityToolkit.Maui.Core; /// The to execute when the camera is loaded. /// Thrown when no can be resolved. /// Thrown when there are no cameras available. -partial class CameraManager( +sealed partial class CameraManager( IMauiContext mauiContext, ICameraView cameraView, ICameraProvider cameraProvider, @@ -55,7 +53,6 @@ public async Task ConnectCamera(CancellationToken token) /// A that can be awaited. public Task StartCameraPreview(CancellationToken token) => PlatformStartCameraPreview(token); - /// /// Starts the video recording. /// @@ -68,7 +65,6 @@ public Task StartVideoRecording(Stream stream, CancellationToken token) return PlatformStartVideoRecording(stream, token); } - /// /// Stops the video recording. /// @@ -130,31 +126,31 @@ public async ValueTask UpdateCurrentCamera(CameraInfo? cameraInfo, CancellationT /// /// A that can be used to cancel the work. /// A that can be awaited. - protected virtual partial ValueTask PlatformTakePicture(CancellationToken token); + private partial ValueTask PlatformTakePicture(CancellationToken token); /// /// Starts the preview from the camera, at the platform-specific level. /// /// A that can be used to cancel the work. /// A that can be awaited. - protected virtual partial Task PlatformStartCameraPreview(CancellationToken token); + private partial Task PlatformStartCameraPreview(CancellationToken token); /// /// Connects to the camera, at the platform-specific level. /// /// A that can be used to cancel the work. /// A that can be awaited. - protected virtual partial Task PlatformConnectCamera(CancellationToken token); + private partial Task PlatformConnectCamera(CancellationToken token); /// /// Disconnects from the camera, at the platform-specific level. /// - protected virtual partial void PlatformDisconnect(); + private partial void PlatformDisconnect(); /// /// Stops the preview from the camera, at the platform-specific level. /// - protected virtual partial void PlatformStopCameraPreview(); + private partial void PlatformStopCameraPreview(); /// /// Starts video recording and writes the recorded data to the specified stream. @@ -164,7 +160,7 @@ public async ValueTask UpdateCurrentCamera(CameraInfo? cameraInfo, CancellationT /// The stream to which the video data will be written. Must be writable and not null. /// A cancellation token that can be used to cancel the video recording operation. /// A task that represents the asynchronous video recording operation. - protected virtual partial Task PlatformStartVideoRecording(Stream stream, CancellationToken token); + private partial Task PlatformStartVideoRecording(Stream stream, CancellationToken token); /// /// Stops the video recording process asynchronously. @@ -173,5 +169,5 @@ public async ValueTask UpdateCurrentCamera(CameraInfo? cameraInfo, CancellationT /// functionality. /// A cancellation token that can be used to cancel the stop operation. /// A task that represents the asynchronous stop operation. - protected virtual partial Task PlatformStopVideoRecording(CancellationToken token); + private partial Task PlatformStopVideoRecording(CancellationToken token); } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Camera/CameraManager.windows.cs b/src/CommunityToolkit.Maui.Camera/CameraManager.windows.cs index 05567eee82..aeb7c0414c 100644 --- a/src/CommunityToolkit.Maui.Camera/CameraManager.windows.cs +++ b/src/CommunityToolkit.Maui.Camera/CameraManager.windows.cs @@ -23,8 +23,8 @@ public MediaPlayerElement CreatePlatformView() public void Dispose() { - Dispose(true); - GC.SuppressFinalize(this); + PlatformStopCameraPreview(); + mediaCapture?.Dispose(); } public partial void UpdateFlashMode(CameraFlashMode flashMode) @@ -108,15 +108,6 @@ protected virtual async partial ValueTask PlatformTakePicture(CancellationToken } } - protected virtual void Dispose(bool disposing) - { - PlatformStopCameraPreview(); - if (disposing) - { - mediaCapture?.Dispose(); - } - } - protected virtual async partial Task PlatformConnectCamera(CancellationToken token) { await StartCameraPreview(token); @@ -166,7 +157,12 @@ protected virtual partial void PlatformStopCameraPreview() protected async Task PlatformUpdateResolution(Size resolution, CancellationToken token) { - if (!IsInitialized || mediaCapture is null || cameraView.SelectedCamera is null) + if (cameraView.SelectedCamera is null) + { + throw new CameraException($"Unable to update Capture Resolution because {nameof(ICameraView)}.{nameof(ICameraView.SelectedCamera)} is null."); + } + + if (!IsInitialized || mediaCapture is null) { return; } diff --git a/src/CommunityToolkit.Maui.Camera/Handlers/CameraViewHandler.shared.cs b/src/CommunityToolkit.Maui.Camera/Handlers/CameraViewHandler.shared.cs index 9f413d43bc..1a89c9cfd7 100644 --- a/src/CommunityToolkit.Maui.Camera/Handlers/CameraViewHandler.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Handlers/CameraViewHandler.shared.cs @@ -68,7 +68,7 @@ protected override NativePlatformCameraPreviewView CreatePlatformView() ArgumentNullException.ThrowIfNull(MauiContext); cameraManager = new(MauiContext, VirtualView, cameraProvider, () => Init(VirtualView)); - return (NativePlatformCameraPreviewView)CameraManager.CreatePlatformView(); + return CameraManager.CreatePlatformView(); // When camera is loaded(switched), map the current flash mode to the platform view, // reset the zoom factor to 1 @@ -93,6 +93,7 @@ protected override void DisconnectHandler(NativePlatformCameraPreviewView platfo base.DisconnectHandler(platformView); CameraManager.Disconnect(); + Dispose(); } @@ -106,6 +107,8 @@ protected virtual void Dispose(bool disposing) { cameraManager?.Dispose(); cameraManager = null; + + cameraProvider.Dispose(); } } diff --git a/src/CommunityToolkit.Maui.Camera/Interfaces/ICameraProvider.shared.cs b/src/CommunityToolkit.Maui.Camera/Interfaces/ICameraProvider.shared.cs index 9e41e51306..de9fa31f43 100644 --- a/src/CommunityToolkit.Maui.Camera/Interfaces/ICameraProvider.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Interfaces/ICameraProvider.shared.cs @@ -5,7 +5,7 @@ namespace CommunityToolkit.Maui.Core; /// /// Interface to retrieve available cameras /// -public interface ICameraProvider +public interface ICameraProvider : IDisposable { /// /// Event fires when the contents has changed @@ -26,5 +26,5 @@ public interface ICameraProvider /// /// [MemberNotNull(nameof(AvailableCameras))] - ValueTask RefreshAvailableCameras(CancellationToken token); + Task RefreshAvailableCameras(CancellationToken token); } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Camera/Primitives/CameraViewDefaults.shared.cs b/src/CommunityToolkit.Maui.Camera/Primitives/CameraViewDefaults.shared.cs index 6da7e2c028..0077ee383b 100644 --- a/src/CommunityToolkit.Maui.Camera/Primitives/CameraViewDefaults.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Primitives/CameraViewDefaults.shared.cs @@ -10,7 +10,6 @@ namespace CommunityToolkit.Maui.Core; [SupportedOSPlatform("android21.0")] [SupportedOSPlatform("ios")] [SupportedOSPlatform("maccatalyst")] -[SupportedOSPlatform("tizen")] [EditorBrowsable(EditorBrowsableState.Never)] public static class CameraViewDefaults { diff --git a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.android.cs b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.android.cs index 2d811ac819..53a2e969e4 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..5892c6a7a8 100644 --- a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.macios.cs +++ b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.macios.cs @@ -8,8 +8,40 @@ namespace CommunityToolkit.Maui.Core; partial class CameraProvider { static readonly AVCaptureDeviceType[] captureDevices = InitializeCaptureDevices(); + + static AVCaptureDeviceType[] InitializeCaptureDevices() + { + AVCaptureDeviceType[] deviceTypes = + [ + AVCaptureDeviceType.BuiltInWideAngleCamera, + AVCaptureDeviceType.BuiltInTelephotoCamera, + AVCaptureDeviceType.BuiltInDualCamera + ]; + + if (UIDevice.CurrentDevice.CheckSystemVersion(11, 1)) + { + deviceTypes = [.. deviceTypes, + AVCaptureDeviceType.BuiltInTrueDepthCamera]; + } + + if (UIDevice.CurrentDevice.CheckSystemVersion(13, 0)) + { + deviceTypes = [.. deviceTypes, + AVCaptureDeviceType.BuiltInUltraWideCamera, + AVCaptureDeviceType.BuiltInTripleCamera, + AVCaptureDeviceType.BuiltInDualWideCamera]; + } + + if (UIDevice.CurrentDevice.CheckSystemVersion(15, 4)) + { + deviceTypes = [.. deviceTypes, + AVCaptureDeviceType.BuiltInLiDarDepthCamera]; + } - internal partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token) + return deviceTypes; + } + + private partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token) { var discoverySession = AVCaptureDeviceDiscoverySession.Create(captureDevices, AVMediaTypes.Video, AVCaptureDevicePosition.Unspecified); var availableCameras = new List(); @@ -63,36 +95,4 @@ internal partial ValueTask PlatformRefreshAvailableCameras(CancellationToken tok return ValueTask.CompletedTask; } - - static AVCaptureDeviceType[] InitializeCaptureDevices() - { - AVCaptureDeviceType[] deviceTypes = - [ - AVCaptureDeviceType.BuiltInWideAngleCamera, - AVCaptureDeviceType.BuiltInTelephotoCamera, - AVCaptureDeviceType.BuiltInDualCamera - ]; - - if (UIDevice.CurrentDevice.CheckSystemVersion(11, 1)) - { - deviceTypes = [.. deviceTypes, - AVCaptureDeviceType.BuiltInTrueDepthCamera]; - } - - if (UIDevice.CurrentDevice.CheckSystemVersion(13, 0)) - { - deviceTypes = [.. deviceTypes, - AVCaptureDeviceType.BuiltInUltraWideCamera, - AVCaptureDeviceType.BuiltInTripleCamera, - AVCaptureDeviceType.BuiltInDualWideCamera]; - } - - if (UIDevice.CurrentDevice.CheckSystemVersion(15, 4)) - { - deviceTypes = [.. deviceTypes, - AVCaptureDeviceType.BuiltInLiDarDepthCamera]; - } - - return deviceTypes; - } } \ No newline at end of file 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 7c2cfd127b..8e287bba56 100644 --- a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs @@ -3,9 +3,11 @@ /// /// Implementation that provides the ability to discover cameras that are attached to the current device. /// -partial class CameraProvider : ICameraProvider +sealed partial class CameraProvider : ICameraProvider { readonly WeakEventManager availableCamerasChangedEventManager = new(); + readonly SemaphoreSlim refreshAvailableCamerasSemaphore = new(1, 1); + Task? refreshAvailableCamerasTask; public event EventHandler?> AvailableCamerasChanged @@ -14,6 +16,11 @@ public event EventHandler?> AvailableCamerasChanged remove => availableCamerasChangedEventManager.RemoveEventHandler(value); } + public void Dispose() + { + refreshAvailableCamerasSemaphore.Dispose(); + } + /// public IReadOnlyList? AvailableCameras { @@ -28,17 +35,24 @@ private set } } - internal partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token); - /// - public async ValueTask RefreshAvailableCameras(CancellationToken token) + public async Task RefreshAvailableCameras(CancellationToken token) { - if (refreshAvailableCamerasTask is null || refreshAvailableCamerasTask.IsCompleted) + await refreshAvailableCamerasSemaphore.WaitAsync(token); + + try { - refreshAvailableCamerasTask = PlatformRefreshAvailableCameras(token).AsTask(); + if (refreshAvailableCamerasTask is null || refreshAvailableCamerasTask.IsCompleted) + { + refreshAvailableCamerasTask = PlatformRefreshAvailableCameras(token).AsTask(); + } + } + finally + { + refreshAvailableCamerasSemaphore.Release(); } - await refreshAvailableCamerasTask; + await refreshAvailableCamerasTask.WaitAsync(token); } internal static bool AreCameraInfoListsEqual(in IReadOnlyList? cameraInfoList1, in IReadOnlyList? cameraInfoList2) @@ -58,4 +72,6 @@ internal static bool AreCameraInfoListsEqual(in IReadOnlyList? camer return cameraInfosInList1ButNotInList2.Count is 0 && cameraInfosInList2ButNotInList1.Count is 0; } -} + + private partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token); +} \ No newline at end of file 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 diff --git a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.windows.cs b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.windows.cs index d89398f5ba..351c507ffc 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); @@ -67,5 +67,4 @@ internal async partial ValueTask PlatformRefreshAvailableCameras(CancellationTok AvailableCameras = availableCameras; } - } diff --git a/src/CommunityToolkit.Maui.UnitTests/Mocks/MockCameraProvider.cs b/src/CommunityToolkit.Maui.UnitTests/Mocks/MockCameraProvider.cs index 593c4ce70a..0d635c2690 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Mocks/MockCameraProvider.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Mocks/MockCameraProvider.cs @@ -20,7 +20,7 @@ private set } } - public ValueTask RefreshAvailableCameras(CancellationToken token) + public Task RefreshAvailableCameras(CancellationToken token) { AvailableCameras = [ @@ -35,6 +35,6 @@ public ValueTask RefreshAvailableCameras(CancellationToken token) ]) ]; - return ValueTask.CompletedTask; + return Task.CompletedTask; } } \ No newline at end of file From b4861725ae1b2c5bd42aa47738d0ff0918aa9081 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Tue, 18 Nov 2025 11:09:08 -0800 Subject: [PATCH 06/16] Fix Warnings --- .../Primitives/CameraViewDefaults.shared.cs | 4 ---- src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs | 4 ---- 2 files changed, 8 deletions(-) diff --git a/src/CommunityToolkit.Maui.Camera/Primitives/CameraViewDefaults.shared.cs b/src/CommunityToolkit.Maui.Camera/Primitives/CameraViewDefaults.shared.cs index 0077ee383b..e946945a6e 100644 --- a/src/CommunityToolkit.Maui.Camera/Primitives/CameraViewDefaults.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Primitives/CameraViewDefaults.shared.cs @@ -6,10 +6,6 @@ namespace CommunityToolkit.Maui.Core; /// Default Values for -[SupportedOSPlatform("windows10.0.10240.0")] -[SupportedOSPlatform("android21.0")] -[SupportedOSPlatform("ios")] -[SupportedOSPlatform("maccatalyst")] [EditorBrowsable(EditorBrowsableState.Never)] public static class CameraViewDefaults { diff --git a/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs b/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs index 10df8a94c8..abe92387e1 100644 --- a/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs @@ -9,10 +9,6 @@ namespace CommunityToolkit.Maui.Views; /// /// A visual element that provides the ability to show a camera preview and capture images. /// -[SupportedOSPlatform("windows10.0.10240.0")] -[SupportedOSPlatform("android21.0")] -[SupportedOSPlatform("ios")] -[SupportedOSPlatform("maccatalyst")] public partial class CameraView : View, ICameraView, IDisposable { static readonly BindablePropertyKey isAvailablePropertyKey = From 8e2993f5617f4e65055b9d9d5a6a5649f22fd729 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Tue, 18 Nov 2025 11:10:40 -0800 Subject: [PATCH 07/16] Use iOS 26 / MacCatalyst 26 API --- .../CameraManager.macios.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs b/src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs index ff78ff7490..4997045306 100644 --- a/src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs +++ b/src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs @@ -416,9 +416,20 @@ private async partial ValueTask PlatformTakePicture(CancellationToken token) static AVCaptureVideoOrientation GetVideoOrientation() { IEnumerable scenes = UIApplication.SharedApplication.ConnectedScenes; - var interfaceOrientation = scenes.FirstOrDefault() is UIWindowScene windowScene - ? windowScene.InterfaceOrientation - : UIApplication.SharedApplication.StatusBarOrientation; + + UIInterfaceOrientation interfaceOrientation; + if (!(OperatingSystem.IsMacCatalystVersionAtLeast(26) || OperatingSystem.IsIOSVersionAtLeast(26))) + { + interfaceOrientation = scenes.FirstOrDefault() is UIWindowScene windowScene + ? windowScene.InterfaceOrientation + : UIApplication.SharedApplication.StatusBarOrientation; + } + else + { + interfaceOrientation = scenes.FirstOrDefault() is UIWindowScene windowScene + ? windowScene.EffectiveGeometry.InterfaceOrientation + : UIApplication.SharedApplication.StatusBarOrientation; + } return interfaceOrientation switch { From 9ff400d551e89f61212a1975d8af8d798dde881f Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Tue, 18 Nov 2025 11:12:51 -0800 Subject: [PATCH 08/16] Add link to open Issue --- .../Pages/Views/CameraView/CameraViewPage.xaml.cs | 1 + 1 file changed, 1 insertion(+) 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 dbe05c0a3b..7149cf7d6b 100644 --- a/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml.cs +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml.cs @@ -52,6 +52,7 @@ protected override async void OnAppearing() isInitialized = true; } + // https://github.com/dotnet/maui/issues/15833 protected override void OnNavigatedFrom(NavigatedFromEventArgs args) { base.OnNavigatedFrom(args); From 88276ff83e312e237b23962097b24b58c8f8eb95 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Tue, 18 Nov 2025 11:22:42 -0800 Subject: [PATCH 09/16] Remove Unnecessary Code --- .../Pages/Views/CameraView/CameraViewPage.xaml.cs | 13 +------------ .../Views/CameraView/CameraViewViewModel.cs | 3 --- 2 files changed, 1 insertion(+), 15 deletions(-) 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 7149cf7d6b..f1ae71a283 100644 --- a/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml.cs +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml.cs @@ -8,9 +8,7 @@ public sealed partial class CameraViewPage : BasePage { readonly IFileSaver fileSaver; readonly string imagePath; - bool isInitialized = false; - int pageCount; Stream videoRecordingStream = Stream.Null; public CameraViewPage(CameraViewViewModel viewModel, IFileSystem fileSystem, IFileSaver fileSaver) : base(viewModel) @@ -41,15 +39,6 @@ protected override async void OnAppearing() await Shell.Current.CurrentPage.DisplayAlertAsync("Microphone permission is not granted.", "Please grant the permission to use this feature.", "OK"); return; } - - if (isInitialized) - { - return; - } - - var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(3)); - await BindingContext.RefreshCamerasCommand.ExecuteAsync(cancellationTokenSource.Token); - isInitialized = true; } // https://github.com/dotnet/maui/issues/15833 @@ -137,7 +126,7 @@ async void SaveVideo(object? sender, EventArgs? e) var status = await Permissions.RequestAsync(); if (status is not PermissionStatus.Granted) { - await Shell.Current.CurrentPage.DisplayAlert("Storage permission is not granted.", "Please grant the permission to use this feature.", "OK"); + await Shell.Current.CurrentPage.DisplayAlertAsync("Storage permission is not granted.", "Please grant the permission to use this feature.", "OK"); return; } diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/CameraView/CameraViewViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/CameraView/CameraViewViewModel.cs index 5f44560acc..fb8f25a2de 100644 --- a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/CameraView/CameraViewViewModel.cs +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/CameraView/CameraViewViewModel.cs @@ -48,9 +48,6 @@ public CameraViewViewModel(ICameraProvider cameraProvider) [ObservableProperty] public partial string ResolutionText { get; set; } = string.Empty; - [RelayCommand] - async Task RefreshCameras(CancellationToken token) => await cameraProvider.RefreshAvailableCameras(token); - partial void OnFlashModeChanged(CameraFlashMode value) { UpdateFlashModeText(); From 067aa18dd9714008f06ee6e87251dd7d5881f3be Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Tue, 18 Nov 2025 11:32:47 -0800 Subject: [PATCH 10/16] `dotnet format` --- .../Views/CameraView/CameraViewPage.xaml.cs | 4 ++-- .../CameraManager.macios.cs | 12 +++++----- .../CameraManager.windows.cs | 16 +++++++------- .../Extensions/CameraViewExtensions.macios.cs | 22 +++++++++---------- .../Handlers/CameraViewHandler.shared.cs | 4 ++-- .../Providers/CameraProvider.macios.cs | 2 +- .../Providers/CameraProvider.shared.cs | 2 +- .../Providers/CameraProvider.windows.cs | 2 +- .../Views/MediaManager.macios.cs | 8 +++---- 9 files changed, 36 insertions(+), 36 deletions(-) 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 f1ae71a283..c51a203e04 100644 --- a/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml.cs +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml.cs @@ -24,10 +24,10 @@ public CameraViewPage(CameraViewViewModel viewModel, IFileSystem fileSystem, IFi protected override async void OnAppearing() { base.OnAppearing(); - + var cameraPermissionsRequest = await Permissions.RequestAsync(); var microphonePermissionsRequest = await Permissions.RequestAsync(); - + if (cameraPermissionsRequest is not PermissionStatus.Granted) { await Shell.Current.CurrentPage.DisplayAlertAsync("Camera permission is not granted.", "Please grant the permission to use this feature.", "OK"); diff --git a/src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs b/src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs index 4997045306..8b105f9fec 100644 --- a/src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs +++ b/src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs @@ -289,11 +289,11 @@ private async partial Task PlatformStartVideoRecording(Stream stream, Cancellati private async partial Task PlatformStopVideoRecording(CancellationToken token) { if (captureSession is null - || videoRecordingFileName is null - || videoInput is null - || videoOutput is null - || videoRecordingStream is null - || videoRecordingFinalizeTcs is null) + || videoRecordingFileName is null + || videoInput is null + || videoOutput is null + || videoRecordingStream is null + || videoRecordingFinalizeTcs is null) { return Stream.Null; } @@ -462,7 +462,7 @@ static bool MatchesResolution(AVCaptureDeviceFormat format, Size resolution) { var dimensions = ((CMVideoFormatDescription)format.FormatDescription).Dimensions; return dimensions.Width <= resolution.Width - && dimensions.Height <= resolution.Height; + && dimensions.Height <= resolution.Height; } sealed class AVCapturePhotoCaptureDelegateWrapper : AVCapturePhotoCaptureDelegate diff --git a/src/CommunityToolkit.Maui.Camera/CameraManager.windows.cs b/src/CommunityToolkit.Maui.Camera/CameraManager.windows.cs index aeb7c0414c..15b889f550 100644 --- a/src/CommunityToolkit.Maui.Camera/CameraManager.windows.cs +++ b/src/CommunityToolkit.Maui.Camera/CameraManager.windows.cs @@ -78,11 +78,11 @@ public async partial ValueTask UpdateCaptureResolution(Size resolution, Cancella await PlatformUpdateResolution(resolution, token); } - protected virtual partial void PlatformDisconnect() + private partial void PlatformDisconnect() { } - protected virtual async partial ValueTask PlatformTakePicture(CancellationToken token) + private async partial ValueTask PlatformTakePicture(CancellationToken token) { if (mediaCapture is null) { @@ -108,12 +108,12 @@ protected virtual async partial ValueTask PlatformTakePicture(CancellationToken } } - protected virtual async partial Task PlatformConnectCamera(CancellationToken token) + private async partial Task PlatformConnectCamera(CancellationToken token) { await StartCameraPreview(token); } - protected virtual async partial Task PlatformStartCameraPreview(CancellationToken token) + private async partial Task PlatformStartCameraPreview(CancellationToken token) { if (mediaElement is null) { @@ -141,7 +141,7 @@ protected virtual async partial Task PlatformStartCameraPreview(CancellationToke OnLoaded.Invoke(); } - protected virtual partial void PlatformStopCameraPreview() + private partial void PlatformStopCameraPreview() { if (mediaElement is null) { @@ -161,7 +161,7 @@ protected async Task PlatformUpdateResolution(Size resolution, CancellationToken { throw new CameraException($"Unable to update Capture Resolution because {nameof(ICameraView)}.{nameof(ICameraView.SelectedCamera)} is null."); } - + if (!IsInitialized || mediaCapture is null) { return; @@ -180,7 +180,7 @@ protected async Task PlatformUpdateResolution(Size resolution, CancellationToken } } - protected virtual async partial Task PlatformStartVideoRecording(Stream stream, CancellationToken token) + private async partial Task PlatformStartVideoRecording(Stream stream, CancellationToken token) { if (!IsInitialized || mediaCapture is null || mediaElement is null) { @@ -211,7 +211,7 @@ protected virtual async partial Task PlatformStartVideoRecording(Stream stream, } } - protected virtual async partial Task PlatformStopVideoRecording(CancellationToken token) + private async partial Task PlatformStopVideoRecording(CancellationToken token) { if (!IsInitialized || mediaElement is null || mediaRecording is null || videoCaptureStream is null) { diff --git a/src/CommunityToolkit.Maui.Camera/Extensions/CameraViewExtensions.macios.cs b/src/CommunityToolkit.Maui.Camera/Extensions/CameraViewExtensions.macios.cs index 1b53ce2c7f..2a564c9975 100644 --- a/src/CommunityToolkit.Maui.Camera/Extensions/CameraViewExtensions.macios.cs +++ b/src/CommunityToolkit.Maui.Camera/Extensions/CameraViewExtensions.macios.cs @@ -33,21 +33,21 @@ public static void UpdateAvailability(this ICameraView cameraView) } extension(AVCaptureDeviceFormat avCaptureDeviceFormat) - { + { /// - /// Gets the total resolution area in pixels (width × height) of the . - /// - /// - /// The total number of pixels, calculated as width multiplied by height. - /// - public int ResolutionArea + /// Gets the total resolution area in pixels (width × height) of the . + /// + /// + /// The total number of pixels, calculated as width multiplied by height. + /// + public int ResolutionArea { - get - { + get + { var dimensions = ((CMVideoFormatDescription)avCaptureDeviceFormat.FormatDescription).Dimensions; return dimensions.Width * dimensions.Height; - } + } } - } + } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Camera/Handlers/CameraViewHandler.shared.cs b/src/CommunityToolkit.Maui.Camera/Handlers/CameraViewHandler.shared.cs index 1a89c9cfd7..166c8c4406 100644 --- a/src/CommunityToolkit.Maui.Camera/Handlers/CameraViewHandler.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Handlers/CameraViewHandler.shared.cs @@ -91,9 +91,9 @@ protected override async void ConnectHandler(NativePlatformCameraPreviewView pla protected override void DisconnectHandler(NativePlatformCameraPreviewView platformView) { base.DisconnectHandler(platformView); - + CameraManager.Disconnect(); - + Dispose(); } diff --git a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.macios.cs b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.macios.cs index 5892c6a7a8..aa01adc5fd 100644 --- a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.macios.cs +++ b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.macios.cs @@ -8,7 +8,7 @@ namespace CommunityToolkit.Maui.Core; partial class CameraProvider { static readonly AVCaptureDeviceType[] captureDevices = InitializeCaptureDevices(); - + static AVCaptureDeviceType[] InitializeCaptureDevices() { AVCaptureDeviceType[] deviceTypes = diff --git a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs index 8e287bba56..6a67f56d5f 100644 --- a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs @@ -39,7 +39,7 @@ private set public async Task RefreshAvailableCameras(CancellationToken token) { await refreshAvailableCamerasSemaphore.WaitAsync(token); - + try { if (refreshAvailableCamerasTask is null || refreshAvailableCamerasTask.IsCompleted) diff --git a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.windows.cs b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.windows.cs index 351c507ffc..5132c8ad44 100644 --- a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.windows.cs +++ b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.windows.cs @@ -67,4 +67,4 @@ private async partial ValueTask PlatformRefreshAvailableCameras(CancellationToke AvailableCameras = availableCameras; } -} +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.macios.cs b/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.macios.cs index 472409d522..de7a4be6b8 100644 --- a/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.macios.cs +++ b/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.macios.cs @@ -279,7 +279,7 @@ protected virtual async partial ValueTask PlatformUpdateSource() } var message = $"{Player.CurrentItem?.Error?.LocalizedDescription} - " + - $"{Player.CurrentItem?.Error?.LocalizedFailureReason}"; + $"{Player.CurrentItem?.Error?.LocalizedFailureReason}"; MediaElement.MediaFailed( new MediaFailedEventArgs(message)); @@ -492,7 +492,7 @@ protected virtual void Dispose(bool disposing) static async Task GetTrack(AVAsset asset) { if (!(OperatingSystem.IsMacCatalystVersionAtLeast(18) - || OperatingSystem.IsIOSVersionAtLeast(18))) + || OperatingSystem.IsIOSVersionAtLeast(18))) { // AVAsset.TracksWithMediaType is Obsolete on iOS 18+ and MacCatalyst 18+ return asset.TracksWithMediaType(AVMediaTypes.Video.GetConstant() ?? "0").FirstOrDefault(); @@ -637,7 +637,7 @@ void StatusChanged(NSObservedChange obj) void TimeControlStatusChanged(NSObservedChange obj) { if (Player is null || Player.Status is AVPlayerStatus.Unknown - || Player.CurrentItem?.Error is not null) + || Player.CurrentItem?.Error is not null) { return; } @@ -672,7 +672,7 @@ void ErrorOccurred(object? sender, NSNotificationEventArgs args) { // Non-fatal error, just log message = args.Notification?.ToString() ?? - "Media playback failed for an unknown reason."; + "Media playback failed for an unknown reason."; Logger?.LogWarning("{LogMessage}", message); } From a249817cb2d3d156a49fa869196c48528ccf938b Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Tue, 18 Nov 2025 11:36:50 -0800 Subject: [PATCH 11/16] Update CameraManager.windows.cs --- src/CommunityToolkit.Maui.Camera/CameraManager.windows.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/CommunityToolkit.Maui.Camera/CameraManager.windows.cs b/src/CommunityToolkit.Maui.Camera/CameraManager.windows.cs index 15b889f550..f1932e1556 100644 --- a/src/CommunityToolkit.Maui.Camera/CameraManager.windows.cs +++ b/src/CommunityToolkit.Maui.Camera/CameraManager.windows.cs @@ -157,14 +157,14 @@ private partial void PlatformStopCameraPreview() protected async Task PlatformUpdateResolution(Size resolution, CancellationToken token) { - if (cameraView.SelectedCamera is null) + if (!IsInitialized || mediaCapture is null) { - throw new CameraException($"Unable to update Capture Resolution because {nameof(ICameraView)}.{nameof(ICameraView.SelectedCamera)} is null."); + return; } - if (!IsInitialized || mediaCapture is null) + if (cameraView.SelectedCamera is null) { - return; + throw new CameraException($"Unable to update Capture Resolution because {nameof(ICameraView)}.{nameof(ICameraView.SelectedCamera)} is null."); } var filteredPropertiesList = cameraView.SelectedCamera.ImageEncodingProperties.Where(p => p.Width <= resolution.Width && p.Height <= resolution.Height).ToList(); From cdada88d20a7c6b9ee9fd70b1aae37ca0f2d76a1 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Tue, 18 Nov 2025 11:37:54 -0800 Subject: [PATCH 12/16] Update CameraManager.windows.cs --- .../CameraManager.windows.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/CommunityToolkit.Maui.Camera/CameraManager.windows.cs b/src/CommunityToolkit.Maui.Camera/CameraManager.windows.cs index f1932e1556..b4d692093a 100644 --- a/src/CommunityToolkit.Maui.Camera/CameraManager.windows.cs +++ b/src/CommunityToolkit.Maui.Camera/CameraManager.windows.cs @@ -155,7 +155,7 @@ private partial void PlatformStopCameraPreview() IsInitialized = false; } - protected async Task PlatformUpdateResolution(Size resolution, CancellationToken token) + async Task PlatformUpdateResolution(Size resolution, CancellationToken token) { if (!IsInitialized || mediaCapture is null) { @@ -192,9 +192,10 @@ private async partial Task PlatformStartVideoRecording(Stream stream, Cancellati var profile = MediaEncodingProfile.CreateMp4(VideoEncodingQuality.Auto); mediaRecording = await mediaCapture.PrepareLowLagRecordToStreamAsync(profile, stream.AsRandomAccessStream()); - frameSource = mediaCapture.FrameSources.FirstOrDefault(source => - source.Value.Info.MediaStreamType == MediaStreamType.VideoRecord && - source.Value.Info.SourceKind == MediaFrameSourceKind.Color).Value; + frameSource = mediaCapture.FrameSources + .FirstOrDefault(static source => source.Value.Info.MediaStreamType is MediaStreamType.VideoRecord && source.Value.Info.SourceKind is MediaFrameSourceKind.Color) + .Value; + if (frameSource is not null) { var frameFormat = frameSource.SupportedFormats From e7fbc73df6b3ef520df2abb96fd0b0224b9941ea Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Tue, 18 Nov 2025 11:41:02 -0800 Subject: [PATCH 13/16] Implement `IDisposable` --- .../Mocks/MockCameraProvider.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Maui.UnitTests/Mocks/MockCameraProvider.cs b/src/CommunityToolkit.Maui.UnitTests/Mocks/MockCameraProvider.cs index 0d635c2690..176283847c 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Mocks/MockCameraProvider.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Mocks/MockCameraProvider.cs @@ -1,9 +1,8 @@ using CommunityToolkit.Maui.Core; -using CommunityToolkit.Maui.Core.Primitives; namespace CommunityToolkit.Maui.UnitTests.Mocks; -public class MockCameraProvider : ICameraProvider +public sealed class MockCameraProvider : ICameraProvider, IDisposable { public event EventHandler?>? AvailableCamerasChanged; @@ -20,6 +19,11 @@ private set } } + public void Dispose() + { + + } + public Task RefreshAvailableCameras(CancellationToken token) { AvailableCameras = From dd0b9da4c0de640a81657009693c3712c3111783 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Tue, 18 Nov 2025 11:41:28 -0800 Subject: [PATCH 14/16] `dotnet format` --- .../Views/AvatarView/AvatarViewImageTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Maui.UnitTests/Views/AvatarView/AvatarViewImageTests.cs b/src/CommunityToolkit.Maui.UnitTests/Views/AvatarView/AvatarViewImageTests.cs index 44123ee502..e74b7754ee 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Views/AvatarView/AvatarViewImageTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Views/AvatarView/AvatarViewImageTests.cs @@ -139,7 +139,7 @@ public void ImageSourceParentSize_WhenStrokeShapeNotSet() } }; avatarView.Arrange(new Rect(0, 0, 73, 73)); - + avatarView.ImageSource.Should().NotBeNull(); avatarView.Content.Should().BeOfType(); if (avatarView.Content is not Image avatarImage) @@ -176,7 +176,7 @@ public void ImageSourceParentSize_WhenStrokeShapeSet() }; avatarView.ImageSource.Should().NotBeNull(); avatarView.Content.Should().BeOfType(); - + avatarView.Arrange(new Rect(0, 0, 73, 73)); if (avatarView.Content is not Image avatarImage) From 9ede2eceab9e8ce6fb569b0f3aa40d121e48acc5 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Tue, 18 Nov 2025 12:06:10 -0800 Subject: [PATCH 15/16] `dotnet format` --- samples/CommunityToolkit.Maui.Sample/Pages/Base/BasePage.cs | 1 + .../ViewModels/Converters/ConvertersGalleryViewModel.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Base/BasePage.cs b/samples/CommunityToolkit.Maui.Sample/Pages/Base/BasePage.cs index 50801ecc53..1fa93b7ae2 100644 --- a/samples/CommunityToolkit.Maui.Sample/Pages/Base/BasePage.cs +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Base/BasePage.cs @@ -13,6 +13,7 @@ public abstract class BasePage(TViewModel viewModel, bool shouldUseS public abstract class BasePage : ContentPage { + [Obsolete] protected BasePage(object? viewModel = null, bool shouldUseSafeArea = true) { BindingContext = viewModel; diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Converters/ConvertersGalleryViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Converters/ConvertersGalleryViewModel.cs index febf791102..0cc3201bcf 100644 --- a/samples/CommunityToolkit.Maui.Sample/ViewModels/Converters/ConvertersGalleryViewModel.cs +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Converters/ConvertersGalleryViewModel.cs @@ -3,6 +3,7 @@ namespace CommunityToolkit.Maui.Sample.ViewModels.Converters; +[Obsolete] public partial class ConvertersGalleryViewModel() : BaseGalleryViewModel( [ SectionModel.Create(nameof(BoolToObjectConverter), "A converter that allows users to convert a bool value binding to a specific object."), From 0ff0a41ebab76fc6d4b316e46a8471acb10bfc87 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Tue, 18 Nov 2025 12:06:16 -0800 Subject: [PATCH 16/16] Revert "`dotnet format`" This reverts commit 9ede2eceab9e8ce6fb569b0f3aa40d121e48acc5. --- samples/CommunityToolkit.Maui.Sample/Pages/Base/BasePage.cs | 1 - .../ViewModels/Converters/ConvertersGalleryViewModel.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Base/BasePage.cs b/samples/CommunityToolkit.Maui.Sample/Pages/Base/BasePage.cs index 1fa93b7ae2..50801ecc53 100644 --- a/samples/CommunityToolkit.Maui.Sample/Pages/Base/BasePage.cs +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Base/BasePage.cs @@ -13,7 +13,6 @@ public abstract class BasePage(TViewModel viewModel, bool shouldUseS public abstract class BasePage : ContentPage { - [Obsolete] protected BasePage(object? viewModel = null, bool shouldUseSafeArea = true) { BindingContext = viewModel; diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Converters/ConvertersGalleryViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Converters/ConvertersGalleryViewModel.cs index 0cc3201bcf..febf791102 100644 --- a/samples/CommunityToolkit.Maui.Sample/ViewModels/Converters/ConvertersGalleryViewModel.cs +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Converters/ConvertersGalleryViewModel.cs @@ -3,7 +3,6 @@ namespace CommunityToolkit.Maui.Sample.ViewModels.Converters; -[Obsolete] public partial class ConvertersGalleryViewModel() : BaseGalleryViewModel( [ SectionModel.Create(nameof(BoolToObjectConverter), "A converter that allows users to convert a bool value binding to a specific object."),