diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml b/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml index ea13274a1..504d352ef 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; - int pageCount; Stream videoRecordingStream = Stream.Null; public CameraViewPage(CameraViewViewModel viewModel, IFileSystem fileSystem, IFileSaver fileSaver) : base(viewModel) @@ -21,17 +19,15 @@ 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(); - + 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"); @@ -43,20 +39,14 @@ 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; } - - var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(3)); - await BindingContext.RefreshCamerasCommand.ExecuteAsync(cancellationTokenSource.Token); } - // 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(); } @@ -75,12 +65,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) @@ -93,7 +77,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 @@ -142,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 5f44560ac..fb8f25a2d 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(); diff --git a/src/CommunityToolkit.Maui.Camera/AppBuilderExtensions.shared.cs b/src/CommunityToolkit.Maui.Camera/AppBuilderExtensions.shared.cs index 3961e7fe5..6e09e1151 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 0a175c952..f3af41e44 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; @@ -63,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 @@ -138,55 +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?.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) @@ -200,16 +191,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(); @@ -218,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) { @@ -265,22 +246,14 @@ 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) { 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; @@ -293,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) { @@ -304,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); @@ -317,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 @@ -332,15 +305,7 @@ protected virtual async partial Task PlatformStartVideoRecording(Stream stream, 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()) { @@ -367,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 6c79dc4b2..8b105f9fe 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() @@ -84,24 +104,23 @@ 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 (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; + 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 formatsMatchingResolution = cameraView.SelectedCamera.SupportedFormats @@ -122,24 +141,15 @@ public async partial ValueTask UpdateCaptureResolution(Size resolution, Cancella } captureDevice.UnlockForConfiguration(); + return ValueTask.CompletedTask; } - protected virtual async partial Task PlatformConnectCamera(CancellationToken token) + private 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); } - protected virtual async partial Task PlatformStartCameraPreview(CancellationToken token) + private async partial Task PlatformStartCameraPreview(CancellationToken token) { if (captureSession is null) { @@ -154,11 +164,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 _); @@ -178,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) { @@ -193,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) @@ -280,7 +286,7 @@ 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 @@ -352,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); @@ -407,41 +413,23 @@ 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; - 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 { @@ -465,7 +453,7 @@ IEnumerable GetPhotoCompatibleFormats(IEnumerable photoPixelFormats.Contains((NSNumber)format.FormatDescription.MediaSubType)); - } + } return formats; } @@ -473,8 +461,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 4542a7fa8..f86754ed8 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, @@ -28,7 +26,15 @@ partial class CameraManager( /// 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. @@ -47,7 +53,6 @@ partial class CameraManager( /// A that can be awaited. public Task StartCameraPreview(CancellationToken token) => PlatformStartCameraPreview(token); - /// /// Starts the video recording. /// @@ -60,7 +65,6 @@ public Task StartVideoRecording(Stream stream, CancellationToken token) return PlatformStartVideoRecording(stream, token); } - /// /// Stops the video recording. /// @@ -122,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. @@ -156,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. @@ -165,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 ddef2fc46..b4d692093 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) @@ -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,44 +108,21 @@ protected virtual async partial ValueTask PlatformTakePicture(CancellationToken } } - protected virtual void Dispose(bool disposing) + private async partial Task PlatformConnectCamera(CancellationToken token) { - PlatformStopCameraPreview(); - if (disposing) - { - mediaCapture?.Dispose(); - } - } - - 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); } - protected virtual async partial Task PlatformStartCameraPreview(CancellationToken token) + private async partial Task PlatformStartCameraPreview(CancellationToken token) { if (mediaElement is null) { 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); @@ -164,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) { @@ -178,7 +155,7 @@ protected virtual 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) { @@ -187,15 +164,15 @@ protected async Task PlatformUpdateResolution(Size resolution, CancellationToken if (cameraView.SelectedCamera is null) { - await cameraProvider.RefreshAvailableCameras(token); - cameraView.SelectedCamera = cameraProvider.AvailableCameras?.FirstOrDefault() ?? throw new CameraException("No camera available on device"); + 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(); - 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) { @@ -203,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) { @@ -215,9 +192,10 @@ protected virtual async partial Task PlatformStartVideoRecording(Stream stream, 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 @@ -234,7 +212,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 1b53ce2c7..2a564c997 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 22d64c9bb..166c8c440 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 @@ -85,13 +85,15 @@ protected override async void ConnectHandler(NativePlatformCameraPreviewView pla base.ConnectHandler(platformView); await CameraManager.ConnectCamera(CancellationToken.None); - await cameraProvider.RefreshAvailableCameras(CancellationToken.None); } /// protected override void DisconnectHandler(NativePlatformCameraPreviewView platformView) { base.DisconnectHandler(platformView); + + CameraManager.Disconnect(); + Dispose(); } @@ -105,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 9e41e5130..de9fa31f4 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 6da7e2c02..e946945a6 100644 --- a/src/CommunityToolkit.Maui.Camera/Primitives/CameraViewDefaults.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Primitives/CameraViewDefaults.shared.cs @@ -6,11 +6,6 @@ namespace CommunityToolkit.Maui.Core; /// Default Values for -[SupportedOSPlatform("windows10.0.10240.0")] -[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 c5a45ca1f..53a2e969e 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 1a9363130..aa01adc5f 100644 --- a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.macios.cs +++ b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.macios.cs @@ -9,7 +9,39 @@ partial class CameraProvider { static readonly AVCaptureDeviceType[] captureDevices = InitializeCaptureDevices(); - public partial ValueTask RefreshAvailableCameras(CancellationToken token) + 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; + } + + private partial ValueTask PlatformRefreshAvailableCameras(CancellationToken token) { var discoverySession = AVCaptureDeviceDiscoverySession.Create(captureDevices, AVMediaTypes.Video, AVCaptureDevicePosition.Unspecified); var availableCameras = new List(); @@ -63,36 +95,4 @@ public partial ValueTask RefreshAvailableCameras(CancellationToken token) 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 d4c7daf35..57a58ae47 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 1bf348cb3..6a67f56d5 100644 --- a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.shared.cs @@ -3,9 +3,12 @@ /// /// 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 { @@ -13,6 +16,11 @@ public event EventHandler?> AvailableCamerasChanged remove => availableCamerasChangedEventManager.RemoveEventHandler(value); } + public void Dispose() + { + refreshAvailableCamerasSemaphore.Dispose(); + } + /// public IReadOnlyList? AvailableCameras { @@ -28,7 +36,24 @@ private set } /// - public partial ValueTask RefreshAvailableCameras(CancellationToken token); + public async Task RefreshAvailableCameras(CancellationToken token) + { + await refreshAvailableCamerasSemaphore.WaitAsync(token); + + try + { + if (refreshAvailableCamerasTask is null || refreshAvailableCamerasTask.IsCompleted) + { + refreshAvailableCamerasTask = PlatformRefreshAvailableCameras(token).AsTask(); + } + } + finally + { + refreshAvailableCamerasSemaphore.Release(); + } + + await refreshAvailableCamerasTask.WaitAsync(token); + } internal static bool AreCameraInfoListsEqual(in IReadOnlyList? cameraInfoList1, in IReadOnlyList? cameraInfoList2) { @@ -47,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 03140011e..5ae4c99da 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 c8064b0b4..5132c8ad4 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); @@ -67,5 +67,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 03ab095ab..abe92387e 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 = @@ -238,14 +234,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 diff --git a/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.macios.cs b/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.macios.cs index 472409d52..de7a4be6b 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); } diff --git a/src/CommunityToolkit.Maui.UnitTests/Mocks/MockCameraProvider.cs b/src/CommunityToolkit.Maui.UnitTests/Mocks/MockCameraProvider.cs index 593c4ce70..176283847 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,7 +19,12 @@ private set } } - public ValueTask RefreshAvailableCameras(CancellationToken token) + public void Dispose() + { + + } + + public Task RefreshAvailableCameras(CancellationToken token) { AvailableCameras = [ @@ -35,6 +39,6 @@ public ValueTask RefreshAvailableCameras(CancellationToken token) ]) ]; - return ValueTask.CompletedTask; + return Task.CompletedTask; } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.UnitTests/Views/AvatarView/AvatarViewImageTests.cs b/src/CommunityToolkit.Maui.UnitTests/Views/AvatarView/AvatarViewImageTests.cs index 44123ee50..e74b7754e 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)