Add more core APIs to CameraController.
Users have been requesting to add more core APIs to CameraController. After another round of internal discussion(go/camera-controller-do-not-inherit), we have decided CameraController should continue to be a wrapper that forwards API calls. This CL adds some of the core features to CameraController.
- Getters/setters for target aspect ratio and target resolution. A new OutputSize class that defines the aspect ratio or resolution.
- Getter for CameraControl
- Getters/setters for capture mode.
- Getters/fetters for custom executors.
- Unlink VideoCapture in javadoc because it's hidden now.
Relnote: adding more camera-core features to CameraController: getters/setters for target aspect ratio, target resolution, capture mode, CameraControl and custom executors.
Bug: 188543457
Test: manual test and ./gradlew bOS
Change-Id: Iea8f2b507fa9783e6b63d42ee5f47ec867c5151a
diff --git a/camera/camera-view/api/current.txt b/camera/camera-view/api/current.txt
index d9c61fc..a56d9d4 100644
--- a/camera/camera-view/api/current.txt
+++ b/camera/camera-view/api/current.txt
@@ -4,13 +4,21 @@
public abstract class CameraController {
method @MainThread public void clearImageAnalysisAnalyzer();
method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enableTorch(boolean);
+ method @MainThread public androidx.camera.core.CameraControl? getCameraControl();
method @MainThread public androidx.camera.core.CameraInfo? getCameraInfo();
method @MainThread public androidx.camera.core.CameraSelector getCameraSelector();
+ method @MainThread public java.util.concurrent.Executor? getImageAnalysisBackgroundExecutor();
method @MainThread public int getImageAnalysisBackpressureStrategy();
method @MainThread public int getImageAnalysisImageQueueDepth();
+ method @MainThread public androidx.camera.view.CameraController.OutputSize? getImageAnalysisTargetSize();
method @MainThread public int getImageCaptureFlashMode();
+ method @MainThread public java.util.concurrent.Executor? getImageCaptureIoExecutor();
+ method @MainThread public int getImageCaptureMode();
+ method @MainThread public androidx.camera.view.CameraController.OutputSize? getImageCaptureTargetSize();
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> getInitializationFuture();
+ method @MainThread public androidx.camera.view.CameraController.OutputSize? getPreviewTargetSize();
method @MainThread public androidx.lifecycle.LiveData<java.lang.Integer!> getTorchState();
+ method @MainThread public androidx.camera.view.CameraController.OutputSize? getVideoCaptureTargetSize();
method @MainThread public androidx.lifecycle.LiveData<androidx.camera.core.ZoomState!> getZoomState();
method @MainThread public boolean hasCamera(androidx.camera.core.CameraSelector);
method @MainThread public boolean isImageAnalysisEnabled();
@@ -20,12 +28,19 @@
method @MainThread public void setCameraSelector(androidx.camera.core.CameraSelector);
method @MainThread public void setEnabledUseCases(int);
method @MainThread public void setImageAnalysisAnalyzer(java.util.concurrent.Executor, androidx.camera.core.ImageAnalysis.Analyzer);
+ method @MainThread public void setImageAnalysisBackgroundExecutor(java.util.concurrent.Executor?);
method @MainThread public void setImageAnalysisBackpressureStrategy(int);
method @MainThread public void setImageAnalysisImageQueueDepth(int);
+ method @MainThread public void setImageAnalysisTargetSize(androidx.camera.view.CameraController.OutputSize?);
method @MainThread public void setImageCaptureFlashMode(int);
+ method @MainThread public void setImageCaptureIoExecutor(java.util.concurrent.Executor?);
+ method @MainThread public void setImageCaptureMode(int);
+ method @MainThread public void setImageCaptureTargetSize(androidx.camera.view.CameraController.OutputSize?);
method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setLinearZoom(@FloatRange(from=0.0f, to=1.0f) float);
method @MainThread public void setPinchToZoomEnabled(boolean);
+ method @MainThread public void setPreviewTargetSize(androidx.camera.view.CameraController.OutputSize?);
method @MainThread public void setTapToFocusEnabled(boolean);
+ method @MainThread public void setVideoCaptureTargetSize(androidx.camera.view.CameraController.OutputSize?);
method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setZoomRatio(float);
method @MainThread public void takePicture(androidx.camera.core.ImageCapture.OutputFileOptions, java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageSavedCallback);
method @MainThread public void takePicture(java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageCapturedCallback);
@@ -33,6 +48,14 @@
field public static final int IMAGE_CAPTURE = 1; // 0x1
}
+ public static class CameraController.OutputSize {
+ ctor public CameraController.OutputSize(int);
+ ctor public CameraController.OutputSize(android.util.Size);
+ method public int getAspectRatio();
+ method public android.util.Size? getResolution();
+ field public static final int UNASSIGNED_ASPECT_RATIO = -1; // 0xffffffff
+ }
+
public final class LifecycleCameraController extends androidx.camera.view.CameraController {
ctor public LifecycleCameraController(android.content.Context);
method @MainThread public void bindToLifecycle(androidx.lifecycle.LifecycleOwner);
diff --git a/camera/camera-view/api/public_plus_experimental_current.txt b/camera/camera-view/api/public_plus_experimental_current.txt
index a02bc5e..798e132 100644
--- a/camera/camera-view/api/public_plus_experimental_current.txt
+++ b/camera/camera-view/api/public_plus_experimental_current.txt
@@ -4,13 +4,21 @@
public abstract class CameraController {
method @MainThread public void clearImageAnalysisAnalyzer();
method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enableTorch(boolean);
+ method @MainThread public androidx.camera.core.CameraControl? getCameraControl();
method @MainThread public androidx.camera.core.CameraInfo? getCameraInfo();
method @MainThread public androidx.camera.core.CameraSelector getCameraSelector();
+ method @MainThread public java.util.concurrent.Executor? getImageAnalysisBackgroundExecutor();
method @MainThread @androidx.camera.core.ImageAnalysis.BackpressureStrategy public int getImageAnalysisBackpressureStrategy();
method @MainThread public int getImageAnalysisImageQueueDepth();
+ method @MainThread public androidx.camera.view.CameraController.OutputSize? getImageAnalysisTargetSize();
method @MainThread @androidx.camera.core.ImageCapture.FlashMode public int getImageCaptureFlashMode();
+ method @MainThread public java.util.concurrent.Executor? getImageCaptureIoExecutor();
+ method @MainThread public int getImageCaptureMode();
+ method @MainThread public androidx.camera.view.CameraController.OutputSize? getImageCaptureTargetSize();
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> getInitializationFuture();
+ method @MainThread public androidx.camera.view.CameraController.OutputSize? getPreviewTargetSize();
method @MainThread public androidx.lifecycle.LiveData<java.lang.Integer!> getTorchState();
+ method @MainThread public androidx.camera.view.CameraController.OutputSize? getVideoCaptureTargetSize();
method @MainThread public androidx.lifecycle.LiveData<androidx.camera.core.ZoomState!> getZoomState();
method @MainThread public boolean hasCamera(androidx.camera.core.CameraSelector);
method @MainThread public boolean isImageAnalysisEnabled();
@@ -22,12 +30,19 @@
method @MainThread public void setCameraSelector(androidx.camera.core.CameraSelector);
method @MainThread public void setEnabledUseCases(int);
method @MainThread public void setImageAnalysisAnalyzer(java.util.concurrent.Executor, androidx.camera.core.ImageAnalysis.Analyzer);
+ method @MainThread public void setImageAnalysisBackgroundExecutor(java.util.concurrent.Executor?);
method @MainThread public void setImageAnalysisBackpressureStrategy(@androidx.camera.core.ImageAnalysis.BackpressureStrategy int);
method @MainThread public void setImageAnalysisImageQueueDepth(int);
+ method @MainThread public void setImageAnalysisTargetSize(androidx.camera.view.CameraController.OutputSize?);
method @MainThread public void setImageCaptureFlashMode(@androidx.camera.core.ImageCapture.FlashMode int);
+ method @MainThread public void setImageCaptureIoExecutor(java.util.concurrent.Executor?);
+ method @MainThread public void setImageCaptureMode(@androidx.camera.core.ImageCapture.CaptureMode int);
+ method @MainThread public void setImageCaptureTargetSize(androidx.camera.view.CameraController.OutputSize?);
method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setLinearZoom(@FloatRange(from=0.0f, to=1.0f) float);
method @MainThread public void setPinchToZoomEnabled(boolean);
+ method @MainThread public void setPreviewTargetSize(androidx.camera.view.CameraController.OutputSize?);
method @MainThread public void setTapToFocusEnabled(boolean);
+ method @MainThread public void setVideoCaptureTargetSize(androidx.camera.view.CameraController.OutputSize?);
method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setZoomRatio(float);
method @MainThread @androidx.camera.view.video.ExperimentalVideo public void startRecording(androidx.camera.view.video.OutputFileOptions, java.util.concurrent.Executor, androidx.camera.view.video.OnVideoSavedCallback);
method @MainThread @androidx.camera.view.video.ExperimentalVideo public void stopRecording();
@@ -38,6 +53,14 @@
field @androidx.camera.view.video.ExperimentalVideo public static final int VIDEO_CAPTURE = 4; // 0x4
}
+ public static class CameraController.OutputSize {
+ ctor public CameraController.OutputSize(@androidx.camera.core.AspectRatio.Ratio int);
+ ctor public CameraController.OutputSize(android.util.Size);
+ method public int getAspectRatio();
+ method public android.util.Size? getResolution();
+ field public static final int UNASSIGNED_ASPECT_RATIO = -1; // 0xffffffff
+ }
+
public final class LifecycleCameraController extends androidx.camera.view.CameraController {
ctor public LifecycleCameraController(android.content.Context);
method @MainThread public void bindToLifecycle(androidx.lifecycle.LifecycleOwner);
diff --git a/camera/camera-view/api/restricted_current.txt b/camera/camera-view/api/restricted_current.txt
index a47bbc3..c880826 100644
--- a/camera/camera-view/api/restricted_current.txt
+++ b/camera/camera-view/api/restricted_current.txt
@@ -4,13 +4,21 @@
public abstract class CameraController {
method @MainThread public void clearImageAnalysisAnalyzer();
method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enableTorch(boolean);
+ method @MainThread public androidx.camera.core.CameraControl? getCameraControl();
method @MainThread public androidx.camera.core.CameraInfo? getCameraInfo();
method @MainThread public androidx.camera.core.CameraSelector getCameraSelector();
+ method @MainThread public java.util.concurrent.Executor? getImageAnalysisBackgroundExecutor();
method @MainThread @androidx.camera.core.ImageAnalysis.BackpressureStrategy public int getImageAnalysisBackpressureStrategy();
method @MainThread public int getImageAnalysisImageQueueDepth();
+ method @MainThread public androidx.camera.view.CameraController.OutputSize? getImageAnalysisTargetSize();
method @MainThread @androidx.camera.core.ImageCapture.FlashMode public int getImageCaptureFlashMode();
+ method @MainThread public java.util.concurrent.Executor? getImageCaptureIoExecutor();
+ method @MainThread public int getImageCaptureMode();
+ method @MainThread public androidx.camera.view.CameraController.OutputSize? getImageCaptureTargetSize();
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> getInitializationFuture();
+ method @MainThread public androidx.camera.view.CameraController.OutputSize? getPreviewTargetSize();
method @MainThread public androidx.lifecycle.LiveData<java.lang.Integer!> getTorchState();
+ method @MainThread public androidx.camera.view.CameraController.OutputSize? getVideoCaptureTargetSize();
method @MainThread public androidx.lifecycle.LiveData<androidx.camera.core.ZoomState!> getZoomState();
method @MainThread public boolean hasCamera(androidx.camera.core.CameraSelector);
method @MainThread public boolean isImageAnalysisEnabled();
@@ -20,12 +28,19 @@
method @MainThread public void setCameraSelector(androidx.camera.core.CameraSelector);
method @MainThread public void setEnabledUseCases(int);
method @MainThread public void setImageAnalysisAnalyzer(java.util.concurrent.Executor, androidx.camera.core.ImageAnalysis.Analyzer);
+ method @MainThread public void setImageAnalysisBackgroundExecutor(java.util.concurrent.Executor?);
method @MainThread public void setImageAnalysisBackpressureStrategy(@androidx.camera.core.ImageAnalysis.BackpressureStrategy int);
method @MainThread public void setImageAnalysisImageQueueDepth(int);
+ method @MainThread public void setImageAnalysisTargetSize(androidx.camera.view.CameraController.OutputSize?);
method @MainThread public void setImageCaptureFlashMode(@androidx.camera.core.ImageCapture.FlashMode int);
+ method @MainThread public void setImageCaptureIoExecutor(java.util.concurrent.Executor?);
+ method @MainThread public void setImageCaptureMode(@androidx.camera.core.ImageCapture.CaptureMode int);
+ method @MainThread public void setImageCaptureTargetSize(androidx.camera.view.CameraController.OutputSize?);
method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setLinearZoom(@FloatRange(from=0.0f, to=1.0f) float);
method @MainThread public void setPinchToZoomEnabled(boolean);
+ method @MainThread public void setPreviewTargetSize(androidx.camera.view.CameraController.OutputSize?);
method @MainThread public void setTapToFocusEnabled(boolean);
+ method @MainThread public void setVideoCaptureTargetSize(androidx.camera.view.CameraController.OutputSize?);
method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setZoomRatio(float);
method @MainThread public void takePicture(androidx.camera.core.ImageCapture.OutputFileOptions, java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageSavedCallback);
method @MainThread public void takePicture(java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageCapturedCallback);
@@ -33,6 +48,14 @@
field public static final int IMAGE_CAPTURE = 1; // 0x1
}
+ public static class CameraController.OutputSize {
+ ctor public CameraController.OutputSize(@androidx.camera.core.AspectRatio.Ratio int);
+ ctor public CameraController.OutputSize(android.util.Size);
+ method public int getAspectRatio();
+ method public android.util.Size? getResolution();
+ field public static final int UNASSIGNED_ASPECT_RATIO = -1; // 0xffffffff
+ }
+
public final class LifecycleCameraController extends androidx.camera.view.CameraController {
ctor public LifecycleCameraController(android.content.Context);
method @MainThread public void bindToLifecycle(androidx.lifecycle.LifecycleOwner);
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
index 98d607f..2fe7a79 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
@@ -16,12 +16,15 @@
package androidx.camera.view;
+import static androidx.camera.view.CameraController.OutputSize.UNASSIGNED_ASPECT_RATIO;
+
import android.annotation.SuppressLint;
import android.content.Context;
import android.hardware.display.DisplayManager;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
+import android.util.Size;
import android.view.Display;
import androidx.annotation.DoNotInline;
@@ -34,6 +37,7 @@
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
+import androidx.camera.core.AspectRatio;
import androidx.camera.core.Camera;
import androidx.camera.core.CameraControl;
import androidx.camera.core.CameraInfo;
@@ -56,6 +60,7 @@
import androidx.camera.core.VideoCapture;
import androidx.camera.core.ViewPort;
import androidx.camera.core.ZoomState;
+import androidx.camera.core.impl.ImageOutputConfig;
import androidx.camera.core.impl.utils.Threads;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.impl.utils.futures.Futures;
@@ -150,32 +155,50 @@
// Synthetic access
@SuppressWarnings("WeakerAccess")
@NonNull
- final Preview mPreview;
+ Preview mPreview;
+
+ @Nullable
+ OutputSize mPreviewTargetSize;
// Synthetic access
@SuppressWarnings("WeakerAccess")
@NonNull
- final ImageCapture mImageCapture;
+ ImageCapture mImageCapture;
+
+ @Nullable
+ OutputSize mImageCaptureTargetSize;
+
+ @Nullable
+ Executor mImageCaptureIoExecutor;
@Nullable
private Executor mAnalysisExecutor;
@Nullable
+ private Executor mAnalysisBackgroundExecutor;
+
+ @Nullable
private ImageAnalysis.Analyzer mAnalysisAnalyzer;
@NonNull
ImageAnalysis mImageAnalysis;
+ @Nullable
+ OutputSize mImageAnalysisTargetSize;
+
// Synthetic access
@SuppressWarnings("WeakerAccess")
@NonNull
- final VideoCapture mVideoCapture;
+ VideoCapture mVideoCapture;
// Synthetic access
@SuppressWarnings("WeakerAccess")
@NonNull
final AtomicBoolean mVideoIsRecording = new AtomicBoolean(false);
+ @Nullable
+ OutputSize mVideoCaptureOutputSize;
+
// The latest bound camera.
// Synthetic access
@SuppressWarnings("WeakerAccess")
@@ -375,6 +398,35 @@
return (mEnabledUseCases & useCaseMask) != 0;
}
+ /**
+ * Sets the target aspect ratio or target resolution based on {@link OutputSize}.
+ */
+ private void setTargetOutputSize(@NonNull ImageOutputConfig.Builder<?> builder,
+ @Nullable OutputSize outputSize) {
+ if (outputSize == null) {
+ return;
+ }
+ if (outputSize.getResolution() != null) {
+ builder.setTargetResolution(outputSize.getResolution());
+ } else if (outputSize.getAspectRatio() != UNASSIGNED_ASPECT_RATIO) {
+ builder.setTargetAspectRatio(outputSize.getAspectRatio());
+ } else {
+ Logger.e(TAG, "Invalid target surface size. " + outputSize);
+ }
+ }
+
+ /**
+ * Checks if two {@link OutputSize} are equal.
+ */
+ private boolean isOutputSizeEqual(
+ @Nullable OutputSize currentSize,
+ @Nullable OutputSize newSize) {
+ if (currentSize == newSize) {
+ return true;
+ }
+ return currentSize != null && currentSize.equals(newSize);
+ }
+
// ------------------
// Preview use case.
// ------------------
@@ -433,6 +485,55 @@
return (DisplayManager) mAppContext.getSystemService(Context.DISPLAY_SERVICE);
}
+ /**
+ * Sets the intended output size for {@link Preview}.
+ *
+ * <p> The value is used as a hint when determining the resolution and aspect ratio of the
+ * preview. The actual output may differ from the requested value due to device constraints.
+ *
+ * <p> When set to null, the output will be based on the default config of {@link Preview}.
+ *
+ * <p> Changing the target size will reconfigure the camera which will cause additional latency.
+ * To avoid this, set the target size before controller is bound to lifecycle.
+ *
+ * @param targetSize the intended output size for {@link Preview}.
+ * @see Preview.Builder#setTargetAspectRatio(int)
+ * @see Preview.Builder#setTargetResolution(Size)
+ */
+ @MainThread
+ public void setPreviewTargetSize(@Nullable OutputSize targetSize) {
+ Threads.checkMainThread();
+ if (isOutputSizeEqual(mPreviewTargetSize, targetSize)) {
+ return;
+ }
+ mPreviewTargetSize = targetSize;
+ unbindPreviewAndRecreate();
+ startCameraAndTrackStates();
+ }
+
+ /**
+ * Returns the intended output size for {@link Preview} set by
+ * {@link #setPreviewTargetSize(OutputSize)}, or null if not set.
+ */
+ @MainThread
+ @Nullable
+ public OutputSize getPreviewTargetSize() {
+ Threads.checkMainThread();
+ return mPreviewTargetSize;
+ }
+
+ /**
+ * Unbinds {@link Preview} and recreates with the latest parameters.
+ */
+ private void unbindPreviewAndRecreate() {
+ if (isCameraInitialized()) {
+ mCameraProvider.unbind(mPreview);
+ }
+ Preview.Builder builder = new Preview.Builder();
+ setTargetOutputSize(builder, mPreviewTargetSize);
+ mPreview = builder.build();
+ }
+
// ----------------------
// ImageCapture UseCase.
// ----------------------
@@ -546,6 +647,120 @@
mImageCapture.takePicture(executor, callback);
}
+ /**
+ * Sets the image capture mode.
+ *
+ * <p>Valid capture modes are {@link ImageCapture.CaptureMode#CAPTURE_MODE_MINIMIZE_LATENCY},
+ * which prioritizes latency over image quality, or
+ * {@link ImageCapture.CaptureMode#CAPTURE_MODE_MAXIMIZE_QUALITY},
+ * which prioritizes image quality over latency.
+ *
+ * @param captureMode the requested image capture mode.
+ */
+ @MainThread
+ public void setImageCaptureMode(@ImageCapture.CaptureMode int captureMode) {
+ Threads.checkMainThread();
+ if (mImageCapture.getCaptureMode() == captureMode) {
+ return;
+ }
+ unbindImageCaptureAndRecreate(captureMode);
+ startCameraAndTrackStates();
+ }
+
+ /**
+ * Returns the image capture mode.
+ *
+ * @see ImageCapture#getCaptureMode()
+ */
+ @MainThread
+ public int getImageCaptureMode() {
+ Threads.checkMainThread();
+ return mImageCapture.getCaptureMode();
+ }
+
+ /**
+ * Sets the intended image size for {@link ImageCapture}.
+ *
+ * <p> The value is used as a hint when determining the resolution and aspect ratio of
+ * the captured image. The actual output may differ from the requested value due to device
+ * constraints.
+ *
+ * <p> When set to null, the output will be based on the default config of {@link ImageCapture}.
+ *
+ * <p> Changing the target size will reconfigure the camera which will cause additional latency.
+ * To avoid this, set the target size before controller is bound to lifecycle.
+ *
+ * @param targetSize the intended image size for {@link ImageCapture}.
+ */
+ @MainThread
+ public void setImageCaptureTargetSize(@Nullable OutputSize targetSize) {
+ Threads.checkMainThread();
+ if (isOutputSizeEqual(mImageCaptureTargetSize, targetSize)) {
+ return;
+ }
+ mImageCaptureTargetSize = targetSize;
+ unbindImageCaptureAndRecreate(getImageCaptureMode());
+ startCameraAndTrackStates();
+ }
+
+ /**
+ * Returns the intended output size for {@link ImageCapture} set by
+ * {@link #setImageCaptureTargetSize(OutputSize)}, or null if not set.
+ */
+ @MainThread
+ @Nullable
+ public OutputSize getImageCaptureTargetSize() {
+ Threads.checkMainThread();
+ return mImageCaptureTargetSize;
+ }
+
+ /**
+ * Sets the default executor that will be used for {@link ImageCapture} IO tasks.
+ *
+ * <p> This executor will be used for any IO tasks specifically for {@link ImageCapture},
+ * such as {@link #takePicture(ImageCapture.OutputFileOptions, Executor,
+ * ImageCapture.OnImageSavedCallback)}. If no executor is set, then a default Executor
+ * specifically for IO will be used instead.
+ *
+ * @param executor The executor which will be used for IO tasks.
+ * TODO(b/187842789) add @see link for ImageCapture.
+ */
+ @MainThread
+ public void setImageCaptureIoExecutor(@Nullable Executor executor) {
+ Threads.checkMainThread();
+ if (mImageCaptureIoExecutor == executor) {
+ return;
+ }
+ mImageCaptureIoExecutor = executor;
+ unbindImageCaptureAndRecreate(mImageCapture.getCaptureMode());
+ startCameraAndTrackStates();
+ }
+
+ /**
+ * Gets the default executor for {@link ImageCapture} IO tasks.
+ */
+ @MainThread
+ @Nullable
+ public Executor getImageCaptureIoExecutor() {
+ Threads.checkMainThread();
+ return mImageCaptureIoExecutor;
+ }
+
+ /**
+ * Unbinds {@link ImageCapture} and recreates with the latest parameters.
+ */
+ private void unbindImageCaptureAndRecreate(int imageCaptureMode) {
+ if (isCameraInitialized()) {
+ mCameraProvider.unbind(mImageCapture);
+ }
+ ImageCapture.Builder builder = new ImageCapture.Builder().setCaptureMode(imageCaptureMode);
+ setTargetOutputSize(builder, mImageCaptureTargetSize);
+ if (mImageCaptureIoExecutor != null) {
+ builder.setIoExecutor(mImageCaptureIoExecutor);
+ }
+ mImageCapture = builder.build();
+ }
+
// -----------------
// Image analysis
// -----------------
@@ -673,19 +888,94 @@
}
/**
- * Unbinds {@link ImageAnalysis} and recreates with the given parameters.
+ * Sets the intended output size for {@link ImageAnalysis}.
*
- * <p> This is necessary because unlike other use cases, {@link ImageAnalysis}'s parameters
- * cannot be updated without recreating the use case.
+ * <p> The value is used as a hint when determining the resolution and aspect ratio of
+ * the output buffer. The actual output may differ from the requested value due to device
+ * constraints.
+ *
+ * <p> When set to null, the output will be based on the default config of
+ * {@link ImageAnalysis}.
+ *
+ * <p> Changing the target size will reconfigure the camera which will cause additional latency.
+ * To avoid this, set the target size before controller is bound to lifecycle.
+ *
+ * @param targetSize the intended output size for {@link ImageAnalysis}.
+ * @see ImageAnalysis.Builder#setTargetAspectRatio(int)
+ * @see ImageAnalysis.Builder#setTargetResolution(Size)
+ */
+ @MainThread
+ public void setImageAnalysisTargetSize(@Nullable OutputSize targetSize) {
+ Threads.checkMainThread();
+ if (isOutputSizeEqual(mImageAnalysisTargetSize, targetSize)) {
+ return;
+ }
+ mImageAnalysisTargetSize = targetSize;
+ unbindImageAnalysisAndRecreate(
+ mImageAnalysis.getBackpressureStrategy(),
+ mImageAnalysis.getImageQueueDepth());
+ startCameraAndTrackStates();
+ }
+
+ /**
+ * Returns the intended output size for {@link ImageAnalysis} set by
+ * {@link #setImageAnalysisTargetSize(OutputSize)}, or null if not set.
+ */
+ @MainThread
+ @Nullable
+ public OutputSize getImageAnalysisTargetSize() {
+ Threads.checkMainThread();
+ return mImageAnalysisTargetSize;
+ }
+
+ /**
+ * Sets the executor that will be used for {@link ImageAnalysis} background tasks.
+ *
+ * <p>If not set, the background executor will default to an automatically generated
+ * {@link Executor}.
+ *
+ * @param executor the executor for {@link ImageAnalysis} background tasks.
+ * @see ImageAnalysis.Builder#setBackgroundExecutor(Executor)
+ */
+ @MainThread
+ public void setImageAnalysisBackgroundExecutor(@Nullable Executor executor) {
+ Threads.checkMainThread();
+ if (mAnalysisBackgroundExecutor == executor) {
+ return;
+ }
+ mAnalysisBackgroundExecutor = executor;
+ unbindImageAnalysisAndRecreate(mImageAnalysis.getBackpressureStrategy(),
+ mImageAnalysis.getImageQueueDepth());
+ startCameraAndTrackStates();
+ }
+
+ /**
+ * Gets the default executor for {@link ImageAnalysis} background tasks.
+ *
+ * @see ImageAnalysis.Builder#setBackgroundExecutor(Executor)
+ */
+ @MainThread
+ @Nullable
+ public Executor getImageAnalysisBackgroundExecutor() {
+ Threads.checkMainThread();
+ return mAnalysisBackgroundExecutor;
+ }
+
+ /**
+ * Unbinds {@link ImageAnalysis} and recreates with the latest parameters.
*/
private void unbindImageAnalysisAndRecreate(int strategy, int imageQueueDepth) {
if (isCameraInitialized()) {
mCameraProvider.unbind(mImageAnalysis);
}
- mImageAnalysis = new ImageAnalysis.Builder()
+ ImageAnalysis.Builder builder = new ImageAnalysis.Builder()
.setBackpressureStrategy(strategy)
- .setImageQueueDepth(imageQueueDepth)
- .build();
+ .setImageQueueDepth(imageQueueDepth);
+ setTargetOutputSize(builder, mImageAnalysisTargetSize);
+ if (mAnalysisBackgroundExecutor != null) {
+ builder.setBackgroundExecutor(mAnalysisBackgroundExecutor);
+ }
+ mImageAnalysis = builder.build();
if (mAnalysisExecutor != null && mAnalysisAnalyzer != null) {
mImageAnalysis.setAnalyzer(mAnalysisExecutor, mAnalysisAnalyzer);
}
@@ -765,6 +1055,53 @@
return mVideoIsRecording.get();
}
+ /**
+ * Sets the intended video size for {@code VideoCapture}.
+ *
+ * <p> The value is used as a hint when determining the resolution and aspect ratio of
+ * the video. The actual output may differ from the requested value due to device constraints.
+ *
+ * <p> When set to null, the output will be based on the default config of {@code VideoCapture}.
+ *
+ * <p> Changing the target size will reconfigure the camera which will cause additional latency.
+ * To avoid this, set the target size before controller is bound to lifecycle.
+ *
+ * @param targetSize the intended video size for {@code VideoCapture}.
+ */
+ @MainThread
+ public void setVideoCaptureTargetSize(@Nullable OutputSize targetSize) {
+ Threads.checkMainThread();
+ if (isOutputSizeEqual(mVideoCaptureOutputSize, targetSize)) {
+ return;
+ }
+ mVideoCaptureOutputSize = targetSize;
+ unbindVideoAndRecreate();
+ startCameraAndTrackStates();
+ }
+
+ /**
+ * Returns the intended output size for {@code VideoCapture} set by
+ * {@link #setVideoCaptureTargetSize(OutputSize)}, or null if not set.
+ */
+ @MainThread
+ @Nullable
+ public OutputSize getVideoCaptureTargetSize() {
+ Threads.checkMainThread();
+ return mVideoCaptureOutputSize;
+ }
+
+ /**
+ * Unbinds VideoCapture and recreate with the latest parameters.
+ */
+ private void unbindVideoAndRecreate() {
+ if (isCameraInitialized()) {
+ mCameraProvider.unbind(mVideoCapture);
+ }
+ VideoCapture.Builder builder = new VideoCapture.Builder();
+ setTargetOutputSize(builder, mVideoCaptureOutputSize);
+ mVideoCapture = builder.build();
+ }
+
// -----------------
// Camera control
// -----------------
@@ -985,6 +1322,12 @@
/**
* Gets the {@link CameraInfo} of the currently attached camera.
*
+ * <p> For info available directly through CameraController as well as {@link CameraInfo},
+ * it's recommended to use the ones with CameraController, e.g. {@link #getTorchState()} v.s.
+ * {@link CameraInfo#getTorchState()}. {@link CameraInfo} is a lower-layer API and may
+ * require more steps to achieve the same effect, and will not maintain values when switching
+ * between cameras.
+ *
* @return the {@link CameraInfo} of the current camera. Returns null if camera is not ready.
* @see Camera#getCameraInfo()
*/
@@ -996,6 +1339,25 @@
}
/**
+ * Gets the {@link CameraControl} of the currently attached camera.
+ *
+ * <p> For controls available directly through CameraController as well as
+ * {@link CameraControl}, it's recommended to use the ones with CameraController, e.g.
+ * {@link #setLinearZoom(float)} v.s. {@link CameraControl#setLinearZoom(float)}.
+ * CameraControl is a lower-layer API and may require more steps to achieve the same effect,
+ * and will not maintain control values when switching between cameras.
+ *
+ * @return the {@link CameraControl} of the current camera. Returns null if camera is not ready.
+ * @see Camera#getCameraControl()
+ */
+ @Nullable
+ @MainThread
+ public CameraControl getCameraControl() {
+ Threads.checkMainThread();
+ return mCamera == null ? null : mCamera.getCameraControl();
+ }
+
+ /**
* Sets current zoom by ratio.
*
* <p>Valid zoom values range from {@link ZoomState#getMinZoomRatio()} to
@@ -1221,4 +1583,84 @@
return context.getAttributionTag();
}
}
+
+ /**
+ * Represents the output size of a {@link UseCase}.
+ *
+ * <p> This class is a preferred output size to be used with {@link CameraController}. The
+ * preferred output size can be based on either resolution or aspect ratio, but not both.
+ *
+ * @see #setImageAnalysisTargetSize(OutputSize)
+ * @see #setPreviewTargetSize(OutputSize)
+ * @see #setImageCaptureTargetSize(OutputSize)
+ * @see #setVideoCaptureTargetSize(OutputSize)
+ */
+ public static class OutputSize {
+
+ /**
+ * A value that represents the aspect ratio is not assigned.
+ */
+ public static final int UNASSIGNED_ASPECT_RATIO = -1;
+
+ /**
+ * Possible value for {@link #getAspectRatio()}
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {UNASSIGNED_ASPECT_RATIO, AspectRatio.RATIO_4_3, AspectRatio.RATIO_16_9})
+ public @interface OutputAspectRatio {
+ }
+
+ @OutputAspectRatio
+ private final int mAspectRatio;
+
+ @Nullable
+ private final Size mResolution;
+
+ /**
+ * Creates a {@link OutputSize} that is based on aspect ratio.
+ *
+ * @see Preview.Builder#setTargetAspectRatio(int)
+ * @see ImageAnalysis.Builder#setTargetAspectRatio(int)
+ */
+ public OutputSize(@AspectRatio.Ratio int aspectRatio) {
+ Preconditions.checkArgument(aspectRatio != UNASSIGNED_ASPECT_RATIO);
+ mAspectRatio = aspectRatio;
+ mResolution = null;
+ }
+
+ /**
+ * Creates a {@link OutputSize} that is based on resolution.
+ *
+ * @see Preview.Builder#setTargetResolution(Size)
+ * @see ImageAnalysis.Builder#setTargetResolution(Size)
+ */
+ public OutputSize(@NonNull Size resolution) {
+ Preconditions.checkNotNull(resolution);
+ mAspectRatio = UNASSIGNED_ASPECT_RATIO;
+ mResolution = resolution;
+ }
+
+ /**
+ * Gets the value of aspect ratio.
+ *
+ * @return {@link #UNASSIGNED_ASPECT_RATIO} if the size is not based on aspect ratio.
+ */
+ @OutputAspectRatio
+ public int getAspectRatio() {
+ return mAspectRatio;
+ }
+
+ /**
+ * Gets the value of resolution.
+ *
+ * @return null if the size is not based on resolution.
+ */
+ @Nullable
+ public Size getResolution() {
+ return mResolution;
+ }
+ }
}
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/CameraControllerTest.kt b/camera/camera-view/src/test/java/androidx/camera/view/CameraControllerTest.kt
index fe3a1c0..ce2e610 100644
--- a/camera/camera-view/src/test/java/androidx/camera/view/CameraControllerTest.kt
+++ b/camera/camera-view/src/test/java/androidx/camera/view/CameraControllerTest.kt
@@ -18,10 +18,17 @@
import android.content.Context
import android.os.Build
+import android.util.Size
import android.view.Surface
+import androidx.camera.core.AspectRatio
import androidx.camera.core.CameraSelector
import androidx.camera.core.CameraX
import androidx.camera.core.CameraXConfig
+import androidx.camera.core.ImageAnalysis
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.impl.ImageAnalysisConfig
+import androidx.camera.core.impl.ImageCaptureConfig
+import androidx.camera.core.impl.ImageOutputConfig
import androidx.camera.testing.fakes.FakeAppConfig
import androidx.test.annotation.UiThreadTest
import androidx.test.core.app.ApplicationProvider
@@ -33,6 +40,7 @@
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.annotation.internal.DoNotInstrument
+import java.util.concurrent.Executors
/**
* Unit tests for [CameraController].
@@ -43,6 +51,11 @@
public class CameraControllerTest {
private val context = ApplicationProvider.getApplicationContext<Context>()
+ private lateinit var controller: CameraController
+ private val targetSizeWithAspectRatio =
+ CameraController.OutputSize(AspectRatio.RATIO_16_9)
+ private val targetSizeWithResolution =
+ CameraController.OutputSize(Size(1080, 1960))
@Before
public fun setUp() {
@@ -50,6 +63,7 @@
FakeAppConfig.create()
).build()
CameraX.initialize(context, cameraXConfig).get()
+ controller = LifecycleCameraController(context)
}
@After
@@ -59,24 +73,139 @@
@UiThreadTest
@Test
- public fun sensorRotationChanges_useCaseTargetRotationUpdated() {
- // Arrange.
- val controller = LifecycleCameraController(context)
+ public fun setPreviewAspectRatio() {
+ controller.previewTargetSize = targetSizeWithAspectRatio
+ assertThat(controller.previewTargetSize).isEqualTo(targetSizeWithAspectRatio)
+ val config = controller.mPreview.currentConfig as ImageOutputConfig
+ assertThat(config.targetAspectRatio).isEqualTo(targetSizeWithAspectRatio.aspectRatio)
+ }
+
+ @UiThreadTest
+ @Test
+ public fun setPreviewResolution() {
+ controller.previewTargetSize = targetSizeWithResolution
+ assertThat(controller.previewTargetSize).isEqualTo(targetSizeWithResolution)
+
+ val config = controller.mPreview.currentConfig as ImageOutputConfig
+ assertThat(config.targetResolution).isEqualTo(targetSizeWithResolution.resolution)
+ }
+
+ @UiThreadTest
+ @Test
+ public fun setAnalysisAspectRatio() {
+ controller.imageAnalysisTargetSize = targetSizeWithAspectRatio
+ assertThat(controller.imageAnalysisTargetSize).isEqualTo(targetSizeWithAspectRatio)
+
+ val config = controller.mImageAnalysis.currentConfig as ImageOutputConfig
+ assertThat(config.targetAspectRatio).isEqualTo(targetSizeWithAspectRatio.aspectRatio)
+ }
+
+ @UiThreadTest
+ @Test
+ public fun setAnalysisBackgroundExecutor() {
+ val executor = Executors.newSingleThreadExecutor()
+ controller.imageAnalysisBackgroundExecutor = executor
+ assertThat(controller.imageAnalysisBackgroundExecutor).isEqualTo(executor)
+ val config = controller.mImageAnalysis.currentConfig as ImageAnalysisConfig
+ assertThat(config.backgroundExecutor).isEqualTo(executor)
+ }
+
+ @UiThreadTest
+ @Test
+ public fun setAnalysisQueueDepth() {
+ controller.imageAnalysisImageQueueDepth = 100
+ assertThat(controller.imageAnalysisImageQueueDepth).isEqualTo(100)
+ assertThat(controller.mImageAnalysis.imageQueueDepth).isEqualTo(100)
+ }
+
+ @UiThreadTest
+ @Test
+ public fun setAnalysisBackpressureStrategy() {
+ controller.imageAnalysisBackpressureStrategy = ImageAnalysis.STRATEGY_BLOCK_PRODUCER
+ assertThat(controller.imageAnalysisBackpressureStrategy)
+ .isEqualTo(ImageAnalysis.STRATEGY_BLOCK_PRODUCER)
+ assertThat(controller.mImageAnalysis.backpressureStrategy)
+ .isEqualTo(ImageAnalysis.STRATEGY_BLOCK_PRODUCER)
+ }
+
+ @UiThreadTest
+ @Test
+ public fun setImageCaptureResolution() {
+ controller.imageCaptureTargetSize = targetSizeWithResolution
+ assertThat(controller.imageCaptureTargetSize).isEqualTo(targetSizeWithResolution)
+
+ val config = controller.mImageCapture.currentConfig as ImageOutputConfig
+ assertThat(config.targetResolution).isEqualTo(targetSizeWithResolution.resolution)
+ }
+
+ @UiThreadTest
+ @Test
+ public fun setImageCaptureAspectRatio() {
+ controller.imageCaptureTargetSize = targetSizeWithAspectRatio
+ assertThat(controller.imageCaptureTargetSize).isEqualTo(targetSizeWithAspectRatio)
+
+ val config = controller.mImageCapture.currentConfig as ImageOutputConfig
+ assertThat(config.targetAspectRatio).isEqualTo(targetSizeWithAspectRatio.aspectRatio)
+ }
+
+ @UiThreadTest
+ @Test
+ public fun setImageCaptureMode() {
+ controller.imageCaptureMode = ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY
+ assertThat(controller.imageCaptureMode)
+ .isEqualTo(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
+ assertThat(controller.mImageCapture.captureMode)
+ .isEqualTo(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
+ }
+
+ @UiThreadTest
+ @Test
+ public fun setImageCaptureIoExecutor() {
+ val ioExecutor = Executors.newSingleThreadExecutor()
+ controller.imageCaptureIoExecutor = ioExecutor
+ assertThat(controller.imageCaptureIoExecutor).isEqualTo(ioExecutor)
+ val config = controller.mImageCapture.currentConfig as ImageCaptureConfig
+ assertThat(config.ioExecutor).isEqualTo(ioExecutor)
+ }
+
+ @UiThreadTest
+ @Test
+ public fun setVideoCaptureResolution() {
+ controller.videoCaptureTargetSize = targetSizeWithResolution
+ assertThat(controller.videoCaptureTargetSize).isEqualTo(targetSizeWithResolution)
+
+ val config = controller.mVideoCapture.currentConfig as ImageOutputConfig
+ assertThat(config.targetResolution).isEqualTo(targetSizeWithResolution.resolution)
+ }
+
+ @UiThreadTest
+ @Test
+ public fun setVideoCaptureAspectRatio() {
+ controller.videoCaptureTargetSize = targetSizeWithAspectRatio
+ assertThat(controller.videoCaptureTargetSize).isEqualTo(targetSizeWithAspectRatio)
+
+ val config = controller.mVideoCapture.currentConfig as ImageOutputConfig
+ assertThat(config.targetAspectRatio).isEqualTo(targetSizeWithAspectRatio.aspectRatio)
+ }
+
+ @UiThreadTest
+ @Test
+ public fun sensorRotationChanges_useCaseTargetRotationUpdated() {
// Act.
controller.mRotationReceiver.onRotationChanged(Surface.ROTATION_180)
// Assert.
assertThat(controller.mImageAnalysis.targetRotation).isEqualTo(Surface.ROTATION_180)
assertThat(controller.mImageCapture.targetRotation).isEqualTo(Surface.ROTATION_180)
- // TODO(b/177276479): verify VideoCapture once it supports getTargetRotation().
+ val videoConfig = controller.mVideoCapture.currentConfig as ImageOutputConfig
+ assertThat(videoConfig.targetRotation).isEqualTo(Surface.ROTATION_180)
}
@UiThreadTest
@Test
public fun setSelectorBeforeBound_selectorSet() {
// Arrange.
- val controller = LifecycleCameraController(context)
assertThat(controller.cameraSelector.lensFacing).isEqualTo(CameraSelector.LENS_FACING_BACK)
// Act.
diff --git a/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt b/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt
index 5ee2e66..e1ea9bb 100644
--- a/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt
+++ b/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt
@@ -118,6 +118,14 @@
}
@Test
+ fun controllerBound_canGetCameraControl() {
+ fragment.assertPreviewIsStreaming()
+ instrumentation.runOnMainSync {
+ assertThat(fragment.cameraController.cameraControl).isNotNull()
+ }
+ }
+
+ @Test
fun controllerBound_canGetCameraInfo() {
fragment.assertPreviewIsStreaming()
instrumentation.runOnMainSync {