diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index f1d771496dde..eb88683bee8f 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 e867491fcf8f..5c43f78a8c4b 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -345,6 +345,7 @@ @interface FLTCam : NSObject _videoWriter.status == AVAssetWriterStatusCompleted) { + [self updateOrientation]; result(self->_videoRecordingPath); self->_videoRecordingPath = nil; } else { @@ -854,12 +884,18 @@ - (void)lockCaptureOrientationWithResult:(FlutterResult)result 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 @@ - (BOOL)setupWriterForPath:(NSString *)path { if (_enableAudio && !_isAudioSetup) { [self setUpCaptureSessionForAudio]; } + _videoWriter = [[AVAssetWriter alloc] initWithURL:outputURL fileType:AVFileTypeMPEG4 error:&error]; @@ -1109,11 +1146,9 @@ - (BOOL)setupWriterForPath:(NSString *)path { [_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 @@ - (BOOL)setupWriterForPath:(NSString *)path { }]; 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 @@ - (void)setUpCaptureSessionForAudio { } } } - -- (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)startOrientationListener { - (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 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re 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 517751b7bf64..e2f1ff931e42 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 @@ class CameraPreview extends StatelessWidget { 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 @@ class CameraPreview extends StatelessWidget { : 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 @@ class CameraPreview extends StatelessWidget { } int _getQuarterTurns() { - int platformOffset = defaultTargetPlatform == TargetPlatform.iOS ? 1 : 0; Map 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 53f9b3b40ad1..58bfa85d6b99 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -2,7 +2,7 @@ name: camera 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 000000000000..d579341c0e58 --- /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 + implements CameraController { + FakeController() : super(const CameraValue.uninitialized()); + + @override + Future 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 getExposureOffsetStepSize() async => 1.0; + + @override + Future getMaxExposureOffset() async => 1.0; + + @override + Future getMaxZoomLevel() async => 1.0; + + @override + Future getMinExposureOffset() async => 1.0; + + @override + Future getMinZoomLevel() async => 1.0; + + @override + ImageFormatGroup? get imageFormatGroup => null; + + @override + Future initialize() async {} + + @override + Future lockCaptureOrientation([DeviceOrientation? orientation]) async {} + + @override + Future pauseVideoRecording() async {} + + @override + Future prepareForVideoRecording() async {} + + @override + ResolutionPreset get resolutionPreset => ResolutionPreset.low; + + @override + Future resumeVideoRecording() async {} + + @override + Future setExposureMode(ExposureMode mode) async {} + + @override + Future setExposureOffset(double offset) async => offset; + + @override + Future setExposurePoint(Offset? point) async {} + + @override + Future setFlashMode(FlashMode mode) async {} + + @override + Future setFocusMode(FocusMode mode) async {} + + @override + Future setFocusPoint(Offset? point) async {} + + @override + Future setZoomLevel(double zoom) async {} + + @override + Future startImageStream(onAvailable) async {} + + @override + Future startVideoRecording() async {} + + @override + Future stopImageStream() async {} + + @override + Future stopVideoRecording() async => XFile(''); + + @override + Future takePicture() async => XFile(''); + + @override + Future 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(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(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(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; + }); +}