diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml b/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml index d90bcd224f..60d4c620b4 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 IFileSaver fileSaver; readonly string imagePath; + bool isInitialized = false; int pageCount; Stream videoRecordingStream = Stream.Null; @@ -21,27 +21,27 @@ public CameraViewPage(CameraViewViewModel viewModel, IFileSystem fileSystem, IFi 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(); } @@ -60,12 +60,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) @@ -78,7 +72,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 a7e5b10138..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; } @@ -164,6 +163,7 @@ protected virtual void Dispose(bool disposing) previewView?.Dispose(); previewView = null; + processCameraProvider?.UnbindAll(); processCameraProvider?.Dispose(); processCameraProvider = null; @@ -196,16 +196,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(); @@ -258,15 +248,7 @@ protected virtual async partial Task PlatformStartCameraPreview(CancellationToke return; } - 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"); camera = await RebindCamera(processCameraProvider, cameraView.SelectedCamera, token, cameraPreview, imageCapture, videoCapture); cameraControl = camera.CameraControl; @@ -306,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()) { @@ -352,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; } @@ -373,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() diff --git a/src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs b/src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs index a3d91ce63e..badee14ed5 100644 --- a/src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs +++ b/src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs @@ -84,24 +84,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 => @@ -123,20 +117,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); } @@ -155,11 +140,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 fd16ef81a8..1da8d2db17 100644 --- a/src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs @@ -51,7 +51,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 ddef2fc467..05567eee82 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..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 1bf348cb37..7c2cfd127b 100644 --- a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs @@ -6,6 +6,7 @@ partial class CameraProvider : ICameraProvider { readonly WeakEventManager availableCamerasChangedEventManager = new(); + Task? refreshAvailableCamerasTask; public event EventHandler?> AvailableCamerasChanged { @@ -27,8 +28,18 @@ private set } } + internal 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..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 c8064b0b49..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 { - 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); @@ -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 03ab095ab0..10df8a94c8 100644 --- a/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs @@ -238,14 +238,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"); } #if ANDROID