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

[Camera] Improved resolution presets #1952

Merged
merged 12 commits into from
Aug 8, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/camera/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -46,7 +48,6 @@ public class Camera {
private final String cameraName;
private final Size captureSize;
private final Size previewSize;
private final Size videoSize;
private final boolean enableAudio;

private CameraDevice cameraDevice;
Expand All @@ -57,8 +58,19 @@ public class Camera {
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,
Expand Down Expand Up @@ -87,21 +99,6 @@ public void onOrientationChanged(int i) {
};
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);
Expand All @@ -110,12 +107,11 @@ public void onOrientationChanged(int i) {
//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) {
Expand Down Expand Up @@ -143,13 +139,13 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException {
// 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());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -25,61 +24,14 @@ public final class CameraUtils {

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;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do you set the preset to high here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

i think it's that high res previews negatively impact performance. The current code in master is setting previewSize to be the absolute minimum resolution that's the correct aspect ratio, and is also capped at 1080 (line 37 in the original patch).

Copy link

Choose a reason for hiding this comment

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

Maybe you could improve to let the choice to the user here. My current use case need a stream at the highest resolution. And without this clamp, the function startPreviewWithImageStream works perfectly.

Choose a reason for hiding this comment

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

It would be acceptable to set the default to high or even medium. But this change actually cripples the powerful Android devices that default to 1080p video even at 60fps.

}

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) {
Expand Down Expand Up @@ -118,6 +70,46 @@ public static List<Map<String, Object>> getAvailableCameras(Activity activity)
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) {
Expand Down
2 changes: 1 addition & 1 deletion packages/camera/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
}
controller = CameraController(
cameraDescription,
ResolutionPreset.high,
ResolutionPreset.medium,
enableAudio: enableAudio,
);

Expand Down
2 changes: 2 additions & 0 deletions packages/camera/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
flutter_driver:
sdk: flutter

flutter:
uses-material-design: true
Loading