Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
dd28128
Fix CameraView crash when switching camera on Windows
zhitaop Apr 17, 2025
b345a7f
Merge branch 'main' into fix/camera-provider-refresh
ne0rrmatrix May 5, 2025
bc41901
Fix torch still in use after image capture on Android
zhitaop May 26, 2025
2dc4463
Merge branch 'main' into fix/camera-provider-refresh
zhitaop Jun 26, 2025
4476ad6
Move Permission and SelectedCamera to ConnectCamera
zhitaop Jun 26, 2025
35d859f
Refactor CameraProvider initialization and refresh logic, improve thr…
zhitaop Jun 27, 2025
e881807
Merge branch 'main' into fix/camera-provider-refresh
TheCodeTraveler Jun 27, 2025
79da15b
Rename refreshLock and refreshTask
zhitaop Jul 16, 2025
dbd3221
Merge branch 'main' into fix/camera-provider-refresh
zhitaop Jul 16, 2025
04d09d2
Simplify camera refresh task
zhitaop Jul 16, 2025
b618521
Merge branch 'main' into fix/camera-provider-refresh
zhitaop Aug 20, 2025
ccf2e83
Merge branch 'main' into fix/camera-provider-refresh
bijington Aug 28, 2025
4a15f4a
Merge branch 'main' into fix/camera-provider-refresh
TheCodeTraveler Aug 28, 2025
05ad3f5
Use `ObservableCollection`
TheCodeTraveler Aug 28, 2025
c3ecbf1
Update `ICameraProvider`, Use `SemaphoreSlim` in `CameraProvider` to …
TheCodeTraveler Aug 28, 2025
61bd8ac
Change method access modifier to private
TheCodeTraveler Aug 28, 2025
de245ce
Update CameraProvider.tizen.cs
TheCodeTraveler Aug 28, 2025
c452a81
Merge branch 'fix/camera-provider-refresh' of https://github.com/zhit…
TheCodeTraveler Aug 28, 2025
8953ce9
Refactor CameraViewPage and ViewModel: Remove unused event handlers a…
zhitaop Sep 1, 2025
c0d810b
Refactor CameraProvider: Improve camera refresh logic and ensure prop…
zhitaop Sep 1, 2025
b0fa9a8
Merge branch 'main' into fix/camera-provider-refresh
ne0rrmatrix Oct 2, 2025
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 All @@ -36,13 +35,13 @@
</VerticalStackLayout>
</Grid>

<ContentView Grid.Column="1" Grid.Row="0" ZIndex="100" BackgroundColor="#80CCCCCC">
<Grid Grid.Column="1" Grid.Row="0" ZIndex="100" BackgroundColor="#80CCCCCC">
<Image x:Name="image" VerticalOptions="Fill" HorizontalOptions="Fill">
<Image.GestureRecognizers>
<TapGestureRecognizer Tapped="OnImageTapped" />
</Image.GestureRecognizers>
</Image>
</ContentView>
</Grid>

<Grid ColumnDefinitions="Auto,*,Auto" Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="2" ColumnSpacing="10" Margin="10"
BackgroundColor="#00000000">
Expand All @@ -61,6 +60,12 @@

<FlexLayout Margin="5" JustifyContent="SpaceBetween" Wrap="Wrap">

<Picker
Title="Cameras"
ItemsSource="{Binding Cameras}"
ItemDisplayBinding="{Binding Name}"
SelectedItem="{Binding SelectedCamera}" />

<Picker
Title="Flash"
IsVisible="{Binding Path=SelectedCamera.IsFlashSupported, FallbackValue=false}"
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;

Expand All @@ -7,7 +6,6 @@ namespace CommunityToolkit.Maui.Sample.Pages.Views;
public partial class CameraViewPage : BasePage<CameraViewViewModel>
{
readonly string imagePath;
int pageCount;

public CameraViewPage(CameraViewViewModel viewModel, IFileSystem fileSystem) : base(viewModel)
{
Expand All @@ -16,30 +14,20 @@ 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();

var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(3));
await BindingContext.RefreshCamerasCommand.ExecuteAsync(cancellationTokenSource.Token);
await BindingContext.InitializeAsync();
}

// https://github.com/dotnet/maui/issues/16697
// https://github.com/dotnet/maui/issues/15833
protected override void OnNavigatedFrom(NavigatedFromEventArgs args)
{
base.OnNavigatedFrom(args);

Debug.WriteLine($"< < OnNavigatedFrom {pageCount} {Navigation.NavigationStack.Count}");

if (Navigation.NavigationStack.Count < pageCount)
if (!Shell.Current.Navigation.NavigationStack.Contains(this))
{
Cleanup();
}
Expand All @@ -57,12 +45,6 @@ async void OnImageTapped(object? sender, TappedEventArgs args)
void Cleanup()
{
Camera.MediaCaptured -= OnMediaCaptured;
Camera.Handler?.DisconnectHandler();
}

void OnUnloaded(object? sender, EventArgs e)
{
//Cleanup();
}

void OnMediaCaptured(object? sender, MediaCapturedEventArgs e)
Expand All @@ -75,7 +57,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
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using CommunityToolkit.Maui.Core;
using CommunityToolkit.Maui.Core.Primitives;
using System.Collections.ObjectModel;
using CommunityToolkit.Maui.Core;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

Expand All @@ -9,10 +9,12 @@ public partial class CameraViewViewModel(ICameraProvider cameraProvider) : BaseV
{
readonly ICameraProvider cameraProvider = cameraProvider;

public IReadOnlyList<CameraInfo> Cameras => cameraProvider.AvailableCameras ?? [];
bool isInitialized = false;

public CancellationToken Token => CancellationToken.None;

public ObservableCollection<CameraInfo> Cameras { get; } = [];

public ICollection<CameraFlashMode> FlashModes { get; } = Enum.GetValues<CameraFlashMode>();

[ObservableProperty]
Expand Down Expand Up @@ -42,6 +44,22 @@ public partial class CameraViewViewModel(ICameraProvider cameraProvider) : BaseV
[ObservableProperty]
public partial string ResolutionText { get; set; } = string.Empty;

public async ValueTask InitializeAsync()
{
if (isInitialized)
{
return;
}

await cameraProvider.InitializeAsync(CancellationToken.None);
foreach (var camera in cameraProvider.AvailableCameras ?? [])
{
Cameras.Add(camera);
}

isInitialized = true;
}

[RelayCommand]
async Task RefreshCameras(CancellationToken token) => await cameraProvider.RefreshAvailableCameras(token);

Expand All @@ -60,6 +78,22 @@ partial void OnSelectedResolutionChanged(Size value)
UpdateResolutionText();
}

partial void OnSelectedCameraChanged(CameraInfo? oldValue, CameraInfo? newValue)
{
UpdateCameraInfoText();
}

void UpdateCameraInfoText()
{
if (SelectedCamera is null)
{
return;
}
CameraNameText = $"{SelectedCamera.Name}";
ZoomRangeText = $"Min Zoom: {SelectedCamera.MinimumZoomFactor}, Max Zoom: {SelectedCamera.MaximumZoomFactor}";
UpdateFlashModeText();
}

void UpdateFlashModeText()
{
if (SelectedCamera is null)
Expand Down
26 changes: 5 additions & 21 deletions src/CommunityToolkit.Maui.Camera/CameraManager.android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ protected virtual void Dispose(bool disposing)
previewView?.Dispose();
previewView = null;

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

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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)}");

Expand All @@ -231,6 +214,7 @@ protected virtual async partial Task PlatformStartCameraPreview(CancellationToke

IsInitialized = true;
OnLoaded.Invoke();
return Task.CompletedTask;
}

protected virtual partial void PlatformStopCameraPreview()
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 @@ -77,24 +77,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 @@ -116,20 +110,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 @@ -148,11 +133,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 @@ -203,7 +184,7 @@
var photoOutputConnection = photoOutput.ConnectionFromMediaType(avMediaTypeVideo);
if (photoOutputConnection is not null)
{
photoOutputConnection.VideoOrientation = videoOrientation;

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

View workflow job for this annotation

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

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)

Check warning on line 187 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 Expand Up @@ -270,7 +251,7 @@
IEnumerable<UIScene> scenes = UIApplication.SharedApplication.ConnectedScenes;
var interfaceOrientation = scenes.FirstOrDefault() is UIWindowScene windowScene
? windowScene.InterfaceOrientation
: UIApplication.SharedApplication.StatusBarOrientation;

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

View workflow job for this annotation

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

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)

Check warning on line 254 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 @@ -346,7 +327,7 @@
{
if (PreviewLayer.Connection is not null)
{
PreviewLayer.Connection.VideoOrientation = videoOrientation;

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

View workflow job for this annotation

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

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)

Check warning on line 330 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
13 changes: 12 additions & 1 deletion src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,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 16 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 16 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 16 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 16 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 16 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 16 in src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs

View workflow job for this annotation

GitHub Actions / Run Benchmarks (windows-latest)

Parameter 'mauiContext' is unread.

Check warning on line 16 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 16 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 16 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 16 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 16 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.
ICameraView cameraView,
ICameraProvider cameraProvider,
Action onLoaded) : IDisposable
Expand All @@ -33,7 +33,18 @@
/// 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 (await ArePermissionsGranted() is false)
{
throw new PermissionException("Camera permissions not granted");
}

await cameraProvider.InitializeAsync(token);
cameraView.SelectedCamera ??= cameraProvider.AvailableCameras?.FirstOrDefault() ?? throw new CameraException("No camera available on device");

await PlatformConnectCamera(token);
}

/// <summary>
/// Disconnects from the camera.
Expand Down
32 changes: 9 additions & 23 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,22 @@ protected virtual partial void PlatformStopCameraPreview()

protected async Task PlatformUpdateResolution(Size resolution, CancellationToken token)
{
if (!IsInitialized || mediaCapture is null)
if (!IsInitialized || mediaCapture is null || cameraView.SelectedCamera is null)
{
return;
}

if (cameraView.SelectedCamera is null)
if (mediaCapture.VideoDeviceController.Id != cameraView.SelectedCamera.DeviceId)
{
await cameraProvider.RefreshAvailableCameras(token);
cameraView.SelectedCamera = cameraProvider.AvailableCameras?.FirstOrDefault() ?? throw new CameraException("No camera available on device");
return;
}

var filteredPropertiesList = cameraView.SelectedCamera.ImageEncodingProperties.Where(p => p.Width <= resolution.Width && p.Height <= resolution.Height).ToList();

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

if (filteredPropertiesList.Count is not 0)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,7 @@ void Init(ICameraView view)
protected override async void ConnectHandler(NativePlatformCameraPreviewView platformView)
{
base.ConnectHandler(platformView);

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

/// <inheritdoc/>
Expand Down
Loading
Loading