11using System . Diagnostics ;
2+ using System . Diagnostics . CodeAnalysis ;
23using AVFoundation ;
34using CommunityToolkit . Maui . Extensions ;
45using CoreMedia ;
6+ using CoreMotion ;
57using Foundation ;
68using ObjCRuntime ;
79using UIKit ;
@@ -14,7 +16,7 @@ partial class CameraManager
1416 readonly NSDictionary < NSString , NSObject > codecSettings = new ( [ AVVideo . CodecKey ] , [ new NSString ( "jpeg" ) ] ) ;
1517 AVCaptureDeviceInput ? audioInput ;
1618 AVCaptureDevice ? captureDevice ;
17- AVCaptureInput ? captureInput ;
19+ AVCaptureDeviceInput ? captureInput ;
1820
1921 AVCaptureSession ? captureSession ;
2022
@@ -24,12 +26,13 @@ partial class CameraManager
2426 AVCapturePhotoOutput ? photoOutput ;
2527 PreviewView ? previewView ;
2628
27- AVCaptureDeviceInput ? videoInput ;
2829 AVCaptureVideoOrientation videoOrientation ;
2930 AVCaptureMovieFileOutput ? videoOutput ;
31+ AVCaptureDeviceRotationCoordinator ? rotationCoordinator ;
3032 string ? videoRecordingFileName ;
3133 TaskCompletionSource ? videoRecordingFinalizeTcs ;
3234 Stream ? videoRecordingStream ;
35+ CMMotionManager ? motionManager ;
3336
3437 /// <inheritdoc />
3538 public void Dispose ( )
@@ -56,6 +59,13 @@ public void Dispose()
5659
5760 videoRecordingStream ? . Dispose ( ) ;
5861 videoRecordingStream = null ;
62+
63+ rotationCoordinator ? . Dispose ( ) ;
64+ rotationCoordinator = null ;
65+
66+ motionManager ? . StopAccelerometerUpdates ( ) ;
67+ motionManager ? . Dispose ( ) ;
68+ motionManager = null ;
5969 }
6070
6171 public NativePlatformCameraPreviewView CreatePlatformView ( )
@@ -70,6 +80,12 @@ public NativePlatformCameraPreviewView CreatePlatformView()
7080 Session = captureSession
7181 } ;
7282
83+ // use CMMotionManager to get device orientation on iOS 16 or lower, since AVCaptureDeviceRotationCoordinator is unavailable
84+ if ( ! UIDevice . CurrentDevice . CheckSystemVersion ( 17 , 0 ) )
85+ {
86+ motionManager ??= new ( ) ;
87+ motionManager . StartAccelerometerUpdates ( ) ;
88+ }
7389 orientationDidChangeObserver = UIDevice . Notifications . ObserveOrientationDidChange ( ( _ , _ ) => UpdateVideoOrientation ( ) ) ;
7490 UpdateVideoOrientation ( ) ;
7591
@@ -83,7 +99,7 @@ public partial void UpdateFlashMode(CameraFlashMode flashMode)
8399
84100 public partial void UpdateZoom ( float zoomLevel )
85101 {
86- if ( ! IsInitialized || captureDevice is null )
102+ if ( ! isInitialized || captureDevice is null )
87103 {
88104 return ;
89105 }
@@ -162,8 +178,30 @@ private async partial Task PlatformStartCameraPreview(CancellationToken token)
162178 cameraView . SelectedCamera ??= cameraProvider . AvailableCameras ? . FirstOrDefault ( ) ?? throw new CameraException ( "No camera available on device" ) ;
163179
164180 captureDevice = cameraView . SelectedCamera . CaptureDevice ?? throw new CameraException ( $ "No Camera found") ;
165- captureInput = new AVCaptureDeviceInput ( captureDevice , out _ ) ;
166- captureSession . AddInput ( captureInput ) ;
181+ captureInput = new AVCaptureDeviceInput ( captureDevice , out NSError ? error ) ;
182+
183+ if ( error is null && captureSession . CanAddInput ( captureInput ) )
184+ {
185+ captureSession . AddInput ( captureInput ) ;
186+ }
187+ else
188+ {
189+ var errorMessage = error is not null
190+ ? $ "Error creating capture device input: { error . LocalizedDescription } "
191+ : "Unable to add capture device input to capture session." ;
192+
193+ captureInput . Dispose ( ) ;
194+ captureInput = null ;
195+ captureSession . CommitConfiguration ( ) ;
196+ throw new CameraException ( errorMessage ) ;
197+ }
198+
199+ // On iOS 17+, create a new instance of AVCaptureDeviceRotationCoordinator when switching to a new camera
200+ if ( UIDevice . CurrentDevice . CheckSystemVersion ( 17 , 0 ) )
201+ {
202+ rotationCoordinator ? . Dispose ( ) ;
203+ rotationCoordinator = new ( captureDevice , previewView ? . Layer ) ;
204+ }
167205
168206 if ( photoOutput is null )
169207 {
@@ -175,8 +213,8 @@ private async partial Task PlatformStartCameraPreview(CancellationToken token)
175213
176214 captureSession . CommitConfiguration ( ) ;
177215 captureSession . StartRunning ( ) ;
178- IsInitialized = true ;
179- OnLoaded . Invoke ( ) ;
216+ isInitialized = true ;
217+ onLoaded . Invoke ( ) ;
180218 }
181219
182220 private partial void PlatformStopCameraPreview ( )
@@ -191,7 +229,7 @@ private partial void PlatformStopCameraPreview()
191229 captureSession . StopRunning ( ) ;
192230 }
193231
194- IsInitialized = false ;
232+ isInitialized = false ;
195233 }
196234
197235 private partial void PlatformDisconnect ( )
@@ -213,22 +251,7 @@ private async partial Task PlatformStartVideoRecording(Stream stream, Cancellati
213251
214252 CleanupVideoRecordingResources ( ) ;
215253
216- var videoDevice = AVCaptureDevice . GetDefaultDevice ( AVMediaTypes . Video ) ?? throw new CameraException ( "Unable to get video device" ) ;
217-
218- videoInput = new AVCaptureDeviceInput ( videoDevice , out NSError ? error ) ;
219- if ( error is not null )
220- {
221- throw new CameraException ( $ "Error creating video input: { error . LocalizedDescription } ") ;
222- }
223-
224- if ( ! captureSession . CanAddInput ( videoInput ) )
225- {
226- videoInput ? . Dispose ( ) ;
227- throw new CameraException ( "Unable to add video input to capture session." ) ;
228- }
229-
230254 captureSession . BeginConfiguration ( ) ;
231- captureSession . AddInput ( videoInput ) ;
232255
233256 try
234257 {
@@ -256,15 +279,13 @@ private async partial Task PlatformStartVideoRecording(Stream stream, Cancellati
256279
257280 if ( ! captureSession . CanAddOutput ( videoOutput ) )
258281 {
259- captureSession . RemoveInput ( videoInput ) ;
260282 if ( audioInput is not null )
261283 {
262284 captureSession . RemoveInput ( audioInput ) ;
263285 audioInput . Dispose ( ) ;
264286 audioInput = null ;
265287 }
266288
267- videoInput ? . Dispose ( ) ;
268289 videoOutput ? . Dispose ( ) ;
269290 captureSession . CommitConfiguration ( ) ;
270291 throw new CameraException ( "Unable to add video output to capture session." ) ;
@@ -273,6 +294,11 @@ private async partial Task PlatformStartVideoRecording(Stream stream, Cancellati
273294 captureSession . AddOutput ( videoOutput ) ;
274295 captureSession . CommitConfiguration ( ) ;
275296
297+ if ( ! TryConfigureAVCaptureConnection ( videoOutput , out var error ) )
298+ {
299+ Trace . TraceWarning ( error ) ;
300+ }
301+
276302 videoRecordingStream = stream ;
277303 videoRecordingFinalizeTcs = new TaskCompletionSource ( ) ;
278304 videoRecordingFileName = Path . Combine ( Path . GetTempPath ( ) , $ "{ Guid . NewGuid ( ) } .mov") ;
@@ -285,7 +311,6 @@ private async partial Task<Stream> PlatformStopVideoRecording(CancellationToken
285311 {
286312 if ( captureSession is null
287313 || videoRecordingFileName is null
288- || videoInput is null
289314 || videoOutput is null
290315 || videoRecordingStream is null
291316 || videoRecordingFinalizeTcs is null )
@@ -318,25 +343,22 @@ void CleanupVideoRecordingResources()
318343 {
319344 captureSession . BeginConfiguration ( ) ;
320345
321- foreach ( var input in captureSession . Inputs )
346+ if ( audioInput is not null )
322347 {
323- captureSession . RemoveInput ( input ) ;
324- input . Dispose ( ) ;
348+ captureSession . RemoveInput ( audioInput ) ;
349+ audioInput . Dispose ( ) ;
325350 }
326351
327- foreach ( var output in captureSession . Outputs )
352+ if ( videoOutput is not null )
328353 {
329- captureSession . RemoveOutput ( output ) ;
330- output . Dispose ( ) ;
354+ captureSession . RemoveOutput ( videoOutput ) ;
355+ videoOutput . Dispose ( ) ;
331356 }
332357
333- // Restore to photo preset for preview after video recording
334- captureSession . SessionPreset = AVCaptureSession . PresetPhoto ;
335358 captureSession . CommitConfiguration ( ) ;
336359 }
337360
338361 videoOutput = null ;
339- videoInput = null ;
340362 audioInput = null ;
341363
342364 // Clean up temporary file
@@ -360,13 +382,9 @@ private async partial ValueTask PlatformTakePicture(CancellationToken token)
360382 var capturePhotoSettings = AVCapturePhotoSettings . FromFormat ( codecSettings ) ;
361383 capturePhotoSettings . FlashMode = photoOutput . SupportedFlashModes . Contains ( flashMode ) ? flashMode : photoOutput . SupportedFlashModes . First ( ) ;
362384
363- if ( AVMediaTypes . Video . GetConstant ( ) is NSString avMediaTypeVideo )
385+ if ( ! TryConfigureAVCaptureConnection ( photoOutput , out var errorMessage ) )
364386 {
365- var photoOutputConnection = photoOutput . ConnectionFromMediaType ( avMediaTypeVideo ) ;
366- if ( photoOutputConnection is not null )
367- {
368- photoOutputConnection . VideoOrientation = videoOrientation ;
369- }
387+ Trace . TraceWarning ( errorMessage ) ;
370388 }
371389
372390 var wrapper = new AVCapturePhotoCaptureDelegateWrapper ( ) ;
@@ -408,6 +426,20 @@ private async partial ValueTask PlatformTakePicture(CancellationToken token)
408426 }
409427 }
410428
429+ static AVCaptureVideoOrientation GetVideoOrientationFromAccelerometer ( double x , double y )
430+ {
431+ // Absolute values help determine which axis is dominant
432+ if ( Math . Abs ( y ) >= Math . Abs ( x ) )
433+ {
434+ return y > 0 ? AVCaptureVideoOrientation . PortraitUpsideDown : AVCaptureVideoOrientation . Portrait ;
435+ }
436+ else
437+ {
438+ // x > 0 is LandscapeRight for device, which is LandscapeLeft for Video
439+ return x > 0 ? AVCaptureVideoOrientation . LandscapeLeft : AVCaptureVideoOrientation . LandscapeRight ;
440+ }
441+ }
442+
411443 static AVCaptureVideoOrientation GetVideoOrientation ( )
412444 {
413445 IEnumerable < UIScene > scenes = UIApplication . SharedApplication . ConnectedScenes ;
@@ -436,6 +468,50 @@ static AVCaptureVideoOrientation GetVideoOrientation()
436468 } ;
437469 }
438470
471+ bool TryConfigureAVCaptureConnection ( in AVCaptureOutput captureOutput , [ NotNullWhen ( false ) ] out string ? errorMessage )
472+ {
473+ errorMessage = null ;
474+
475+ if ( AVMediaTypes . Video . GetConstant ( ) is not NSString avMediaTypeVideo )
476+ {
477+ errorMessage = "Unable to determine video format." ;
478+ return false ;
479+ }
480+
481+ if ( captureOutput . ConnectionFromMediaType ( avMediaTypeVideo ) is not AVCaptureConnection captureConnection )
482+ {
483+ errorMessage = "Unable to determine video connection from media type." ;
484+ return false ;
485+ }
486+
487+ // use AVCaptureDeviceRotationCoordinator to set captured photo and video orientation on iOS 17+
488+ if ( UIDevice . CurrentDevice . CheckSystemVersion ( 17 , 0 ) )
489+ {
490+ if ( rotationCoordinator is not null )
491+ {
492+ captureConnection . VideoRotationAngle = rotationCoordinator . VideoRotationAngleForHorizonLevelCapture ;
493+ }
494+ }
495+ // use CMMotionManager to set captured photo and video orientation on iOS 16 and lower
496+ else
497+ {
498+ var data = motionManager ? . AccelerometerData ;
499+ if ( data is not null )
500+ {
501+ var orientation = GetVideoOrientationFromAccelerometer ( data . Acceleration . X , data . Acceleration . Y ) ;
502+ captureConnection . VideoOrientation = orientation ;
503+ }
504+ }
505+
506+ if ( captureConnection . SupportsVideoMirroring )
507+ {
508+ captureConnection . AutomaticallyAdjustsVideoMirroring = false ;
509+ captureConnection . VideoMirrored = cameraView . SelectedCamera ? . Position is CameraPosition . Front ;
510+ }
511+
512+ return true ;
513+ }
514+
439515 void UpdateVideoOrientation ( )
440516 {
441517 videoOrientation = GetVideoOrientation ( ) ;
0 commit comments