Skip to content

Commit ac8b610

Browse files
authored
Improve CameraView Performance (#2909)
1 parent 03e9a06 commit ac8b610

File tree

23 files changed

+263
-329
lines changed

23 files changed

+263
-329
lines changed

samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@
55
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
66
xmlns:viewModels="clr-namespace:CommunityToolkit.Maui.Sample.ViewModels.Views"
77
Title="CameraView"
8-
Unloaded="OnUnloaded"
98
x:Class="CommunityToolkit.Maui.Sample.Pages.Views.CameraViewPage"
109
x:TypeArguments="viewModels:CameraViewViewModel"
1110
x:DataType="viewModels:CameraViewViewModel">
1211

13-
<Grid RowDefinitions="200,*,Auto,Auto" ColumnDefinitions="3*,*">
12+
<Grid RowDefinitions="Auto,*,Auto,Auto" ColumnDefinitions="3*,*">
1413
<toolkit:CameraView
1514
x:Name="Camera"
1615
Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="2" Grid.RowSpan="3"

samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml.cs

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System.Diagnostics;
21
using CommunityToolkit.Maui.Core;
32
using CommunityToolkit.Maui.Sample.ViewModels.Views;
43
using CommunityToolkit.Maui.Storage;
@@ -10,7 +9,6 @@ public sealed partial class CameraViewPage : BasePage<CameraViewViewModel>
109
readonly IFileSaver fileSaver;
1110
readonly string imagePath;
1211

13-
int pageCount;
1412
Stream videoRecordingStream = Stream.Null;
1513

1614
public CameraViewPage(CameraViewViewModel viewModel, IFileSystem fileSystem, IFileSaver fileSaver) : base(viewModel)
@@ -21,17 +19,15 @@ public CameraViewPage(CameraViewViewModel viewModel, IFileSystem fileSystem, IFi
2119
imagePath = Path.Combine(fileSystem.CacheDirectory, "camera-view-image.jpg");
2220

2321
Camera.MediaCaptured += OnMediaCaptured;
24-
25-
Loaded += (s, e) => { pageCount = Navigation.NavigationStack.Count; };
2622
}
2723

2824
protected override async void OnAppearing()
2925
{
3026
base.OnAppearing();
31-
27+
3228
var cameraPermissionsRequest = await Permissions.RequestAsync<Permissions.Camera>();
3329
var microphonePermissionsRequest = await Permissions.RequestAsync<Permissions.Microphone>();
34-
30+
3531
if (cameraPermissionsRequest is not PermissionStatus.Granted)
3632
{
3733
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()
4339
await Shell.Current.CurrentPage.DisplayAlertAsync("Microphone permission is not granted.", "Please grant the permission to use this feature.", "OK");
4440
return;
4541
}
46-
47-
var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(3));
48-
await BindingContext.RefreshCamerasCommand.ExecuteAsync(cancellationTokenSource.Token);
4942
}
5043

51-
// https://github.com/dotnet/maui/issues/16697
5244
// https://github.com/dotnet/maui/issues/15833
5345
protected override void OnNavigatedFrom(NavigatedFromEventArgs args)
5446
{
5547
base.OnNavigatedFrom(args);
5648

57-
Debug.WriteLine($"< < OnNavigatedFrom {pageCount} {Navigation.NavigationStack.Count}");
58-
59-
if (Navigation.NavigationStack.Count < pageCount)
49+
if (!Shell.Current.Navigation.NavigationStack.Contains(this))
6050
{
6151
Cleanup();
6252
}
@@ -75,12 +65,6 @@ async void OnImageTapped(object? sender, TappedEventArgs args)
7565
void Cleanup()
7666
{
7767
Camera.MediaCaptured -= OnMediaCaptured;
78-
Camera.Handler?.DisconnectHandler();
79-
}
80-
81-
void OnUnloaded(object? sender, EventArgs? e)
82-
{
83-
//Cleanup();
8468
}
8569

8670
void OnMediaCaptured(object? sender, MediaCapturedEventArgs e)
@@ -93,7 +77,7 @@ void OnMediaCaptured(object? sender, MediaCapturedEventArgs e)
9377
{
9478
// workaround for https://github.com/dotnet/maui/issues/13858
9579
#if ANDROID
96-
image.Source = ImageSource.FromStream(() => File.OpenRead(imagePath));
80+
image.Source = ImageSource.FromStream(() => File.OpenRead(imagePath));
9781
#else
9882
image.Source = ImageSource.FromFile(imagePath);
9983
#endif
@@ -142,7 +126,7 @@ async void SaveVideo(object? sender, EventArgs? e)
142126
var status = await Permissions.RequestAsync<Permissions.StorageWrite>();
143127
if (status is not PermissionStatus.Granted)
144128
{
145-
await Shell.Current.CurrentPage.DisplayAlert("Storage permission is not granted.", "Please grant the permission to use this feature.", "OK");
129+
await Shell.Current.CurrentPage.DisplayAlertAsync("Storage permission is not granted.", "Please grant the permission to use this feature.", "OK");
146130
return;
147131
}
148132

samples/CommunityToolkit.Maui.Sample/ViewModels/Views/CameraView/CameraViewViewModel.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,6 @@ public CameraViewViewModel(ICameraProvider cameraProvider)
4848
[ObservableProperty]
4949
public partial string ResolutionText { get; set; } = string.Empty;
5050

51-
[RelayCommand]
52-
async Task RefreshCameras(CancellationToken token) => await cameraProvider.RefreshAvailableCameras(token);
53-
5451
partial void OnFlashModeChanged(CameraFlashMode value)
5552
{
5653
UpdateFlashModeText();

src/CommunityToolkit.Maui.Camera/AppBuilderExtensions.shared.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace CommunityToolkit.Maui;
1212
[SupportedOSPlatform("android21.0")]
1313
[SupportedOSPlatform("ios15.0")]
1414
[SupportedOSPlatform("maccatalyst15.0")]
15-
[SupportedOSPlatform("tizen6.5")]
15+
[UnsupportedOSPlatform("tizen")]
1616
public static class AppBuilderExtensions
1717
{
1818
/// <summary>
@@ -23,7 +23,7 @@ public static class AppBuilderExtensions
2323
public static MauiAppBuilder UseMauiCommunityToolkitCamera(this MauiAppBuilder builder)
2424
{
2525
builder.Services.AddSingleton<ICameraProvider, CameraProvider>();
26-
builder.ConfigureMauiHandlers(h => h.AddHandler<CameraView, CameraViewHandler>());
26+
builder.ConfigureMauiHandlers(static h => h.AddHandler<CameraView, CameraViewHandler>());
2727

2828
return builder;
2929
}

src/CommunityToolkit.Maui.Camera/CameraManager.android.cs

Lines changed: 52 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System.Runtime.Versioning;
22
using Android.Content;
3-
using Android.Provider;
43
using Android.Runtime;
54
using Android.Views;
65
using AndroidX.Camera.Core;
@@ -63,8 +62,48 @@ public async Task SetExtensionMode(int mode, CancellationToken token)
6362

6463
public void Dispose()
6564
{
66-
Dispose(true);
67-
GC.SuppressFinalize(this);
65+
CleanupVideoRecordingResources();
66+
67+
camera?.Dispose();
68+
camera = null;
69+
70+
cameraControl?.Dispose();
71+
cameraControl = null;
72+
73+
cameraPreview?.Dispose();
74+
cameraPreview = null;
75+
76+
cameraExecutor?.Dispose();
77+
cameraExecutor = null;
78+
79+
imageCapture?.Dispose();
80+
imageCapture = null;
81+
82+
videoCapture?.Dispose();
83+
videoCapture = null;
84+
85+
imageCallback?.Dispose();
86+
imageCallback = null;
87+
88+
previewView?.Dispose();
89+
previewView = null;
90+
91+
processCameraProvider?.UnbindAll();
92+
processCameraProvider?.Dispose();
93+
processCameraProvider = null;
94+
95+
resolutionSelector?.Dispose();
96+
resolutionSelector = null;
97+
98+
resolutionFilter?.Dispose();
99+
resolutionFilter = null;
100+
101+
orientationListener?.Disable();
102+
orientationListener?.Dispose();
103+
orientationListener = null;
104+
105+
videoRecordingStream?.Dispose();
106+
videoRecordingStream = null;
68107
}
69108

70109
// IN the future change the return type to be an alias
@@ -138,55 +177,7 @@ public async partial ValueTask UpdateCaptureResolution(Size resolution, Cancella
138177
}
139178
}
140179

141-
protected virtual void Dispose(bool disposing)
142-
{
143-
if (disposing)
144-
{
145-
CleanupVideoRecordingResources();
146-
147-
camera?.Dispose();
148-
camera = null;
149-
150-
cameraControl?.Dispose();
151-
cameraControl = null;
152-
153-
cameraPreview?.Dispose();
154-
cameraPreview = null;
155-
156-
cameraExecutor?.Dispose();
157-
cameraExecutor = null;
158-
159-
imageCapture?.Dispose();
160-
imageCapture = null;
161-
162-
videoCapture?.Dispose();
163-
videoCapture = null;
164-
165-
imageCallback?.Dispose();
166-
imageCallback = null;
167-
168-
previewView?.Dispose();
169-
previewView = null;
170-
171-
processCameraProvider?.Dispose();
172-
processCameraProvider = null;
173-
174-
resolutionSelector?.Dispose();
175-
resolutionSelector = null;
176-
177-
resolutionFilter?.Dispose();
178-
resolutionFilter = null;
179-
180-
orientationListener?.Disable();
181-
orientationListener?.Dispose();
182-
orientationListener = null;
183-
184-
videoRecordingStream?.Dispose();
185-
videoRecordingStream = null;
186-
}
187-
}
188-
189-
protected virtual async partial Task PlatformConnectCamera(CancellationToken token)
180+
private async partial Task PlatformConnectCamera(CancellationToken token)
190181
{
191182
var cameraProviderFuture = ProcessCameraProvider.GetInstance(context);
192183
if (previewView is null)
@@ -200,16 +191,6 @@ protected virtual async partial Task PlatformConnectCamera(CancellationToken tok
200191
{
201192
processCameraProvider = (ProcessCameraProvider)(cameraProviderFuture.Get() ?? throw new CameraException($"Unable to retrieve {nameof(ProcessCameraProvider)}"));
202193

203-
if (cameraProvider.AvailableCameras is null)
204-
{
205-
await cameraProvider.RefreshAvailableCameras(token);
206-
207-
if (cameraProvider.AvailableCameras is null)
208-
{
209-
throw new CameraException("Unable to refresh available cameras");
210-
}
211-
}
212-
213194
await StartUseCase(token);
214195

215196
cameraProviderTCS.SetResult();
@@ -218,7 +199,7 @@ protected virtual async partial Task PlatformConnectCamera(CancellationToken tok
218199
await cameraProviderTCS.Task.WaitAsync(token);
219200
}
220201

221-
protected async Task StartUseCase(CancellationToken token)
202+
async Task StartUseCase(CancellationToken token)
222203
{
223204
if (resolutionSelector is null || cameraExecutor is null)
224205
{
@@ -265,22 +246,14 @@ protected async Task StartUseCase(CancellationToken token)
265246
await StartCameraPreview(token);
266247
}
267248

268-
protected virtual async partial Task PlatformStartCameraPreview(CancellationToken token)
249+
private async partial Task PlatformStartCameraPreview(CancellationToken token)
269250
{
270251
if (previewView is null || processCameraProvider is null || cameraPreview is null || imageCapture is null || videoCapture is null)
271252
{
272253
return;
273254
}
274255

275-
if (cameraView.SelectedCamera is null)
276-
{
277-
if (cameraProvider.AvailableCameras is null)
278-
{
279-
await cameraProvider.RefreshAvailableCameras(token);
280-
}
281-
282-
cameraView.SelectedCamera = cameraProvider.AvailableCameras?.FirstOrDefault() ?? throw new CameraException("No camera available on device");
283-
}
256+
cameraView.SelectedCamera ??= cameraProvider.AvailableCameras?.FirstOrDefault() ?? throw new CameraException("No camera available on device");
284257

285258
camera = await RebindCamera(processCameraProvider, cameraView.SelectedCamera, token, cameraPreview, imageCapture, videoCapture);
286259
cameraControl = camera.CameraControl;
@@ -293,7 +266,7 @@ protected virtual async partial Task PlatformStartCameraPreview(CancellationToke
293266
OnLoaded.Invoke();
294267
}
295268

296-
protected virtual partial void PlatformStopCameraPreview()
269+
private partial void PlatformStopCameraPreview()
297270
{
298271
if (processCameraProvider is null)
299272
{
@@ -304,11 +277,11 @@ protected virtual partial void PlatformStopCameraPreview()
304277
IsInitialized = false;
305278
}
306279

307-
protected virtual partial void PlatformDisconnect()
280+
private partial void PlatformDisconnect()
308281
{
309282
}
310283

311-
protected virtual partial ValueTask PlatformTakePicture(CancellationToken token)
284+
private partial ValueTask PlatformTakePicture(CancellationToken token)
312285
{
313286
ArgumentNullException.ThrowIfNull(cameraExecutor);
314287
ArgumentNullException.ThrowIfNull(imageCallback);
@@ -317,7 +290,7 @@ protected virtual partial ValueTask PlatformTakePicture(CancellationToken token)
317290
return ValueTask.CompletedTask;
318291
}
319292

320-
protected virtual async partial Task PlatformStartVideoRecording(Stream stream, CancellationToken token)
293+
private async partial Task PlatformStartVideoRecording(Stream stream, CancellationToken token)
321294
{
322295
if (previewView is null
323296
|| processCameraProvider is null
@@ -332,15 +305,7 @@ protected virtual async partial Task PlatformStartVideoRecording(Stream stream,
332305

333306
videoRecordingStream = stream;
334307

335-
if (cameraView.SelectedCamera is null)
336-
{
337-
if (cameraProvider.AvailableCameras is null)
338-
{
339-
await cameraProvider.RefreshAvailableCameras(token);
340-
}
341-
342-
cameraView.SelectedCamera = cameraProvider.AvailableCameras?.FirstOrDefault() ?? throw new CameraException("No camera available on device");
343-
}
308+
cameraView.SelectedCamera ??= cameraProvider.AvailableCameras?.FirstOrDefault() ?? throw new CameraException("No camera available on device");
344309

345310
if (camera is null || !IsVideoCaptureAlreadyBound())
346311
{
@@ -367,7 +332,7 @@ protected virtual async partial Task PlatformStartVideoRecording(Stream stream,
367332
// https://developer.android.com/reference/androidx/camera/video/Recorder#prepareRecording(android.content.Context,androidx.camera.video.MediaStoreOutputOptions)
368333
}
369334

370-
protected virtual async partial Task<Stream> PlatformStopVideoRecording(CancellationToken token)
335+
private async partial Task<Stream> PlatformStopVideoRecording(CancellationToken token)
371336
{
372337
ArgumentNullException.ThrowIfNull(cameraExecutor);
373338
if (videoRecording is null

0 commit comments

Comments
 (0)