diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index f2e845ff78b..de7c4d96618 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.0+14 + +* Wraps classes needed to implement resolution configuration for video recording. + ## 0.5.0+13 * Migrates `styleFrom` usage in examples off of deprecated `primary` and `onPrimary` parameters. diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java index dbae2a46805..d2a9c277a37 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java @@ -103,6 +103,10 @@ public void setUp( binaryMessenger, new ResolutionStrategyHostApiImpl(instanceManager)); GeneratedCameraXLibrary.AspectRatioStrategyHostApi.setup( binaryMessenger, new AspectRatioStrategyHostApiImpl(instanceManager)); + GeneratedCameraXLibrary.FallbackStrategyHostApi.setup( + binaryMessenger, new FallbackStrategyHostApiImpl(instanceManager)); + GeneratedCameraXLibrary.QualitySelectorHostApi.setup( + binaryMessenger, new QualitySelectorHostApiImpl(instanceManager)); } @Override diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/FallbackStrategyHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/FallbackStrategyHostApiImpl.java new file mode 100644 index 00000000000..0309d5476a0 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/FallbackStrategyHostApiImpl.java @@ -0,0 +1,83 @@ +// 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 androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.camera.video.FallbackStrategy; +import androidx.camera.video.Quality; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.FallbackStrategyHostApi; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.VideoQualityConstraint; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.VideoResolutionFallbackRule; + +/** + * Host API implementation for {@link FallbackStrategy}. + * + *

This class may handle 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 FallbackStrategyHostApiImpl implements FallbackStrategyHostApi { + private final InstanceManager instanceManager; + + private final FallbackStrategyProxy proxy; + + /** Proxy for constructors and static method of {@link FallbackStrategy}. */ + @VisibleForTesting + public static class FallbackStrategyProxy { + /** Creates an instance of {@link FallbackStrategy}. */ + public @NonNull FallbackStrategy create( + @NonNull VideoQualityConstraint videoQualityConstraint, + @NonNull VideoResolutionFallbackRule fallbackRule) { + Quality videoQuality = + QualitySelectorHostApiImpl.getQualityFromVideoQualityConstraint(videoQualityConstraint); + + switch (fallbackRule) { + case HIGHER_QUALITY_OR_LOWER_THAN: + return FallbackStrategy.higherQualityOrLowerThan(videoQuality); + case HIGHER_QUALITY_THAN: + return FallbackStrategy.higherQualityThan(videoQuality); + case LOWER_QUALITY_OR_HIGHER_THAN: + return FallbackStrategy.lowerQualityOrHigherThan(videoQuality); + case LOWER_QUALITY_THAN: + return FallbackStrategy.lowerQualityThan(videoQuality); + } + throw new IllegalArgumentException( + "Specified fallback rule " + fallbackRule + " unrecognized."); + } + } + + /** + * Constructs a {@link FallbackStrategyHostApiImpl}. + * + * @param instanceManager maintains instances stored to communicate with attached Dart objects + */ + public FallbackStrategyHostApiImpl(@NonNull InstanceManager instanceManager) { + this(instanceManager, new FallbackStrategyProxy()); + } + + /** + * Constructs a {@link FallbackStrategyHostApiImpl}. + * + * @param instanceManager maintains instances stored to communicate with attached Dart objects + * @param proxy proxy for constructors and static method of {@link FallbackStrategy} + */ + FallbackStrategyHostApiImpl( + @NonNull InstanceManager instanceManager, @NonNull FallbackStrategyProxy proxy) { + this.instanceManager = instanceManager; + this.proxy = proxy; + } + + /** + * Creates a {@link FallbackStrategy} instance with the video quality and fallback rule specified. + */ + @Override + public void create( + @NonNull Long identifier, + @NonNull VideoQualityConstraint videoQualityConstraint, + @NonNull VideoResolutionFallbackRule fallbackRule) { + instanceManager.addDartCreatedInstance( + proxy.create(videoQualityConstraint, fallbackRule), identifier); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java index 680ecb80525..d7d378d158b 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java @@ -104,6 +104,43 @@ private LiveDataSupportedType(final int index) { } } + /** + * Video quality constraints that will be used by a QualitySelector to choose an appropriate video + * resolution. + * + *

These are pre-defined quality constants that are universally used for video. + * + *

See https://developer.android.com/reference/androidx/camera/video/Quality. + */ + public enum VideoQualityConstraint { + SD(0), + HD(1), + FHD(2), + UHD(3), + LOWEST(4), + HIGHEST(5); + + final int index; + + private VideoQualityConstraint(final int index) { + this.index = index; + } + } + + /** Fallback rules for selecting video resolution. */ + public enum VideoResolutionFallbackRule { + HIGHER_QUALITY_OR_LOWER_THAN(0), + HIGHER_QUALITY_THAN(1), + LOWER_QUALITY_OR_HIGHER_THAN(2), + LOWER_QUALITY_THAN(3); + + final int index; + + private VideoResolutionFallbackRule(final int index) { + this.index = index; + } + } + /** Generated class from Pigeon that represents data sent in messages. */ public static final class ResolutionInfo { private @NonNull Long width; @@ -1556,7 +1593,11 @@ public void create(@NonNull Long identifierArg, @NonNull Reply callback) { /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface RecorderHostApi { - void create(@NonNull Long identifier, @Nullable Long aspectRatio, @Nullable Long bitRate); + void create( + @NonNull Long identifier, + @Nullable Long aspectRatio, + @Nullable Long bitRate, + @Nullable Long qualitySelectorId); @NonNull Long getAspectRatio(@NonNull Long identifier); @@ -1587,11 +1628,13 @@ static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable RecorderHo Number identifierArg = (Number) args.get(0); Number aspectRatioArg = (Number) args.get(1); Number bitRateArg = (Number) args.get(2); + Number qualitySelectorIdArg = (Number) args.get(3); try { api.create( (identifierArg == null) ? null : identifierArg.longValue(), (aspectRatioArg == null) ? null : aspectRatioArg.longValue(), - (bitRateArg == null) ? null : bitRateArg.longValue()); + (bitRateArg == null) ? null : bitRateArg.longValue(), + (qualitySelectorIdArg == null) ? null : qualitySelectorIdArg.longValue()); wrapped.add(0, null); } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); @@ -2937,4 +2980,165 @@ public void create( channelReply -> callback.reply(null)); } } + + private static class QualitySelectorHostApiCodec extends StandardMessageCodec { + public static final QualitySelectorHostApiCodec INSTANCE = new QualitySelectorHostApiCodec(); + + private QualitySelectorHostApiCodec() {} + + @Override + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { + switch (type) { + case (byte) 128: + return ResolutionInfo.fromList((ArrayList) 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 QualitySelectorHostApi { + + void create( + @NonNull Long identifier, + @NonNull List videoQualityConstraintIndexList, + @Nullable Long fallbackStrategyId); + + @NonNull + ResolutionInfo getResolution( + @NonNull Long cameraInfoId, @NonNull VideoQualityConstraint quality); + + /** The codec used by QualitySelectorHostApi. */ + static @NonNull MessageCodec getCodec() { + return QualitySelectorHostApiCodec.INSTANCE; + } + /** + * Sets up an instance of `QualitySelectorHostApi` to handle messages through the + * `binaryMessenger`. + */ + static void setup( + @NonNull BinaryMessenger binaryMessenger, @Nullable QualitySelectorHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.QualitySelectorHostApi.create", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + List videoQualityConstraintIndexListArg = (List) args.get(1); + Number fallbackStrategyIdArg = (Number) args.get(2); + try { + api.create( + (identifierArg == null) ? null : identifierArg.longValue(), + videoQualityConstraintIndexListArg, + (fallbackStrategyIdArg == null) ? null : fallbackStrategyIdArg.longValue()); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.QualitySelectorHostApi.getResolution", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number cameraInfoIdArg = (Number) args.get(0); + VideoQualityConstraint qualityArg = + args.get(1) == null ? null : VideoQualityConstraint.values()[(int) args.get(1)]; + try { + ResolutionInfo output = + api.getResolution( + (cameraInfoIdArg == null) ? null : cameraInfoIdArg.longValue(), + qualityArg); + wrapped.add(0, output); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface FallbackStrategyHostApi { + + void create( + @NonNull Long identifier, + @NonNull VideoQualityConstraint quality, + @NonNull VideoResolutionFallbackRule fallbackRule); + + /** The codec used by FallbackStrategyHostApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + /** + * Sets up an instance of `FallbackStrategyHostApi` to handle messages through the + * `binaryMessenger`. + */ + static void setup( + @NonNull BinaryMessenger binaryMessenger, @Nullable FallbackStrategyHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.FallbackStrategyHostApi.create", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + VideoQualityConstraint qualityArg = + args.get(1) == null ? null : VideoQualityConstraint.values()[(int) args.get(1)]; + VideoResolutionFallbackRule fallbackRuleArg = + args.get(2) == null + ? null + : VideoResolutionFallbackRule.values()[(int) args.get(2)]; + try { + api.create( + (identifierArg == null) ? null : identifierArg.longValue(), + qualityArg, + fallbackRuleArg); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/QualitySelectorHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/QualitySelectorHostApiImpl.java new file mode 100644 index 00000000000..c747c75962b --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/QualitySelectorHostApiImpl.java @@ -0,0 +1,149 @@ +// 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.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.camera.video.FallbackStrategy; +import androidx.camera.video.Quality; +import androidx.camera.video.QualitySelector; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.QualitySelectorHostApi; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ResolutionInfo; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.VideoQualityConstraint; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Host API implementation for {@link QualitySelector}. + * + *

This class may handle 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 QualitySelectorHostApiImpl implements QualitySelectorHostApi { + private final InstanceManager instanceManager; + + private final QualitySelectorProxy proxy; + + /** Proxy for constructors and static method of {@link QualitySelector}. */ + @VisibleForTesting + public static class QualitySelectorProxy { + /** Creates an instance of {@link QualitySelector}. */ + public @NonNull QualitySelector create( + @NonNull List videoQualityConstraintIndexList, + @Nullable FallbackStrategy fallbackStrategy) { + // Convert each index of VideoQualityConstraint to Quality. + List qualityList = new ArrayList(); + for (Long qualityIndex : videoQualityConstraintIndexList) { + qualityList.add(getQualityConstant(qualityIndex)); + } + + boolean fallbackStrategySpecified = fallbackStrategy != null; + if (qualityList.size() == 0) { + throw new IllegalArgumentException( + "List of at least one Quality must be supplied to create QualitySelector."); + } else if (qualityList.size() == 1) { + Quality quality = qualityList.get(0); + return fallbackStrategySpecified + ? QualitySelector.from(quality, fallbackStrategy) + : QualitySelector.from(quality); + } + + return fallbackStrategySpecified + ? QualitySelector.fromOrderedList(qualityList, fallbackStrategy) + : QualitySelector.fromOrderedList(qualityList); + } + + /** Converts from index of {@link VideoQualityConstraint} to {@link Quality}. */ + private Quality getQualityConstant(@NonNull Long qualityIndex) { + VideoQualityConstraint quality = VideoQualityConstraint.values()[qualityIndex.intValue()]; + return getQualityFromVideoQualityConstraint(quality); + } + } + + /** + * Constructs a {@link QualitySelectorHostApiImpl}. + * + * @param instanceManager maintains instances stored to communicate with attached Dart objects + */ + public QualitySelectorHostApiImpl(@NonNull InstanceManager instanceManager) { + this(instanceManager, new QualitySelectorProxy()); + } + + /** + * Constructs a {@link QualitySelectorHostApiImpl}. + * + * @param instanceManager maintains instances stored to communicate with attached Dart objects + * @param proxy proxy for constructors and static method of {@link QualitySelector} + */ + QualitySelectorHostApiImpl( + @NonNull InstanceManager instanceManager, @NonNull QualitySelectorProxy proxy) { + this.instanceManager = instanceManager; + this.proxy = proxy; + } + + /** + * Creates a {@link QualitySelector} instance with the quality list and {@link FallbackStrategy} + * with the identifier specified. + */ + @Override + public void create( + @NonNull Long identifier, + @NonNull List videoQualityConstraintIndexList, + @Nullable Long fallbackStrategyIdentifier) { + instanceManager.addDartCreatedInstance( + proxy.create( + videoQualityConstraintIndexList, + fallbackStrategyIdentifier == null + ? null + : Objects.requireNonNull(instanceManager.getInstance(fallbackStrategyIdentifier))), + identifier); + } + + /** + * Retrieves the corresponding resolution from the input quality for the camera represented by the + * {@link CameraInfo} represented by the identifier specified. + */ + @Override + public @NonNull ResolutionInfo getResolution( + @NonNull Long cameraInfoIdentifier, @NonNull VideoQualityConstraint quality) { + final Size result = + QualitySelector.getResolution( + Objects.requireNonNull(instanceManager.getInstance(cameraInfoIdentifier)), + getQualityFromVideoQualityConstraint(quality)); + return new ResolutionInfo.Builder() + .setWidth(Long.valueOf(result.getWidth())) + .setHeight(Long.valueOf(result.getHeight())) + .build(); + } + + /** + * Converts the specified {@link VideoQualityConstraint} to a {@link Quality} that is understood + * by CameraX. + */ + public static @NonNull Quality getQualityFromVideoQualityConstraint( + @NonNull VideoQualityConstraint videoQualityConstraint) { + switch (videoQualityConstraint) { + case SD: + return Quality.SD; + case HD: + return Quality.HD; + case FHD: + return Quality.FHD; + case UHD: + return Quality.UHD; + case LOWEST: + return Quality.LOWEST; + case HIGHEST: + return Quality.HIGHEST; + } + throw new IllegalArgumentException( + "VideoQualityConstraint " + + videoQualityConstraint + + " is unhandled by QualitySelectorHostApiImpl."); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecorderHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecorderHostApiImpl.java index 660c469c88b..8f7e8d293f4 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecorderHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecorderHostApiImpl.java @@ -41,7 +41,11 @@ public RecorderHostApiImpl( } @Override - public void create(@NonNull Long instanceId, @Nullable Long aspectRatio, @Nullable Long bitRate) { + public void create( + @NonNull Long instanceId, + @Nullable Long aspectRatio, + @Nullable Long bitRate, + @Nullable Long qualitySelector) { Recorder.Builder recorderBuilder = cameraXProxy.createRecorderBuilder(); if (aspectRatio != null) { recorderBuilder.setAspectRatio(aspectRatio.intValue()); @@ -49,6 +53,10 @@ public void create(@NonNull Long instanceId, @Nullable Long aspectRatio, @Nullab if (bitRate != null) { recorderBuilder.setTargetVideoEncodingBitRate(bitRate.intValue()); } + if (qualitySelector != null) { + recorderBuilder.setQualitySelector( + Objects.requireNonNull(instanceManager.getInstance(qualitySelector))); + } Recorder recorder = recorderBuilder.setExecutor(ContextCompat.getMainExecutor(context)).build(); instanceManager.addDartCreatedInstance(recorder, instanceId); } diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/FallbackStrategyTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/FallbackStrategyTest.java new file mode 100644 index 00000000000..4a2eeef7f13 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/FallbackStrategyTest.java @@ -0,0 +1,121 @@ +// 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 static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; + +import androidx.camera.video.FallbackStrategy; +import androidx.camera.video.Quality; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.VideoQualityConstraint; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.VideoResolutionFallbackRule; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.mockito.stubbing.Answer; + +public class FallbackStrategyTest { + + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + @Mock public FallbackStrategy mockFallbackStrategy; + + InstanceManager instanceManager; + + @Before + public void setUp() { + instanceManager = InstanceManager.create(identifier -> {}); + } + + @After + public void tearDown() { + instanceManager.stopFinalizationListener(); + } + + @Test + public void hostApiCreate_makesCallToCreateExpectedFallbackStrategy() { + final FallbackStrategyHostApiImpl hostApi = new FallbackStrategyHostApiImpl(instanceManager); + final long instanceIdentifier = 45; + final FallbackStrategy mockFallbackStrategy = mock(FallbackStrategy.class); + + try (MockedStatic mockedFallbackStrategy = + mockStatic(FallbackStrategy.class)) { + for (VideoQualityConstraint videoQualityConstraint : VideoQualityConstraint.values()) { + for (VideoResolutionFallbackRule fallbackRule : VideoResolutionFallbackRule.values()) { + // Determine expected Quality based on videoQualityConstraint being tested. + Quality convertedQuality = null; + switch (videoQualityConstraint) { + case SD: + convertedQuality = Quality.SD; + break; + case HD: + convertedQuality = Quality.HD; + break; + case FHD: + convertedQuality = Quality.FHD; + break; + case UHD: + convertedQuality = Quality.UHD; + break; + case LOWEST: + convertedQuality = Quality.LOWEST; + break; + case HIGHEST: + convertedQuality = Quality.HIGHEST; + break; + default: + fail( + "The VideoQualityConstraint " + + videoQualityConstraint.toString() + + "is unhandled by this test."); + } + // Set Quality as final local variable to avoid error about using non-final (or effecitvely final) local variables in lambda expressions. + final Quality expectedQuality = convertedQuality; + + // Mock calls to create FallbackStrategy according to fallbackRule being tested. + switch (fallbackRule) { + case HIGHER_QUALITY_OR_LOWER_THAN: + mockedFallbackStrategy + .when(() -> FallbackStrategy.higherQualityOrLowerThan(expectedQuality)) + .thenAnswer((Answer) invocation -> mockFallbackStrategy); + break; + case HIGHER_QUALITY_THAN: + mockedFallbackStrategy + .when(() -> FallbackStrategy.higherQualityThan(expectedQuality)) + .thenAnswer((Answer) invocation -> mockFallbackStrategy); + break; + case LOWER_QUALITY_OR_HIGHER_THAN: + mockedFallbackStrategy + .when(() -> FallbackStrategy.lowerQualityOrHigherThan(expectedQuality)) + .thenAnswer((Answer) invocation -> mockFallbackStrategy); + break; + case LOWER_QUALITY_THAN: + mockedFallbackStrategy + .when(() -> FallbackStrategy.lowerQualityThan(expectedQuality)) + .thenAnswer((Answer) invocation -> mockFallbackStrategy); + break; + default: + fail( + "The VideoResolutionFallbackRule " + + fallbackRule.toString() + + "is unhandled by this test."); + } + hostApi.create(instanceIdentifier, videoQualityConstraint, fallbackRule); + assertEquals(instanceManager.getInstance(instanceIdentifier), mockFallbackStrategy); + + // Clear/reset FallbackStrategy mock and InstanceManager. + mockedFallbackStrategy.reset(); + instanceManager.clear(); + } + } + } + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/QualitySelectorTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/QualitySelectorTest.java new file mode 100644 index 00000000000..55195831f98 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/QualitySelectorTest.java @@ -0,0 +1,155 @@ +// 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 static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; + +import android.util.Size; +import androidx.camera.core.CameraInfo; +import androidx.camera.video.FallbackStrategy; +import androidx.camera.video.Quality; +import androidx.camera.video.QualitySelector; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ResolutionInfo; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.VideoQualityConstraint; +import java.util.Arrays; +import java.util.List; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.mockito.stubbing.Answer; + +public class QualitySelectorTest { + + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + @Mock public QualitySelector mockQualitySelectorWithoutFallbackStrategy; + @Mock public QualitySelector mockQualitySelectorWithFallbackStrategy; + + InstanceManager instanceManager; + + @Before + public void setUp() { + instanceManager = InstanceManager.create(identifier -> {}); + } + + @After + public void tearDown() { + instanceManager.stopFinalizationListener(); + } + + @Test + public void hostApiCreate_createsExpectedQualitySelectorWhenOneQualitySpecified() { + final Long expectedVideoQualityConstraintIndex = + Long.valueOf(VideoQualityConstraint.UHD.ordinal()); + final List videoQualityConstraintList = + Arrays.asList(expectedVideoQualityConstraintIndex); + final FallbackStrategy mockFallbackStrategy = mock(FallbackStrategy.class); + final long fallbackStrategyIdentifier = 9; + final QualitySelectorHostApiImpl hostApi = new QualitySelectorHostApiImpl(instanceManager); + + instanceManager.addDartCreatedInstance(mockFallbackStrategy, fallbackStrategyIdentifier); + + try (MockedStatic mockedQualitySelector = mockStatic(QualitySelector.class)) { + mockedQualitySelector + .when(() -> QualitySelector.from(Quality.UHD)) + .thenAnswer( + (Answer) invocation -> mockQualitySelectorWithoutFallbackStrategy); + mockedQualitySelector + .when(() -> QualitySelector.from(Quality.UHD, mockFallbackStrategy)) + .thenAnswer( + (Answer) invocation -> mockQualitySelectorWithFallbackStrategy); + + // Test with no fallback strategy. + long instanceIdentifier = 0; + hostApi.create(instanceIdentifier, videoQualityConstraintList, null); + + assertEquals( + instanceManager.getInstance(instanceIdentifier), + mockQualitySelectorWithoutFallbackStrategy); + + // Test with fallback strategy. + instanceIdentifier = 1; + hostApi.create(instanceIdentifier, videoQualityConstraintList, fallbackStrategyIdentifier); + + assertEquals( + instanceManager.getInstance(instanceIdentifier), mockQualitySelectorWithFallbackStrategy); + } + } + + @Test + public void hostApiCreate_createsExpectedQualitySelectorWhenOrderedListOfQualitiesSpecified() { + final List expectedIndices = + Arrays.asList( + Long.valueOf(VideoQualityConstraint.UHD.ordinal()), + Long.valueOf(VideoQualityConstraint.HIGHEST.ordinal())); + final List videoQualityConstraintList = + Arrays.asList(VideoQualityConstraint.UHD, VideoQualityConstraint.HIGHEST); + final List expectedVideoQualityConstraintList = + Arrays.asList(Quality.UHD, Quality.HIGHEST); + final FallbackStrategy mockFallbackStrategy = mock(FallbackStrategy.class); + final long fallbackStrategyIdentifier = 9; + final QualitySelectorHostApiImpl hostApi = new QualitySelectorHostApiImpl(instanceManager); + + instanceManager.addDartCreatedInstance(mockFallbackStrategy, fallbackStrategyIdentifier); + + try (MockedStatic mockedQualitySelector = mockStatic(QualitySelector.class)) { + mockedQualitySelector + .when(() -> QualitySelector.fromOrderedList(expectedVideoQualityConstraintList)) + .thenAnswer( + (Answer) invocation -> mockQualitySelectorWithoutFallbackStrategy); + mockedQualitySelector + .when( + () -> + QualitySelector.fromOrderedList( + expectedVideoQualityConstraintList, mockFallbackStrategy)) + .thenAnswer( + (Answer) invocation -> mockQualitySelectorWithFallbackStrategy); + + // Test with no fallback strategy. + long instanceIdentifier = 0; + hostApi.create(instanceIdentifier, expectedIndices, null); + + assertEquals( + instanceManager.getInstance(instanceIdentifier), + mockQualitySelectorWithoutFallbackStrategy); + + // Test with fallback strategy. + instanceIdentifier = 1; + hostApi.create(instanceIdentifier, expectedIndices, fallbackStrategyIdentifier); + + assertEquals( + instanceManager.getInstance(instanceIdentifier), mockQualitySelectorWithFallbackStrategy); + } + } + + @Test + public void getResolution_returnsExpectedResolutionInfo() { + final CameraInfo mockCameraInfo = mock(CameraInfo.class); + final long cameraInfoIdentifier = 6; + final VideoQualityConstraint videoQualityConstraint = VideoQualityConstraint.FHD; + final Size sizeResult = new Size(30, 40); + final QualitySelectorHostApiImpl hostApi = new QualitySelectorHostApiImpl(instanceManager); + + instanceManager.addDartCreatedInstance(mockCameraInfo, cameraInfoIdentifier); + + try (MockedStatic mockedQualitySelector = mockStatic(QualitySelector.class)) { + mockedQualitySelector + .when(() -> QualitySelector.getResolution(mockCameraInfo, Quality.FHD)) + .thenAnswer((Answer) invocation -> sizeResult); + + final ResolutionInfo result = + hostApi.getResolution(cameraInfoIdentifier, videoQualityConstraint); + + assertEquals(result.getWidth(), Long.valueOf(sizeResult.getWidth())); + assertEquals(result.getHeight(), Long.valueOf(sizeResult.getHeight())); + } + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/RecorderTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/RecorderTest.java index 72355d260c5..a859e9d513e 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/RecorderTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/RecorderTest.java @@ -18,6 +18,7 @@ import android.content.Context; import androidx.camera.video.FileOutputOptions; import androidx.camera.video.PendingRecording; +import androidx.camera.video.QualitySelector; import androidx.camera.video.Recorder; import androidx.test.core.app.ApplicationProvider; import io.flutter.plugin.common.BinaryMessenger; @@ -56,16 +57,18 @@ public void tearDown() { } @Test - public void createTest() { + public void create_createsExpectedRecorderInstance() { final int recorderId = 0; final int aspectRatio = 1; final int bitRate = 2; + final int qualitySelectorId = 3; final RecorderHostApiImpl recorderHostApi = new RecorderHostApiImpl(mockBinaryMessenger, testInstanceManager, context); final CameraXProxy mockCameraXProxy = mock(CameraXProxy.class); final Recorder.Builder mockRecorderBuilder = mock(Recorder.Builder.class); + final QualitySelector mockQualitySelector = mock(QualitySelector.class); recorderHostApi.cameraXProxy = mockCameraXProxy; when(mockCameraXProxy.createRecorderBuilder()).thenReturn(mockRecorderBuilder); when(mockRecorderBuilder.setAspectRatio(aspectRatio)).thenReturn(mockRecorderBuilder); @@ -73,12 +76,18 @@ public void createTest() { .thenReturn(mockRecorderBuilder); when(mockRecorderBuilder.setExecutor(any(Executor.class))).thenReturn(mockRecorderBuilder); when(mockRecorderBuilder.build()).thenReturn(mockRecorder); + testInstanceManager.addDartCreatedInstance( + mockQualitySelector, Long.valueOf(qualitySelectorId)); recorderHostApi.create( - Long.valueOf(recorderId), Long.valueOf(aspectRatio), Long.valueOf(bitRate)); + Long.valueOf(recorderId), + Long.valueOf(aspectRatio), + Long.valueOf(bitRate), + Long.valueOf(qualitySelectorId)); verify(mockCameraXProxy).createRecorderBuilder(); verify(mockRecorderBuilder).setAspectRatio(aspectRatio); verify(mockRecorderBuilder).setTargetVideoEncodingBitRate(bitRate); + verify(mockRecorderBuilder).setQualitySelector(mockQualitySelector); verify(mockRecorderBuilder).build(); assertEquals(testInstanceManager.getInstance(Long.valueOf(recorderId)), mockRecorder); testInstanceManager.remove(Long.valueOf(recorderId)); diff --git a/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart b/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart index 2d1dccf971c..b97d19360d8 100644 --- a/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart +++ b/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart @@ -43,6 +43,29 @@ enum LiveDataSupportedType { zoomState, } +/// Video quality constraints that will be used by a QualitySelector to choose +/// an appropriate video resolution. +/// +/// These are pre-defined quality constants that are universally used for video. +/// +/// See https://developer.android.com/reference/androidx/camera/video/Quality. +enum VideoQualityConstraint { + SD, + HD, + FHD, + UHD, + lowest, + highest, +} + +/// Fallback rules for selecting video resolution. +enum VideoResolutionFallbackRule { + higherQualityOrLowerThan, + higherQualityThan, + lowerQualityOrHigherThan, + lowerQualityThan, +} + class ResolutionInfo { ResolutionInfo({ required this.width, @@ -1183,14 +1206,17 @@ class RecorderHostApi { static const MessageCodec codec = StandardMessageCodec(); - Future create( - int arg_identifier, int? arg_aspectRatio, int? arg_bitRate) async { + Future create(int arg_identifier, int? arg_aspectRatio, + int? arg_bitRate, int? arg_qualitySelectorId) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.RecorderHostApi.create', codec, binaryMessenger: _binaryMessenger); - final List? replyList = await channel - .send([arg_identifier, arg_aspectRatio, arg_bitRate]) - as List?; + final List? replyList = await channel.send([ + arg_identifier, + arg_aspectRatio, + arg_bitRate, + arg_qualitySelectorId + ]) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', @@ -2444,3 +2470,130 @@ abstract class PlaneProxyFlutterApi { } } } + +class _QualitySelectorHostApiCodec extends StandardMessageCodec { + const _QualitySelectorHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is ResolutionInfo) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return ResolutionInfo.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +class QualitySelectorHostApi { + /// Constructor for [QualitySelectorHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + QualitySelectorHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _QualitySelectorHostApiCodec(); + + Future create( + int arg_identifier, + List arg_videoQualityConstraintIndexList, + int? arg_fallbackStrategyId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.QualitySelectorHostApi.create', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel.send([ + arg_identifier, + arg_videoQualityConstraintIndexList, + arg_fallbackStrategyId + ]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + Future getResolution( + int arg_cameraInfoId, VideoQualityConstraint arg_quality) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.QualitySelectorHostApi.getResolution', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_cameraInfoId, arg_quality.index]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as ResolutionInfo?)!; + } + } +} + +class FallbackStrategyHostApi { + /// Constructor for [FallbackStrategyHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + FallbackStrategyHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = StandardMessageCodec(); + + Future create(int arg_identifier, VideoQualityConstraint arg_quality, + VideoResolutionFallbackRule arg_fallbackRule) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FallbackStrategyHostApi.create', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel.send([ + arg_identifier, + arg_quality.index, + arg_fallbackRule.index + ]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/fallback_strategy.dart b/packages/camera/camera_android_camerax/lib/src/fallback_strategy.dart new file mode 100644 index 00000000000..ee098fafd14 --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/fallback_strategy.dart @@ -0,0 +1,90 @@ +// 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. + +import 'package:flutter/services.dart'; +import 'package:meta/meta.dart' show immutable; + +import 'camerax_library.g.dart'; +import 'instance_manager.dart'; +import 'java_object.dart'; + +/// Strategy that will be adopted when the device in use does not support all +/// of the desired quality specified for a particular QualitySelector instance. +/// +/// See https://developer.android.com/reference/androidx/camera/video/FallbackStrategy. +@immutable +class FallbackStrategy extends JavaObject { + /// Creates a [FallbackStrategy]. + FallbackStrategy( + {BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + required this.quality, + required this.fallbackRule}) + : super.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager) { + _api = _FallbackStrategyHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + _api.createFromInstance(this, quality, fallbackRule); + } + + /// Constructs a [FallbackStrategy] that is not automatically attached to a native object. + FallbackStrategy.detached( + {super.binaryMessenger, + super.instanceManager, + required this.quality, + required this.fallbackRule}) + : super.detached(); + + late final _FallbackStrategyHostApiImpl _api; + + /// The input quality used to specify this fallback strategy relative to. + final VideoQualityConstraint quality; + + /// The fallback rule that this strategy will follow. + final VideoResolutionFallbackRule fallbackRule; +} + +/// Host API implementation of [FallbackStrategy]. +class _FallbackStrategyHostApiImpl extends FallbackStrategyHostApi { + /// Constructs a [FallbackStrategyHostApiImpl]. + /// + /// If [binaryMessenger] is null, the default [BinaryMessenger] will be used, + /// which routes to the host platform. + /// + /// An [instanceManager] is typically passed when a copy of an instance + /// contained by an [InstanceManager] is being created. If left null, it + /// will default to the global instance defined in [JavaObject]. + _FallbackStrategyHostApiImpl( + {this.binaryMessenger, InstanceManager? instanceManager}) { + this.instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + } + + /// Receives binary data across the Flutter platform barrier. + /// + /// If it is null, the default [BinaryMessenger] will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with native language objects. + late final InstanceManager instanceManager; + + /// Creates a [FallbackStrategy] instance with the specified video [quality] + /// and [fallbackRule]. + void createFromInstance( + FallbackStrategy instance, + VideoQualityConstraint quality, + VideoResolutionFallbackRule fallbackRule) { + final int identifier = instanceManager.addDartCreatedInstance(instance, + onCopy: (FallbackStrategy original) { + return FallbackStrategy.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + quality: original.quality, + fallbackRule: original.fallbackRule, + ); + }); + create(identifier, quality, fallbackRule); + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/quality_selector.dart b/packages/camera/camera_android_camerax/lib/src/quality_selector.dart new file mode 100644 index 00000000000..303d4aecaf5 --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/quality_selector.dart @@ -0,0 +1,150 @@ +// 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. + +import 'package:flutter/services.dart'; +import 'package:meta/meta.dart' show immutable; + +import 'camera_info.dart'; +import 'camerax_library.g.dart'; +import 'fallback_strategy.dart'; +import 'instance_manager.dart'; +import 'java_object.dart'; + +/// Quality setting used to configure components with quality setting +/// requirements such as creating a Recorder. +/// +/// See https://developer.android.com/reference/androidx/camera/video/QualitySelector. +@immutable +class QualitySelector extends JavaObject { + /// Creates a [QualitySelector] with the desired quality and fallback + /// strategy, if specified. + QualitySelector.from( + {BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + required VideoQualityConstraint quality, + this.fallbackStrategy}) + : qualityList = [quality], + super.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager) { + _api = _QualitySelectorHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + _api.createFromInstance(this, qualityList, fallbackStrategy); + } + + /// Creates a [QualitySelector] with ordered desired qualities and fallback + /// strategy, if specified. + /// + /// The final quality will be selected according to the order in which they are + /// specified. + QualitySelector.fromOrderedList( + {BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + required this.qualityList, + this.fallbackStrategy}) + : assert(qualityList.isNotEmpty, + 'Quality list specified must be non-empty.'), + super.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager) { + _api = _QualitySelectorHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + _api.createFromInstance(this, qualityList, fallbackStrategy); + } + + /// Creates a [QualitySelector] that is not automatically attached to a + /// native object. + QualitySelector.detached({ + super.binaryMessenger, + super.instanceManager, + required this.qualityList, + this.fallbackStrategy, + }) : super.detached(); + + late final _QualitySelectorHostApiImpl _api; + + /// Desired qualities for this selector instance. + final List qualityList; + + /// Desired fallback strategy for this selector instance. + final FallbackStrategy? fallbackStrategy; + + /// Retrieves the corresponding resolution from the input [quality] for the + /// camera represented by [cameraInfo]. + static Future getResolution( + CameraInfo cameraInfo, VideoQualityConstraint quality, + {BinaryMessenger? binaryMessenger, InstanceManager? instanceManager}) { + final _QualitySelectorHostApiImpl api = _QualitySelectorHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + return api.getResolutionFromInstance(cameraInfo, quality); + } +} + +/// Host API implementation of [QualitySelector]. +class _QualitySelectorHostApiImpl extends QualitySelectorHostApi { + /// Constructs a [QualitySelectorHostApiImpl]. + /// + /// If [binaryMessenger] is null, the default [BinaryMessenger] will be used, + /// which routes to the host platform. + /// + /// An [instanceManager] is typically passed when a copy of an instance + /// contained by an [InstanceManager] is being created. If left null, it + /// will default to the global instance defined in [JavaObject]. + _QualitySelectorHostApiImpl( + {this.binaryMessenger, InstanceManager? instanceManager}) { + this.instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + } + + /// Receives binary data across the Flutter platform barrier. + /// + /// If it is null, the default [BinaryMessenger] will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with native language objects. + late final InstanceManager instanceManager; + + /// Creates a [QualitySelector] instance with the desired qualities and + /// fallback strategy specified. + void createFromInstance( + QualitySelector instance, + List qualityList, + FallbackStrategy? fallbackStrategy) { + final int identifier = instanceManager.addDartCreatedInstance(instance, + onCopy: (QualitySelector original) { + return QualitySelector.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + qualityList: original.qualityList, + fallbackStrategy: original.fallbackStrategy, + ); + }); + final List qualityIndices = qualityList + .map((VideoQualityConstraint quality) => quality.index) + .toList(); + + create( + identifier, + qualityIndices, + fallbackStrategy == null + ? null + : instanceManager.getIdentifier(fallbackStrategy)); + } + + /// Retrieves the corresponding resolution from the input [quality] for the + /// camera represented by [cameraInfo]. + Future getResolutionFromInstance( + CameraInfo cameraInfo, VideoQualityConstraint quality) async { + final int? cameraInfoIdentifier = instanceManager.getIdentifier(cameraInfo); + + if (cameraInfoIdentifier == null) { + throw ArgumentError( + 'The CameraInfo instance specified needs to be added to the InstanceManager instance in use.'); + } + + final ResolutionInfo resolution = + await getResolution(cameraInfoIdentifier, quality); + return resolution; + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/recorder.dart b/packages/camera/camera_android_camerax/lib/src/recorder.dart index 43f33e31734..13953621552 100644 --- a/packages/camera/camera_android_camerax/lib/src/recorder.dart +++ b/packages/camera/camera_android_camerax/lib/src/recorder.dart @@ -7,9 +7,11 @@ import 'package:meta/meta.dart' show immutable; import 'android_camera_camerax_flutter_api_impls.dart'; import 'camerax_library.g.dart'; +import 'fallback_strategy.dart'; import 'instance_manager.dart'; import 'java_object.dart'; import 'pending_recording.dart'; +import 'quality_selector.dart'; /// A dart wrapping of the CameraX Recorder class. /// @@ -21,14 +23,15 @@ class Recorder extends JavaObject { {BinaryMessenger? binaryMessenger, InstanceManager? instanceManager, this.aspectRatio, - this.bitRate}) + this.bitRate, + this.qualitySelector}) : super.detached( binaryMessenger: binaryMessenger, instanceManager: instanceManager) { AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); _api = RecorderHostApiImpl( binaryMessenger: binaryMessenger, instanceManager: instanceManager); - _api.createFromInstance(this, aspectRatio, bitRate); + _api.createFromInstance(this, aspectRatio, bitRate, qualitySelector); } /// Creates a [Recorder] that is not automatically attached to a native object @@ -36,7 +39,8 @@ class Recorder extends JavaObject { {BinaryMessenger? binaryMessenger, InstanceManager? instanceManager, this.aspectRatio, - this.bitRate}) + this.bitRate, + this.qualitySelector}) : super.detached( binaryMessenger: binaryMessenger, instanceManager: instanceManager) { @@ -45,14 +49,42 @@ class Recorder extends JavaObject { AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); } + /// Returns default [QualitySelector] for recordings. + /// + /// See https://developer.android.com/reference/androidx/camera/video/Recorder#DEFAULT_QUALITY_SELECTOR(). + static QualitySelector getDefaultQualitySelector({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) { + return QualitySelector.fromOrderedList( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + qualityList: const [ + VideoQualityConstraint.FHD, + VideoQualityConstraint.HD, + VideoQualityConstraint.SD + ], + fallbackStrategy: FallbackStrategy( + quality: VideoQualityConstraint.FHD, + fallbackRule: VideoResolutionFallbackRule.higherQualityOrLowerThan), + ); + } + late final RecorderHostApiImpl _api; - /// The video aspect ratio of this Recorder. + /// The video aspect ratio of this [Recorder]. final int? aspectRatio; /// The intended video encoding bitrate for recording. final int? bitRate; + /// The [QualitySelector] of this [Recorder] used to select the resolution of + /// the recording depending on the resoutions supported by the camera. + /// + /// Default selector is that returned by [getDefaultQualitySelector], and it + /// is compatible with setting the aspect ratio. + final QualitySelector? qualitySelector; + /// Prepare a recording that will be saved to a file. Future prepareRecording(String path) { return _api.prepareRecordingFromInstance(this, path); @@ -77,7 +109,8 @@ class RecorderHostApiImpl extends RecorderHostApi { late final InstanceManager instanceManager; /// Creates a [Recorder] with the provided aspect ratio and bitrate if specified. - void createFromInstance(Recorder instance, int? aspectRatio, int? bitRate) { + void createFromInstance(Recorder instance, int? aspectRatio, int? bitRate, + QualitySelector? qualitySelector) { int? identifier = instanceManager.getIdentifier(instance); identifier ??= instanceManager.addDartCreatedInstance(instance, onCopy: (Recorder original) { @@ -85,9 +118,16 @@ class RecorderHostApiImpl extends RecorderHostApi { binaryMessenger: binaryMessenger, instanceManager: instanceManager, aspectRatio: aspectRatio, - bitRate: bitRate); + bitRate: bitRate, + qualitySelector: qualitySelector); }); - create(identifier, aspectRatio, bitRate); + create( + identifier, + aspectRatio, + bitRate, + qualitySelector == null + ? null + : instanceManager.getIdentifier(qualitySelector)!); } /// Prepares a [Recording] using this recorder. The output file will be saved diff --git a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart index a22d7f6e351..02ea78c049a 100644 --- a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart +++ b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart @@ -96,6 +96,31 @@ class ExposureCompensationRange { int maxCompensation; } +/// Video quality constraints that will be used by a QualitySelector to choose +/// an appropriate video resolution. +/// +/// These are pre-defined quality constants that are universally used for video. +/// +/// See https://developer.android.com/reference/androidx/camera/video/Quality. +enum VideoQualityConstraint { + SD, // 480p + HD, // 720p + FHD, // 1080p + UHD, // 2160p + lowest, + highest, +} + +/// Fallback rules for selecting video resolution. +/// +/// See https://developer.android.com/reference/androidx/camera/video/FallbackStrategy. +enum VideoResolutionFallbackRule { + higherQualityOrLowerThan, + higherQualityThan, + lowerQualityOrHigherThan, + lowerQualityThan, +} + @HostApi(dartHostTestHandler: 'TestInstanceManagerHostApi') abstract class InstanceManagerHostApi { /// Clear the native `InstanceManager`. @@ -219,7 +244,8 @@ abstract class VideoCaptureFlutterApi { @HostApi(dartHostTestHandler: 'TestRecorderHostApi') abstract class RecorderHostApi { - void create(int identifier, int? aspectRatio, int? bitRate); + void create( + int identifier, int? aspectRatio, int? bitRate, int? qualitySelectorId); int getAspectRatio(int identifier); @@ -372,3 +398,20 @@ abstract class ImageProxyFlutterApi { abstract class PlaneProxyFlutterApi { void create(int identifier, Uint8List buffer, int pixelStride, int rowStride); } + +@HostApi(dartHostTestHandler: 'TestQualitySelectorHostApi') +abstract class QualitySelectorHostApi { + // TODO(camsim99): Change qualityList to List when + // enums are supported for collection types. + void create(int identifier, List videoQualityConstraintIndexList, + int? fallbackStrategyId); + + ResolutionInfo getResolution( + int cameraInfoId, VideoQualityConstraint quality); +} + +@HostApi(dartHostTestHandler: 'TestFallbackStrategyHostApi') +abstract class FallbackStrategyHostApi { + void create(int identifier, VideoQualityConstraint quality, + VideoResolutionFallbackRule fallbackRule); +} diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index 88e408d1df8..b233b0c18a4 100644 --- a/packages/camera/camera_android_camerax/pubspec.yaml +++ b/packages/camera/camera_android_camerax/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_android_camerax description: Android implementation of the camera plugin using the CameraX library. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android_camerax issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.5.0+13 +version: 0.5.0+14 environment: sdk: ">=2.19.0 <4.0.0" diff --git a/packages/camera/camera_android_camerax/test/fallback_strategy_test.dart b/packages/camera/camera_android_camerax/test/fallback_strategy_test.dart new file mode 100644 index 00000000000..8c6960c082c --- /dev/null +++ b/packages/camera/camera_android_camerax/test/fallback_strategy_test.dart @@ -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. + +import 'package:camera_android_camerax/src/camerax_library.g.dart'; +import 'package:camera_android_camerax/src/fallback_strategy.dart'; +import 'package:camera_android_camerax/src/instance_manager.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'fallback_strategy_test.mocks.dart'; +import 'test_camerax_library.g.dart'; + +@GenerateMocks([TestFallbackStrategyHostApi, TestInstanceManagerHostApi]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('FallbackStrategy', () { + tearDown(() { + TestFallbackStrategyHostApi.setup(null); + TestInstanceManagerHostApi.setup(null); + }); + + test('detached constructor does not call create on the Java side', + () async { + final MockTestFallbackStrategyHostApi mockApi = + MockTestFallbackStrategyHostApi(); + TestFallbackStrategyHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + FallbackStrategy.detached( + quality: VideoQualityConstraint.UHD, + fallbackRule: VideoResolutionFallbackRule.higherQualityThan, + instanceManager: instanceManager, + ); + + verifyNever(mockApi.create( + argThat(isA()), + argThat(isA()), + argThat(isA()), + )); + }); + + test('constructor calls create on the Java side', () { + final MockTestFallbackStrategyHostApi mockApi = + MockTestFallbackStrategyHostApi(); + TestFallbackStrategyHostApi.setup(mockApi); + TestInstanceManagerHostApi.setup(MockTestInstanceManagerHostApi()); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + const VideoQualityConstraint quality = VideoQualityConstraint.HD; + + const VideoResolutionFallbackRule fallbackRule = + VideoResolutionFallbackRule.lowerQualityThan; + + final FallbackStrategy instance = FallbackStrategy( + quality: quality, + fallbackRule: fallbackRule, + instanceManager: instanceManager, + ); + + verify(mockApi.create( + instanceManager.getIdentifier(instance), + quality, + fallbackRule, + )); + }); + }); +} diff --git a/packages/camera/camera_android_camerax/test/fallback_strategy_test.mocks.dart b/packages/camera/camera_android_camerax/test/fallback_strategy_test.mocks.dart new file mode 100644 index 00000000000..02d185a7222 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/fallback_strategy_test.mocks.dart @@ -0,0 +1,69 @@ +// Mocks generated by Mockito 5.4.1 from annotations +// in camera_android_camerax/test/fallback_strategy_test.dart. +// Do not manually edit this file. + +// @dart=2.19 + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:camera_android_camerax/src/camerax_library.g.dart' as _i3; +import 'package:mockito/mockito.dart' as _i1; + +import 'test_camerax_library.g.dart' as _i2; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [TestFallbackStrategyHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestFallbackStrategyHostApi extends _i1.Mock + implements _i2.TestFallbackStrategyHostApi { + MockTestFallbackStrategyHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create( + int? identifier, + _i3.VideoQualityConstraint? quality, + _i3.VideoResolutionFallbackRule? fallbackRule, + ) => + super.noSuchMethod( + Invocation.method( + #create, + [ + identifier, + quality, + fallbackRule, + ], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [TestInstanceManagerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestInstanceManagerHostApi extends _i1.Mock + implements _i2.TestInstanceManagerHostApi { + MockTestInstanceManagerHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void clear() => super.noSuchMethod( + Invocation.method( + #clear, + [], + ), + returnValueForMissingStub: null, + ); +} diff --git a/packages/camera/camera_android_camerax/test/quality_selector_test.dart b/packages/camera/camera_android_camerax/test/quality_selector_test.dart new file mode 100644 index 00000000000..8c05aa7be43 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/quality_selector_test.dart @@ -0,0 +1,180 @@ +// 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. + +import 'package:camera_android_camerax/src/camera_info.dart'; +import 'package:camera_android_camerax/src/camerax_library.g.dart'; +import 'package:camera_android_camerax/src/fallback_strategy.dart'; +import 'package:camera_android_camerax/src/instance_manager.dart'; +import 'package:camera_android_camerax/src/quality_selector.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'quality_selector_test.mocks.dart'; +import 'test_camerax_library.g.dart'; + +@GenerateMocks([ + CameraInfo, + FallbackStrategy, + TestQualitySelectorHostApi, + TestInstanceManagerHostApi +]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('QualitySelector', () { + tearDown(() { + TestQualitySelectorHostApi.setup(null); + TestInstanceManagerHostApi.setup(null); + }); + + test('detached constructor does not make call to create on the Java side', + () { + final MockTestQualitySelectorHostApi mockApi = + MockTestQualitySelectorHostApi(); + TestQualitySelectorHostApi.setup(mockApi); + TestInstanceManagerHostApi.setup(MockTestInstanceManagerHostApi()); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + QualitySelector.detached( + qualityList: const [VideoQualityConstraint.FHD], + fallbackStrategy: MockFallbackStrategy(), + instanceManager: instanceManager, + ); + + verifyNever(mockApi.create( + argThat(isA()), argThat(isA>()), argThat(isA()))); + }); + + test('single quality constructor calls create on the Java side', () { + final MockTestQualitySelectorHostApi mockApi = + MockTestQualitySelectorHostApi(); + TestQualitySelectorHostApi.setup(mockApi); + TestInstanceManagerHostApi.setup(MockTestInstanceManagerHostApi()); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + const VideoQualityConstraint quality = VideoQualityConstraint.FHD; + final FallbackStrategy fallbackStrategy = MockFallbackStrategy(); + const int fallbackStrategyIdentifier = 9; + + instanceManager.addHostCreatedInstance( + fallbackStrategy, + fallbackStrategyIdentifier, + onCopy: (_) => MockFallbackStrategy(), + ); + + final QualitySelector instance = QualitySelector.from( + quality: quality, + fallbackStrategy: fallbackStrategy, + instanceManager: instanceManager, + ); + + verify(mockApi.create( + instanceManager.getIdentifier(instance), + [2], + fallbackStrategyIdentifier, + )); + }); + + test('quality list constructor calls create on the Java side', () { + final MockTestQualitySelectorHostApi mockApi = + MockTestQualitySelectorHostApi(); + TestQualitySelectorHostApi.setup(mockApi); + TestInstanceManagerHostApi.setup(MockTestInstanceManagerHostApi()); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + const List qualityList = [ + VideoQualityConstraint.FHD, + VideoQualityConstraint.highest + ]; + + final FallbackStrategy fallbackStrategy = MockFallbackStrategy(); + + const int fallbackStrategyIdentifier = 9; + instanceManager.addHostCreatedInstance( + fallbackStrategy, + fallbackStrategyIdentifier, + onCopy: (_) => MockFallbackStrategy(), + ); + + final QualitySelector instance = QualitySelector.fromOrderedList( + qualityList: qualityList, + fallbackStrategy: fallbackStrategy, + instanceManager: instanceManager, + ); + + verify(mockApi.create( + instanceManager.getIdentifier(instance), + [2, 5], + fallbackStrategyIdentifier, + )); + }); + + test('getResolution returns expected resolution info', () async { + final MockTestQualitySelectorHostApi mockApi = + MockTestQualitySelectorHostApi(); + TestQualitySelectorHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final QualitySelector instance = QualitySelector.detached( + instanceManager: instanceManager, + qualityList: const [VideoQualityConstraint.HD], + fallbackStrategy: MockFallbackStrategy(), + ); + const int instanceIdentifier = 0; + instanceManager.addHostCreatedInstance( + instance, + instanceIdentifier, + onCopy: (QualitySelector original) => QualitySelector.detached( + qualityList: original.qualityList, + fallbackStrategy: original.fallbackStrategy, + instanceManager: instanceManager, + ), + ); + + final CameraInfo cameraInfo = MockCameraInfo(); + const int cameraInfoIdentifier = 6; + instanceManager.addHostCreatedInstance( + cameraInfo, + cameraInfoIdentifier, + onCopy: (_) => MockCameraInfo(), + ); + + const VideoQualityConstraint quality = VideoQualityConstraint.FHD; + final ResolutionInfo expectedResult = + ResolutionInfo(width: 34, height: 23); + + when(mockApi.getResolution( + cameraInfoIdentifier, + quality, + )).thenAnswer((_) { + return expectedResult; + }); + + final ResolutionInfo result = await QualitySelector.getResolution( + cameraInfo, quality, + instanceManager: instanceManager); + + expect(result.width, expectedResult.width); + expect(result.height, expectedResult.height); + + verify(mockApi.getResolution( + cameraInfoIdentifier, + quality, + )); + }); + }); +} diff --git a/packages/camera/camera_android_camerax/test/quality_selector_test.mocks.dart b/packages/camera/camera_android_camerax/test/quality_selector_test.mocks.dart new file mode 100644 index 00000000000..ba08711ba73 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/quality_selector_test.mocks.dart @@ -0,0 +1,218 @@ +// Mocks generated by Mockito 5.4.1 from annotations +// in camera_android_camerax/test/quality_selector_test.dart. +// Do not manually edit this file. + +// @dart=2.19 + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i6; + +import 'package:camera_android_camerax/src/camera_info.dart' as _i5; +import 'package:camera_android_camerax/src/camera_state.dart' as _i7; +import 'package:camera_android_camerax/src/camerax_library.g.dart' as _i4; +import 'package:camera_android_camerax/src/exposure_state.dart' as _i3; +import 'package:camera_android_camerax/src/fallback_strategy.dart' as _i9; +import 'package:camera_android_camerax/src/live_data.dart' as _i2; +import 'package:camera_android_camerax/src/zoom_state.dart' as _i8; +import 'package:mockito/mockito.dart' as _i1; + +import 'test_camerax_library.g.dart' as _i10; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeLiveData_0 extends _i1.SmartFake + implements _i2.LiveData { + _FakeLiveData_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeExposureState_1 extends _i1.SmartFake implements _i3.ExposureState { + _FakeExposureState_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeResolutionInfo_2 extends _i1.SmartFake + implements _i4.ResolutionInfo { + _FakeResolutionInfo_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [CameraInfo]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockCameraInfo extends _i1.Mock implements _i5.CameraInfo { + MockCameraInfo() { + _i1.throwOnMissingStub(this); + } + + @override + _i6.Future getSensorRotationDegrees() => (super.noSuchMethod( + Invocation.method( + #getSensorRotationDegrees, + [], + ), + returnValue: _i6.Future.value(0), + ) as _i6.Future); + @override + _i6.Future<_i2.LiveData<_i7.CameraState>> getCameraState() => + (super.noSuchMethod( + Invocation.method( + #getCameraState, + [], + ), + returnValue: _i6.Future<_i2.LiveData<_i7.CameraState>>.value( + _FakeLiveData_0<_i7.CameraState>( + this, + Invocation.method( + #getCameraState, + [], + ), + )), + ) as _i6.Future<_i2.LiveData<_i7.CameraState>>); + @override + _i6.Future<_i3.ExposureState> getExposureState() => (super.noSuchMethod( + Invocation.method( + #getExposureState, + [], + ), + returnValue: _i6.Future<_i3.ExposureState>.value(_FakeExposureState_1( + this, + Invocation.method( + #getExposureState, + [], + ), + )), + ) as _i6.Future<_i3.ExposureState>); + @override + _i6.Future<_i2.LiveData<_i8.ZoomState>> getZoomState() => (super.noSuchMethod( + Invocation.method( + #getZoomState, + [], + ), + returnValue: _i6.Future<_i2.LiveData<_i8.ZoomState>>.value( + _FakeLiveData_0<_i8.ZoomState>( + this, + Invocation.method( + #getZoomState, + [], + ), + )), + ) as _i6.Future<_i2.LiveData<_i8.ZoomState>>); +} + +/// A class which mocks [FallbackStrategy]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockFallbackStrategy extends _i1.Mock implements _i9.FallbackStrategy { + MockFallbackStrategy() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.VideoQualityConstraint get quality => (super.noSuchMethod( + Invocation.getter(#quality), + returnValue: _i4.VideoQualityConstraint.SD, + ) as _i4.VideoQualityConstraint); + @override + _i4.VideoResolutionFallbackRule get fallbackRule => (super.noSuchMethod( + Invocation.getter(#fallbackRule), + returnValue: _i4.VideoResolutionFallbackRule.higherQualityOrLowerThan, + ) as _i4.VideoResolutionFallbackRule); +} + +/// A class which mocks [TestQualitySelectorHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestQualitySelectorHostApi extends _i1.Mock + implements _i10.TestQualitySelectorHostApi { + MockTestQualitySelectorHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create( + int? identifier, + List? videoQualityConstraintIndexList, + int? fallbackStrategyId, + ) => + super.noSuchMethod( + Invocation.method( + #create, + [ + identifier, + videoQualityConstraintIndexList, + fallbackStrategyId, + ], + ), + returnValueForMissingStub: null, + ); + @override + _i4.ResolutionInfo getResolution( + int? cameraInfoId, + _i4.VideoQualityConstraint? quality, + ) => + (super.noSuchMethod( + Invocation.method( + #getResolution, + [ + cameraInfoId, + quality, + ], + ), + returnValue: _FakeResolutionInfo_2( + this, + Invocation.method( + #getResolution, + [ + cameraInfoId, + quality, + ], + ), + ), + ) as _i4.ResolutionInfo); +} + +/// A class which mocks [TestInstanceManagerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestInstanceManagerHostApi extends _i1.Mock + implements _i10.TestInstanceManagerHostApi { + MockTestInstanceManagerHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void clear() => super.noSuchMethod( + Invocation.method( + #clear, + [], + ), + returnValueForMissingStub: null, + ); +} diff --git a/packages/camera/camera_android_camerax/test/recorder_test.dart b/packages/camera/camera_android_camerax/test/recorder_test.dart index 6dc398bbdad..994c0f91b83 100644 --- a/packages/camera/camera_android_camerax/test/recorder_test.dart +++ b/packages/camera/camera_android_camerax/test/recorder_test.dart @@ -2,8 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:camera_android_camerax/src/camerax_library.g.dart'; import 'package:camera_android_camerax/src/instance_manager.dart'; import 'package:camera_android_camerax/src/pending_recording.dart'; +import 'package:camera_android_camerax/src/quality_selector.dart'; import 'package:camera_android_camerax/src/recorder.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; @@ -12,8 +14,14 @@ import 'package:mockito/mockito.dart'; import 'recorder_test.mocks.dart'; import 'test_camerax_library.g.dart'; -@GenerateMocks( - [TestRecorderHostApi, TestInstanceManagerHostApi, PendingRecording]) +@GenerateMocks([ + QualitySelector, + TestInstanceManagerHostApi, + TestFallbackStrategyHostApi, + TestRecorderHostApi, + TestQualitySelectorHostApi, + PendingRecording +]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -33,8 +41,8 @@ void main() { Recorder.detached( instanceManager: instanceManager, aspectRatio: 0, bitRate: 0); - verifyNever(mockApi.create( - argThat(isA()), argThat(isA()), argThat(isA()))); + verifyNever(mockApi.create(argThat(isA()), argThat(isA()), + argThat(isA()), argThat(isA()))); }); test('create does call create on the Java side', () async { @@ -46,13 +54,52 @@ void main() { const int aspectRatio = 1; const int bitRate = 2; + final QualitySelector qualitySelector = MockQualitySelector(); + const int qualitySelectorIdentifier = 33; + + instanceManager.addHostCreatedInstance( + qualitySelector, + qualitySelectorIdentifier, + onCopy: (_) => MockQualitySelector(), + ); Recorder( instanceManager: instanceManager, aspectRatio: aspectRatio, - bitRate: bitRate); + bitRate: bitRate, + qualitySelector: qualitySelector); + + verify(mockApi.create(argThat(isA()), aspectRatio, bitRate, + qualitySelectorIdentifier)); + }); + + test('getDefaultQualitySelector returns expected QualitySelector', + () async { + final MockTestQualitySelectorHostApi mockQualitySelectorApi = + MockTestQualitySelectorHostApi(); + final MockTestFallbackStrategyHostApi mockFallbackStrategyApi = + MockTestFallbackStrategyHostApi(); + TestQualitySelectorHostApi.setup(mockQualitySelectorApi); + TestFallbackStrategyHostApi.setup(mockFallbackStrategyApi); - verify(mockApi.create(argThat(isA()), aspectRatio, bitRate)); + final QualitySelector defaultQualitySelector = + Recorder.getDefaultQualitySelector(); + + expect( + defaultQualitySelector.qualityList, + equals(const [ + VideoQualityConstraint.FHD, + VideoQualityConstraint.HD, + VideoQualityConstraint.SD + ])); + expect(defaultQualitySelector.fallbackStrategy!.quality, + equals(VideoQualityConstraint.FHD)); + expect(defaultQualitySelector.fallbackStrategy!.fallbackRule, + equals(VideoResolutionFallbackRule.higherQualityOrLowerThan)); + + // Cleanup test Host APIs used only for this test. + TestQualitySelectorHostApi.setup(null); + TestFallbackStrategyHostApi.setup(null); }); test('prepareRecording calls prepareRecording on Java side', () async { @@ -84,7 +131,9 @@ void main() { expect(pendingRecording, mockPendingRecording); }); - test('flutterApiCreateTest', () { + test( + 'flutterApi create makes call to create Recorder instance with expected identifier', + () { final InstanceManager instanceManager = InstanceManager( onWeakReferenceRemoved: (_) {}, ); diff --git a/packages/camera/camera_android_camerax/test/recorder_test.mocks.dart b/packages/camera/camera_android_camerax/test/recorder_test.mocks.dart index b563ba1bb78..c7850c70e53 100644 --- a/packages/camera/camera_android_camerax/test/recorder_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/recorder_test.mocks.dart @@ -5,13 +5,15 @@ // @dart=2.19 // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i5; +import 'dart:async' as _i7; -import 'package:camera_android_camerax/src/pending_recording.dart' as _i4; -import 'package:camera_android_camerax/src/recording.dart' as _i2; +import 'package:camera_android_camerax/src/camerax_library.g.dart' as _i2; +import 'package:camera_android_camerax/src/pending_recording.dart' as _i6; +import 'package:camera_android_camerax/src/quality_selector.dart' as _i4; +import 'package:camera_android_camerax/src/recording.dart' as _i3; import 'package:mockito/mockito.dart' as _i1; -import 'test_camerax_library.g.dart' as _i3; +import 'test_camerax_library.g.dart' as _i5; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -24,8 +26,9 @@ import 'test_camerax_library.g.dart' as _i3; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeRecording_0 extends _i1.SmartFake implements _i2.Recording { - _FakeRecording_0( +class _FakeResolutionInfo_0 extends _i1.SmartFake + implements _i2.ResolutionInfo { + _FakeResolutionInfo_0( Object parent, Invocation parentInvocation, ) : super( @@ -34,11 +37,84 @@ class _FakeRecording_0 extends _i1.SmartFake implements _i2.Recording { ); } +class _FakeRecording_1 extends _i1.SmartFake implements _i3.Recording { + _FakeRecording_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [QualitySelector]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockQualitySelector extends _i1.Mock implements _i4.QualitySelector { + MockQualitySelector() { + _i1.throwOnMissingStub(this); + } + + @override + List<_i2.VideoQualityConstraint> get qualityList => (super.noSuchMethod( + Invocation.getter(#qualityList), + returnValue: <_i2.VideoQualityConstraint>[], + ) as List<_i2.VideoQualityConstraint>); +} + +/// A class which mocks [TestInstanceManagerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestInstanceManagerHostApi extends _i1.Mock + implements _i5.TestInstanceManagerHostApi { + MockTestInstanceManagerHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void clear() => super.noSuchMethod( + Invocation.method( + #clear, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [TestFallbackStrategyHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestFallbackStrategyHostApi extends _i1.Mock + implements _i5.TestFallbackStrategyHostApi { + MockTestFallbackStrategyHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create( + int? identifier, + _i2.VideoQualityConstraint? quality, + _i2.VideoResolutionFallbackRule? fallbackRule, + ) => + super.noSuchMethod( + Invocation.method( + #create, + [ + identifier, + quality, + fallbackRule, + ], + ), + returnValueForMissingStub: null, + ); +} + /// A class which mocks [TestRecorderHostApi]. /// /// See the documentation for Mockito's code generation for more information. class MockTestRecorderHostApi extends _i1.Mock - implements _i3.TestRecorderHostApi { + implements _i5.TestRecorderHostApi { MockTestRecorderHostApi() { _i1.throwOnMissingStub(this); } @@ -48,6 +124,7 @@ class MockTestRecorderHostApi extends _i1.Mock int? identifier, int? aspectRatio, int? bitRate, + int? qualitySelectorId, ) => super.noSuchMethod( Invocation.method( @@ -56,6 +133,7 @@ class MockTestRecorderHostApi extends _i1.Mock identifier, aspectRatio, bitRate, + qualitySelectorId, ], ), returnValueForMissingStub: null, @@ -93,46 +171,79 @@ class MockTestRecorderHostApi extends _i1.Mock ) as int); } -/// A class which mocks [TestInstanceManagerHostApi]. +/// A class which mocks [TestQualitySelectorHostApi]. /// /// See the documentation for Mockito's code generation for more information. -class MockTestInstanceManagerHostApi extends _i1.Mock - implements _i3.TestInstanceManagerHostApi { - MockTestInstanceManagerHostApi() { +class MockTestQualitySelectorHostApi extends _i1.Mock + implements _i5.TestQualitySelectorHostApi { + MockTestQualitySelectorHostApi() { _i1.throwOnMissingStub(this); } @override - void clear() => super.noSuchMethod( + void create( + int? identifier, + List? videoQualityConstraintIndexList, + int? fallbackStrategyId, + ) => + super.noSuchMethod( Invocation.method( - #clear, - [], + #create, + [ + identifier, + videoQualityConstraintIndexList, + fallbackStrategyId, + ], ), returnValueForMissingStub: null, ); + @override + _i2.ResolutionInfo getResolution( + int? cameraInfoId, + _i2.VideoQualityConstraint? quality, + ) => + (super.noSuchMethod( + Invocation.method( + #getResolution, + [ + cameraInfoId, + quality, + ], + ), + returnValue: _FakeResolutionInfo_0( + this, + Invocation.method( + #getResolution, + [ + cameraInfoId, + quality, + ], + ), + ), + ) as _i2.ResolutionInfo); } /// A class which mocks [PendingRecording]. /// /// See the documentation for Mockito's code generation for more information. // ignore: must_be_immutable -class MockPendingRecording extends _i1.Mock implements _i4.PendingRecording { +class MockPendingRecording extends _i1.Mock implements _i6.PendingRecording { MockPendingRecording() { _i1.throwOnMissingStub(this); } @override - _i5.Future<_i2.Recording> start() => (super.noSuchMethod( + _i7.Future<_i3.Recording> start() => (super.noSuchMethod( Invocation.method( #start, [], ), - returnValue: _i5.Future<_i2.Recording>.value(_FakeRecording_0( + returnValue: _i7.Future<_i3.Recording>.value(_FakeRecording_1( this, Invocation.method( #start, [], ), )), - ) as _i5.Future<_i2.Recording>); + ) as _i7.Future<_i3.Recording>); } diff --git a/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart b/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart index 6a80cf0541f..d4c935e819c 100644 --- a/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart +++ b/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart @@ -780,7 +780,8 @@ abstract class TestRecorderHostApi { TestDefaultBinaryMessengerBinding.instance; static const MessageCodec codec = StandardMessageCodec(); - void create(int identifier, int? aspectRatio, int? bitRate); + void create( + int identifier, int? aspectRatio, int? bitRate, int? qualitySelectorId); int getAspectRatio(int identifier); @@ -809,7 +810,9 @@ abstract class TestRecorderHostApi { 'Argument for dev.flutter.pigeon.RecorderHostApi.create was null, expected non-null int.'); final int? arg_aspectRatio = (args[1] as int?); final int? arg_bitRate = (args[2] as int?); - api.create(arg_identifier!, arg_aspectRatio, arg_bitRate); + final int? arg_qualitySelectorId = (args[3] as int?); + api.create(arg_identifier!, arg_aspectRatio, arg_bitRate, + arg_qualitySelectorId); return []; }); } @@ -1576,3 +1579,143 @@ abstract class TestImageProxyHostApi { } } } + +class _TestQualitySelectorHostApiCodec extends StandardMessageCodec { + const _TestQualitySelectorHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is ResolutionInfo) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return ResolutionInfo.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TestQualitySelectorHostApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; + static const MessageCodec codec = _TestQualitySelectorHostApiCodec(); + + void create(int identifier, List videoQualityConstraintIndexList, + int? fallbackStrategyId); + + ResolutionInfo getResolution( + int cameraInfoId, VideoQualityConstraint quality); + + static void setup(TestQualitySelectorHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.QualitySelectorHostApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.QualitySelectorHostApi.create was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.QualitySelectorHostApi.create was null, expected non-null int.'); + final List? arg_videoQualityConstraintIndexList = + (args[1] as List?)?.cast(); + assert(arg_videoQualityConstraintIndexList != null, + 'Argument for dev.flutter.pigeon.QualitySelectorHostApi.create was null, expected non-null List.'); + final int? arg_fallbackStrategyId = (args[2] as int?); + api.create(arg_identifier!, arg_videoQualityConstraintIndexList!, + arg_fallbackStrategyId); + return []; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.QualitySelectorHostApi.getResolution', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.QualitySelectorHostApi.getResolution was null.'); + final List args = (message as List?)!; + final int? arg_cameraInfoId = (args[0] as int?); + assert(arg_cameraInfoId != null, + 'Argument for dev.flutter.pigeon.QualitySelectorHostApi.getResolution was null, expected non-null int.'); + final VideoQualityConstraint? arg_quality = args[1] == null + ? null + : VideoQualityConstraint.values[args[1] as int]; + assert(arg_quality != null, + 'Argument for dev.flutter.pigeon.QualitySelectorHostApi.getResolution was null, expected non-null VideoQualityConstraint.'); + final ResolutionInfo output = + api.getResolution(arg_cameraInfoId!, arg_quality!); + return [output]; + }); + } + } + } +} + +abstract class TestFallbackStrategyHostApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; + static const MessageCodec codec = StandardMessageCodec(); + + void create(int identifier, VideoQualityConstraint quality, + VideoResolutionFallbackRule fallbackRule); + + static void setup(TestFallbackStrategyHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FallbackStrategyHostApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.FallbackStrategyHostApi.create was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.FallbackStrategyHostApi.create was null, expected non-null int.'); + final VideoQualityConstraint? arg_quality = args[1] == null + ? null + : VideoQualityConstraint.values[args[1] as int]; + assert(arg_quality != null, + 'Argument for dev.flutter.pigeon.FallbackStrategyHostApi.create was null, expected non-null VideoQualityConstraint.'); + final VideoResolutionFallbackRule? arg_fallbackRule = args[2] == null + ? null + : VideoResolutionFallbackRule.values[args[2] as int]; + assert(arg_fallbackRule != null, + 'Argument for dev.flutter.pigeon.FallbackStrategyHostApi.create was null, expected non-null VideoResolutionFallbackRule.'); + api.create(arg_identifier!, arg_quality!, arg_fallbackRule!); + return []; + }); + } + } + } +}