Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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">

<Grid RowDefinitions="200,*,Auto,Auto" ColumnDefinitions="3*,*">
<Grid RowDefinitions="Auto,*,Auto,Auto" ColumnDefinitions="3*,*">
<toolkit:CameraView
x:Name="Camera"
Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="2" Grid.RowSpan="3"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Diagnostics;
using CommunityToolkit.Maui.Core;
using CommunityToolkit.Maui.Sample.ViewModels.Views;
using CommunityToolkit.Maui.Storage;
Expand All @@ -9,6 +8,7 @@ public sealed partial class CameraViewPage : BasePage<CameraViewViewModel>
{
readonly IFileSaver fileSaver;
readonly string imagePath;
bool isInitialized = false;

int pageCount;
Stream videoRecordingStream = Stream.Null;
Expand All @@ -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();
}
Expand All @@ -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)
Expand All @@ -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
Expand Down
64 changes: 19 additions & 45 deletions src/CommunityToolkit.Maui.Camera/CameraManager.android.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Runtime.Versioning;
using Android.Content;
using Android.Provider;
using Android.Runtime;
using Android.Views;
using AndroidX.Camera.Core;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -164,6 +163,7 @@ protected virtual void Dispose(bool disposing)
previewView?.Dispose();
previewView = null;

processCameraProvider?.UnbindAll();
processCameraProvider?.Dispose();
processCameraProvider = null;

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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())
{
Expand All @@ -352,9 +326,9 @@ protected virtual async partial Task<Stream> 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;
}
Expand All @@ -373,8 +347,8 @@ protected virtual async partial Task<Stream> 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()
Expand Down
31 changes: 6 additions & 25 deletions src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,24 +84,18 @@
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 =>
Expand All @@ -123,20 +117,11 @@
}

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);
}

Expand All @@ -155,11 +140,7 @@
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 _);
Expand Down Expand Up @@ -441,8 +422,8 @@
{
IEnumerable<UIScene> scenes = UIApplication.SharedApplication.ConnectedScenes;
var interfaceOrientation = scenes.FirstOrDefault() is UIWindowScene windowScene
? windowScene.InterfaceOrientation

Check warning on line 425 in src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs

View workflow job for this annotation

GitHub Actions / Build Sample App using Latest .NET SDK (macos-15)

This call site is reachable on: 'MacCatalyst' 15.0 and later. 'UIWindowScene.InterfaceOrientation' is obsoleted on: 'maccatalyst' 26.0 and later (Use 'EffectiveGeometry.InterfaceOrientation' instead.). (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1422)
: UIApplication.SharedApplication.StatusBarOrientation;

Check warning on line 426 in src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs

View workflow job for this annotation

GitHub Actions / Build Sample App using Latest .NET SDK (macos-15)

This call site is reachable on: 'MacCatalyst' 15.0 and later. 'UIApplication.StatusBarOrientation' is obsoleted on: 'maccatalyst' 9.0 and later. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1422)

return interfaceOrientation switch
{
Expand Down Expand Up @@ -518,7 +499,7 @@
{
if (PreviewLayer.Connection is not null)
{
PreviewLayer.Connection.VideoOrientation = videoOrientation;

Check warning on line 502 in src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs

View workflow job for this annotation

GitHub Actions / Build Sample App using Latest .NET SDK (macos-15)

This call site is reachable on: 'MacCatalyst' 15.0 and later. 'AVCaptureConnection.VideoOrientation' is obsoleted on: 'maccatalyst' 17.0 and later (Use VideoRotationAngle instead.). (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1422)
}
}
}
Expand Down
10 changes: 9 additions & 1 deletion src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
/// <exception cref="NullReferenceException">Thrown when no <see cref="CameraProvider"/> can be resolved.</exception>
/// <exception cref="InvalidOperationException">Thrown when there are no cameras available.</exception>
partial class CameraManager(
IMauiContext mauiContext,

Check warning on line 18 in src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs

View workflow job for this annotation

GitHub Actions / Build Library (macos-15)

Parameter 'mauiContext' is unread.

Check warning on line 18 in src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs

View workflow job for this annotation

GitHub Actions / Build Library (macos-15)

Parameter 'mauiContext' is unread.

Check warning on line 18 in src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs

View workflow job for this annotation

GitHub Actions / Build Library (macos-15)

Parameter 'mauiContext' is unread.

Check warning on line 18 in src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs

View workflow job for this annotation

GitHub Actions / Build Sample App using Latest .NET SDK (macos-15)

Parameter 'mauiContext' is unread.

Check warning on line 18 in src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs

View workflow job for this annotation

GitHub Actions / Build Library (windows-latest)

Parameter 'mauiContext' is unread.

Check warning on line 18 in src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs

View workflow job for this annotation

GitHub Actions / Build Library (windows-latest)

Parameter 'mauiContext' is unread.

Check warning on line 18 in src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs

View workflow job for this annotation

GitHub Actions / Build Library (windows-latest)

Parameter 'mauiContext' is unread.

Check warning on line 18 in src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs

View workflow job for this annotation

GitHub Actions / Build Library (windows-latest)

Parameter 'mauiContext' is unread.

Check warning on line 18 in src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs

View workflow job for this annotation

GitHub Actions / Build Sample App using Latest .NET SDK (windows-latest)

Parameter 'mauiContext' is unread.

Check warning on line 18 in src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs

View workflow job for this annotation

GitHub Actions / Run Benchmarks (macos-15)

Parameter 'mauiContext' is unread.

Check warning on line 18 in src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs

View workflow job for this annotation

GitHub Actions / Run Benchmarks (windows-latest)

Parameter 'mauiContext' is unread.
ICameraView cameraView,
ICameraProvider cameraProvider,
Action onLoaded) : IDisposable
Expand Down Expand Up @@ -51,7 +51,15 @@
/// Connects to the camera.
/// </summary>
/// <returns>A <see cref="ValueTask"/> that can be awaited.</returns>
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");
Copy link

Copilot AI Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] This line combines null-conditional assignment, null-conditional access, null-coalescing, and exception throwing in a single statement, making it difficult to read and debug. Consider breaking this into multiple lines for better clarity.

Suggested change
cameraView.SelectedCamera ??= cameraProvider.AvailableCameras?.FirstOrDefault() ?? throw new CameraException("No camera available on device");
var availableCameras = cameraProvider.AvailableCameras;
if (availableCameras is null || !availableCameras.Any())
{
throw new CameraException("No camera available on device");
}
if (cameraView.SelectedCamera is null)
{
cameraView.SelectedCamera = availableCameras.First();
}

Copilot uses AI. Check for mistakes.

await PlatformConnectCamera(token);
}

/// <summary>
/// Disconnects from the camera.
Expand Down
33 changes: 7 additions & 26 deletions src/CommunityToolkit.Maui.Camera/CameraManager.windows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -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);

Expand Down Expand Up @@ -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)
Comment on lines +176 to 181
Copy link

Copilot AI Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The conditional pattern Count is 0 is inconsistent with the pattern used elsewhere in the codebase. Consider using Count == 0 or Count is not 0 consistently throughout the file for better readability.

Suggested change
if (filteredPropertiesList.Count is 0)
{
filteredPropertiesList = [.. cameraView.SelectedCamera.ImageEncodingProperties.OrderByDescending(p => p.Width * p.Height)];
}
if (filteredPropertiesList.Count is not 0)
if (filteredPropertiesList.Count == 0)
{
filteredPropertiesList = [.. cameraView.SelectedCamera.ImageEncodingProperties.OrderByDescending(p => p.Width * p.Height)];
}
if (filteredPropertiesList.Count != 0)

Copilot uses AI. Check for mistakes.

{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
}

/// <inheritdoc/>
public void Dispose()

Check warning on line 58 in src/CommunityToolkit.Maui.Camera/Handlers/CameraViewHandler.shared.cs

View workflow job for this annotation

GitHub Actions / Build Sample App using Latest .NET SDK (windows-latest)

'CameraViewHandler.Dispose()' hides inherited member 'ViewHandler<ICameraView, View>.Dispose()'. Use the new keyword if hiding was intended.
{
Dispose(true);
GC.SuppressFinalize(this);
Expand Down Expand Up @@ -90,7 +90,6 @@

await CameraManager.ArePermissionsGranted();
await CameraManager.ConnectCamera(CancellationToken.None);
await cameraProvider.RefreshAvailableCameras(CancellationToken.None);
}

/// <inheritdoc/>
Expand All @@ -104,7 +103,7 @@
/// Releases the unmanaged resources used by the <see cref="CameraViewHandler"/> and optionally releases the managed resources.
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing)

Check warning on line 106 in src/CommunityToolkit.Maui.Camera/Handlers/CameraViewHandler.shared.cs

View workflow job for this annotation

GitHub Actions / Build Sample App using Latest .NET SDK (windows-latest)

'CameraViewHandler.Dispose(bool)' hides inherited member 'ViewHandler<ICameraView, View>.Dispose(bool)'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword.
{
if (disposing)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<CameraInfo>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Loading
Loading