[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;
+ });
+}