Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Added API documentation
  • Loading branch information
mvanbeusekom committed May 31, 2021
commit 84f5e73e635d3a83210a46190321a1bc81c2f0ef
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,25 @@
package io.flutter.plugins.camera.features.fpsrange;

import android.hardware.camera2.CaptureRequest;
import android.util.Log;
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 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);

Log.i("Camera", "getAvailableFpsRange");

Range<Integer>[] ranges = cameraProperties.getControlAutoExposureAvailableTargetFpsRanges();

if (ranges != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,50 @@
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.
*
* 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 final Size captureSize;
private final Size previewSize;
private final CamcorderProfile recordingProfile;
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 cameraId Camera identifier of the camera for which to configure the resolution.
*/
public ResolutionFeature(
CameraProperties cameraProperties, ResolutionPreset initialSetting, String cameraName) {
CameraProperties cameraProperties, ResolutionPreset resolutionPreset, int cameraId) {
super(cameraProperties);
setValue(initialSetting);
this.currentSetting = resolutionPreset;
this.cameraId = cameraId;

// Resolution configuration
recordingProfile =
getBestAvailableCamcorderProfileForResolutionPreset(cameraName, initialSetting);
captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight);

previewSize = computeBestPreviewSize(cameraName, initialSetting);
configureResolution(resolutionPreset, cameraId);
}

/**
* 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(
String cameraName, ResolutionPreset preset) {
int cameraId = Integer.parseInt(cameraName);
int cameraId, ResolutionPreset preset) {

switch (preset) {
// All of these cases deliberately fall through to get the best available profile.
case max:
Expand Down Expand Up @@ -68,16 +87,25 @@ public static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPres
}
}

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

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

private void configureResolution(ResolutionPreset resolutionPreset, int cameraId) {
recordingProfile =
getBestAvailableCamcorderProfileForResolutionPreset(cameraId, resolutionPreset);
captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight);

previewSize = computeBestPreviewSize(cameraId, resolutionPreset);
}

@Override
public String getDebugName() {
return "ResolutionFeature";
Expand All @@ -91,6 +119,7 @@ public ResolutionPreset getValue() {
@Override
public void setValue(ResolutionPreset value) {
this.currentSetting = value;
configureResolution(currentSetting, cameraId);
}

// Always supported
Expand All @@ -104,14 +133,30 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) {
// No-op: when setting a resolution there is no need to update the request builder.
}

/**
* 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;
}

/**
* Get the optimal capture size based on the configured resolution.
*
* @return The optimal capture size.
*/
public Size getCaptureSize() {
return this.captureSize;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
import io.flutter.embedding.engine.systemchannels.PlatformChannel;
import io.flutter.plugins.camera.DartMessenger;

/**
* Support class to help to determine the media orientation based on the orientation of the
* device.
*/
public class DeviceOrientationManager {

private static final IntentFilter orientationIntentFilter =
Expand Down Expand Up @@ -53,20 +57,61 @@ private DeviceOrientationManager(
this.sensorOrientation = sensorOrientation;
}

/**
* Starts listening to the device's sensors and UI for orientation updates.
*
* When either the sensor or UI listeners indicate the orientation has changed the updated
* orientation is send to the client using the {@link DartMessenger}.
*/
public void start() {
startSensorListener();
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you add a comment here explaining why we listen to both? It's not clear to me why we need to compute the orientation two different ways. Is it a case of needing a fallback if one isn't available?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

startUIListener();
}

/**
* Stops listening for orientation updates.
*/
public void stop() {
stopSensorListener();
stopUIListener();
}

/**
* Returns the last captured orientation in degrees based on sensor or UI information.
*
* The orientation is returned in degrees and could be one of the following values:
* <p>
* <ul>
* <li>0: Indicates the device is currently in portrait.</li>
* <li>90: Indicates the device is currently in landscape left.</li>
* <li>180: Indicates the device is currently in portrait down.</li>
* <li>270: Indicates the device is currently in landscape right.</li>
* </ul>
* </p>
*
* @return The last captured orientation in degrees
*/
public int getMediaOrientation() {
return this.getMediaOrientation(this.lastOrientation);
}

/**
* Returns the device's orientation in degrees based on the supplied {@link
* PlatformChannel.DeviceOrientation} value.
*
* <p>
*
* <ul>
* <li>PORTRAIT_UP: converts to 0 degrees.</li>
* <li>LANDSCAPE_LEFT: converts to 90 degrees.</li>
* <li>PORTRAIT_DOWN: converts to 180 degrees.</li>
* <li>LANDSCAPE_RIGHT: converts to 270 degrees.</li>
* </ul>
*
* @param orientation The {@link PlatformChannel.DeviceOrientation} value that is to be converted
* into degrees.
* @return The device's orientation in degrees.
*/
public int getMediaOrientation(PlatformChannel.DeviceOrientation orientation) {
int angle = 0;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,23 @@
import io.flutter.plugins.camera.CameraProperties;
import io.flutter.plugins.camera.DartMessenger;
import io.flutter.plugins.camera.features.CameraFeature;
import io.flutter.plugins.camera.features.resolution.ResolutionFeature;

/** Provides access to the sensor orientation of the camera devices. */
public class SensorOrientationFeature extends CameraFeature<Integer> {
private Integer currentSetting = 0;
private final DeviceOrientationManager deviceOrientationListener;
private PlatformChannel.DeviceOrientation lockedCaptureOrientation;

/**
* Creates a new instance of the {@link ResolutionFeature}.
*
* @param cameraProperties Collection of characteristics for the current camera device.
* @param activity Current Android {@link android.app.Activity}, used to detect UI orientation
* changes.
* @param dartMessenger Instance of a {@link DartMessenger} used to communicate orientation
* updates back to the client.
*/
public SensorOrientationFeature(
@NonNull CameraProperties cameraProperties,
@NonNull Activity activity,
Expand Down Expand Up @@ -56,18 +67,37 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) {
// Noop: when setting the sensor orientation there is no need to update the request builder.
}

/**
* Gets the instance of the {@link DeviceOrientationManager} used to detect orientation changes.
* @return The instance of the {@link DeviceOrientationManager}.
*/
public DeviceOrientationManager getDeviceOrientationManager() {
return this.deviceOrientationListener;
}

/**
* Lock the capture orientation, indicating that the device orientation should not influence the
* capture orientation.
*
* @param orientation The orientation in which to lock the capture orientation.
*/
public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) {
this.lockedCaptureOrientation = orientation;
}

/**
* Unlock the capture orientation, indicating that the device orientation should be used to
* configure the capture orientation.
*/
public void unlockCaptureOrientation() {
this.lockedCaptureOrientation = null;
}

/**
* Gets the configured locked capture orientation.
*
* @return The configured locked capture orientation.
*/
public PlatformChannel.DeviceOrientation getLockedCaptureOrientation() {
return this.lockedCaptureOrientation;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import org.mockito.MockedStatic;

public class ResolutionFeatureTest {
private static final String cameraId = "1";
private static final int cameraId = 1;
private CamcorderProfile mockProfileLow;
private MockedStatic<CamcorderProfile> mockedStaticProfile;

Expand Down Expand Up @@ -143,47 +143,47 @@ public void getBestAvailableCamcorderProfileForResolutionPreset_should_fall_thro
assertEquals(
mockProfileLow,
ResolutionFeature.getBestAvailableCamcorderProfileForResolutionPreset(
"1", ResolutionPreset.max));
1, ResolutionPreset.max));
}

@Test
public void computeBestPreviewSize_should_use_720P_when_resolution_preset_max() {
ResolutionFeature.computeBestPreviewSize("1", ResolutionPreset.max);
ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.max);

mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P));
}

@Test
public void computeBestPreviewSize_should_use_720P_when_resolution_preset_ultraHigh() {
ResolutionFeature.computeBestPreviewSize("1", ResolutionPreset.ultraHigh);
ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.ultraHigh);

mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P));
}

@Test
public void computeBestPreviewSize_should_use_720P_when_resolution_preset_veryHigh() {
ResolutionFeature.computeBestPreviewSize("1", ResolutionPreset.veryHigh);
ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.veryHigh);

mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P));
}

@Test
public void computeBestPreviewSize_should_use_720P_when_resolution_preset_high() {
ResolutionFeature.computeBestPreviewSize("1", ResolutionPreset.high);
ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.high);

mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P));
}

@Test
public void computeBestPreviewSize_should_use_480P_when_resolution_preset_medium() {
ResolutionFeature.computeBestPreviewSize("1", ResolutionPreset.medium);
ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.medium);

mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_480P));
}

@Test
public void computeBestPreviewSize_should_use_QVGA_when_resolution_preset_low() {
ResolutionFeature.computeBestPreviewSize("1", ResolutionPreset.low);
ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.low);

mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_QVGA));
}
Expand Down