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

[camera] android-rework part 5: Android FPS range, resolution and sensor orientation features #3799

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2b7aa9b
Base classes to support Android camera features
mvanbeusekom Apr 8, 2021
f780742
Fixed formatting
mvanbeusekom Apr 8, 2021
76bc5bd
Applied feedback from PR
mvanbeusekom Apr 20, 2021
0bbed99
Added Android FPS range, resolution and sensor orientation features
mvanbeusekom Apr 8, 2021
1ba738d
Use mockito-inline
mvanbeusekom Apr 9, 2021
de4e70f
Merge remote-tracking branch 'upstream/master' into camera-android/fp…
mvanbeusekom Apr 21, 2021
728346a
Fix issue Pixel 4A
mvanbeusekom May 26, 2021
c014fe3
Merge remote-tracking branch 'upstream/master' into camera-android/fp…
mvanbeusekom May 31, 2021
84f5e73
Added API documentation
mvanbeusekom May 31, 2021
f763f77
Processed feedback on PR
mvanbeusekom May 31, 2021
4a7c73a
Fix formatting
mvanbeusekom May 31, 2021
a890919
Fix formatting
mvanbeusekom May 31, 2021
55a6702
Only exclude 60 FPS limit for Pixel 4a
mvanbeusekom Jun 8, 2021
cd53321
Removed redundant empty line
mvanbeusekom Jun 8, 2021
35831d3
Fixed comment
mvanbeusekom Jun 8, 2021
a9f3142
Test Pixel 4a workaround
mvanbeusekom Jun 8, 2021
551800e
Add tests for orientation updates
mvanbeusekom Jun 10, 2021
68cbc56
Fix formatting
mvanbeusekom Jun 10, 2021
1b137c2
Fix formatting
mvanbeusekom Jun 10, 2021
6514a00
Added missing license header
mvanbeusekom Jun 10, 2021
7f0180e
Accept cameraName as String
mvanbeusekom Jun 16, 2021
24af367
Format
mvanbeusekom Jun 16, 2021
8313dd0
Removed obsolete comment
mvanbeusekom Jun 16, 2021
a39c2e1
update method structure in class to a more logical order
mvanbeusekom Jun 16, 2021
3eecfe9
Merge remote-tracking branch 'origin/master' into camera-android/fps_…
mvanbeusekom Jun 22, 2021
7299b1d
Fix formatting
mvanbeusekom Jun 22, 2021
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import java.util.HashMap;
import java.util.Map;

class DartMessenger {
public class DartMessenger {
@NonNull private final Handler handler;
@Nullable private MethodChannel cameraChannel;
@Nullable private MethodChannel deviceChannel;
Expand Down Expand Up @@ -48,7 +48,7 @@ enum CameraEventType {
this.handler = handler;
}

void sendDeviceOrientationChangeEvent(PlatformChannel.DeviceOrientation orientation) {
public void sendDeviceOrientationChangeEvent(PlatformChannel.DeviceOrientation orientation) {
assert (orientation != null);
this.send(
DeviceEventType.ORIENTATION_CHANGED,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// 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.

package io.flutter.plugins.camera.features.fpsrange;

import android.hardware.camera2.CaptureRequest;
import android.os.Build;
import android.util.Range;
import io.flutter.plugins.camera.CameraProperties;
import io.flutter.plugins.camera.features.CameraFeature;

/**
* Controls the frames per seconds (FPS) range configuration on the {@link android.hardware.camera2}
* API.
*/
public class FpsRangeFeature extends CameraFeature<Range<Integer>> {
private static final Range<Integer> MAX_PIXEL4A_RANGE = new Range<>(30, 30);
private Range<Integer> currentSetting;

/**
* Creates a new instance of the {@link FpsRangeFeature}.
*
* @param cameraProperties Collection of characteristics for the current camera device.
*/
public FpsRangeFeature(CameraProperties cameraProperties) {
super(cameraProperties);

if (isPixel4A()) {
// HACK: There is a bug in the Pixel 4A where it cannot support 60fps modes
// even though they are reported as supported by
// `getControlAutoExposureAvailableTargetFpsRanges`.
// For max device compatibility we will keep FPS under 60 even if they report they are
// capable of achieving 60 fps. Highest working FPS is 30.
// https://issuetracker.google.com/issues/189237151
currentSetting = MAX_PIXEL4A_RANGE;
} else {
Range<Integer>[] ranges = cameraProperties.getControlAutoExposureAvailableTargetFpsRanges();

if (ranges != null) {
for (Range<Integer> range : ranges) {
int upper = range.getUpper();

if (upper >= 10) {
if (currentSetting == null || upper > currentSetting.getUpper()) {
currentSetting = range;
}
}
}
}
}
}

private boolean isPixel4A() {
return Build.BRAND.equals("google") && Build.MODEL.equals("Pixel 4a");
}

@Override
public String getDebugName() {
return "FpsRangeFeature";
}

@Override
public Range<Integer> getValue() {
return currentSetting;
}

@Override
public void setValue(Range<Integer> value) {
this.currentSetting = value;
}

// Always supported
@Override
public boolean checkIsSupported() {
return true;
}

@Override
public void updateBuilder(CaptureRequest.Builder requestBuilder) {
if (!checkIsSupported()) {
return;
}

requestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, currentSetting);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// 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.

package io.flutter.plugins.camera.features.resolution;

import android.hardware.camera2.CaptureRequest;
import android.media.CamcorderProfile;
import android.util.Size;
import androidx.annotation.VisibleForTesting;
import io.flutter.plugins.camera.CameraProperties;
import io.flutter.plugins.camera.features.CameraFeature;

/**
* Controls the resolutions configuration on the {@link android.hardware.camera2} API.
*
* <p>The {@link ResolutionFeature} is responsible for converting the platform independent {@link
* ResolutionPreset} into a {@link android.media.CamcorderProfile} which contains all the properties
* required to configure the resolution using the {@link android.hardware.camera2} API.
*/
public class ResolutionFeature extends CameraFeature<ResolutionPreset> {
private Size captureSize;
private Size previewSize;
private CamcorderProfile recordingProfile;
private ResolutionPreset currentSetting;
private int cameraId;

/**
* Creates a new instance of the {@link ResolutionFeature}.
*
* @param cameraProperties Collection of characteristics for the current camera device.
* @param resolutionPreset Platform agnostic enum containing resolution information.
* @param cameraName Camera identifier of the camera for which to configure the resolution.
*/
public ResolutionFeature(
CameraProperties cameraProperties, ResolutionPreset resolutionPreset, String cameraName) {
super(cameraProperties);
this.currentSetting = resolutionPreset;
try {
this.cameraId = Integer.parseInt(cameraName, 10);
} catch (NumberFormatException e) {
this.cameraId = -1;
return;
}
configureResolution(resolutionPreset, cameraId);
}

/**
* Gets the {@link android.media.CamcorderProfile} containing the information to configure the
* resolution using the {@link android.hardware.camera2} API.
*
* @return Resolution information to configure the {@link android.hardware.camera2} API.
*/
public CamcorderProfile getRecordingProfile() {
return this.recordingProfile;
}

/**
* Gets the optimal preview size based on the configured resolution.
*
* @return The optimal preview size.
*/
public Size getPreviewSize() {
return this.previewSize;
}

/**
* Gets the optimal capture size based on the configured resolution.
*
* @return The optimal capture size.
*/
public Size getCaptureSize() {
return this.captureSize;
}

@Override
public String getDebugName() {
return "ResolutionFeature";
}

@Override
public ResolutionPreset getValue() {
return currentSetting;
}

@Override
public void setValue(ResolutionPreset value) {
this.currentSetting = value;
configureResolution(currentSetting, cameraId);
}

@Override
public boolean checkIsSupported() {
return cameraId >= 0;
}

@Override
public void updateBuilder(CaptureRequest.Builder requestBuilder) {
// No-op: when setting a resolution there is no need to update the request builder.
}

@VisibleForTesting
static Size computeBestPreviewSize(int cameraId, ResolutionPreset preset) {
if (preset.ordinal() > ResolutionPreset.high.ordinal()) {
preset = ResolutionPreset.high;
}

CamcorderProfile profile =
getBestAvailableCamcorderProfileForResolutionPreset(cameraId, preset);
return new Size(profile.videoFrameWidth, profile.videoFrameHeight);
}

/**
* Gets the best possible {@link android.media.CamcorderProfile} for the supplied {@link
* ResolutionPreset}.
*
* @param cameraId Camera identifier which indicates the device's camera for which to select a
* {@link android.media.CamcorderProfile}.
* @param preset The {@link ResolutionPreset} for which is to be translated to a {@link
* android.media.CamcorderProfile}.
* @return The best possible {@link android.media.CamcorderProfile} that matches the supplied
* {@link ResolutionPreset}.
*/
public static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPreset(
int cameraId, ResolutionPreset preset) {
if (cameraId < 0) {
throw new AssertionError(
"getBestAvailableCamcorderProfileForResolutionPreset can only be used with valid (>=0) camera identifiers.");
}

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(cameraId, CamcorderProfile.QUALITY_HIGH);
}
case ultraHigh:
if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_2160P)) {
return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_2160P);
}
case veryHigh:
if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_1080P)) {
return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_1080P);
}
case high:
if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_720P)) {
return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_720P);
}
case medium:
if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_480P)) {
return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_480P);
}
case low:
if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_QVGA)) {
return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_QVGA);
}
default:
if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_LOW)) {
return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_LOW);
} else {
throw new IllegalArgumentException(
"No capture session available for current capture session.");
}
}
}

private void configureResolution(ResolutionPreset resolutionPreset, int cameraId) {
if (!checkIsSupported()) {
return;
}
recordingProfile =
getBestAvailableCamcorderProfileForResolutionPreset(cameraId, resolutionPreset);
captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight);
previewSize = computeBestPreviewSize(cameraId, resolutionPreset);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// 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.

package io.flutter.plugins.camera.features.resolution;

// Mirrors camera.dart
public enum ResolutionPreset {
low,
medium,
high,
veryHigh,
ultraHigh,
max,
}
Loading