Skip to content

Commit

Permalink
Fix bug CameraView on iOS: camera preview is not always rotated corre…
Browse files Browse the repository at this point in the history
…ct and photos are rotated (CommunityToolkit#2312)

* Fix bug CameraView on iOS: camera preview is not always rotated correct and photos are rotated

* Update src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs

Co-authored-by: Shaun Lawrence <[email protected]>

* Removed the null forgiving operator ! and added null checks. Removed TODO comment.

* Add exception message to the OnMediaCaptureFailed call

* Use `static` on iOS Devices

* Assign field `previewView`, Use pattern matching on `AVMediaTypes.Video.GetConstant()` , remove iOS 11 and iOS 13 checks since `CommunityToolkit.Maui.Camera` only supports iOS 15+

* Initialize `previewView` first

---------

Co-authored-by: Shaun Lawrence <[email protected]>
Co-authored-by: James Crutchley <[email protected]>
Co-authored-by: Brandon Minnick <[email protected]>
  • Loading branch information
4 people authored Dec 24, 2024
1 parent e97b4a6 commit 3c44236
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@

<!-- Fixes Static Registrar causing Linker error: https://github.com/xamarin/xamarin-macios/blob/main/docs/managed-static-registrar.md -->
<Target Name="SelectStaticRegistrar" AfterTargets="SelectRegistrar">
<PropertyGroup Condition="'$(Registrar)' == 'managed-static' and $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)'))=='maccatalyst'">
<PropertyGroup Condition="'$(Registrar)' == 'managed-static'">
<Registrar>static</Registrar>
</PropertyGroup>
</Target>
Expand Down
104 changes: 70 additions & 34 deletions src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System.Buffers;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Diagnostics;
using AVFoundation;
using CommunityToolkit.Maui.Core.Primitives;
using CommunityToolkit.Maui.Extensions;
Expand All @@ -22,19 +20,26 @@ partial class CameraManager

AVCaptureFlashMode flashMode;

IDisposable? orientationDidChangeObserver;
PreviewView? previewView;
AVCaptureVideoOrientation videoOrientation;

// IN the future change the return type to be an alias
public UIView CreatePlatformView()
{
captureSession = new AVCaptureSession
{
SessionPreset = AVCaptureSession.PresetPhoto
};

var previewView = new PreviewView
previewView = new PreviewView
{
Session = captureSession
};

orientationDidChangeObserver = UIDevice.Notifications.ObserveOrientationDidChange((_, _) => UpdateVideoOrientation());
UpdateVideoOrientation();

return previewView;
}

Expand Down Expand Up @@ -194,41 +199,51 @@ protected virtual async partial ValueTask PlatformTakePicture(CancellationToken
var capturePhotoSettings = AVCapturePhotoSettings.FromFormat(codecSettings);
capturePhotoSettings.FlashMode = photoOutput.SupportedFlashModes.Contains(flashMode) ? flashMode : photoOutput.SupportedFlashModes.First();

if (AVMediaTypes.Video.GetConstant() is NSString avMediaTypeVideo)
{
var photoOutputConnection = photoOutput.ConnectionFromMediaType(avMediaTypeVideo);
if (photoOutputConnection is not null)
{
photoOutputConnection.VideoOrientation = videoOrientation;
}
}

var wrapper = new AVCapturePhotoCaptureDelegateWrapper();

photoOutput.CapturePhoto(capturePhotoSettings, wrapper);

var result = await wrapper.Task.WaitAsync(token);
var data = result.Photo.FileDataRepresentation;

if (result.Error is not null)
{
cameraView.OnMediaCapturedFailed(result.Error.LocalizedFailureReason);
return;
}
var failureReason = result.Error.LocalizedDescription;
if (!string.IsNullOrEmpty(result.Error.LocalizedFailureReason))
{
failureReason = $"{failureReason} - {result.Error.LocalizedFailureReason}";
}

if (data is null)
{
cameraView.OnMediaCapturedFailed("Unable to retrieve the file data representation from the captured result.");
cameraView.OnMediaCapturedFailed(failureReason);
return;
}

var dataBytes = ArrayPool<byte>.Shared.Rent((int)data.Length);

Stream? imageData;
try
{
Marshal.Copy(data.Bytes, dataBytes, 0, (int)data.Length);

cameraView.OnMediaCaptured(new MemoryStream(dataBytes));
imageData = result.Photo.FileDataRepresentation?.AsStream();
}
catch (Exception e)
{
// possible exception: ObjCException NSInvalidArgumentException NSAllocateMemoryPages(...) failed in AVCapturePhoto.get_FileDataRepresentation()
cameraView.OnMediaCapturedFailed($"Unable to retrieve the file data representation from the captured result: {e.Message}");
return;
}
catch (Exception ex)

if (imageData is null)
{
cameraView.OnMediaCapturedFailed(ex.Message);
throw;
cameraView.OnMediaCapturedFailed("Unable to retrieve the file data representation from the captured result.");
}
finally
else
{
ArrayPool<byte>.Shared.Return(dataBytes);
cameraView.OnMediaCaptured(imageData);
}
}

Expand All @@ -243,11 +258,37 @@ protected virtual void Dispose(bool disposing)
captureInput?.Dispose();
captureInput = null;

orientationDidChangeObserver?.Dispose();
orientationDidChangeObserver = null;

photoOutput?.Dispose();
photoOutput = null;
}
}

static AVCaptureVideoOrientation GetVideoOrientation()
{
IEnumerable<UIScene> scenes = UIApplication.SharedApplication.ConnectedScenes;
var interfaceOrientation = scenes.FirstOrDefault() is UIWindowScene windowScene
? windowScene.InterfaceOrientation
: UIApplication.SharedApplication.StatusBarOrientation;

return interfaceOrientation switch
{
UIInterfaceOrientation.Portrait => AVCaptureVideoOrientation.Portrait,
UIInterfaceOrientation.PortraitUpsideDown => AVCaptureVideoOrientation.PortraitUpsideDown,
UIInterfaceOrientation.LandscapeRight => AVCaptureVideoOrientation.LandscapeRight,
UIInterfaceOrientation.LandscapeLeft => AVCaptureVideoOrientation.LandscapeLeft,
_ => AVCaptureVideoOrientation.Portrait
};
}

void UpdateVideoOrientation()
{
videoOrientation = GetVideoOrientation();
previewView?.UpdatePreviewVideoOrientation(videoOrientation);
}

sealed class AVCapturePhotoCaptureDelegateWrapper : AVCapturePhotoCaptureDelegate
{
readonly TaskCompletionSource<CapturePhotoResult> taskCompletionSource = new();
Expand Down Expand Up @@ -299,20 +340,15 @@ public AVCaptureSession? Session
public override void LayoutSubviews()
{
base.LayoutSubviews();
UpdatePreviewVideoOrientation(GetVideoOrientation());
}

if (PreviewLayer.Connection is null)
public void UpdatePreviewVideoOrientation(AVCaptureVideoOrientation videoOrientation)
{
if (PreviewLayer.Connection is not null)
{
return;
PreviewLayer.Connection.VideoOrientation = videoOrientation;
}

PreviewLayer.Connection.VideoOrientation = UIDevice.CurrentDevice.Orientation switch
{
UIDeviceOrientation.Portrait => AVCaptureVideoOrientation.Portrait,
UIDeviceOrientation.PortraitUpsideDown => AVCaptureVideoOrientation.PortraitUpsideDown,
UIDeviceOrientation.LandscapeLeft => AVCaptureVideoOrientation.LandscapeRight,
UIDeviceOrientation.LandscapeRight => AVCaptureVideoOrientation.LandscapeLeft,
_ => PreviewLayer.Connection.VideoOrientation
};
}
}
}

0 comments on commit 3c44236

Please sign in to comment.