Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[camerax] Implement resolution configuration #3799

Merged
merged 43 commits into from
Sep 8, 2023
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
44d7828
Add resolution configuration
camsim99 Apr 22, 2023
6281eb5
dart side of impl
bparrishMines Apr 26, 2023
9a74cd1
java impls
bparrishMines Apr 26, 2023
03ef376
Merge remote-tracking branch 'upstream/main' into camx_resconfig
camsim99 May 15, 2023
2a5c0fc
Fix implementation
camsim99 May 15, 2023
9001ab0
Small cleanup
camsim99 May 15, 2023
fd19de6
Fixing tests and cleanup minus plugin dart impl test
camsim99 May 17, 2023
c9b62ef
Merge remote-tracking branch 'upstream/main' into camx_resconfig
camsim99 Jul 18, 2023
725be10
Merge remote-tracking branch 'upstream/main' into camx_resconfig
camsim99 Jul 18, 2023
6389ae1
Work on Dart side -- tests, cleanup, bug id
camsim99 Jul 18, 2023
16770de
Fix java unit tests
camsim99 Jul 18, 2023
a8144fa
Fix typo
camsim99 Jul 18, 2023
aba4c34
Fix dart tests
camsim99 Jul 19, 2023
ca5604a
Add integration tests
camsim99 Jul 20, 2023
474745c
Merge remote-tracking branch 'upstream/main' into camx_resconfig
camsim99 Aug 16, 2023
b778894
Add res support
camsim99 Aug 16, 2023
8d2cb3a
correct overrides
camsim99 Aug 16, 2023
a145afe
Merge remote-tracking branch 'upstream/main' into camx_resconfig
camsim99 Aug 16, 2023
fd3ade2
Self review
camsim99 Aug 16, 2023
1dc6fbd
formatting
camsim99 Aug 16, 2023
fad7cbc
Undo strange change
camsim99 Aug 16, 2023
08edc4c
Fix dart unit tests
camsim99 Aug 16, 2023
1def519
Fix integration test
camsim99 Aug 17, 2023
5bab620
Merge remote-tracking branch 'upstream/main' into camx_resconfig
camsim99 Aug 17, 2023
caf819e
Nits
camsim99 Aug 17, 2023
c775756
Update readme
camsim99 Aug 17, 2023
fabaab7
Update packages/camera/camera_android_camerax/lib/src/android_camera_…
camsim99 Aug 22, 2023
810cb54
Update packages/camera/camera_android_camerax/lib/src/android_camera_…
camsim99 Aug 22, 2023
60a47ed
Update packages/camera/camera_android_camerax/lib/src/android_camera_…
camsim99 Aug 22, 2023
474d84c
Update packages/camera/camera_android_camerax/lib/src/android_camera_…
camsim99 Aug 22, 2023
6f45ecb
Add comments to integration tests
camsim99 Aug 22, 2023
5c31de4
Merge branch 'camx_resconfig' of github.com:camsim99/packages into ca…
camsim99 Aug 22, 2023
e4134bd
Updated integration test comment
camsim99 Aug 23, 2023
bb35a61
start quality re-write
camsim99 Aug 29, 2023
5ac1c7c
Replace vid qual const with quality/qualitydata
camsim99 Aug 29, 2023
860b4db
Replace Quality indices with VideoQuality/VideoQualityData class
camsim99 Aug 29, 2023
5ea328e
Merge remote-tracking branch 'upstream/main' into camx_resconfig
camsim99 Aug 29, 2023
a163f0e
Fix versionining
camsim99 Aug 29, 2023
2a9671e
Format
camsim99 Aug 29, 2023
0178493
run tests
camsim99 Aug 29, 2023
ff9bd3a
nits
camsim99 Sep 7, 2023
396091c
Merge remote-tracking branch 'upstream/main' into camx_resconfig
camsim99 Sep 7, 2023
206fbb6
Fix changelog
camsim99 Sep 7, 2023
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
3 changes: 2 additions & 1 deletion packages/camera/camera_android_camerax/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## NEXT
## 0.5.0+16

* Updates minimum supported SDK version to Flutter 3.7/Dart 2.19.
* Implements resolution configuration for all camera use cases.

## 0.5.0+15

Expand Down
10 changes: 6 additions & 4 deletions packages/camera/camera_android_camerax/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,18 @@ dependencies:

## Missing features and limitations

### Resolution configuration \[[Issue #120462][120462]\]

Any specified `ResolutionPreset` wll go unused in favor of CameraX defaults and
`onCameraResolutionChanged` is unimplemented.
### 240p resolution configuration for video recording

240p resolution configuration for video recording is unsupported by CameraX,
and thus, the plugin will fall back to 480p if configured with a
`ResolutionPreset`.

### Locking/Unlocking capture orientation \[[Issue #125915][125915]\]

`lockCaptureOrientation` & `unLockCaptureOrientation` are unimplemented.

### Flash mode configuration \[[Issue #120715][120715]\]
### Torch mode \[[Issue #120715][120715]\]

Calling `setFlashMode` with mode `FlashMode.torch` currently does nothing.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,11 @@ private VideoQualityConstraint(final int index) {
}
}

/** Fallback rules for selecting video resolution. */
/**
* Fallback rules for selecting video resolution.
*
* <p>See https://developer.android.com/reference/androidx/camera/video/FallbackStrategy.
*/
public enum VideoResolutionFallbackRule {
HIGHER_QUALITY_OR_LOWER_THAN(0),
HIGHER_QUALITY_THAN(1),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ public static class QualitySelectorProxy {
@Nullable FallbackStrategy fallbackStrategy) {
// Convert each index of VideoQualityConstraint to Quality.
List<Quality> qualityList = new ArrayList<Quality>();
for (Long qualityIndex : videoQualityConstraintIndexList) {
for (int i = 0; i < videoQualityConstraintIndexList.size(); i++) {
int qualityIndex = ((Number) videoQualityConstraintIndexList.get(i)).intValue();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a fix for Long -> Integer casting issues that the integration test revealed.

Copy link
Member

Choose a reason for hiding this comment

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

What was the issue? Its not clear to me why the previous code would fail - it looks like the logical change here is to go from: calling intValue() on the Long, to: casting to a Number and then using the intValue() implementation for Number instead.

But I would expect either of those to work, and if they both did, would prefer the former. Does this only work with the cast to Number added in?

Copy link
Contributor Author

@camsim99 camsim99 Aug 22, 2023

Choose a reason for hiding this comment

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

Using your former suggestion, this is an example of the error I got: https://firebase.corp.google.com/project/flutter-infra-staging/testlab/histories/bh.90e767c88242d274/matrices/6437969997948729501/executions/bs.a725e88a067fbf4d/testcases/2/test-cases.

I'm open to suggestions, but I could not find a fix and after investigating, it's unclear why I'm getting the error and only in the Firebase test lab like that. May be something with Java versions but unsure.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Turns out this is an issue because I was trying to index an array with a Long, most likely. After talking to @reidbaker, I'm going to take his suggestion and rework how I send aQuality from Dart to Java rather than trying to send indices of enums that represent Qualitys.

qualityList.add(getQualityConstant(qualityIndex));
}

Expand All @@ -59,8 +60,8 @@ public static class QualitySelectorProxy {
}

/** Converts from index of {@link VideoQualityConstraint} to {@link Quality}. */
private Quality getQualityConstant(@NonNull Long qualityIndex) {
VideoQualityConstraint quality = VideoQualityConstraint.values()[qualityIndex.intValue()];
private Quality getQualityConstant(@NonNull int qualityIndex) {
VideoQualityConstraint quality = VideoQualityConstraint.values()[qualityIndex];
return getQualityFromVideoQualityConstraint(quality);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
import 'dart:io';
import 'dart:ui';

import 'package:camera_android_camerax/camera_android_camerax.dart';
import 'package:camera_android_camerax_example/camera_controller.dart';
import 'package:camera_android_camerax_example/camera_image.dart';
import 'package:camera_platform_interface/camera_platform_interface.dart';
import 'package:flutter/painting.dart';
import 'package:flutter_test/flutter_test.dart';
Expand All @@ -19,6 +21,46 @@ void main() {
CameraPlatform.instance = AndroidCameraCameraX();
});

final Map<ResolutionPreset, Size> presetExpectedSizes =
<ResolutionPreset, Size>{
ResolutionPreset.low: const Size(240, 320),
ResolutionPreset.medium: const Size(480, 720),
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
camsim99 marked this conversation as resolved.
Show resolved Hide resolved
/// [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]!;

// Take Picture
final XFile file = await controller.takePicture();

// Load picture
final File fileImage = File(file.path);
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()));
}

testWidgets('availableCameras only supports valid back or front cameras',
(WidgetTester tester) async {
final List<CameraDescription> availableCameras =
Expand All @@ -31,27 +73,94 @@ void main() {
}
});

testWidgets('takePictures stores a valid image in memory',
testWidgets('Capture specific image resolutions',
(WidgetTester tester) async {
final List<CameraDescription> availableCameras =
final List<CameraDescription> cameras =
await CameraPlatform.instance.availableCameras();
if (availableCameras.isEmpty) {
if (cameras.isEmpty) {
return;
}
for (final CameraDescription cameraDescription in availableCameras) {
final CameraController controller =
CameraController(cameraDescription, ResolutionPreset.high);
await controller.initialize();
for (final CameraDescription cameraDescription in cameras) {
bool previousPresetExactlySupported = true;
for (final 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);
expect(!(!previousPresetExactlySupported && presetExactlySupported),
isTrue,
reason:
'The camera took higher resolution pictures at a lower resolution.');
previousPresetExactlySupported = presetExactlySupported;
await controller.dispose();
}
}
});

testWidgets('Preview takes expected resolution from preset',
(WidgetTester tester) async {
final List<CameraDescription> cameras =
await CameraPlatform.instance.availableCameras();
if (cameras.isEmpty) {
return;
}
for (final CameraDescription cameraDescription in cameras) {
bool previousPresetExactlySupported = true;
for (final MapEntry<ResolutionPreset, Size> preset
in presetExpectedSizes.entries) {
final CameraController controller =
CameraController(cameraDescription, preset.key);

await controller.initialize();

while (controller.value.previewSize == null) {
// Wait for preview size to update.
}

final bool presetExactlySupported = assertExpectedDimensions(
presetExpectedSizes[preset.key]!, controller.value.previewSize!);
expect(!(!previousPresetExactlySupported && presetExactlySupported),
isTrue,
reason: 'The preview has a lower resolution than that specified.');
previousPresetExactlySupported = presetExactlySupported;
await controller.dispose();
}
}
});

// Take Picture
final XFile file = await controller.takePicture();
testWidgets('Images from streaming have expected resolution from preset',
(WidgetTester tester) async {
final List<CameraDescription> cameras =
await CameraPlatform.instance.availableCameras();
if (cameras.isEmpty) {
return;
}
for (final CameraDescription cameraDescription in cameras) {
bool previousPresetExactlySupported = true;
for (final MapEntry<ResolutionPreset, Size> preset
in presetExpectedSizes.entries) {
final CameraController controller =
CameraController(cameraDescription, preset.key);
final Completer<CameraImage> imageCompleter = Completer<CameraImage>();
await controller.initialize();
await controller.startImageStream((CameraImage image) {
imageCompleter.complete(image);
controller.stopImageStream();
});

// Try loading picture
final File fileImage = File(file.path);
final Image image =
await decodeImageFromList(fileImage.readAsBytesSync());
final CameraImage image = await imageCompleter.future;
final bool presetExactlySupported = assertExpectedDimensions(
presetExpectedSizes[preset.key]!,
camsim99 marked this conversation as resolved.
Show resolved Hide resolved
Size(image.height.toDouble(), image.width.toDouble()));
expect(!(!previousPresetExactlySupported && presetExactlySupported),
Copy link
Member

Choose a reason for hiding this comment

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

This expect is checking that (for a specific camera on the device) presetExactlySupported is either always false or always true (across all resolutions). Is the reason here that it should either always fallback, or always be able to exactly support?

Can you add a comment either way explaining the purpose, as its not immediately clear to me from reading the test

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added a comment and also re-wrote the boolean since it's overcomplicated. By the way, this is the integration test in camera_android, so I too had to interpret these test cases. My understanding is that if one of the lower resolution presets must fall back, then we wouldn't expect the camera to suddenly be able to reach higher resolutions; that would indicate something is wrong with our logic.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Replaced VideoQualityConstraint with VideoQuality/VideoQualityData to take advantage of pigeon's support for enums + classes.

isTrue,
reason: 'The preview has a lower resolution than that specified.');
previousPresetExactlySupported = presetExactlySupported;

expect(image, isNotNull);
await controller.dispose();
}
}
});
}
Loading