[Camera] Improved resolution presets (#1952)

This PR is here to address issues in the quality of video recording for the camera plugin on Android, as well as discrepancies in quality level between Android and iOS. It also allows to control the still picture quality (which was so far only maximum quality). The existing resolution presets have been updated to match between Android and iOS, and new presets have been added.

There are no breaking changes but the quality of the presets have been adjusted which will result in different picture/video quality on apps using this plugin.

We believe this change is necessary as, for the same resolution preset, Android video quality was very poor and the quality on iOS was too high, generating 4k files that are too heavy to upload.
diff --git a/packages/camera/CHANGELOG.md b/packages/camera/CHANGELOG.md
index 2d09af5..196cd3e 100644
--- a/packages/camera/CHANGELOG.md
+++ b/packages/camera/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 0.5.3
+
+* Added new quality presets.
+* Now all quality presets can be used to control image capture quality.
+
 ## 0.5.2+2
 
 * Fix memory leak related to not unregistering stream handler in FlutterEventChannel when disposing camera.
diff --git a/packages/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java
index bf99f8d..763e3b5 100644
--- a/packages/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java
+++ b/packages/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java
@@ -1,6 +1,7 @@
 package io.flutter.plugins.camera;
 
 import static android.view.OrientationEventListener.ORIENTATION_UNKNOWN;
+import static io.flutter.plugins.camera.CameraUtils.computeBestPreviewSize;
 
 import android.annotation.SuppressLint;
 import android.app.Activity;
@@ -16,6 +17,7 @@
 import android.hardware.camera2.CaptureFailure;
 import android.hardware.camera2.CaptureRequest;
 import android.hardware.camera2.params.StreamConfigurationMap;
+import android.media.CamcorderProfile;
 import android.media.Image;
 import android.media.ImageReader;
 import android.media.MediaRecorder;
@@ -46,7 +48,6 @@
   private final String cameraName;
   private final Size captureSize;
   private final Size previewSize;
-  private final Size videoSize;
   private final boolean enableAudio;
 
   private CameraDevice cameraDevice;
@@ -57,8 +58,19 @@
   private CaptureRequest.Builder captureRequestBuilder;
   private MediaRecorder mediaRecorder;
   private boolean recordingVideo;
+  private CamcorderProfile recordingProfile;
   private int currentOrientation = ORIENTATION_UNKNOWN;
 
+  // Mirrors camera.dart
+  public enum ResolutionPreset {
+    low,
+    medium,
+    high,
+    veryHigh,
+    ultraHigh,
+    max,
+  }
+
   public Camera(
       final Activity activity,
       final FlutterView flutterView,
@@ -87,21 +99,6 @@
         };
     orientationEventListener.enable();
 
-    int minHeight;
-    switch (resolutionPreset) {
-      case "high":
-        minHeight = 720;
-        break;
-      case "medium":
-        minHeight = 480;
-        break;
-      case "low":
-        minHeight = 240;
-        break;
-      default:
-        throw new IllegalArgumentException("Unknown preset: " + resolutionPreset);
-    }
-
     CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraName);
     StreamConfigurationMap streamConfigurationMap =
         characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
@@ -110,12 +107,11 @@
     //noinspection ConstantConditions
     isFrontFacing =
         characteristics.get(CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_FRONT;
-    captureSize = CameraUtils.computeBestCaptureSize(streamConfigurationMap);
-    Size[] sizes =
-        CameraUtils.computeBestPreviewAndRecordingSize(
-            activity, streamConfigurationMap, minHeight, getMediaOrientation(), captureSize);
-    videoSize = sizes[0];
-    previewSize = sizes[1];
+    ResolutionPreset preset = ResolutionPreset.valueOf(resolutionPreset);
+    recordingProfile =
+        CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset);
+    captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight);
+    previewSize = computeBestPreviewSize(cameraName, preset);
   }
 
   public void setupCameraEventChannel(EventChannel cameraEventChannel) {
@@ -143,13 +139,13 @@
     // of these function calls.
     if (enableAudio) mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
     mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
-    mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
-    if (enableAudio) mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
-    mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
-    mediaRecorder.setVideoEncodingBitRate(1024 * 1000);
-    if (enableAudio) mediaRecorder.setAudioSamplingRate(16000);
-    mediaRecorder.setVideoFrameRate(27);
-    mediaRecorder.setVideoSize(videoSize.getWidth(), videoSize.getHeight());
+    mediaRecorder.setOutputFormat(recordingProfile.fileFormat);
+    if (enableAudio) mediaRecorder.setAudioEncoder(recordingProfile.audioCodec);
+    mediaRecorder.setVideoEncoder(recordingProfile.videoCodec);
+    mediaRecorder.setVideoEncodingBitRate(recordingProfile.videoBitRate);
+    if (enableAudio) mediaRecorder.setAudioSamplingRate(recordingProfile.audioSampleRate);
+    mediaRecorder.setVideoFrameRate(recordingProfile.videoFrameRate);
+    mediaRecorder.setVideoSize(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight);
     mediaRecorder.setOutputFile(outputFilePath);
     mediaRecorder.setOrientationHint(getMediaOrientation());
 
diff --git a/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java b/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java
index 517db15..e4613fb 100644
--- a/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java
+++ b/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java
@@ -3,15 +3,14 @@
 import android.app.Activity;
 import android.content.Context;
 import android.graphics.ImageFormat;
-import android.graphics.Point;
-import android.graphics.SurfaceTexture;
 import android.hardware.camera2.CameraAccessException;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CameraManager;
 import android.hardware.camera2.CameraMetadata;
 import android.hardware.camera2.params.StreamConfigurationMap;
+import android.media.CamcorderProfile;
 import android.util.Size;
-import android.view.Display;
+import io.flutter.plugins.camera.Camera.ResolutionPreset;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -25,61 +24,14 @@
 
   private CameraUtils() {}
 
-  static Size[] computeBestPreviewAndRecordingSize(
-      Activity activity,
-      StreamConfigurationMap streamConfigurationMap,
-      int minHeight,
-      int orientation,
-      Size captureSize) {
-    Size previewSize, videoSize;
-    Size[] sizes = streamConfigurationMap.getOutputSizes(SurfaceTexture.class);
-
-    // Preview size and video size should not be greater than screen resolution or 1080.
-    Point screenResolution = new Point();
-
-    Display display = activity.getWindowManager().getDefaultDisplay();
-    display.getRealSize(screenResolution);
-
-    final boolean swapWH = orientation % 180 == 90;
-    int screenWidth = swapWH ? screenResolution.y : screenResolution.x;
-    int screenHeight = swapWH ? screenResolution.x : screenResolution.y;
-
-    List<Size> goodEnough = new ArrayList<>();
-    for (Size s : sizes) {
-      if (minHeight <= s.getHeight()
-          && s.getWidth() <= screenWidth
-          && s.getHeight() <= screenHeight
-          && s.getHeight() <= 1080) {
-        goodEnough.add(s);
-      }
+  static Size computeBestPreviewSize(String cameraName, ResolutionPreset preset) {
+    if (preset.ordinal() > ResolutionPreset.high.ordinal()) {
+      preset = ResolutionPreset.high;
     }
 
-    Collections.sort(goodEnough, new CompareSizesByArea());
-
-    if (goodEnough.isEmpty()) {
-      previewSize = sizes[0];
-      videoSize = sizes[0];
-    } else {
-      float captureSizeRatio = (float) captureSize.getWidth() / captureSize.getHeight();
-
-      previewSize = goodEnough.get(0);
-      for (Size s : goodEnough) {
-        if ((float) s.getWidth() / s.getHeight() == captureSizeRatio) {
-          previewSize = s;
-          break;
-        }
-      }
-
-      Collections.reverse(goodEnough);
-      videoSize = goodEnough.get(0);
-      for (Size s : goodEnough) {
-        if ((float) s.getWidth() / s.getHeight() == captureSizeRatio) {
-          videoSize = s;
-          break;
-        }
-      }
-    }
-    return new Size[] {videoSize, previewSize};
+    CamcorderProfile profile =
+        getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset);
+    return new Size(profile.videoFrameWidth, profile.videoFrameHeight);
   }
 
   static Size computeBestCaptureSize(StreamConfigurationMap streamConfigurationMap) {
@@ -118,6 +70,46 @@
     return cameras;
   }
 
+  static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPreset(
+      String cameraName, ResolutionPreset preset) {
+    int cameraId = Integer.parseInt(cameraName);
+    switch (preset) {
+        // All of these cases deliberately fall through to get the best available profile.
+      case max:
+        if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_HIGH)) {
+          return CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH);
+        }
+      case ultraHigh:
+        if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_2160P)) {
+          return CamcorderProfile.get(CamcorderProfile.QUALITY_2160P);
+        }
+      case veryHigh:
+        if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_1080P)) {
+          return CamcorderProfile.get(CamcorderProfile.QUALITY_1080P);
+        }
+      case high:
+        if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_720P)) {
+          return CamcorderProfile.get(CamcorderProfile.QUALITY_720P);
+        }
+      case medium:
+        if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_480P)) {
+          return CamcorderProfile.get(CamcorderProfile.QUALITY_480P);
+        }
+      case low:
+        if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_QVGA)) {
+          return CamcorderProfile.get(CamcorderProfile.QUALITY_QVGA);
+        }
+      default:
+        if (CamcorderProfile.hasProfile(
+            Integer.parseInt(cameraName), CamcorderProfile.QUALITY_LOW)) {
+          return CamcorderProfile.get(CamcorderProfile.QUALITY_LOW);
+        } else {
+          throw new IllegalArgumentException(
+              "No capture session available for current capture session.");
+        }
+    }
+  }
+
   private static class CompareSizesByArea implements Comparator<Size> {
     @Override
     public int compare(Size lhs, Size rhs) {
diff --git a/packages/camera/example/lib/main.dart b/packages/camera/example/lib/main.dart
index 70b7ef2..2e2aa4d 100644
--- a/packages/camera/example/lib/main.dart
+++ b/packages/camera/example/lib/main.dart
@@ -258,7 +258,7 @@
     }
     controller = CameraController(
       cameraDescription,
-      ResolutionPreset.high,
+      ResolutionPreset.medium,
       enableAudio: enableAudio,
     );
 
diff --git a/packages/camera/example/pubspec.yaml b/packages/camera/example/pubspec.yaml
index 834fe1b..59f3821 100644
--- a/packages/camera/example/pubspec.yaml
+++ b/packages/camera/example/pubspec.yaml
@@ -13,6 +13,8 @@
 dev_dependencies:
   flutter_test:
     sdk: flutter
+  flutter_driver:
+    sdk: flutter
 
 flutter:
   uses-material-design: true
diff --git a/packages/camera/example/test_driver/camera.dart b/packages/camera/example/test_driver/camera.dart
new file mode 100644
index 0000000..7d59016
--- /dev/null
+++ b/packages/camera/example/test_driver/camera.dart
@@ -0,0 +1,146 @@
+import 'dart:async';
+import 'dart:io';
+import 'dart:ui';
+
+import 'package:flutter/painting.dart';
+import 'package:flutter_driver/driver_extension.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:camera/camera.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:video_player/video_player.dart';
+
+void main() {
+  final Completer<String> completer = Completer<String>();
+  Directory testDir;
+  enableFlutterDriverExtension(handler: (_) => completer.future);
+
+  setUpAll(() async {
+    final Directory extDir = await getTemporaryDirectory();
+    testDir = await Directory('${extDir.path}/test').create(recursive: true);
+  });
+
+  tearDownAll(() async {
+    await testDir.delete(recursive: true);
+    completer.complete(null);
+  });
+
+  final Map<ResolutionPreset, Size> presetExpectedSizes =
+      <ResolutionPreset, Size>{
+    ResolutionPreset.low:
+        Platform.isAndroid ? const Size(240, 320) : const Size(288, 352),
+    ResolutionPreset.medium:
+        Platform.isAndroid ? const Size(480, 720) : const Size(480, 640),
+    ResolutionPreset.high: const Size(720, 1280),
+    ResolutionPreset.veryHigh: const Size(1080, 1920),
+    ResolutionPreset.ultraHigh: const Size(2160, 3840),
+    // Don't bother checking for max here since it could be anything.
+  };
+
+  /// Verify that [actual] has dimensions that are at least as large as
+  /// [expectedSize]. Allows for a mismatch in portrait vs landscape. Returns
+  /// whether the dimensions exactly match.
+  bool assertExpectedDimensions(Size expectedSize, Size actual) {
+    expect(actual.shortestSide, lessThanOrEqualTo(expectedSize.shortestSide));
+    expect(actual.longestSide, lessThanOrEqualTo(expectedSize.longestSide));
+    return actual.shortestSide == expectedSize.shortestSide &&
+        actual.longestSide == expectedSize.longestSide;
+  }
+
+  // This tests that the capture is no bigger than the preset, since we have
+  // automatic code to fall back to smaller sizes when we need to. Returns
+  // whether the image is exactly the desired resolution.
+  Future<bool> testCaptureImageResolution(
+      CameraController controller, ResolutionPreset preset) async {
+    final Size expectedSize = presetExpectedSizes[preset];
+    print(
+        'Capturing photo at $preset (${expectedSize.width}x${expectedSize.height}) using camera ${controller.description.name}');
+
+    // Take Picture
+    final String filePath =
+        '${testDir.path}/${DateTime.now().millisecondsSinceEpoch}.jpg';
+    await controller.takePicture(filePath);
+
+    // Load picture
+    final File fileImage = File(filePath);
+    final Image image = await decodeImageFromList(fileImage.readAsBytesSync());
+
+    // Verify image dimensions are as expected
+    expect(image, isNotNull);
+    return assertExpectedDimensions(
+        expectedSize, Size(image.height.toDouble(), image.width.toDouble()));
+  }
+
+  test('Capture specific image resolutions', () async {
+    final List<CameraDescription> cameras = await availableCameras();
+    if (cameras.isEmpty) {
+      return;
+    }
+    for (CameraDescription cameraDescription in cameras) {
+      bool previousPresetExactlySupported = true;
+      for (MapEntry<ResolutionPreset, Size> preset
+          in presetExpectedSizes.entries) {
+        final CameraController controller =
+            CameraController(cameraDescription, preset.key);
+        await controller.initialize();
+        final bool presetExactlySupported =
+            await testCaptureImageResolution(controller, preset.key);
+        assert(!(!previousPresetExactlySupported && presetExactlySupported),
+            'The camera took higher resolution pictures at a lower resolution.');
+        previousPresetExactlySupported = presetExactlySupported;
+        await controller.dispose();
+      }
+    }
+  });
+
+  // This tests that the capture is no bigger than the preset, since we have
+  // automatic code to fall back to smaller sizes when we need to. Returns
+  // whether the image is exactly the desired resolution.
+  Future<bool> testCaptureVideoResolution(
+      CameraController controller, ResolutionPreset preset) async {
+    final Size expectedSize = presetExpectedSizes[preset];
+    print(
+        'Capturing video at $preset (${expectedSize.width}x${expectedSize.height}) using camera ${controller.description.name}');
+
+    // Take Video
+    final String filePath =
+        '${testDir.path}/${DateTime.now().millisecondsSinceEpoch}.mp4';
+    await controller.startVideoRecording(filePath);
+    sleep(const Duration(milliseconds: 300));
+    await controller.stopVideoRecording();
+
+    // Load video metadata
+    final File videoFile = File(filePath);
+    final VideoPlayerController videoController =
+        VideoPlayerController.file(videoFile);
+    await videoController.initialize();
+    final Size video = videoController.value.size;
+
+    // Verify image dimensions are as expected
+    expect(video, isNotNull);
+    return assertExpectedDimensions(
+        expectedSize, Size(video.height, video.width));
+  }
+
+  test('Capture specific video resolutions', () async {
+    final List<CameraDescription> cameras = await availableCameras();
+    if (cameras.isEmpty) {
+      return;
+    }
+    for (CameraDescription cameraDescription in cameras) {
+      bool previousPresetExactlySupported = true;
+      for (MapEntry<ResolutionPreset, Size> preset
+          in presetExpectedSizes.entries) {
+        final CameraController controller =
+            CameraController(cameraDescription, preset.key);
+        await controller.initialize();
+        await controller.prepareForVideoRecording();
+        final bool presetExactlySupported =
+            await testCaptureVideoResolution(controller, preset.key);
+        assert(!(!previousPresetExactlySupported && presetExactlySupported),
+            'The camera took higher resolution pictures at a lower resolution.');
+        previousPresetExactlySupported = presetExactlySupported;
+        await controller.dispose();
+      }
+    }
+  });
+}
diff --git a/packages/camera/example/test_driver/camera_test.dart b/packages/camera/example/test_driver/camera_test.dart
new file mode 100644
index 0000000..38fe6c4
--- /dev/null
+++ b/packages/camera/example/test_driver/camera_test.dart
@@ -0,0 +1,7 @@
+import 'package:flutter_driver/flutter_driver.dart';
+
+Future<void> main() async {
+  final FlutterDriver driver = await FlutterDriver.connect();
+  await driver.requestData(null, timeout: const Duration(minutes: 1));
+  driver.close();
+}
diff --git a/packages/camera/ios/Classes/CameraPlugin.m b/packages/camera/ios/Classes/CameraPlugin.m
index 4c00192..3aeaa46 100644
--- a/packages/camera/ios/Classes/CameraPlugin.m
+++ b/packages/camera/ios/Classes/CameraPlugin.m
@@ -6,8 +6,8 @@
 
 static FlutterError *getFlutterError(NSError *error) {
   return [FlutterError errorWithCode:[NSString stringWithFormat:@"Error %d", (int)error.code]
-                             message:error.domain
-                             details:error.localizedDescription];
+                             message:error.localizedDescription
+                             details:error.domain];
 }
 
 @interface FLTSavePhotoDelegate : NSObject <AVCapturePhotoCaptureDelegate>
@@ -114,6 +114,43 @@
 }
 @end
 
+// Mirrors ResolutionPreset in camera.dart
+typedef enum {
+  veryLow,
+  low,
+  medium,
+  high,
+  veryHigh,
+  ultraHigh,
+  max,
+} ResolutionPreset;
+
+static ResolutionPreset getResolutionPresetForString(NSString *preset) {
+  if ([preset isEqualToString:@"veryLow"]) {
+    return veryLow;
+  } else if ([preset isEqualToString:@"low"]) {
+    return low;
+  } else if ([preset isEqualToString:@"medium"]) {
+    return medium;
+  } else if ([preset isEqualToString:@"high"]) {
+    return high;
+  } else if ([preset isEqualToString:@"veryHigh"]) {
+    return veryHigh;
+  } else if ([preset isEqualToString:@"ultraHigh"]) {
+    return ultraHigh;
+  } else if ([preset isEqualToString:@"max"]) {
+    return max;
+  } else {
+    NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain
+                                         code:NSURLErrorUnknown
+                                     userInfo:@{
+                                       NSLocalizedDescriptionKey : [NSString
+                                           stringWithFormat:@"Unknown resolution preset %@", preset]
+                                     }];
+    @throw error;
+  }
+}
+
 @interface FLTCam : NSObject <FlutterTexture,
                               AVCaptureVideoDataOutputSampleBufferDelegate,
                               AVCaptureAudioDataOutputSampleBufferDelegate,
@@ -141,6 +178,7 @@
 @property(assign, nonatomic) BOOL isRecording;
 @property(assign, nonatomic) BOOL isAudioSetup;
 @property(assign, nonatomic) BOOL isStreamingImages;
+@property(assign, nonatomic) ResolutionPreset resolutionPreset;
 @property(nonatomic) CMMotionManager *motionManager;
 - (instancetype)initWithCameraName:(NSString *)cameraName
                   resolutionPreset:(NSString *)resolutionPreset
@@ -170,6 +208,11 @@
                              error:(NSError **)error {
   self = [super init];
   NSAssert(self, @"super init cannot be nil");
+  @try {
+    _resolutionPreset = getResolutionPresetForString(resolutionPreset);
+  } @catch (NSError *e) {
+    *error = e;
+  }
   _enableAudio = enableAudio;
   _dispatchQueue = dispatchQueue;
   _captureSession = [[AVCaptureSession alloc] init];
@@ -205,8 +248,7 @@
   _motionManager = [[CMMotionManager alloc] init];
   [_motionManager startAccelerometerUpdates];
 
-  [self setCaptureSessionPreset:resolutionPreset];
-
+  [self setCaptureSessionPreset:_resolutionPreset];
   return self;
 }
 
@@ -220,7 +262,9 @@
 
 - (void)captureToFile:(NSString *)path result:(FlutterResult)result {
   AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings];
-  [settings setHighResolutionPhotoEnabled:YES];
+  if (_resolutionPreset == max) {
+    [settings setHighResolutionPhotoEnabled:YES];
+  }
   [_capturePhotoOutput
       capturePhotoWithSettings:settings
                       delegate:[[FLTSavePhotoDelegate alloc] initWithPath:path
@@ -229,56 +273,60 @@
                                                            cameraPosition:_captureDevice.position]];
 }
 
-- (void)setCaptureSessionPreset:(NSString *)resolutionPreset {
-  int presetIndex;
-  if ([resolutionPreset isEqualToString:@"high"]) {
-    presetIndex = 0;
-  } else if ([resolutionPreset isEqualToString:@"medium"]) {
-    presetIndex = 2;
-  } else {
-    NSAssert([resolutionPreset isEqualToString:@"low"], @"Unknown resolution preset %@",
-             resolutionPreset);
-    presetIndex = 3;
-  }
-
-  switch (presetIndex) {
-    case 0:
+- (void)setCaptureSessionPreset:(ResolutionPreset)resolutionPreset {
+  switch (resolutionPreset) {
+    case max:
+      if ([_captureSession canSetSessionPreset:AVCaptureSessionPresetHigh]) {
+        _captureSession.sessionPreset = AVCaptureSessionPresetHigh;
+        _previewSize =
+            CGSizeMake(_captureDevice.activeFormat.highResolutionStillImageDimensions.width,
+                       _captureDevice.activeFormat.highResolutionStillImageDimensions.height);
+        break;
+      }
+    case ultraHigh:
       if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset3840x2160]) {
         _captureSession.sessionPreset = AVCaptureSessionPreset3840x2160;
         _previewSize = CGSizeMake(3840, 2160);
         break;
       }
-    case 1:
+    case veryHigh:
       if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset1920x1080]) {
         _captureSession.sessionPreset = AVCaptureSessionPreset1920x1080;
         _previewSize = CGSizeMake(1920, 1080);
         break;
       }
-    case 2:
+    case high:
       if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset1280x720]) {
         _captureSession.sessionPreset = AVCaptureSessionPreset1280x720;
         _previewSize = CGSizeMake(1280, 720);
         break;
       }
-    case 3:
+    case medium:
       if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset640x480]) {
         _captureSession.sessionPreset = AVCaptureSessionPreset640x480;
         _previewSize = CGSizeMake(640, 480);
         break;
       }
-    case 4:
+    case low:
       if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset352x288]) {
         _captureSession.sessionPreset = AVCaptureSessionPreset352x288;
         _previewSize = CGSizeMake(352, 288);
         break;
       }
-    default: {
-      NSException *exception = [NSException
-          exceptionWithName:@"NoAvailableCaptureSessionException"
-                     reason:@"No capture session available for current capture session."
-                   userInfo:nil];
-      @throw exception;
-    }
+    default:
+      if ([_captureSession canSetSessionPreset:AVCaptureSessionPresetLow]) {
+        _captureSession.sessionPreset = AVCaptureSessionPresetLow;
+        _previewSize = CGSizeMake(352, 288);
+      } else {
+        NSError *error =
+            [NSError errorWithDomain:NSCocoaErrorDomain
+                                code:NSURLErrorUnknown
+                            userInfo:@{
+                              NSLocalizedDescriptionKey :
+                                  @"No capture session available for current capture session."
+                            }];
+        @throw error;
+      }
   }
 }
 
diff --git a/packages/camera/lib/camera.dart b/packages/camera/lib/camera.dart
index 6364e1d..cd2b399 100644
--- a/packages/camera/lib/camera.dart
+++ b/packages/camera/lib/camera.dart
@@ -15,13 +15,40 @@
 
 enum CameraLensDirection { front, back, external }
 
-enum ResolutionPreset { low, medium, high }
+/// Affect the quality of video recording and image capture:
+///
+/// If a preset is not available on the camera being used a preset of lower quality will be selected automatically.
+enum ResolutionPreset {
+  /// 352x288 on iOS, 240p (320x240) on Android
+  low,
+
+  /// 480p (640x480 on iOS, 720x480 on Android)
+  medium,
+
+  /// 720p (1280x720)
+  high,
+
+  /// 1080p (1920x1080)
+  veryHigh,
+
+  /// 2160p (3840x2160)
+  ultraHigh,
+
+  /// The highest resolution available.
+  max,
+}
 
 typedef onLatestImageAvailable = Function(CameraImage image);
 
 /// Returns the resolution preset as a String.
 String serializeResolutionPreset(ResolutionPreset resolutionPreset) {
   switch (resolutionPreset) {
+    case ResolutionPreset.max:
+      return 'max';
+    case ResolutionPreset.ultraHigh:
+      return 'ultraHigh';
+    case ResolutionPreset.veryHigh:
+      return 'veryHigh';
     case ResolutionPreset.high:
       return 'high';
     case ResolutionPreset.medium:
diff --git a/packages/camera/pubspec.yaml b/packages/camera/pubspec.yaml
index a7e65c7..3e290f1 100644
--- a/packages/camera/pubspec.yaml
+++ b/packages/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.5.2+2
+version: 0.5.3
 
 authors:
   - Flutter Team <flutter-dev@googlegroups.com>
@@ -18,10 +18,12 @@
     sdk: flutter
 
 dev_dependencies:
-  flutter_test:
-    sdk: flutter
   path_provider: ^0.5.0
   video_player: ^0.10.0
+  flutter_test:
+    sdk: flutter
+  flutter_driver:
+    sdk: flutter
 
 flutter:
   plugin: