[camera] Fix iOS rotation issue (#3591)

* Fix iOS rotation issue

* Fix orientation issues on iOS

* Merged with master and added test

* Test RotationBox turns according to device orientation

* Fix formatting

* Removed merge conflict tags from CHANGELOG

* Fix license header in test
diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md
index f1d7714..eb88683 100644
--- a/packages/camera/camera/CHANGELOG.md
+++ b/packages/camera/camera/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.8.1
+
+* Solved a rotation issue on iOS which caused the default preview to be displayed as landscape right instead of portrait.
+
 ## 0.8.0
 
 * Stable null safety release.
diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m
index e867491..5c43f78 100644
--- a/packages/camera/camera/ios/Classes/CameraPlugin.m
+++ b/packages/camera/camera/ios/Classes/CameraPlugin.m
@@ -345,6 +345,7 @@
 
 @implementation FLTCam {
   dispatch_queue_t _dispatchQueue;
+  UIDeviceOrientation _deviceOrientation;
 }
 // Format used for video and image streaming.
 FourCharCode videoFormat = kCVPixelFormatType_32BGRA;
@@ -353,6 +354,7 @@
 - (instancetype)initWithCameraName:(NSString *)cameraName
                   resolutionPreset:(NSString *)resolutionPreset
                        enableAudio:(BOOL)enableAudio
+                       orientation:(UIDeviceOrientation)orientation
                      dispatchQueue:(dispatch_queue_t)dispatchQueue
                              error:(NSError **)error {
   self = [super init];
@@ -370,6 +372,7 @@
   _exposureMode = ExposureModeAuto;
   _focusMode = FocusModeAuto;
   _lockedCaptureOrientation = UIDeviceOrientationUnknown;
+  _deviceOrientation = orientation;
 
   NSError *localError = nil;
   _captureVideoInput = [AVCaptureDeviceInput deviceInputWithDevice:_captureDevice
@@ -389,10 +392,11 @@
   AVCaptureConnection *connection =
       [AVCaptureConnection connectionWithInputPorts:_captureVideoInput.ports
                                              output:_captureVideoOutput];
+
   if ([_captureDevice position] == AVCaptureDevicePositionFront) {
     connection.videoMirrored = YES;
   }
-  connection.videoOrientation = AVCaptureVideoOrientationLandscapeRight;
+
   [_captureSession addInputWithNoConnections:_captureVideoInput];
   [_captureSession addOutputWithNoConnections:_captureVideoOutput];
   [_captureSession addConnection:connection];
@@ -406,6 +410,8 @@
   [_motionManager startAccelerometerUpdates];
 
   [self setCaptureSessionPreset:_resolutionPreset];
+  [self updateOrientation];
+
   return self;
 }
 
@@ -417,6 +423,40 @@
   [_captureSession stopRunning];
 }
 
+- (void)setDeviceOrientation:(UIDeviceOrientation)orientation {
+  if (_deviceOrientation == orientation) {
+    return;
+  }
+
+  _deviceOrientation = orientation;
+  [self updateOrientation];
+}
+
+- (void)updateOrientation {
+  if (_isRecording) {
+    return;
+  }
+
+  UIDeviceOrientation orientation = (_lockedCaptureOrientation != UIDeviceOrientationUnknown)
+                                        ? _lockedCaptureOrientation
+                                        : _deviceOrientation;
+
+  [self updateOrientation:orientation forCaptureOutput:_capturePhotoOutput];
+  [self updateOrientation:orientation forCaptureOutput:_captureVideoOutput];
+}
+
+- (void)updateOrientation:(UIDeviceOrientation)orientation
+         forCaptureOutput:(AVCaptureOutput *)captureOutput {
+  if (!captureOutput) {
+    return;
+  }
+
+  AVCaptureConnection *connection = [captureOutput connectionWithMediaType:AVMediaTypeVideo];
+  if (connection && connection.isVideoOrientationSupported) {
+    connection.videoOrientation = [self getVideoOrientationForDeviceOrientation:orientation];
+  }
+}
+
 - (void)captureToFile:(FlutterResult)result API_AVAILABLE(ios(10)) {
   AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings];
   if (_resolutionPreset == max) {
@@ -437,18 +477,6 @@
     return;
   }
 
-  AVCaptureConnection *connection = [_capturePhotoOutput connectionWithMediaType:AVMediaTypeVideo];
-
-  if (connection) {
-    if (_lockedCaptureOrientation != UIDeviceOrientationUnknown) {
-      connection.videoOrientation =
-          [self getVideoOrientationForDeviceOrientation:_lockedCaptureOrientation];
-    } else {
-      connection.videoOrientation =
-          [self getVideoOrientationForDeviceOrientation:[[UIDevice currentDevice] orientation]];
-    }
-  }
-
   [_capturePhotoOutput capturePhotoWithSettings:settings
                                        delegate:[[FLTSavePhotoDelegate alloc] initWithPath:path
                                                                                     result:result]];
@@ -812,9 +840,11 @@
 - (void)stopVideoRecordingWithResult:(FlutterResult)result {
   if (_isRecording) {
     _isRecording = NO;
+
     if (_videoWriter.status != AVAssetWriterStatusUnknown) {
       [_videoWriter finishWritingWithCompletionHandler:^{
         if (self->_videoWriter.status == AVAssetWriterStatusCompleted) {
+          [self updateOrientation];
           result(self->_videoRecordingPath);
           self->_videoRecordingPath = nil;
         } else {
@@ -854,12 +884,18 @@
     result(getFlutterError(e));
     return;
   }
-  _lockedCaptureOrientation = orientation;
+
+  if (_lockedCaptureOrientation != orientation) {
+    _lockedCaptureOrientation = orientation;
+    [self updateOrientation];
+  }
+
   result(nil);
 }
 
 - (void)unlockCaptureOrientationWithResult:(FlutterResult)result {
   _lockedCaptureOrientation = UIDeviceOrientationUnknown;
+  [self updateOrientation];
   result(nil);
 }
 
@@ -1101,6 +1137,7 @@
   if (_enableAudio && !_isAudioSetup) {
     [self setUpCaptureSessionForAudio];
   }
+
   _videoWriter = [[AVAssetWriter alloc] initWithURL:outputURL
                                            fileType:AVFileTypeMPEG4
                                               error:&error];
@@ -1109,11 +1146,9 @@
     [_methodChannel invokeMethod:errorMethod arguments:error.description];
     return NO;
   }
-  NSDictionary *videoSettings = [NSDictionary
-      dictionaryWithObjectsAndKeys:AVVideoCodecH264, AVVideoCodecKey,
-                                   [NSNumber numberWithInt:_previewSize.width], AVVideoWidthKey,
-                                   [NSNumber numberWithInt:_previewSize.height], AVVideoHeightKey,
-                                   nil];
+
+  NSDictionary *videoSettings = [_captureVideoOutput
+      recommendedVideoSettingsForAssetWriterWithOutputFileType:AVFileTypeMPEG4];
   _videoWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo
                                                          outputSettings:videoSettings];
 
@@ -1124,14 +1159,7 @@
                                  }];
 
   NSParameterAssert(_videoWriterInput);
-  CGFloat rotationDegrees;
-  if (_lockedCaptureOrientation != UIDeviceOrientationUnknown) {
-    rotationDegrees = [self getRotationFromDeviceOrientation:_lockedCaptureOrientation];
-  } else {
-    rotationDegrees = [self getRotationFromDeviceOrientation:[UIDevice currentDevice].orientation];
-  }
 
-  _videoWriterInput.transform = CGAffineTransformMakeRotation(rotationDegrees * M_PI / 180);
   _videoWriterInput.expectsMediaDataInRealTime = YES;
 
   // Add the audio input
@@ -1194,21 +1222,6 @@
     }
   }
 }
-
-- (int)getRotationFromDeviceOrientation:(UIDeviceOrientation)orientation {
-  switch (orientation) {
-    case UIDeviceOrientationPortraitUpsideDown:
-      return 270;
-    case UIDeviceOrientationLandscapeRight:
-      return 180;
-    case UIDeviceOrientationLandscapeLeft:
-      return 0;
-    case UIDeviceOrientationPortrait:
-    default:
-      return 90;
-  };
-}
-
 @end
 
 @interface CameraPlugin ()
@@ -1257,7 +1270,13 @@
 
 - (void)orientationChanged:(NSNotification *)note {
   UIDevice *device = note.object;
-  [self sendDeviceOrientation:device.orientation];
+  UIDeviceOrientation orientation = device.orientation;
+
+  if (_camera) {
+    [_camera setDeviceOrientation:orientation];
+  }
+
+  [self sendDeviceOrientation:orientation];
 }
 
 - (void)sendDeviceOrientation:(UIDeviceOrientation)orientation {
@@ -1318,6 +1337,7 @@
     FLTCam *cam = [[FLTCam alloc] initWithCameraName:cameraName
                                     resolutionPreset:resolutionPreset
                                          enableAudio:[enableAudio boolValue]
+                                         orientation:[[UIDevice currentDevice] orientation]
                                        dispatchQueue:_dispatchQueue
                                                error:&error];
 
diff --git a/packages/camera/camera/lib/src/camera_preview.dart b/packages/camera/camera/lib/src/camera_preview.dart
index 517751b..e2f1ff9 100644
--- a/packages/camera/camera/lib/src/camera_preview.dart
+++ b/packages/camera/camera/lib/src/camera_preview.dart
@@ -3,7 +3,6 @@
 // found in the LICENSE file.
 
 import 'package:camera/camera.dart';
-import 'package:camera_platform_interface/camera_platform_interface.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
@@ -29,11 +28,7 @@
             child: Stack(
               fit: StackFit.expand,
               children: [
-                RotatedBox(
-                  quarterTurns: _getQuarterTurns(),
-                  child:
-                      CameraPlatform.instance.buildPreview(controller.cameraId),
-                ),
+                _wrapInRotatedBox(child: controller.buildPreview()),
                 child ?? Container(),
               ],
             ),
@@ -41,11 +36,15 @@
         : Container();
   }
 
-  DeviceOrientation _getApplicableOrientation() {
-    return controller.value.isRecordingVideo
-        ? controller.value.recordingOrientation!
-        : (controller.value.lockedCaptureOrientation ??
-            controller.value.deviceOrientation);
+  Widget _wrapInRotatedBox({required Widget child}) {
+    if (defaultTargetPlatform != TargetPlatform.android) {
+      return child;
+    }
+
+    return RotatedBox(
+      quarterTurns: _getQuarterTurns(),
+      child: child,
+    );
   }
 
   bool _isLandscape() {
@@ -54,13 +53,19 @@
   }
 
   int _getQuarterTurns() {
-    int platformOffset = defaultTargetPlatform == TargetPlatform.iOS ? 1 : 0;
     Map<DeviceOrientation, int> turns = {
       DeviceOrientation.portraitUp: 0,
       DeviceOrientation.landscapeLeft: 1,
       DeviceOrientation.portraitDown: 2,
       DeviceOrientation.landscapeRight: 3,
     };
-    return turns[_getApplicableOrientation()]! + platformOffset;
+    return turns[_getApplicableOrientation()]!;
+  }
+
+  DeviceOrientation _getApplicableOrientation() {
+    return controller.value.isRecordingVideo
+        ? controller.value.recordingOrientation!
+        : (controller.value.lockedCaptureOrientation ??
+            controller.value.deviceOrientation);
   }
 }
diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml
index 53f9b3b..58bfa85 100644
--- a/packages/camera/camera/pubspec.yaml
+++ b/packages/camera/camera/pubspec.yaml
@@ -2,7 +2,7 @@
 description: A Flutter plugin for getting information about and controlling the
   camera on Android and iOS. Supports previewing the camera feed, capturing images, capturing video,
   and streaming image buffers to dart.
-version: 0.8.0
+version: 0.8.1
 homepage: https://github.com/flutter/plugins/tree/master/packages/camera/camera
 
 dependencies:
diff --git a/packages/camera/camera/test/camera_preview_test.dart b/packages/camera/camera/test/camera_preview_test.dart
new file mode 100644
index 0000000..d579341
--- /dev/null
+++ b/packages/camera/camera/test/camera_preview_test.dart
@@ -0,0 +1,239 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:camera/camera.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:quiver/core.dart';
+
+class FakeController extends ValueNotifier<CameraValue>
+    implements CameraController {
+  FakeController() : super(const CameraValue.uninitialized());
+
+  @override
+  Future<void> dispose() async {
+    super.dispose();
+  }
+
+  @override
+  Widget buildPreview() {
+    return Texture(textureId: CameraController.kUninitializedCameraId);
+  }
+
+  @override
+  int get cameraId => CameraController.kUninitializedCameraId;
+
+  @override
+  void debugCheckIsDisposed() {}
+
+  @override
+  CameraDescription get description => CameraDescription(
+      name: '', lensDirection: CameraLensDirection.back, sensorOrientation: 0);
+
+  @override
+  bool get enableAudio => false;
+
+  @override
+  Future<double> getExposureOffsetStepSize() async => 1.0;
+
+  @override
+  Future<double> getMaxExposureOffset() async => 1.0;
+
+  @override
+  Future<double> getMaxZoomLevel() async => 1.0;
+
+  @override
+  Future<double> getMinExposureOffset() async => 1.0;
+
+  @override
+  Future<double> getMinZoomLevel() async => 1.0;
+
+  @override
+  ImageFormatGroup? get imageFormatGroup => null;
+
+  @override
+  Future<void> initialize() async {}
+
+  @override
+  Future<void> lockCaptureOrientation([DeviceOrientation? orientation]) async {}
+
+  @override
+  Future<void> pauseVideoRecording() async {}
+
+  @override
+  Future<void> prepareForVideoRecording() async {}
+
+  @override
+  ResolutionPreset get resolutionPreset => ResolutionPreset.low;
+
+  @override
+  Future<void> resumeVideoRecording() async {}
+
+  @override
+  Future<void> setExposureMode(ExposureMode mode) async {}
+
+  @override
+  Future<double> setExposureOffset(double offset) async => offset;
+
+  @override
+  Future<void> setExposurePoint(Offset? point) async {}
+
+  @override
+  Future<void> setFlashMode(FlashMode mode) async {}
+
+  @override
+  Future<void> setFocusMode(FocusMode mode) async {}
+
+  @override
+  Future<void> setFocusPoint(Offset? point) async {}
+
+  @override
+  Future<void> setZoomLevel(double zoom) async {}
+
+  @override
+  Future<void> startImageStream(onAvailable) async {}
+
+  @override
+  Future<void> startVideoRecording() async {}
+
+  @override
+  Future<void> stopImageStream() async {}
+
+  @override
+  Future<XFile> stopVideoRecording() async => XFile('');
+
+  @override
+  Future<XFile> takePicture() async => XFile('');
+
+  @override
+  Future<void> unlockCaptureOrientation() async {}
+}
+
+void main() {
+  group('RotatedBox (Android only)', () {
+    testWidgets(
+        'when recording rotatedBox should turn according to recording orientation',
+        (
+      WidgetTester tester,
+    ) async {
+      debugDefaultTargetPlatformOverride = TargetPlatform.android;
+
+      final FakeController controller = FakeController();
+      controller.value = controller.value.copyWith(
+        isInitialized: true,
+        isRecordingVideo: true,
+        deviceOrientation: DeviceOrientation.portraitUp,
+        lockedCaptureOrientation:
+            Optional.fromNullable(DeviceOrientation.landscapeRight),
+        recordingOrientation:
+            Optional.fromNullable(DeviceOrientation.landscapeLeft),
+        previewSize: Size(480, 640),
+      );
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: CameraPreview(controller),
+        ),
+      );
+      expect(find.byType(RotatedBox), findsOneWidget);
+
+      RotatedBox rotatedBox =
+          tester.widget<RotatedBox>(find.byType(RotatedBox));
+      expect(rotatedBox.quarterTurns, 1);
+
+      debugDefaultTargetPlatformOverride = null;
+    });
+
+    testWidgets(
+        'when orientation locked rotatedBox should turn according to locked orientation',
+        (
+      WidgetTester tester,
+    ) async {
+      debugDefaultTargetPlatformOverride = TargetPlatform.android;
+
+      final FakeController controller = FakeController();
+      controller.value = controller.value.copyWith(
+        isInitialized: true,
+        deviceOrientation: DeviceOrientation.portraitUp,
+        lockedCaptureOrientation:
+            Optional.fromNullable(DeviceOrientation.landscapeRight),
+        recordingOrientation:
+            Optional.fromNullable(DeviceOrientation.landscapeLeft),
+        previewSize: Size(480, 640),
+      );
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: CameraPreview(controller),
+        ),
+      );
+      expect(find.byType(RotatedBox), findsOneWidget);
+
+      RotatedBox rotatedBox =
+          tester.widget<RotatedBox>(find.byType(RotatedBox));
+      expect(rotatedBox.quarterTurns, 3);
+
+      debugDefaultTargetPlatformOverride = null;
+    });
+
+    testWidgets(
+        'when not locked and not recording rotatedBox should turn according to device orientation',
+        (
+      WidgetTester tester,
+    ) async {
+      debugDefaultTargetPlatformOverride = TargetPlatform.android;
+
+      final FakeController controller = FakeController();
+      controller.value = controller.value.copyWith(
+        isInitialized: true,
+        deviceOrientation: DeviceOrientation.portraitUp,
+        lockedCaptureOrientation: null,
+        recordingOrientation:
+            Optional.fromNullable(DeviceOrientation.landscapeLeft),
+        previewSize: Size(480, 640),
+      );
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: CameraPreview(controller),
+        ),
+      );
+      expect(find.byType(RotatedBox), findsOneWidget);
+
+      RotatedBox rotatedBox =
+          tester.widget<RotatedBox>(find.byType(RotatedBox));
+      expect(rotatedBox.quarterTurns, 0);
+
+      debugDefaultTargetPlatformOverride = null;
+    });
+  });
+
+  testWidgets('when not on Android there should not be a rotated box',
+      (WidgetTester tester) async {
+    debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
+    final FakeController controller = FakeController();
+    controller.value = controller.value.copyWith(
+      isInitialized: true,
+      previewSize: Size(480, 640),
+    );
+
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: CameraPreview(controller),
+      ),
+    );
+    expect(find.byType(RotatedBox), findsNothing);
+    expect(find.byType(Texture), findsOneWidget);
+    debugDefaultTargetPlatformOverride = null;
+  });
+}