Skip to content

Commit f61723b

Browse files
authored
[camerax] Use AspectRatioStrategy to help automatic selection of expected resolution (#6357)
Defines `AspectRatioStategy`s that will help CameraX select the resolution we expect. Fixes flutter/flutter#144363.
1 parent e6b3e11 commit f61723b

22 files changed

+1431
-428
lines changed

packages/camera/camera_android_camerax/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 0.6.1
2+
3+
* Modifies resolution selection logic to use an `AspectRatioStrategy` for all aspect ratios supported by CameraX.
4+
* Adds `ResolutionFilter` to resolution selection logic to prioritize resolutions that match
5+
the defined `ResolutionPreset`s.
6+
17
## 0.6.0+1
28

39
* Updates `README.md` to encourage developers to opt into this implementation of the camera plugin.

packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ public void setUp(
134134
binaryMessenger, new FocusMeteringResultHostApiImpl(instanceManager));
135135
meteringPointHostApiImpl = new MeteringPointHostApiImpl(instanceManager);
136136
GeneratedCameraXLibrary.MeteringPointHostApi.setup(binaryMessenger, meteringPointHostApiImpl);
137+
GeneratedCameraXLibrary.ResolutionFilterHostApi.setup(
138+
binaryMessenger, new ResolutionFilterHostApiImpl(instanceManager));
137139
}
138140

139141
@Override

packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2507,6 +2507,7 @@ public interface ResolutionSelectorHostApi {
25072507
void create(
25082508
@NonNull Long identifier,
25092509
@Nullable Long resolutionStrategyIdentifier,
2510+
@Nullable Long resolutionSelectorIdentifier,
25102511
@Nullable Long aspectRatioStrategyIdentifier);
25112512

25122513
/** The codec used by ResolutionSelectorHostApi. */
@@ -2530,13 +2531,17 @@ static void setup(
25302531
ArrayList<Object> args = (ArrayList<Object>) message;
25312532
Number identifierArg = (Number) args.get(0);
25322533
Number resolutionStrategyIdentifierArg = (Number) args.get(1);
2533-
Number aspectRatioStrategyIdentifierArg = (Number) args.get(2);
2534+
Number resolutionSelectorIdentifierArg = (Number) args.get(2);
2535+
Number aspectRatioStrategyIdentifierArg = (Number) args.get(3);
25342536
try {
25352537
api.create(
25362538
(identifierArg == null) ? null : identifierArg.longValue(),
25372539
(resolutionStrategyIdentifierArg == null)
25382540
? null
25392541
: resolutionStrategyIdentifierArg.longValue(),
2542+
(resolutionSelectorIdentifierArg == null)
2543+
? null
2544+
: resolutionSelectorIdentifierArg.longValue(),
25402545
(aspectRatioStrategyIdentifierArg == null)
25412546
? null
25422547
: aspectRatioStrategyIdentifierArg.longValue());
@@ -4189,4 +4194,77 @@ public void error(Throwable error) {
41894194
}
41904195
}
41914196
}
4197+
4198+
private static class ResolutionFilterHostApiCodec extends StandardMessageCodec {
4199+
public static final ResolutionFilterHostApiCodec INSTANCE = new ResolutionFilterHostApiCodec();
4200+
4201+
private ResolutionFilterHostApiCodec() {}
4202+
4203+
@Override
4204+
protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) {
4205+
switch (type) {
4206+
case (byte) 128:
4207+
return ResolutionInfo.fromList((ArrayList<Object>) readValue(buffer));
4208+
default:
4209+
return super.readValueOfType(type, buffer);
4210+
}
4211+
}
4212+
4213+
@Override
4214+
protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) {
4215+
if (value instanceof ResolutionInfo) {
4216+
stream.write(128);
4217+
writeValue(stream, ((ResolutionInfo) value).toList());
4218+
} else {
4219+
super.writeValue(stream, value);
4220+
}
4221+
}
4222+
}
4223+
4224+
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
4225+
public interface ResolutionFilterHostApi {
4226+
4227+
void createWithOnePreferredSize(
4228+
@NonNull Long identifier, @NonNull ResolutionInfo preferredResolution);
4229+
4230+
/** The codec used by ResolutionFilterHostApi. */
4231+
static @NonNull MessageCodec<Object> getCodec() {
4232+
return ResolutionFilterHostApiCodec.INSTANCE;
4233+
}
4234+
/**
4235+
* Sets up an instance of `ResolutionFilterHostApi` to handle messages through the
4236+
* `binaryMessenger`.
4237+
*/
4238+
static void setup(
4239+
@NonNull BinaryMessenger binaryMessenger, @Nullable ResolutionFilterHostApi api) {
4240+
{
4241+
BasicMessageChannel<Object> channel =
4242+
new BasicMessageChannel<>(
4243+
binaryMessenger,
4244+
"dev.flutter.pigeon.ResolutionFilterHostApi.createWithOnePreferredSize",
4245+
getCodec());
4246+
if (api != null) {
4247+
channel.setMessageHandler(
4248+
(message, reply) -> {
4249+
ArrayList<Object> wrapped = new ArrayList<Object>();
4250+
ArrayList<Object> args = (ArrayList<Object>) message;
4251+
Number identifierArg = (Number) args.get(0);
4252+
ResolutionInfo preferredResolutionArg = (ResolutionInfo) args.get(1);
4253+
try {
4254+
api.createWithOnePreferredSize(
4255+
(identifierArg == null) ? null : identifierArg.longValue(),
4256+
preferredResolutionArg);
4257+
wrapped.add(0, null);
4258+
} catch (Throwable exception) {
4259+
ArrayList<Object> wrappedError = wrapError(exception);
4260+
wrapped = wrappedError;
4261+
}
4262+
reply.reply(wrapped);
4263+
});
4264+
} else {
4265+
channel.setMessageHandler(null);
4266+
}
4267+
}
4268+
}
4269+
}
41924270
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.plugins.camerax;
6+
7+
import android.util.Size;
8+
import androidx.annotation.NonNull;
9+
import androidx.annotation.VisibleForTesting;
10+
import androidx.camera.core.resolutionselector.ResolutionFilter;
11+
import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ResolutionFilterHostApi;
12+
import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ResolutionInfo;
13+
import java.util.List;
14+
15+
/**
16+
* Host API implementation for {@link ResolutionFilter}.
17+
*
18+
* <p>This class handles instantiating and adding native object instances that are attached to a
19+
* Dart instance or handle method calls on the associated native class or an instance of the class.
20+
*/
21+
public class ResolutionFilterHostApiImpl implements ResolutionFilterHostApi {
22+
private final InstanceManager instanceManager;
23+
private final ResolutionFilterFactory resolutionFilterFactory;
24+
25+
/**
26+
* Proxy for constructing {@link ResolutionFilter}s with particular attributes, as detailed by
27+
* documentation below.
28+
*/
29+
@VisibleForTesting
30+
public static class ResolutionFilterFactory {
31+
/**
32+
* Creates an instance of {@link ResolutionFilter} that moves the {@code preferredSize} to the
33+
* front of the list of supported resolutions so that it can be prioritized by CameraX.
34+
*
35+
* <p>If the preferred {@code Size} is not found, then this creates a {@link ResolutionFilter}
36+
* that leaves the priority of supported resolutions unadjusted.
37+
*/
38+
@NonNull
39+
public ResolutionFilter createWithOnePreferredSize(@NonNull Size preferredSize) {
40+
return new ResolutionFilter() {
41+
@Override
42+
@NonNull
43+
public List<Size> filter(@NonNull List<Size> supportedSizes, int rotationDegrees) {
44+
int preferredSizeIndex = supportedSizes.indexOf(preferredSize);
45+
46+
if (preferredSizeIndex > -1) {
47+
supportedSizes.remove(preferredSizeIndex);
48+
supportedSizes.add(0, preferredSize);
49+
}
50+
51+
return supportedSizes;
52+
}
53+
};
54+
}
55+
}
56+
57+
/**
58+
* Constructs a {@link ResolutionFilterHostApiImpl}.
59+
*
60+
* @param instanceManager maintains instances stored to communicate with attached Dart objects
61+
*/
62+
public ResolutionFilterHostApiImpl(@NonNull InstanceManager instanceManager) {
63+
this(instanceManager, new ResolutionFilterFactory());
64+
}
65+
66+
/**
67+
* Constructs a {@link ResolutionFilterHostApiImpl}.
68+
*
69+
* @param instanceManager maintains instances stored to communicate with attached Dart objects
70+
* @param resolutionFilterFactory proxy for constructing different kinds of {@link
71+
* ResolutionFilter}s
72+
*/
73+
@VisibleForTesting
74+
ResolutionFilterHostApiImpl(
75+
@NonNull InstanceManager instanceManager,
76+
@NonNull ResolutionFilterFactory resolutionFilterFactory) {
77+
this.instanceManager = instanceManager;
78+
this.resolutionFilterFactory = resolutionFilterFactory;
79+
}
80+
81+
/**
82+
* Creates a {@link ResolutionFilter} that prioritizes the specified {@code preferredResolution}
83+
* over all other resolutions.
84+
*/
85+
@Override
86+
public void createWithOnePreferredSize(
87+
@NonNull Long identifier, @NonNull ResolutionInfo preferredResolution) {
88+
Size preferredSize =
89+
new Size(
90+
preferredResolution.getWidth().intValue(), preferredResolution.getHeight().intValue());
91+
instanceManager.addDartCreatedInstance(
92+
resolutionFilterFactory.createWithOnePreferredSize(preferredSize), identifier);
93+
}
94+
}

packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ResolutionSelectorHostApiImpl.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import androidx.annotation.Nullable;
99
import androidx.annotation.VisibleForTesting;
1010
import androidx.camera.core.resolutionselector.AspectRatioStrategy;
11+
import androidx.camera.core.resolutionselector.ResolutionFilter;
1112
import androidx.camera.core.resolutionselector.ResolutionSelector;
1213
import androidx.camera.core.resolutionselector.ResolutionStrategy;
1314
import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ResolutionSelectorHostApi;
@@ -30,14 +31,18 @@ public static class ResolutionSelectorProxy {
3031
@NonNull
3132
public ResolutionSelector create(
3233
@Nullable ResolutionStrategy resolutionStrategy,
33-
@Nullable AspectRatioStrategy aspectRatioStrategy) {
34+
@Nullable AspectRatioStrategy aspectRatioStrategy,
35+
@Nullable ResolutionFilter resolutionFilter) {
3436
final ResolutionSelector.Builder builder = new ResolutionSelector.Builder();
3537
if (resolutionStrategy != null) {
3638
builder.setResolutionStrategy(resolutionStrategy);
3739
}
3840
if (aspectRatioStrategy != null) {
3941
builder.setAspectRatioStrategy(aspectRatioStrategy);
4042
}
43+
if (resolutionFilter != null) {
44+
builder.setResolutionFilter(resolutionFilter);
45+
}
4146
return builder.build();
4247
}
4348
}
@@ -65,13 +70,14 @@ public ResolutionSelectorHostApiImpl(@NonNull InstanceManager instanceManager) {
6570
}
6671

6772
/**
68-
* Creates a {@link ResolutionSelector} instance with the {@link ResolutionStrategy} and {@link
69-
* AspectRatio} that have the identifiers specified if provided.
73+
* Creates a {@link ResolutionSelector} instance with the {@link ResolutionStrategy}, {@link
74+
* ResolutionFilter}, and {@link AspectRatio} that have the identifiers specified if provided.
7075
*/
7176
@Override
7277
public void create(
7378
@NonNull Long identifier,
7479
@Nullable Long resolutionStrategyIdentifier,
80+
@Nullable Long resolutionFilterIdentifier,
7581
@Nullable Long aspectRatioStrategyIdentifier) {
7682
instanceManager.addDartCreatedInstance(
7783
proxy.create(
@@ -81,7 +87,10 @@ public void create(
8187
aspectRatioStrategyIdentifier == null
8288
? null
8389
: Objects.requireNonNull(
84-
instanceManager.getInstance(aspectRatioStrategyIdentifier))),
90+
instanceManager.getInstance(aspectRatioStrategyIdentifier)),
91+
resolutionFilterIdentifier == null
92+
? null
93+
: Objects.requireNonNull(instanceManager.getInstance(resolutionFilterIdentifier))),
8594
identifier);
8695
}
8796
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.plugins.camerax;
6+
7+
import static org.junit.Assert.assertEquals;
8+
9+
import android.util.Size;
10+
import androidx.camera.core.resolutionselector.ResolutionFilter;
11+
import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ResolutionInfo;
12+
import java.util.ArrayList;
13+
import java.util.List;
14+
import org.junit.After;
15+
import org.junit.Before;
16+
import org.junit.Rule;
17+
import org.junit.Test;
18+
import org.junit.runner.RunWith;
19+
import org.mockito.junit.MockitoJUnit;
20+
import org.mockito.junit.MockitoRule;
21+
import org.robolectric.RobolectricTestRunner;
22+
23+
@RunWith(RobolectricTestRunner.class)
24+
public class ResolutionFilterTest {
25+
@Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
26+
27+
InstanceManager instanceManager;
28+
29+
@Before
30+
public void setUp() {
31+
instanceManager = InstanceManager.create(identifier -> {});
32+
}
33+
34+
@After
35+
public void tearDown() {
36+
instanceManager.stopFinalizationListener();
37+
}
38+
39+
@Test
40+
public void hostApiCreateWithOnePreferredSize_createsExpectedResolutionFilterInstance() {
41+
final ResolutionFilterHostApiImpl hostApi = new ResolutionFilterHostApiImpl(instanceManager);
42+
final long instanceIdentifier = 50;
43+
final long preferredResolutionWidth = 20;
44+
final long preferredResolutionHeight = 80;
45+
final ResolutionInfo preferredResolution =
46+
new ResolutionInfo.Builder()
47+
.setWidth(preferredResolutionWidth)
48+
.setHeight(preferredResolutionHeight)
49+
.build();
50+
51+
hostApi.createWithOnePreferredSize(instanceIdentifier, preferredResolution);
52+
53+
// Test that instance filters supported resolutions as expected.
54+
final ResolutionFilter resolutionFilter = instanceManager.getInstance(instanceIdentifier);
55+
final Size fakeSupportedSize1 = new Size(720, 480);
56+
final Size fakeSupportedSize2 = new Size(20, 80);
57+
final Size fakeSupportedSize3 = new Size(2, 8);
58+
final Size preferredSize =
59+
new Size((int) preferredResolutionWidth, (int) preferredResolutionHeight);
60+
61+
final ArrayList<Size> fakeSupportedSizes = new ArrayList<Size>();
62+
fakeSupportedSizes.add(fakeSupportedSize1);
63+
fakeSupportedSizes.add(fakeSupportedSize2);
64+
fakeSupportedSizes.add(preferredSize);
65+
fakeSupportedSizes.add(fakeSupportedSize3);
66+
67+
// Test the case where preferred resolution is supported.
68+
List<Size> filteredSizes = resolutionFilter.filter(fakeSupportedSizes, 90);
69+
assertEquals(filteredSizes.get(0), preferredSize);
70+
71+
// Test the case where preferred resolution is not supported.
72+
fakeSupportedSizes.remove(0);
73+
filteredSizes = resolutionFilter.filter(fakeSupportedSizes, 90);
74+
assertEquals(filteredSizes, fakeSupportedSizes);
75+
}
76+
}

packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ResolutionSelectorTest.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import static org.mockito.Mockito.when;
1010

1111
import androidx.camera.core.resolutionselector.AspectRatioStrategy;
12+
import androidx.camera.core.resolutionselector.ResolutionFilter;
1213
import androidx.camera.core.resolutionselector.ResolutionSelector;
1314
import androidx.camera.core.resolutionselector.ResolutionStrategy;
1415
import org.junit.After;
@@ -46,13 +47,21 @@ public void hostApiCreate_createsExpectedResolutionSelectorInstance() {
4647
final long aspectRatioStrategyIdentifier = 15;
4748
instanceManager.addDartCreatedInstance(mockAspectRatioStrategy, aspectRatioStrategyIdentifier);
4849

49-
when(mockProxy.create(mockResolutionStrategy, mockAspectRatioStrategy))
50+
final ResolutionFilter mockResolutionFilter = mock(ResolutionFilter.class);
51+
final long resolutionFilterIdentifier = 33;
52+
instanceManager.addDartCreatedInstance(mockResolutionFilter, resolutionFilterIdentifier);
53+
54+
when(mockProxy.create(mockResolutionStrategy, mockAspectRatioStrategy, mockResolutionFilter))
5055
.thenReturn(mockResolutionSelector);
5156
final ResolutionSelectorHostApiImpl hostApi =
5257
new ResolutionSelectorHostApiImpl(instanceManager, mockProxy);
5358

5459
final long instanceIdentifier = 0;
55-
hostApi.create(instanceIdentifier, resolutionStrategyIdentifier, aspectRatioStrategyIdentifier);
60+
hostApi.create(
61+
instanceIdentifier,
62+
resolutionStrategyIdentifier,
63+
resolutionFilterIdentifier,
64+
aspectRatioStrategyIdentifier);
5665

5766
assertEquals(instanceManager.getInstance(instanceIdentifier), mockResolutionSelector);
5867
}

0 commit comments

Comments
 (0)