Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

[camera] Fix iOS rotation issue #3591

Merged
merged 11 commits into from
Mar 24, 2021
4 changes: 4 additions & 0 deletions packages/camera/camera/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
104 changes: 62 additions & 42 deletions packages/camera/camera/ios/Classes/CameraPlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ @interface FLTCam : NSObject <FlutterTexture,

@implementation FLTCam {
dispatch_queue_t _dispatchQueue;
UIDeviceOrientation _deviceOrientation;
}
// Format used for video and image streaming.
FourCharCode videoFormat = kCVPixelFormatType_32BGRA;
Expand All @@ -353,6 +354,7 @@ @implementation FLTCam {
- (instancetype)initWithCameraName:(NSString *)cameraName
resolutionPreset:(NSString *)resolutionPreset
enableAudio:(BOOL)enableAudio
orientation:(UIDeviceOrientation)orientation
dispatchQueue:(dispatch_queue_t)dispatchQueue
error:(NSError **)error {
self = [super init];
Expand All @@ -370,6 +372,7 @@ - (instancetype)initWithCameraName:(NSString *)cameraName
_exposureMode = ExposureModeAuto;
_focusMode = FocusModeAuto;
_lockedCaptureOrientation = UIDeviceOrientationUnknown;
_deviceOrientation = orientation;

NSError *localError = nil;
_captureVideoInput = [AVCaptureDeviceInput deviceInputWithDevice:_captureDevice
Expand All @@ -389,10 +392,11 @@ - (instancetype)initWithCameraName:(NSString *)cameraName
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];
Expand All @@ -406,6 +410,8 @@ - (instancetype)initWithCameraName:(NSString *)cameraName
[_motionManager startAccelerometerUpdates];

[self setCaptureSessionPreset:_resolutionPreset];
[self updateOrientation];

return self;
}

Expand All @@ -417,6 +423,40 @@ - (void)stop {
[_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) {
Expand All @@ -437,18 +477,6 @@ - (void)captureToFile:(FlutterResult)result API_AVAILABLE(ios(10)) {
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]];
Expand Down Expand Up @@ -812,9 +840,11 @@ - (void)startVideoRecordingWithResult:(FlutterResult)result {
- (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 {
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -1101,6 +1137,7 @@ - (BOOL)setupWriterForPath:(NSString *)path {
if (_enableAudio && !_isAudioSetup) {
[self setUpCaptureSessionForAudio];
}

_videoWriter = [[AVAssetWriter alloc] initWithURL:outputURL
fileType:AVFileTypeMPEG4
error:&error];
Expand All @@ -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];

Expand All @@ -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
Expand Down Expand Up @@ -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 ()
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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];

Expand Down
31 changes: 18 additions & 13 deletions packages/camera/camera/lib/src/camera_preview.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -29,23 +28,23 @@ 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(),
],
),
)
: 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() {
Expand All @@ -54,13 +53,19 @@ class CameraPreview extends StatelessWidget {
}

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() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to confirm, there is no diff in this method right? The method was just moved down?

Copy link
Contributor Author

@mvanbeusekom mvanbeusekom Mar 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, it made more sense to me to keep it closer to where it was used. I see now that it causes a bit of noice

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keeping it close to where it was used makes sense to me!

return controller.value.isRecordingVideo
? controller.value.recordingOrientation!
: (controller.value.lockedCaptureOrientation ??
controller.value.deviceOrientation);
}
}
2 changes: 1 addition & 1 deletion packages/camera/camera/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading