Skip to content
Merged
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 @@ -10,7 +9,6 @@ public sealed partial class CameraViewPage : BasePage<CameraViewViewModel>
readonly IFileSaver fileSaver;
readonly string imagePath;

int pageCount;
Stream videoRecordingStream = Stream.Null;

public CameraViewPage(CameraViewViewModel viewModel, IFileSystem fileSystem, IFileSaver fileSaver) : base(viewModel)
Expand All @@ -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<Permissions.Camera>();
var microphonePermissionsRequest = await Permissions.RequestAsync<Permissions.Microphone>();

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");
Expand All @@ -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();
}
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -142,7 +126,7 @@ async void SaveVideo(object? sender, EventArgs? e)
var status = await Permissions.RequestAsync<Permissions.StorageWrite>();
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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
/// <summary>
Expand All @@ -23,7 +23,7 @@ public static class AppBuilderExtensions
public static MauiAppBuilder UseMauiCommunityToolkitCamera(this MauiAppBuilder builder)
{
builder.Services.AddSingleton<ICameraProvider, CameraProvider>();
builder.ConfigureMauiHandlers(h => h.AddHandler<CameraView, CameraViewHandler>());
builder.ConfigureMauiHandlers(static h => h.AddHandler<CameraView, CameraViewHandler>());

return builder;
}
Expand Down
139 changes: 52 additions & 87 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 @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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();
Expand All @@ -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)
{
Expand Down Expand Up @@ -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;
Expand All @@ -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)
{
Expand All @@ -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);
Expand All @@ -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
Expand All @@ -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())
{
Expand All @@ -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<Stream> PlatformStopVideoRecording(CancellationToken token)
private async partial Task<Stream> PlatformStopVideoRecording(CancellationToken token)
{
ArgumentNullException.ThrowIfNull(cameraExecutor);
if (videoRecording is null
Expand Down
Loading
Loading