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
8 changes: 4 additions & 4 deletions src/CommunityToolkit.Maui.Camera/CameraManager.android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ public async partial ValueTask UpdateCaptureResolution(Size resolution, Cancella
// According to the Android docs, `ResolutionSelector.Builder.setResolutionFilter(ResolutionFilter)` returns a `NonNull` object
// `ResolutionSelector.Builder.SetResolutionFilter(ResolutionFilter)` returning a nullable object in .NET for Android is likely a C# Binding mistake

if (IsInitialized)
if (isInitialized)
{
await StartUseCase(token);
}
Expand Down Expand Up @@ -262,8 +262,8 @@ private async partial Task PlatformStartCameraPreview(CancellationToken token)
var action = new FocusMeteringAction.Builder(point).Build();
camera.CameraControl?.StartFocusAndMetering(action);

IsInitialized = true;
OnLoaded.Invoke();
isInitialized = true;
onLoaded.Invoke();
}

private partial void PlatformStopCameraPreview()
Expand All @@ -274,7 +274,7 @@ private partial void PlatformStopCameraPreview()
}

processCameraProvider.UnbindAll();
IsInitialized = false;
isInitialized = false;
}

private partial void PlatformDisconnect()
Expand Down
158 changes: 117 additions & 41 deletions src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using AVFoundation;
using CommunityToolkit.Maui.Extensions;
using CoreMedia;
using CoreMotion;
using Foundation;
using ObjCRuntime;
using UIKit;
Expand All @@ -14,7 +16,7 @@
readonly NSDictionary<NSString, NSObject> codecSettings = new([AVVideo.CodecKey], [new NSString("jpeg")]);
AVCaptureDeviceInput? audioInput;
AVCaptureDevice? captureDevice;
AVCaptureInput? captureInput;
AVCaptureDeviceInput? captureInput;

AVCaptureSession? captureSession;

Expand All @@ -24,12 +26,13 @@
AVCapturePhotoOutput? photoOutput;
PreviewView? previewView;

AVCaptureDeviceInput? videoInput;
AVCaptureVideoOrientation videoOrientation;
AVCaptureMovieFileOutput? videoOutput;
AVCaptureDeviceRotationCoordinator? rotationCoordinator;
string? videoRecordingFileName;
TaskCompletionSource? videoRecordingFinalizeTcs;
Stream? videoRecordingStream;
CMMotionManager? motionManager;

/// <inheritdoc />
public void Dispose()
Expand All @@ -56,6 +59,13 @@

videoRecordingStream?.Dispose();
videoRecordingStream = null;

rotationCoordinator?.Dispose();
rotationCoordinator = null;

motionManager?.StopAccelerometerUpdates();
motionManager?.Dispose();
motionManager = null;
}

public NativePlatformCameraPreviewView CreatePlatformView()
Expand All @@ -70,6 +80,12 @@
Session = captureSession
};

// use CMMotionManager to get device orientation on iOS 16 or lower, since AVCaptureDeviceRotationCoordinator is unavailable
if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
{
motionManager ??= new();
motionManager.StartAccelerometerUpdates();
}
Comment thread
TheCodeTraveler marked this conversation as resolved.
orientationDidChangeObserver = UIDevice.Notifications.ObserveOrientationDidChange((_, _) => UpdateVideoOrientation());
Comment thread
zhitaop marked this conversation as resolved.
UpdateVideoOrientation();

Expand All @@ -83,7 +99,7 @@

public partial void UpdateZoom(float zoomLevel)
{
if (!IsInitialized || captureDevice is null)
if (!isInitialized || captureDevice is null)
{
return;
}
Expand Down Expand Up @@ -162,8 +178,30 @@
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 _);
captureSession.AddInput(captureInput);
captureInput = new AVCaptureDeviceInput(captureDevice, out NSError? error);

if (error is null && captureSession.CanAddInput(captureInput))
{
captureSession.AddInput(captureInput);
}
Comment thread
TheCodeTraveler marked this conversation as resolved.
else
{
var errorMessage = error is not null
? $"Error creating capture device input: {error.LocalizedDescription}"
: "Unable to add capture device input to capture session.";

captureInput.Dispose();
Comment thread
TheCodeTraveler marked this conversation as resolved.
captureInput = null;
captureSession.CommitConfiguration();
throw new CameraException(errorMessage);
}

// On iOS 17+, create a new instance of AVCaptureDeviceRotationCoordinator when switching to a new camera
if (UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
{
rotationCoordinator?.Dispose();
rotationCoordinator = new(captureDevice, previewView?.Layer);
}

if (photoOutput is null)
{
Expand All @@ -175,8 +213,8 @@

captureSession.CommitConfiguration();
captureSession.StartRunning();
IsInitialized = true;
OnLoaded.Invoke();
isInitialized = true;
onLoaded.Invoke();
}

private partial void PlatformStopCameraPreview()
Expand All @@ -191,7 +229,7 @@
captureSession.StopRunning();
}

IsInitialized = false;
isInitialized = false;
}

private partial void PlatformDisconnect()
Expand All @@ -213,22 +251,7 @@

CleanupVideoRecordingResources();

var videoDevice = AVCaptureDevice.GetDefaultDevice(AVMediaTypes.Video) ?? throw new CameraException("Unable to get video device");

videoInput = new AVCaptureDeviceInput(videoDevice, out NSError? error);
if (error is not null)
{
throw new CameraException($"Error creating video input: {error.LocalizedDescription}");
}

if (!captureSession.CanAddInput(videoInput))
{
videoInput?.Dispose();
throw new CameraException("Unable to add video input to capture session.");
}

captureSession.BeginConfiguration();
captureSession.AddInput(videoInput);

try
{
Expand Down Expand Up @@ -256,15 +279,13 @@

if (!captureSession.CanAddOutput(videoOutput))
{
captureSession.RemoveInput(videoInput);
if (audioInput is not null)
{
captureSession.RemoveInput(audioInput);
audioInput.Dispose();
audioInput = null;
}

videoInput?.Dispose();
videoOutput?.Dispose();
captureSession.CommitConfiguration();
throw new CameraException("Unable to add video output to capture session.");
Expand All @@ -273,6 +294,11 @@
captureSession.AddOutput(videoOutput);
captureSession.CommitConfiguration();

if (!TryConfigureAVCaptureConnection(videoOutput, out var error))
{
Trace.TraceWarning(error);
}

videoRecordingStream = stream;
videoRecordingFinalizeTcs = new TaskCompletionSource();
videoRecordingFileName = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.mov");
Expand All @@ -285,7 +311,6 @@
{
if (captureSession is null
|| videoRecordingFileName is null
|| videoInput is null
|| videoOutput is null
|| videoRecordingStream is null
|| videoRecordingFinalizeTcs is null)
Expand Down Expand Up @@ -318,25 +343,22 @@
{
captureSession.BeginConfiguration();

foreach (var input in captureSession.Inputs)
if (audioInput is not null)
{
captureSession.RemoveInput(input);
input.Dispose();
captureSession.RemoveInput(audioInput);
audioInput.Dispose();
}

foreach (var output in captureSession.Outputs)
if (videoOutput is not null)
{
captureSession.RemoveOutput(output);
output.Dispose();
captureSession.RemoveOutput(videoOutput);
videoOutput.Dispose();
}

// Restore to photo preset for preview after video recording
captureSession.SessionPreset = AVCaptureSession.PresetPhoto;
captureSession.CommitConfiguration();
}

videoOutput = null;
videoInput = null;
audioInput = null;

// Clean up temporary file
Expand All @@ -360,13 +382,9 @@
var capturePhotoSettings = AVCapturePhotoSettings.FromFormat(codecSettings);
capturePhotoSettings.FlashMode = photoOutput.SupportedFlashModes.Contains(flashMode) ? flashMode : photoOutput.SupportedFlashModes.First();

if (AVMediaTypes.Video.GetConstant() is NSString avMediaTypeVideo)
if (!TryConfigureAVCaptureConnection(photoOutput, out var errorMessage))
{
var photoOutputConnection = photoOutput.ConnectionFromMediaType(avMediaTypeVideo);
if (photoOutputConnection is not null)
{
photoOutputConnection.VideoOrientation = videoOrientation;
}
Trace.TraceWarning(errorMessage);
}

var wrapper = new AVCapturePhotoCaptureDelegateWrapper();
Expand Down Expand Up @@ -408,6 +426,20 @@
}
}

static AVCaptureVideoOrientation GetVideoOrientationFromAccelerometer(double x, double y)
{
// Absolute values help determine which axis is dominant
if (Math.Abs(y) >= Math.Abs(x))
{
return y > 0 ? AVCaptureVideoOrientation.PortraitUpsideDown : AVCaptureVideoOrientation.Portrait;
}
else
{
// x > 0 is LandscapeRight for device, which is LandscapeLeft for Video
return x > 0 ? AVCaptureVideoOrientation.LandscapeLeft : AVCaptureVideoOrientation.LandscapeRight;
}
}

static AVCaptureVideoOrientation GetVideoOrientation()
{
IEnumerable<UIScene> scenes = UIApplication.SharedApplication.ConnectedScenes;
Expand All @@ -417,13 +449,13 @@
{
interfaceOrientation = scenes.FirstOrDefault() is UIWindowScene windowScene
? windowScene.InterfaceOrientation
: UIApplication.SharedApplication.StatusBarOrientation;

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

View workflow job for this annotation

GitHub Actions / Build Library (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 452 in src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs

View workflow job for this annotation

GitHub Actions / Build Library (windows-latest)

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

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

View workflow job for this annotation

GitHub Actions / Build Library (macos-26)

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 452 in src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs

View workflow job for this annotation

GitHub Actions / Build Library (macos-26)

This call site is reachable on: 'iOS' 15.0 and later, 'maccatalyst' 15.0 and later. 'UIApplication.StatusBarOrientation' is obsoleted on: 'ios' 9.0 and later, 'maccatalyst' 9.0 and later. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1422)
}
else
{
interfaceOrientation = scenes.FirstOrDefault() is UIWindowScene windowScene
? windowScene.EffectiveGeometry.InterfaceOrientation
: UIApplication.SharedApplication.StatusBarOrientation;

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

View workflow job for this annotation

GitHub Actions / Build Library (windows-latest)

This call site is reachable on: 'MacCatalyst' 26.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 458 in src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs

View workflow job for this annotation

GitHub Actions / Build Library (windows-latest)

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

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

View workflow job for this annotation

GitHub Actions / Build Library (macos-26)

This call site is reachable on: 'MacCatalyst' 26.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 458 in src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs

View workflow job for this annotation

GitHub Actions / Build Library (macos-26)

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

return interfaceOrientation switch
Expand All @@ -436,6 +468,50 @@
};
}

bool TryConfigureAVCaptureConnection(in AVCaptureOutput captureOutput, [NotNullWhen(false)] out string? errorMessage)
{
errorMessage = null;

if (AVMediaTypes.Video.GetConstant() is not NSString avMediaTypeVideo)
{
errorMessage = "Unable to determine video format.";
return false;
}

if (captureOutput.ConnectionFromMediaType(avMediaTypeVideo) is not AVCaptureConnection captureConnection)
{
errorMessage = "Unable to determine video connection from media type.";
return false;
}

// use AVCaptureDeviceRotationCoordinator to set captured photo and video orientation on iOS 17+
if (UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
{
if (rotationCoordinator is not null)
{
captureConnection.VideoRotationAngle = rotationCoordinator.VideoRotationAngleForHorizonLevelCapture;
}
}
// use CMMotionManager to set captured photo and video orientation on iOS 16 and lower
else
{
var data = motionManager?.AccelerometerData;
if (data is not null)
{
var orientation = GetVideoOrientationFromAccelerometer(data.Acceleration.X, data.Acceleration.Y);
captureConnection.VideoOrientation = orientation;
}
}
Comment thread
TheCodeTraveler marked this conversation as resolved.

if (captureConnection.SupportsVideoMirroring)
{
captureConnection.AutomaticallyAdjustsVideoMirroring = false;
captureConnection.VideoMirrored = cameraView.SelectedCamera?.Position is CameraPosition.Front;
}

return true;
}

void UpdateVideoOrientation()
{
videoOrientation = GetVideoOrientation();
Expand Down Expand Up @@ -518,7 +594,7 @@
{
if (PreviewLayer.Connection is not null)
{
PreviewLayer.Connection.VideoOrientation = videoOrientation;

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

View workflow job for this annotation

GitHub Actions / Build Library (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 597 in src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs

View workflow job for this annotation

GitHub Actions / Build Library (windows-latest)

This call site is reachable on: 'iOS' 15.0 and later, 'maccatalyst' 15.0 and later. 'AVCaptureConnection.VideoOrientation' is obsoleted on: 'ios' 17.0 and later (Use VideoRotationAngle instead.), 'maccatalyst' 17.0 and later (Use VideoRotationAngle instead.). (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1422)

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

View workflow job for this annotation

GitHub Actions / Build Library (macos-26)

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 597 in src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs

View workflow job for this annotation

GitHub Actions / Build Library (macos-26)

This call site is reachable on: 'iOS' 15.0 and later, 'maccatalyst' 15.0 and later. 'AVCaptureConnection.VideoOrientation' is obsoleted on: 'ios' 17.0 and later (Use VideoRotationAngle instead.), 'maccatalyst' 17.0 and later (Use VideoRotationAngle instead.). (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1422)
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@
/// <exception cref="NullReferenceException">Thrown when no <see cref="CameraProvider"/> can be resolved.</exception>
/// <exception cref="InvalidOperationException">Thrown when there are no cameras available.</exception>
sealed 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

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 (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-26)

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-26)

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-26)

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-26)

Parameter 'mauiContext' is unread.
ICameraView cameraView,
ICameraProvider cameraProvider,
Action onLoaded) : IDisposable
{
internal Action OnLoaded { get; } = onLoaded;
readonly Action onLoaded = onLoaded;

internal bool IsInitialized { get; private set; }
bool isInitialized;

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

View workflow job for this annotation

GitHub Actions / Run Benchmarks

Field 'CameraManager.isInitialized' is never assigned to, and will always have its default value false

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

View workflow job for this annotation

GitHub Actions / Build Library (windows-latest)

Field 'CameraManager.isInitialized' is never assigned to, and will always have its default value false

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

View workflow job for this annotation

GitHub Actions / Build Library (windows-latest)

Field 'CameraManager.isInitialized' is never assigned to, and will always have its default value false

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

View workflow job for this annotation

GitHub Actions / Build Library (macos-26)

Field 'CameraManager.isInitialized' is never assigned to, and will always have its default value false

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

View workflow job for this annotation

GitHub Actions / Build Library (macos-26)

Field 'CameraManager.isInitialized' is never assigned to, and will always have its default value false

Comment thread
TheCodeTraveler marked this conversation as resolved.
/// <summary>
/// Connects to the camera.
Expand Down Expand Up @@ -94,7 +94,7 @@
return;
}

if (IsInitialized)
if (isInitialized)
{
PlatformStopCameraPreview();
await PlatformStartCameraPreview(token);
Expand Down
Loading
Loading