Skip to content

Commit

Permalink
[camerax] Use AspectRatioStrategy to help automatic selection of ex…
Browse files Browse the repository at this point in the history
…pected resolution (flutter#6357)

Defines `AspectRatioStategy`s that will help CameraX select the resolution we expect.

Fixes flutter/flutter#144363.
  • Loading branch information
camsim99 authored and arc-yong committed Jun 14, 2024
1 parent cce4579 commit e78dd63
Show file tree
Hide file tree
Showing 22 changed files with 1,431 additions and 428 deletions.
6 changes: 6 additions & 0 deletions packages/camera/camera_android_camerax/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 0.6.1

* Modifies resolution selection logic to use an `AspectRatioStrategy` for all aspect ratios supported by CameraX.
* Adds `ResolutionFilter` to resolution selection logic to prioritize resolutions that match
the defined `ResolutionPreset`s.

## 0.6.0+1

* Updates `README.md` to encourage developers to opt into this implementation of the camera plugin.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ public void setUp(
binaryMessenger, new FocusMeteringResultHostApiImpl(instanceManager));
meteringPointHostApiImpl = new MeteringPointHostApiImpl(instanceManager);
GeneratedCameraXLibrary.MeteringPointHostApi.setup(binaryMessenger, meteringPointHostApiImpl);
GeneratedCameraXLibrary.ResolutionFilterHostApi.setup(
binaryMessenger, new ResolutionFilterHostApiImpl(instanceManager));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2507,6 +2507,7 @@ public interface ResolutionSelectorHostApi {
void create(
@NonNull Long identifier,
@Nullable Long resolutionStrategyIdentifier,
@Nullable Long resolutionSelectorIdentifier,
@Nullable Long aspectRatioStrategyIdentifier);

/** The codec used by ResolutionSelectorHostApi. */
Expand All @@ -2530,13 +2531,17 @@ static void setup(
ArrayList<Object> args = (ArrayList<Object>) message;
Number identifierArg = (Number) args.get(0);
Number resolutionStrategyIdentifierArg = (Number) args.get(1);
Number aspectRatioStrategyIdentifierArg = (Number) args.get(2);
Number resolutionSelectorIdentifierArg = (Number) args.get(2);
Number aspectRatioStrategyIdentifierArg = (Number) args.get(3);
try {
api.create(
(identifierArg == null) ? null : identifierArg.longValue(),
(resolutionStrategyIdentifierArg == null)
? null
: resolutionStrategyIdentifierArg.longValue(),
(resolutionSelectorIdentifierArg == null)
? null
: resolutionSelectorIdentifierArg.longValue(),
(aspectRatioStrategyIdentifierArg == null)
? null
: aspectRatioStrategyIdentifierArg.longValue());
Expand Down Expand Up @@ -4189,4 +4194,77 @@ public void error(Throwable error) {
}
}
}

private static class ResolutionFilterHostApiCodec extends StandardMessageCodec {
public static final ResolutionFilterHostApiCodec INSTANCE = new ResolutionFilterHostApiCodec();

private ResolutionFilterHostApiCodec() {}

@Override
protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) {
switch (type) {
case (byte) 128:
return ResolutionInfo.fromList((ArrayList<Object>) readValue(buffer));
default:
return super.readValueOfType(type, buffer);
}
}

@Override
protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) {
if (value instanceof ResolutionInfo) {
stream.write(128);
writeValue(stream, ((ResolutionInfo) value).toList());
} else {
super.writeValue(stream, value);
}
}
}

/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
public interface ResolutionFilterHostApi {

void createWithOnePreferredSize(
@NonNull Long identifier, @NonNull ResolutionInfo preferredResolution);

/** The codec used by ResolutionFilterHostApi. */
static @NonNull MessageCodec<Object> getCodec() {
return ResolutionFilterHostApiCodec.INSTANCE;
}
/**
* Sets up an instance of `ResolutionFilterHostApi` to handle messages through the
* `binaryMessenger`.
*/
static void setup(
@NonNull BinaryMessenger binaryMessenger, @Nullable ResolutionFilterHostApi api) {
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger,
"dev.flutter.pigeon.ResolutionFilterHostApi.createWithOnePreferredSize",
getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
ArrayList<Object> args = (ArrayList<Object>) message;
Number identifierArg = (Number) args.get(0);
ResolutionInfo preferredResolutionArg = (ResolutionInfo) args.get(1);
try {
api.createWithOnePreferredSize(
(identifierArg == null) ? null : identifierArg.longValue(),
preferredResolutionArg);
wrapped.add(0, null);
} catch (Throwable exception) {
ArrayList<Object> wrappedError = wrapError(exception);
wrapped = wrappedError;
}
reply.reply(wrapped);
});
} else {
channel.setMessageHandler(null);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// 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.camerax;

import android.util.Size;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.camera.core.resolutionselector.ResolutionFilter;
import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ResolutionFilterHostApi;
import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ResolutionInfo;
import java.util.List;

/**
* Host API implementation for {@link ResolutionFilter}.
*
* <p>This class handles instantiating and adding native object instances that are attached to a
* Dart instance or handle method calls on the associated native class or an instance of the class.
*/
public class ResolutionFilterHostApiImpl implements ResolutionFilterHostApi {
private final InstanceManager instanceManager;
private final ResolutionFilterFactory resolutionFilterFactory;

/**
* Proxy for constructing {@link ResolutionFilter}s with particular attributes, as detailed by
* documentation below.
*/
@VisibleForTesting
public static class ResolutionFilterFactory {
/**
* Creates an instance of {@link ResolutionFilter} that moves the {@code preferredSize} to the
* front of the list of supported resolutions so that it can be prioritized by CameraX.
*
* <p>If the preferred {@code Size} is not found, then this creates a {@link ResolutionFilter}
* that leaves the priority of supported resolutions unadjusted.
*/
@NonNull
public ResolutionFilter createWithOnePreferredSize(@NonNull Size preferredSize) {
return new ResolutionFilter() {
@Override
@NonNull
public List<Size> filter(@NonNull List<Size> supportedSizes, int rotationDegrees) {
int preferredSizeIndex = supportedSizes.indexOf(preferredSize);

if (preferredSizeIndex > -1) {
supportedSizes.remove(preferredSizeIndex);
supportedSizes.add(0, preferredSize);
}

return supportedSizes;
}
};
}
}

/**
* Constructs a {@link ResolutionFilterHostApiImpl}.
*
* @param instanceManager maintains instances stored to communicate with attached Dart objects
*/
public ResolutionFilterHostApiImpl(@NonNull InstanceManager instanceManager) {
this(instanceManager, new ResolutionFilterFactory());
}

/**
* Constructs a {@link ResolutionFilterHostApiImpl}.
*
* @param instanceManager maintains instances stored to communicate with attached Dart objects
* @param resolutionFilterFactory proxy for constructing different kinds of {@link
* ResolutionFilter}s
*/
@VisibleForTesting
ResolutionFilterHostApiImpl(
@NonNull InstanceManager instanceManager,
@NonNull ResolutionFilterFactory resolutionFilterFactory) {
this.instanceManager = instanceManager;
this.resolutionFilterFactory = resolutionFilterFactory;
}

/**
* Creates a {@link ResolutionFilter} that prioritizes the specified {@code preferredResolution}
* over all other resolutions.
*/
@Override
public void createWithOnePreferredSize(
@NonNull Long identifier, @NonNull ResolutionInfo preferredResolution) {
Size preferredSize =
new Size(
preferredResolution.getWidth().intValue(), preferredResolution.getHeight().intValue());
instanceManager.addDartCreatedInstance(
resolutionFilterFactory.createWithOnePreferredSize(preferredSize), identifier);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.camera.core.resolutionselector.AspectRatioStrategy;
import androidx.camera.core.resolutionselector.ResolutionFilter;
import androidx.camera.core.resolutionselector.ResolutionSelector;
import androidx.camera.core.resolutionselector.ResolutionStrategy;
import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ResolutionSelectorHostApi;
Expand All @@ -30,14 +31,18 @@ public static class ResolutionSelectorProxy {
@NonNull
public ResolutionSelector create(
@Nullable ResolutionStrategy resolutionStrategy,
@Nullable AspectRatioStrategy aspectRatioStrategy) {
@Nullable AspectRatioStrategy aspectRatioStrategy,
@Nullable ResolutionFilter resolutionFilter) {
final ResolutionSelector.Builder builder = new ResolutionSelector.Builder();
if (resolutionStrategy != null) {
builder.setResolutionStrategy(resolutionStrategy);
}
if (aspectRatioStrategy != null) {
builder.setAspectRatioStrategy(aspectRatioStrategy);
}
if (resolutionFilter != null) {
builder.setResolutionFilter(resolutionFilter);
}
return builder.build();
}
}
Expand Down Expand Up @@ -65,13 +70,14 @@ public ResolutionSelectorHostApiImpl(@NonNull InstanceManager instanceManager) {
}

/**
* Creates a {@link ResolutionSelector} instance with the {@link ResolutionStrategy} and {@link
* AspectRatio} that have the identifiers specified if provided.
* Creates a {@link ResolutionSelector} instance with the {@link ResolutionStrategy}, {@link
* ResolutionFilter}, and {@link AspectRatio} that have the identifiers specified if provided.
*/
@Override
public void create(
@NonNull Long identifier,
@Nullable Long resolutionStrategyIdentifier,
@Nullable Long resolutionFilterIdentifier,
@Nullable Long aspectRatioStrategyIdentifier) {
instanceManager.addDartCreatedInstance(
proxy.create(
Expand All @@ -81,7 +87,10 @@ public void create(
aspectRatioStrategyIdentifier == null
? null
: Objects.requireNonNull(
instanceManager.getInstance(aspectRatioStrategyIdentifier))),
instanceManager.getInstance(aspectRatioStrategyIdentifier)),
resolutionFilterIdentifier == null
? null
: Objects.requireNonNull(instanceManager.getInstance(resolutionFilterIdentifier))),
identifier);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// 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.camerax;

import static org.junit.Assert.assertEquals;

import android.util.Size;
import androidx.camera.core.resolutionselector.ResolutionFilter;
import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ResolutionInfo;
import java.util.ArrayList;
import java.util.List;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.RobolectricTestRunner;

@RunWith(RobolectricTestRunner.class)
public class ResolutionFilterTest {
@Rule public MockitoRule mockitoRule = MockitoJUnit.rule();

InstanceManager instanceManager;

@Before
public void setUp() {
instanceManager = InstanceManager.create(identifier -> {});
}

@After
public void tearDown() {
instanceManager.stopFinalizationListener();
}

@Test
public void hostApiCreateWithOnePreferredSize_createsExpectedResolutionFilterInstance() {
final ResolutionFilterHostApiImpl hostApi = new ResolutionFilterHostApiImpl(instanceManager);
final long instanceIdentifier = 50;
final long preferredResolutionWidth = 20;
final long preferredResolutionHeight = 80;
final ResolutionInfo preferredResolution =
new ResolutionInfo.Builder()
.setWidth(preferredResolutionWidth)
.setHeight(preferredResolutionHeight)
.build();

hostApi.createWithOnePreferredSize(instanceIdentifier, preferredResolution);

// Test that instance filters supported resolutions as expected.
final ResolutionFilter resolutionFilter = instanceManager.getInstance(instanceIdentifier);
final Size fakeSupportedSize1 = new Size(720, 480);
final Size fakeSupportedSize2 = new Size(20, 80);
final Size fakeSupportedSize3 = new Size(2, 8);
final Size preferredSize =
new Size((int) preferredResolutionWidth, (int) preferredResolutionHeight);

final ArrayList<Size> fakeSupportedSizes = new ArrayList<Size>();
fakeSupportedSizes.add(fakeSupportedSize1);
fakeSupportedSizes.add(fakeSupportedSize2);
fakeSupportedSizes.add(preferredSize);
fakeSupportedSizes.add(fakeSupportedSize3);

// Test the case where preferred resolution is supported.
List<Size> filteredSizes = resolutionFilter.filter(fakeSupportedSizes, 90);
assertEquals(filteredSizes.get(0), preferredSize);

// Test the case where preferred resolution is not supported.
fakeSupportedSizes.remove(0);
filteredSizes = resolutionFilter.filter(fakeSupportedSizes, 90);
assertEquals(filteredSizes, fakeSupportedSizes);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import static org.mockito.Mockito.when;

import androidx.camera.core.resolutionselector.AspectRatioStrategy;
import androidx.camera.core.resolutionselector.ResolutionFilter;
import androidx.camera.core.resolutionselector.ResolutionSelector;
import androidx.camera.core.resolutionselector.ResolutionStrategy;
import org.junit.After;
Expand Down Expand Up @@ -46,13 +47,21 @@ public void hostApiCreate_createsExpectedResolutionSelectorInstance() {
final long aspectRatioStrategyIdentifier = 15;
instanceManager.addDartCreatedInstance(mockAspectRatioStrategy, aspectRatioStrategyIdentifier);

when(mockProxy.create(mockResolutionStrategy, mockAspectRatioStrategy))
final ResolutionFilter mockResolutionFilter = mock(ResolutionFilter.class);
final long resolutionFilterIdentifier = 33;
instanceManager.addDartCreatedInstance(mockResolutionFilter, resolutionFilterIdentifier);

when(mockProxy.create(mockResolutionStrategy, mockAspectRatioStrategy, mockResolutionFilter))
.thenReturn(mockResolutionSelector);
final ResolutionSelectorHostApiImpl hostApi =
new ResolutionSelectorHostApiImpl(instanceManager, mockProxy);

final long instanceIdentifier = 0;
hostApi.create(instanceIdentifier, resolutionStrategyIdentifier, aspectRatioStrategyIdentifier);
hostApi.create(
instanceIdentifier,
resolutionStrategyIdentifier,
resolutionFilterIdentifier,
aspectRatioStrategyIdentifier);

assertEquals(instanceManager.getInstance(instanceIdentifier), mockResolutionSelector);
}
Expand Down
Loading

0 comments on commit e78dd63

Please sign in to comment.