Adjust the VideoCapture crop rect to a size valid for the encoder
* Find valid size by referring to the encoder's width/height alignment and supported width/height.
* After finding a valid size, will enlarge/reduce the original crop rect from its center and keep the original aspect ratio as possible.
Bug: 243808729
Test: ./gradlew camera:camera-video:connectedAndroidTest
Change-Id: I815f2d4b3b8ef0c30cf44ea0247997aa7bff74ff
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java
index f649315..75679d4 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java
@@ -26,6 +26,8 @@
import androidx.annotation.RequiresApi;
import androidx.core.util.Preconditions;
+import java.util.Locale;
+
/**
* Utility class for transform.
*
@@ -53,6 +55,11 @@
return new Size(rect.width(), rect.height());
}
+ /** Returns a formatted string for a Rect. */
+ @NonNull
+ public static String rectToString(@NonNull Rect rect) {
+ return String.format(Locale.US, "%s(%dx%d)", rect, rect.width(), rect.height());
+ }
/**
* Transforms size to a {@link Rect} with zero left and top.
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
index 9d7dcac..ced0af2 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
@@ -29,12 +29,16 @@
import static androidx.camera.core.impl.UseCaseConfig.OPTION_SESSION_CONFIG_UNPACKER;
import static androidx.camera.core.impl.UseCaseConfig.OPTION_SURFACE_OCCUPANCY_PRIORITY;
import static androidx.camera.core.impl.UseCaseConfig.OPTION_ZSL_DISABLED;
+import static androidx.camera.core.impl.utils.TransformUtils.rectToString;
import static androidx.camera.core.internal.TargetConfig.OPTION_TARGET_CLASS;
import static androidx.camera.core.internal.TargetConfig.OPTION_TARGET_NAME;
import static androidx.camera.core.internal.ThreadConfig.OPTION_BACKGROUND_EXECUTOR;
import static androidx.camera.core.internal.UseCaseEventConfig.OPTION_USE_CASE_EVENT_CALLBACK;
import static androidx.camera.video.StreamInfo.STREAM_ID_ERROR;
+import static androidx.camera.video.impl.VideoCaptureConfig.OPTION_VIDEO_ENCODER_INFO_FINDER;
import static androidx.camera.video.impl.VideoCaptureConfig.OPTION_VIDEO_OUTPUT;
+import static androidx.camera.video.internal.config.VideoConfigUtil.resolveVideoEncoderConfig;
+import static androidx.camera.video.internal.config.VideoConfigUtil.resolveVideoMimeInfo;
import static java.util.Collections.singletonList;
import static java.util.Objects.requireNonNull;
@@ -55,6 +59,7 @@
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.annotation.VisibleForTesting;
+import androidx.arch.core.util.Function;
import androidx.camera.core.AspectRatio;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageCapture;
@@ -63,6 +68,7 @@
import androidx.camera.core.SurfaceRequest;
import androidx.camera.core.UseCase;
import androidx.camera.core.ViewPort;
+import androidx.camera.core.impl.CamcorderProfileProxy;
import androidx.camera.core.impl.CameraCaptureCallback;
import androidx.camera.core.impl.CameraCaptureResult;
import androidx.camera.core.impl.CameraInfoInternal;
@@ -92,15 +98,24 @@
import androidx.camera.core.processing.SurfaceEffectNode;
import androidx.camera.video.StreamInfo.StreamState;
import androidx.camera.video.impl.VideoCaptureConfig;
+import androidx.camera.video.internal.config.MimeInfo;
+import androidx.camera.video.internal.encoder.InvalidConfigException;
+import androidx.camera.video.internal.encoder.VideoEncoderConfig;
+import androidx.camera.video.internal.encoder.VideoEncoderInfo;
+import androidx.camera.video.internal.encoder.VideoEncoderInfoImpl;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import androidx.core.util.Preconditions;
+import androidx.core.util.Supplier;
import com.google.common.util.concurrent.ListenableFuture;
import java.lang.reflect.Type;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
import java.util.Objects;
+import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
@@ -146,6 +161,9 @@
private SurfaceEffectInternal mSurfaceEffect;
@Nullable
private SurfaceEffectNode mNode;
+ @Nullable
+
+ private VideoEncoderInfo mVideoEncoderInfo;
/**
* Create a VideoCapture associated with the given {@link VideoOutput}.
@@ -316,6 +334,8 @@
@Override
public void onDetached() {
clearPipeline();
+
+ mVideoEncoderInfo = null;
}
/**
@@ -412,8 +432,9 @@
}
}
+ @VisibleForTesting
@NonNull
- private SettableSurface getCameraSettableSurface() {
+ SettableSurface getCameraSettableSurface() {
Preconditions.checkNotNull(mNode);
return (SettableSurface) requireNonNull(mDeferrableSurface);
}
@@ -446,17 +467,22 @@
// TODO(b/229410005): The expected FPS range will need to come from the camera rather
// than what is requested in the config. For now we use the default range of (30, 30)
// for behavioral consistency.
- Range<Integer> targetFpsRange = config.getTargetFramerate(Defaults.DEFAULT_FPS_RANGE);
+ Range<Integer> targetFpsRange = requireNonNull(
+ config.getTargetFramerate(Defaults.DEFAULT_FPS_RANGE));
if (mSurfaceEffect != null) {
- mNode = new SurfaceEffectNode(camera, APPLY_CROP_ROTATE_AND_MIRRORING,
- mSurfaceEffect);
+ MediaSpec mediaSpec = requireNonNull(getMediaSpec());
+ Rect cropRect = requireNonNull(getCropRect(resolution));
+ cropRect = adjustCropRectIfNeeded(cropRect, resolution,
+ () -> getVideoEncoderInfo(config.getVideoEncoderInfoFinder(), camera, mediaSpec,
+ resolution, targetFpsRange));
+ mNode = new SurfaceEffectNode(camera, APPLY_CROP_ROTATE_AND_MIRRORING, mSurfaceEffect);
SettableSurface cameraSurface = new SettableSurface(
SurfaceEffect.VIDEO_CAPTURE,
resolution,
ImageFormat.PRIVATE,
getSensorToBufferTransformMatrix(),
/*hasEmbeddedTransform=*/true,
- requireNonNull(getCropRect(resolution)),
+ cropRect,
getRelativeRotation(camera),
/*mirroring=*/false);
SurfaceEdge inputEdge = SurfaceEdge.create(singletonList(cameraSurface));
@@ -537,11 +563,22 @@
SurfaceRequest::willNotProvideSurface;
private static final VideoCaptureConfig<?> DEFAULT_CONFIG;
+ private static final Function<VideoEncoderConfig, VideoEncoderInfo>
+ DEFAULT_VIDEO_ENCODER_INFO_FINDER = encoderInfo -> {
+ try {
+ return VideoEncoderInfoImpl.from(encoderInfo);
+ } catch (InvalidConfigException e) {
+ Logger.w(TAG, "Unable to find VideoEncoderInfo", e);
+ return null;
+ }
+ };
+
static final Range<Integer> DEFAULT_FPS_RANGE = new Range<>(30, 30);
static {
Builder<?> builder = new Builder<>(DEFAULT_VIDEO_OUTPUT)
- .setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY);
+ .setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY)
+ .setVideoEncoderInfoFinder(DEFAULT_VIDEO_ENCODER_INFO_FINDER);
DEFAULT_CONFIG = builder.getUseCaseConfig();
}
@@ -630,6 +667,214 @@
}
@MainThread
+ @NonNull
+ private Rect adjustCropRectIfNeeded(@NonNull Rect cropRect, @NonNull Size resolution,
+ @NonNull Supplier<VideoEncoderInfo> videoEncoderInfoFinder) {
+ if (!isCropNeeded(cropRect, resolution)) {
+ return cropRect;
+ }
+ VideoEncoderInfo videoEncoderInfo = videoEncoderInfoFinder.get();
+ if (videoEncoderInfo == null) {
+ Logger.w(TAG, "Crop is needed but can't find the encoder info to adjust the cropRect");
+ return cropRect;
+ }
+ return adjustCropRectToValidSize(cropRect, resolution, videoEncoderInfo);
+ }
+
+ /**
+ * This method resizes the crop rectangle to a valid size.
+ *
+ * <p>The valid size must fulfill
+ * <ul>
+ * <li>The multiple of VideoEncoderInfo.getWidthAlignment()/getHeightAlignment() alignment</li>
+ * <li>In the scope of Surface resolution and VideoEncoderInfo.getSupportedWidths()
+ * /getSupportedHeights().</li>
+ * </ul>
+ *
+ * <p>When the size is not a multiple of the alignment, it seeks to shrink or enlarge the size
+ * with the smallest amount of change and ensures that the size is within the surface
+ * resolution and supported widths and heights. The new cropping rectangle position (left,
+ * right, top, and bottom) is then calculated by extending or indenting from the center of
+ * the original cropping rectangle.
+ */
+ @NonNull
+ private static Rect adjustCropRectToValidSize(@NonNull Rect cropRect, @NonNull Size resolution,
+ @NonNull VideoEncoderInfo videoEncoderInfo) {
+ Logger.d(TAG, String.format("Adjust cropRect %s by width/height alignment %d/%d and "
+ + "supported widths %s / supported heights %s",
+ rectToString(cropRect),
+ videoEncoderInfo.getWidthAlignment(),
+ videoEncoderInfo.getHeightAlignment(),
+ videoEncoderInfo.getSupportedWidths(),
+ videoEncoderInfo.getSupportedHeights()
+ ));
+
+ // Construct all up/down alignment combinations.
+ int widthAlignment = videoEncoderInfo.getWidthAlignment();
+ int heightAlignment = videoEncoderInfo.getHeightAlignment();
+ Range<Integer> supportedWidths = videoEncoderInfo.getSupportedWidths();
+ Range<Integer> supportedHeights = videoEncoderInfo.getSupportedHeights();
+ int widthAlignedDown = alignDown(cropRect.width(), widthAlignment, supportedWidths);
+ int widthAlignedUp = alignUp(cropRect.width(), widthAlignment, supportedWidths);
+ int heightAlignedDown = alignDown(cropRect.height(), heightAlignment, supportedHeights);
+ int heightAlignedUp = alignUp(cropRect.height(), heightAlignment, supportedHeights);
+
+ // Use Set to filter out duplicates.
+ Set<Size> candidateSet = new HashSet<>();
+ addBySupportedSize(candidateSet, widthAlignedDown, heightAlignedDown, resolution,
+ videoEncoderInfo);
+ addBySupportedSize(candidateSet, widthAlignedDown, heightAlignedUp, resolution,
+ videoEncoderInfo);
+ addBySupportedSize(candidateSet, widthAlignedUp, heightAlignedDown, resolution,
+ videoEncoderInfo);
+ addBySupportedSize(candidateSet, widthAlignedUp, heightAlignedUp, resolution,
+ videoEncoderInfo);
+ if (candidateSet.isEmpty()) {
+ Logger.w(TAG, "Can't find valid cropped size");
+ return cropRect;
+ }
+ List<Size> candidatesList = new ArrayList<>(candidateSet);
+ Logger.d(TAG, "candidatesList = " + candidatesList);
+
+ // Find the smallest change in dimensions.
+ Collections.sort(candidatesList,
+ (s1, s2) -> (Math.abs(s1.getWidth() - cropRect.width()) + Math.abs(
+ s1.getHeight() - cropRect.height()))
+ - (Math.abs(s2.getWidth() - cropRect.width()) + Math.abs(
+ s2.getHeight() - cropRect.height())));
+ Logger.d(TAG, "sorted candidatesList = " + candidatesList);
+ Size newSize = candidatesList.get(0);
+ int newWidth = newSize.getWidth();
+ int newHeight = newSize.getHeight();
+
+ if (newWidth == cropRect.width() && newHeight == cropRect.height()) {
+ Logger.d(TAG, "No need to adjust cropRect because crop size is valid.");
+ return cropRect;
+ }
+
+ // New width/height should be multiple of 2 since VideoCapabilities.get*Alignment()
+ // returns power of 2. This ensures width/2 and height/2 are not rounded off.
+ // New width/height smaller than resolution ensures calculated cropRect never exceeds
+ // the resolution.
+ Preconditions.checkState(newWidth % 2 == 0 && newHeight % 2 == 0
+ && newWidth <= resolution.getWidth() && newHeight <= resolution.getHeight());
+ Rect newCropRect = new Rect(cropRect);
+ if (newWidth != cropRect.width()) {
+ // Note: When the width/height of cropRect is odd number, Rect.centerX/Y() will be
+ // offset to the left/top by 0.5.
+ newCropRect.left = Math.max(0, cropRect.centerX() - newWidth / 2);
+ newCropRect.right = newCropRect.left + newWidth;
+ if (newCropRect.right > resolution.getWidth()) {
+ newCropRect.right = resolution.getWidth();
+ newCropRect.left = newCropRect.right - newWidth;
+ }
+ }
+ if (newHeight != cropRect.height()) {
+ newCropRect.top = Math.max(0, cropRect.centerY() - newHeight / 2);
+ newCropRect.bottom = newCropRect.top + newHeight;
+ if (newCropRect.bottom > resolution.getHeight()) {
+ newCropRect.bottom = resolution.getHeight();
+ newCropRect.top = newCropRect.bottom - newHeight;
+ }
+ }
+ Logger.d(TAG, String.format("Adjust cropRect from %s to %s", rectToString(cropRect),
+ rectToString(newCropRect)));
+ return newCropRect;
+ }
+
+ private static void addBySupportedSize(@NonNull Set<Size> candidates, int width, int height,
+ @NonNull Size resolution, @NonNull VideoEncoderInfo videoEncoderInfo) {
+ if (width > resolution.getWidth() || height > resolution.getHeight()) {
+ return;
+ }
+ try {
+ Range<Integer> supportedHeights = videoEncoderInfo.getSupportedHeightsFor(width);
+ candidates.add(new Size(width, supportedHeights.clamp(height)));
+ } catch (IllegalArgumentException e) {
+ Logger.w(TAG, "No supportedHeights for width: " + width, e);
+ }
+ try {
+ Range<Integer> supportedWidths = videoEncoderInfo.getSupportedWidthsFor(height);
+ candidates.add(new Size(supportedWidths.clamp(width), height));
+ } catch (IllegalArgumentException e) {
+ Logger.w(TAG, "No supportedWidths for height: " + height, e);
+ }
+ }
+
+ private static boolean isCropNeeded(@NonNull Rect cropRect, @NonNull Size resolution) {
+ return resolution.getWidth() != cropRect.width()
+ || resolution.getHeight() != cropRect.height();
+ }
+
+ private static int alignDown(int length, int alignment,
+ @NonNull Range<Integer> supportedLength) {
+ return align(true, length, alignment, supportedLength);
+ }
+
+ private static int alignUp(int length, int alignment,
+ @NonNull Range<Integer> supportedRange) {
+ return align(false, length, alignment, supportedRange);
+ }
+
+ private static int align(boolean alignDown, int length, int alignment,
+ @NonNull Range<Integer> supportedRange) {
+ int remainder = length % alignment;
+ int newLength;
+ if (remainder == 0) {
+ newLength = length;
+ } else if (alignDown) {
+ newLength = length - remainder;
+ } else {
+ newLength = length + (alignment - remainder);
+ }
+ // Clamp new length by supportedRange, which is supposed to be valid length.
+ return supportedRange.clamp(newLength);
+ }
+
+ @MainThread
+ @Nullable
+ private VideoEncoderInfo getVideoEncoderInfo(
+ @NonNull Function<VideoEncoderConfig, VideoEncoderInfo> videoEncoderInfoFinder,
+ @NonNull CameraInternal camera,
+ @NonNull MediaSpec mediaSpec,
+ @NonNull Size resolution,
+ @NonNull Range<Integer> targetFps) {
+ if (mVideoEncoderInfo != null) {
+ return mVideoEncoderInfo;
+ }
+ // Cache the VideoEncoderInfo as it should be the same when recreating the pipeline.
+ // This avoids recreating the MediaCodec instance to get encoder information.
+ // Note: We should clear the cache if the MediaSpec changes at any time, especially when
+ // the Encoder-related content in the VideoSpec changes. i.e. when we need to observe the
+ // MediaSpec Observable.
+ return mVideoEncoderInfo = resolveVideoEncoderInfo(videoEncoderInfoFinder, camera,
+ mediaSpec, resolution, targetFps);
+ }
+
+ @Nullable
+ private static VideoEncoderInfo resolveVideoEncoderInfo(
+ @NonNull Function<VideoEncoderConfig, VideoEncoderInfo> videoEncoderInfoFinder,
+ @NonNull CameraInternal camera,
+ @NonNull MediaSpec mediaSpec,
+ @NonNull Size resolution,
+ @NonNull Range<Integer> targetFps) {
+ // Find the nearest CamcorderProfile
+ VideoCapabilities videoCapabilities = VideoCapabilities.from(camera.getCameraInfo());
+ CamcorderProfileProxy camcorderProfileProxy =
+ videoCapabilities.findHighestSupportedCamcorderProfileFor(resolution);
+
+ // Resolve the VideoEncoderConfig
+ MimeInfo videoMimeInfo = resolveVideoMimeInfo(mediaSpec, camcorderProfileProxy);
+ VideoEncoderConfig videoEncoderConfig = resolveVideoEncoderConfig(
+ videoMimeInfo,
+ mediaSpec.getVideoSpec(),
+ resolution,
+ targetFps);
+
+ return videoEncoderInfoFinder.apply(videoEncoderConfig);
+ }
+
+ @MainThread
private void setupSurfaceUpdateNotifier(@NonNull SessionConfig.Builder sessionConfigBuilder,
boolean isStreamActive) {
if (mSurfaceUpdateFuture != null) {
@@ -914,6 +1159,14 @@
return new VideoCaptureConfig<>(OptionsBundle.from(mMutableConfig));
}
+ @NonNull
+ Builder<T> setVideoEncoderInfoFinder(
+ @NonNull Function<VideoEncoderConfig, VideoEncoderInfo> videoEncoderInfoFinder) {
+ getMutableConfig().insertOption(OPTION_VIDEO_ENCODER_INFO_FINDER,
+ videoEncoderInfoFinder);
+ return this;
+ }
+
/**
* Builds an immutable {@link VideoCaptureConfig} from the current state.
*
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/impl/VideoCaptureConfig.java b/camera/camera-video/src/main/java/androidx/camera/video/impl/VideoCaptureConfig.java
index dac4381..c9bd4de 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/impl/VideoCaptureConfig.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/impl/VideoCaptureConfig.java
@@ -18,6 +18,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import androidx.arch.core.util.Function;
import androidx.camera.core.impl.Config;
import androidx.camera.core.impl.ImageFormatConstants;
import androidx.camera.core.impl.ImageOutputConfig;
@@ -26,6 +27,10 @@
import androidx.camera.core.internal.ThreadConfig;
import androidx.camera.video.VideoCapture;
import androidx.camera.video.VideoOutput;
+import androidx.camera.video.internal.encoder.VideoEncoderConfig;
+import androidx.camera.video.internal.encoder.VideoEncoderInfo;
+
+import java.util.Objects;
/**
* Config for a video capture use case.
@@ -46,6 +51,10 @@
public static final Option<VideoOutput> OPTION_VIDEO_OUTPUT =
Option.create("camerax.video.VideoCapture.videoOutput", VideoOutput.class);
+ public static final Option<Function<VideoEncoderConfig, VideoEncoderInfo>>
+ OPTION_VIDEO_ENCODER_INFO_FINDER =
+ Option.create("camerax.video.VideoCapture.videoEncoderInfoFinder", Function.class);
+
// *********************************************************************************************
private final OptionsBundle mConfig;
@@ -60,6 +69,11 @@
return (T) retrieveOption(OPTION_VIDEO_OUTPUT);
}
+ @NonNull
+ public Function<VideoEncoderConfig, VideoEncoderInfo> getVideoEncoderInfoFinder() {
+ return Objects.requireNonNull(retrieveOption(OPTION_VIDEO_ENCODER_INFO_FINDER));
+ }
+
/**
* Retrieves the format of the image that is fed as input.
*
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
index dc31faf..5918033 100644
--- a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
+++ b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
@@ -17,10 +17,13 @@
package androidx.camera.video
import android.content.Context
+import android.graphics.Rect
import android.os.Build
import android.os.Looper
+import android.util.Range
import android.util.Size
import android.view.Surface
+import androidx.arch.core.util.Function
import androidx.camera.core.CameraSelector
import androidx.camera.core.CameraXConfig
import androidx.camera.core.SurfaceRequest
@@ -30,6 +33,7 @@
import androidx.camera.core.impl.ImageOutputConfig
import androidx.camera.core.impl.MutableStateObservable
import androidx.camera.core.impl.Observable
+import androidx.camera.core.impl.utils.TransformUtils.rectToSize
import androidx.camera.core.impl.utils.executor.CameraXExecutors
import androidx.camera.core.internal.CameraUseCaseAdapter
import androidx.camera.testing.CamcorderProfileUtil
@@ -52,6 +56,9 @@
import androidx.camera.testing.fakes.FakeSurfaceEffectInternal
import androidx.camera.video.StreamInfo.StreamState
import androidx.camera.video.impl.VideoCaptureConfig
+import androidx.camera.video.internal.encoder.FakeVideoEncoderInfo
+import androidx.camera.video.internal.encoder.VideoEncoderConfig
+import androidx.camera.video.internal.encoder.VideoEncoderInfo
import androidx.core.util.Consumer
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
@@ -106,12 +113,8 @@
@Test
fun setTargetResolution_throwsException() {
- val videoOutput = createVideoOutput()
-
assertThrows(UnsupportedOperationException::class.java) {
- VideoCapture.Builder(videoOutput)
- .setTargetResolution(ANY_SIZE)
- .build()
+ createVideoCapture(targetResolution = ANY_SIZE)
}
}
@@ -121,7 +124,7 @@
val videoOutput = createVideoOutput()
// Act.
- val videoCapture = VideoCapture.withOutput(videoOutput)
+ val videoCapture = createVideoCapture(videoOutput)
// Assert.
assertThat(videoCapture.output).isEqualTo(videoOutput)
@@ -135,9 +138,7 @@
var surfaceRequest: SurfaceRequest? = null
val videoOutput = createVideoOutput(surfaceRequestListener = { surfaceRequest = it })
- val videoCapture = VideoCapture.Builder(videoOutput)
- .setSessionOptionUnpacker { _, _ -> }
- .build()
+ val videoCapture = createVideoCapture(videoOutput)
// Act.
addAndAttachUseCases(videoCapture)
@@ -153,9 +154,7 @@
createCameraUseCaseAdapter()
val videoOutput = createVideoOutput(mediaSpec = null)
- val videoCapture = VideoCapture.Builder(videoOutput)
- .setSessionOptionUnpacker { _, _ -> }
- .build()
+ val videoCapture = createVideoCapture(videoOutput)
// Assert.
assertThrows(CameraUseCaseAdapter.CameraException::class.java) {
@@ -189,9 +188,7 @@
it.setQualitySelector(QualitySelector.from(quality))
}.build()
)
- val videoCapture = VideoCapture.Builder(videoOutput)
- .setSessionOptionUnpacker { _, _ -> }
- .build()
+ val videoCapture = createVideoCapture(videoOutput)
// Act.
addAndAttachUseCases(videoCapture)
@@ -238,9 +235,7 @@
)
}.build()
)
- val videoCapture = VideoCapture.Builder(videoOutput)
- .setSessionOptionUnpacker { _, _ -> }
- .build()
+ val videoCapture = createVideoCapture(videoOutput)
// Act.
addAndAttachUseCases(videoCapture)
@@ -265,9 +260,7 @@
it.setQualitySelector(QualitySelector.from(Quality.FHD))
}.build()
)
- val videoCapture = VideoCapture.Builder(videoOutput)
- .setSessionOptionUnpacker { _, _ -> }
- .build()
+ val videoCapture = createVideoCapture(videoOutput)
// Assert.
assertThrows(CameraUseCaseAdapter.CameraException::class.java) {
@@ -287,9 +280,7 @@
it.setQualitySelector(QualitySelector.from(Quality.UHD))
}.build()
)
- val videoCapture = VideoCapture.Builder(videoOutput)
- .setSessionOptionUnpacker { _, _ -> }
- .build()
+ val videoCapture = createVideoCapture(videoOutput)
// Act.
addAndAttachUseCases(videoCapture)
@@ -315,9 +306,7 @@
CameraXExecutors.directExecutor()
) { surfaceResult = it }
}
- val videoCapture = VideoCapture.Builder(videoOutput)
- .setSessionOptionUnpacker { _, _ -> }
- .build()
+ val videoCapture = createVideoCapture(videoOutput)
// Act.
addAndAttachUseCases(videoCapture)
@@ -338,7 +327,7 @@
@Test
fun setTargetRotation_rotationIsChanged() {
// Arrange.
- val videoCapture = VideoCapture.withOutput(createVideoOutput())
+ val videoCapture = createVideoCapture()
// Act.
videoCapture.targetRotation = Surface.ROTATION_180
@@ -362,9 +351,7 @@
}
)
- val videoCapture = VideoCapture.Builder(videoOutput)
- .setSessionOptionUnpacker { _, _ -> }
- .build()
+ val videoCapture = createVideoCapture(videoOutput)
// Act.
addAndAttachUseCases(videoCapture)
@@ -388,10 +375,7 @@
}
}
)
- val videoCapture = VideoCapture.Builder(videoOutput)
- .setTargetRotation(Surface.ROTATION_90)
- .setSessionOptionUnpacker { _, _ -> }
- .build()
+ val videoCapture = createVideoCapture(videoOutput, targetRotation = Surface.ROTATION_90)
// Act.
addAndAttachUseCases(videoCapture)
@@ -455,9 +439,7 @@
appSurfaceReadyToRelease = true
}
})
- val videoCapture = VideoCapture.Builder(videoOutput)
- .setSessionOptionUnpacker { _, _ -> }
- .build()
+ val videoCapture = createVideoCapture(videoOutput)
// Act: bind and provide Surface.
videoCapture.setEffect(effect)
@@ -490,6 +472,108 @@
assertThat(appSurfaceReadyToRelease).isTrue()
}
+ @Test
+ fun adjustCropRect_noAdjustment() {
+ testAdjustCropRectToValidSize(
+ videoEncoderInfo = createVideoEncoderInfo(widthAlignment = 8, heightAlignment = 8),
+ cropRect = Rect(8, 8, 808, 608),
+ expectedCropRect = Rect(8, 8, 808, 608),
+ )
+ }
+
+ @Test
+ fun adjustCropRect_toSmallerSize() {
+ testAdjustCropRectToValidSize(
+ videoEncoderInfo = createVideoEncoderInfo(widthAlignment = 8, heightAlignment = 8),
+ cropRect = Rect(8, 8, 811, 608), // 803x600 -> 800x600
+ expectedCropRect = Rect(9, 8, 809, 608),
+ )
+ }
+
+ @Test
+ fun adjustCropRect_toLargerSize() {
+ testAdjustCropRectToValidSize(
+ videoEncoderInfo = createVideoEncoderInfo(widthAlignment = 8, heightAlignment = 8),
+ cropRect = Rect(8, 8, 805, 608), // 797x600 -> 800x600
+ expectedCropRect = Rect(6, 8, 806, 608),
+ )
+ }
+
+ @Test
+ fun adjustCropRect_toLargerSize_fromTopLeft() {
+ testAdjustCropRectToValidSize(
+ videoEncoderInfo = createVideoEncoderInfo(widthAlignment = 8, heightAlignment = 8),
+ cropRect = Rect(0, 0, 797, 600), // 797x600 -> 800x600
+ expectedCropRect = Rect(0, 0, 800, 600),
+ )
+ }
+
+ @Test
+ fun adjustCropRect_toLargerSize_fromBottomRight() {
+ testAdjustCropRectToValidSize(
+ // Quality.HD maps to 1280x720 (4:3)
+ videoEncoderInfo = createVideoEncoderInfo(widthAlignment = 8, heightAlignment = 8),
+ cropRect = Rect(1280 - 797, 720 - 600, 1280, 720), // 797x600 -> 800x600
+ expectedCropRect = Rect(1280 - 800, 720 - 600, 1280, 720),
+ )
+ }
+
+ @Test
+ fun adjustCropRect_clampBySupportedWidthsHeights() {
+ testAdjustCropRectToValidSize(
+ videoEncoderInfo = createVideoEncoderInfo(
+ widthAlignment = 8,
+ heightAlignment = 8,
+ supportedWidths = Range(80, 800),
+ supportedHeights = Range(100, 800),
+ ),
+ cropRect = Rect(8, 8, 48, 48), // 40x40
+ expectedCropRect = Rect(0, 0, 80, 100),
+ )
+ }
+
+ @Test
+ fun adjustCropRect_toSmallestDimensionChange() {
+ testAdjustCropRectToValidSize(
+ videoEncoderInfo = createVideoEncoderInfo(widthAlignment = 8, heightAlignment = 8),
+ cropRect = Rect(8, 8, 811, 607), // 803x599 -> 800x600
+ expectedCropRect = Rect(9, 7, 809, 607),
+ )
+ }
+
+ private fun testAdjustCropRectToValidSize(
+ quality: Quality = Quality.HD, // Quality.HD maps to 1280x720 (4:3)
+ videoEncoderInfo: VideoEncoderInfo = createVideoEncoderInfo(),
+ cropRect: Rect,
+ expectedCropRect: Rect,
+ ) {
+ // Arrange.
+ setupCamera()
+ createCameraUseCaseAdapter()
+ var surfaceRequest: SurfaceRequest? = null
+ val videoOutput = createVideoOutput(
+ mediaSpec = MediaSpec.builder().configureVideo {
+ it.setQualitySelector(QualitySelector.from(quality))
+ }.build(),
+ surfaceRequestListener = { surfaceRequest = it }
+ )
+ val videoCapture = createVideoCapture(
+ videoOutput, videoEncoderInfoFinder = { videoEncoderInfo }
+ )
+ val effect = FakeSurfaceEffectInternal(CameraXExecutors.mainThreadExecutor(), false)
+ videoCapture.setEffect(effect)
+ videoCapture.setViewPortCropRect(cropRect)
+
+ // Act.
+ addAndAttachUseCases(videoCapture)
+ shadowOf(Looper.getMainLooper()).idle()
+
+ // Assert.
+ assertThat(surfaceRequest).isNotNull()
+ assertThat(surfaceRequest!!.resolution).isEqualTo(rectToSize(expectedCropRect))
+ assertThat(videoCapture.cameraSettableSurface.cropRect).isEqualTo(expectedCropRect)
+ }
+
private fun assertSupportedResolutions(
videoCapture: VideoCapture<out VideoOutput>,
vararg expectedResolutions: Size
@@ -502,6 +586,20 @@
}
}
+ private fun createVideoEncoderInfo(
+ widthAlignment: Int = 2,
+ heightAlignment: Int = 2,
+ supportedWidths: Range<Int> = Range.create(0, Integer.MAX_VALUE),
+ supportedHeights: Range<Int> = Range.create(0, Integer.MAX_VALUE),
+ ): VideoEncoderInfo {
+ return FakeVideoEncoderInfo(
+ _widthAlignment = widthAlignment,
+ _heightAlignment = heightAlignment,
+ _supportedWidths = supportedWidths,
+ _supportedHeights = supportedHeights,
+ )
+ }
+
private fun createVideoOutput(
streamState: StreamState = StreamState.ACTIVE,
mediaSpec: MediaSpec? = MediaSpec.builder().build(),
@@ -552,6 +650,19 @@
CameraUtil.createCameraUseCaseAdapter(context, CameraSelector.DEFAULT_BACK_CAMERA)
}
+ private fun createVideoCapture(
+ videoOutput: VideoOutput = createVideoOutput(),
+ targetRotation: Int? = null,
+ targetResolution: Size? = null,
+ videoEncoderInfoFinder: Function<VideoEncoderConfig, VideoEncoderInfo>? = null,
+ ): VideoCapture<VideoOutput> = VideoCapture.Builder(videoOutput)
+ .setSessionOptionUnpacker { _, _ -> }
+ .apply {
+ targetRotation?.let { setTargetRotation(it) }
+ targetResolution?.let { setTargetResolution(it) }
+ videoEncoderInfoFinder?.let { setVideoEncoderInfoFinder(it) }
+ }.build()
+
private fun setupCamera(
cameraId: String = CAMERA_ID_0,
vararg profiles: CamcorderProfileProxy = CAMERA_0_PROFILES
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/internal/encoder/FakeEncoderInfo.kt b/camera/camera-video/src/test/java/androidx/camera/video/internal/encoder/FakeEncoderInfo.kt
new file mode 100644
index 0000000..f78553d
--- /dev/null
+++ b/camera/camera-video/src/test/java/androidx/camera/video/internal/encoder/FakeEncoderInfo.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video.internal.encoder
+
+open class FakeEncoderInfo(var _name: String = "fake.encoder") : EncoderInfo {
+ override fun getName(): String = _name
+}
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/internal/encoder/FakeVideoEncoderInfo.kt b/camera/camera-video/src/test/java/androidx/camera/video/internal/encoder/FakeVideoEncoderInfo.kt
new file mode 100644
index 0000000..1b05685
--- /dev/null
+++ b/camera/camera-video/src/test/java/androidx/camera/video/internal/encoder/FakeVideoEncoderInfo.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video.internal.encoder
+
+import android.util.Range
+
+class FakeVideoEncoderInfo(
+ var _supportedWidths: Range<Int> = Range.create(0, Integer.MAX_VALUE),
+ var _supportedHeights: Range<Int> = Range.create(0, Integer.MAX_VALUE),
+ var _widthAlignment: Int = 2,
+ var _heightAlignment: Int = 2,
+) : FakeEncoderInfo(), VideoEncoderInfo {
+
+ override fun getSupportedWidths(): Range<Int> {
+ return _supportedWidths
+ }
+
+ override fun getSupportedHeights(): Range<Int> {
+ return _supportedHeights
+ }
+
+ override fun getSupportedWidthsFor(height: Int): Range<Int> {
+ return _supportedWidths
+ }
+
+ override fun getSupportedHeightsFor(width: Int): Range<Int> {
+ return _supportedHeights
+ }
+
+ override fun getWidthAlignment(): Int {
+ return _widthAlignment
+ }
+
+ override fun getHeightAlignment(): Int {
+ return _heightAlignment
+ }
+}
\ No newline at end of file