Add Camera Extensions Realtime Latency Estimate API
- adds new realtime latency estimate api to image capture
- the realtime latency can be obtained if the attached session processor supports realtime latency estimates; for example the image capture is associated with a camera extensions session and the current extension supports realtime latency estimates
Bug: 289261257
Test: ./gradlew :camera:camera-extensions:connectedDebugAndroidTest, ./gradlew :camera:camera-core:testDebugUnitTest
Change-Id: I8194ba914e0d4cb408515f10e1d8db589ad6a959
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index d5f554e..dd99ca8 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -105,6 +105,7 @@
import androidx.camera.core.impl.MutableOptionsBundle;
import androidx.camera.core.impl.OptionsBundle;
import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.core.impl.SessionProcessor;
import androidx.camera.core.impl.StreamSpec;
import androidx.camera.core.impl.UseCaseConfig;
import androidx.camera.core.impl.UseCaseConfigFactory;
@@ -1154,6 +1155,37 @@
}
/**
+ * Returns an estimate of the capture and processing sequence duration based on the current
+ * camera configuration and scene conditions. The value will vary as the scene and/or camera
+ * configuration change.
+ *
+ * <p>The processing estimate can vary based on device processing load.
+ *
+ * <p>If the image capture latency estimate is not supported then
+ * {@link ImageCaptureLatencyEstimate#UNDEFINED_IMAGE_CAPTURE_LATENCY} is returned. If the
+ * capture latency is not supported then the capture latency component will be
+ * {@link ImageCaptureLatencyEstimate#UNDEFINED_CAPTURE_LATENCY}. If the processing
+ * latency is not supported then the processing latency component will be
+ * {@link ImageCaptureLatencyEstimate#UNDEFINED_PROCESSING_LATENCY}.
+ **/
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ public ImageCaptureLatencyEstimate getRealtimeCaptureLatencyEstimate() {
+ final CameraInternal camera = getCamera();
+ if (camera == null) {
+ return ImageCaptureLatencyEstimate.UNDEFINED_IMAGE_CAPTURE_LATENCY;
+ }
+
+ final CameraConfig config = camera.getExtendedConfig();
+ final SessionProcessor sessionProcessor = config.getSessionProcessor();
+ final Pair<Long, Long> latencyEstimate = sessionProcessor.getRealtimeCaptureLatency();
+ if (latencyEstimate == null) {
+ return ImageCaptureLatencyEstimate.UNDEFINED_IMAGE_CAPTURE_LATENCY;
+ }
+ return new ImageCaptureLatencyEstimate(latencyEstimate.first, latencyEstimate.second);
+ }
+
+ /**
* Describes the error that occurred during an image capture operation (such as {@link
* ImageCapture#takePicture(Executor, OnImageCapturedCallback)}).
*
@@ -2367,6 +2399,7 @@
* Sets the {@link DynamicRange}.
*
* <p>This is currently only exposed to internally set the dynamic range to SDR.
+ *
* @return The current Builder.
* @see DynamicRange
*/
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCaptureLatencyEstimate.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCaptureLatencyEstimate.java
new file mode 100644
index 0000000..4c5c02d
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCaptureLatencyEstimate.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2023 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.core;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+import java.util.Objects;
+
+/**
+ * Defines the estimated duration an image capture will take capturing and processing for the
+ * current scene condition and/or camera configuration.
+ *
+ * <p>The estimate comprises of two components: {@link #captureLatencyMillis},
+ * {@link #processingLatencyMillis}
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class ImageCaptureLatencyEstimate {
+ /** The capture latency is unsupported or undefined */
+ public static final long UNDEFINED_CAPTURE_LATENCY = -1;
+
+ /** The processing latency is unsupported or undefined */
+ public static final long UNDEFINED_PROCESSING_LATENCY = -1;
+
+ /** The image capture latency estimate is unsupported or undefined */
+ @NonNull
+ public static final ImageCaptureLatencyEstimate UNDEFINED_IMAGE_CAPTURE_LATENCY =
+ new ImageCaptureLatencyEstimate(UNDEFINED_CAPTURE_LATENCY,
+ UNDEFINED_PROCESSING_LATENCY);
+
+ /**
+ * The estimated duration in milliseconds from when the camera begins capturing frames to the
+ * moment the camera has completed capturing frames. If this estimate is not supported or not
+ * available then it will be {@link #UNDEFINED_CAPTURE_LATENCY}.
+ */
+ public final long captureLatencyMillis;
+
+ /**
+ * The estimated duration in milliseconds from when the processing begins until the processing
+ * has completed and the final processed capture is available. If this estimate is not supported
+ * or not available then it will be {@link #UNDEFINED_PROCESSING_LATENCY}.
+ */
+ public final long processingLatencyMillis;
+
+ ImageCaptureLatencyEstimate(long captureLatencyMillis, long processingLatencyMillis) {
+ this.captureLatencyMillis = captureLatencyMillis;
+ this.processingLatencyMillis = processingLatencyMillis;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof ImageCaptureLatencyEstimate)) return false;
+ ImageCaptureLatencyEstimate that = (ImageCaptureLatencyEstimate) o;
+ return captureLatencyMillis == that.captureLatencyMillis
+ && processingLatencyMillis == that.processingLatencyMillis;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(captureLatencyMillis, processingLatencyMillis);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "captureLatencyMillis=" + captureLatencyMillis
+ + ", processingLatencyMillis=" + processingLatencyMillis;
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionProcessor.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionProcessor.java
index 70b5c2b..f644291 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionProcessor.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionProcessor.java
@@ -18,6 +18,7 @@
import android.hardware.camera2.CaptureResult;
import android.media.ImageReader;
+import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -137,6 +138,30 @@
}
/**
+ * Returns the dynamically calculated capture latency pair in milliseconds.
+ *
+ * The measurement is expected to take in to account dynamic parameters such as the current
+ * scene, the state of 3A algorithms, the state of internal HW modules and return a more
+ * accurate assessment of the capture and/or processing latency.</p>
+ *
+ * @return pair that includes the estimated input frame/frames camera capture latency as the
+ * first field. This is the time between {@link CaptureCallback#onCaptureStarted} and
+ * {@link CaptureCallback#onCaptureProcessStarted}. The second field value includes the
+ * estimated post-processing latency. This is the time between
+ * {@link CaptureCallback#onCaptureProcessStarted} until the processed frame returns back to the
+ * client registered surface.
+ * Both first and second values will be in milliseconds. The total still capture latency will be
+ * the sum of both the first and second values of the pair.
+ * The pair is expected to be null if the dynamic latency estimation is not supported.
+ * If clients have not configured a still capture output, then this method can also return a
+ * null pair.
+ */
+ @Nullable
+ default Pair<Long, Long> getRealtimeCaptureLatency() {
+ return null;
+ }
+
+ /**
* Callback for {@link #startRepeating} and {@link #startCapture}.
*/
interface CaptureCallback {
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/AdvancedSessionProcessorTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/AdvancedSessionProcessorTest.kt
index eb342e6..0570e39 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/AdvancedSessionProcessorTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/AdvancedSessionProcessorTest.kt
@@ -44,6 +44,7 @@
import android.media.ImageReader
import android.media.ImageWriter
import android.os.Build
+import android.util.Pair
import android.util.Size
import android.view.Surface
import androidx.annotation.RequiresApi
@@ -225,6 +226,22 @@
fakeSessionProcessImpl.assertStartTriggerIsCalledWithParameters(parametersMap)
}
+ @Test
+ fun getRealtimeLatencyEstimate_advancedSessionProcessorInvokesSessionProcessorImpl() =
+ runBlocking {
+ val fakeSessionProcessImpl = object : SessionProcessorImpl by FakeSessionProcessImpl() {
+ override fun getRealtimeCaptureLatency(): Pair<Long, Long> = Pair(1000L, 10L)
+ }
+ val advancedSessionProcessor = AdvancedSessionProcessor(
+ fakeSessionProcessImpl, emptyList(), context
+ )
+
+ val realtimeCaptureLatencyEstimate = advancedSessionProcessor.realtimeCaptureLatency
+
+ assertThat(realtimeCaptureLatencyEstimate?.first).isEqualTo(1000L)
+ assertThat(realtimeCaptureLatencyEstimate?.second).isEqualTo(10L)
+ }
+
private suspend fun assumeAllowsSharedSurface() = withContext(Dispatchers.Main) {
val imageReader = ImageReader.newInstance(640, 480, ImageFormat.YUV_420_888, 2)
val maxSharedSurfaceCount =
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
index 1634996..b1b7d73 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
@@ -337,6 +337,20 @@
)
}
+ @Test
+ fun getRealtimeCaptureLatencyEstimate_invokesCaptureExtenderImpl(): Unit = runBlocking {
+ assumeTrue(hasCaptureProcessor)
+ fakeCaptureExtenderImpl = object : FakeImageCaptureExtenderImpl(hasCaptureProcessor) {
+ override fun getRealtimeCaptureLatency(): Pair<Long, Long> = Pair(1000L, 10L)
+ }
+
+ basicExtenderSessionProcessor = BasicExtenderSessionProcessor(
+ fakePreviewExtenderImpl, fakeCaptureExtenderImpl, emptyList(), emptyList(), context
+ )
+
+ assertThat(basicExtenderSessionProcessor.realtimeCaptureLatency).isEqualTo(Pair(1000L, 10L))
+ }
+
class ResultMonitor {
private var latch: CountDownLatch? = null
private var keyToCheck: CaptureRequest.Key<*>? = null
@@ -833,7 +847,7 @@
}
}
- private class FakeImageCaptureExtenderImpl(
+ private open class FakeImageCaptureExtenderImpl(
private val hasCaptureProcessor: Boolean = false,
private val throwErrorOnProcess: Boolean = false
) : ImageCaptureExtenderImpl, FakeExtenderStateListener() {
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/Version.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/Version.java
index c740199..f34287a 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/Version.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/Version.java
@@ -39,6 +39,7 @@
public static final Version VERSION_1_1 = Version.create(1, 1, 0, "");
public static final Version VERSION_1_2 = Version.create(1, 2, 0, "");
public static final Version VERSION_1_3 = Version.create(1, 3, 0, "");
+ public static final Version VERSION_1_4 = Version.create(1, 4, 0, "");
private static final Pattern VERSION_STRING_PATTERN =
Pattern.compile("(\\d+)(?:\\.(\\d+))(?:\\.(\\d+))(?:\\-(.+))?");
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/AdvancedSessionProcessor.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/AdvancedSessionProcessor.java
index a9c4432..85c344d 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/AdvancedSessionProcessor.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/AdvancedSessionProcessor.java
@@ -23,6 +23,7 @@
import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.TotalCaptureResult;
import android.media.Image;
+import android.util.Pair;
import android.util.Size;
import android.view.Surface;
@@ -187,6 +188,16 @@
mImpl.abortCapture(captureSequenceId);
}
+ @Nullable
+ @Override
+ public Pair<Long, Long> getRealtimeCaptureLatency() {
+ if (ClientVersion.isMinimumCompatibleVersion(Version.VERSION_1_4)
+ && ExtensionVersion.isMinimumCompatibleVersion(Version.VERSION_1_4)) {
+ return mImpl.getRealtimeCaptureLatency();
+ }
+ return null;
+ }
+
/**
* Adapter to transform a {@link OutputSurface} to a {@link OutputSurfaceImpl}.
*/
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java
index 649640c..51fe764 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java
@@ -647,4 +647,14 @@
return captureSequenceId;
}
+
+ @Nullable
+ @Override
+ public Pair<Long, Long> getRealtimeCaptureLatency() {
+ if (ClientVersion.isMinimumCompatibleVersion(Version.VERSION_1_4)
+ && ExtensionVersion.isMinimumCompatibleVersion(Version.VERSION_1_4)) {
+ return mImageCaptureExtenderImpl.getRealtimeCaptureLatency();
+ }
+ return null;
+ }
}
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ImageCaptureExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ImageCaptureExtenderImpl.java
index 6ed8f4d..f235fbe 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ImageCaptureExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ImageCaptureExtenderImpl.java
@@ -170,4 +170,27 @@
*/
@NonNull
List<CaptureResult.Key> getAvailableCaptureResultKeys();
+
+ /**
+ * Returns the dynamically calculated capture latency pair in milliseconds.
+ *
+ * <p>In contrast to {@link #getEstimatedCaptureLatencyRange} this method is guaranteed to be
+ * called after the camera capture session is initialized and camera preview is enabled.
+ * The measurement is expected to take in to account dynamic parameters such as the current
+ * scene, the state of 3A algorithms, the state of internal HW modules and return a more
+ * accurate assessment of the still capture latency.</p>
+ *
+ * @return pair that includes the estimated input frame/frames camera capture latency as the
+ * first field and the estimated post-processing latency {@link CaptureProcessorImpl#process}
+ * as the second pair field. Both first and second fields will be in milliseconds. The total
+ * still capture latency will be the sum of both the first and second values.
+ * The pair is expected to be null if the dynamic latency estimation is not supported.
+ * If clients have not configured a still capture output, then this method can also return a
+ * null pair.
+ * @since 1.4
+ */
+ @Nullable
+ default Pair<Long, Long> getRealtimeCaptureLatency() {
+ return null;
+ };
}
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/SessionProcessorImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/SessionProcessorImpl.java
index fabfc2b..e3ace72 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/SessionProcessorImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/SessionProcessorImpl.java
@@ -21,8 +21,11 @@
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
+import android.util.Pair;
import android.view.Surface;
+import androidx.annotation.Nullable;
+
import java.util.Map;
/**
@@ -193,6 +196,32 @@
void abortCapture(int captureSequenceId);
/**
+ * Returns the dynamically calculated capture latency pair in milliseconds.
+ *
+ * <p>In contrast to {@link AdvancedExtenderImpl#getEstimatedCaptureLatencyRange} this method is
+ * guaranteed to be called after {@link #onCaptureSessionStart}.
+ * The measurement is expected to take in to account dynamic parameters such as the current
+ * scene, the state of 3A algorithms, the state of internal HW modules and return a more
+ * accurate assessment of the still capture latency.</p>
+ *
+ * @return pair that includes the estimated input frame/frames camera capture latency as the
+ * first field. This is the time between {@link #onCaptureStarted} and
+ * {@link #onCaptureProcessStarted}. The second field value includes the estimated
+ * post-processing latency. This is the time between {@link #onCaptureProcessStarted} until
+ * the processed frame returns back to the client registered surface.
+ * Both first and second values will be in milliseconds. The total still capture latency will be
+ * the sum of both the first and second values of the pair.
+ * The pair is expected to be null if the dynamic latency estimation is not supported.
+ * If clients have not configured a still capture output, then this method can also return a
+ * null pair.
+ * @since 1.4
+ */
+ @Nullable
+ default Pair<Long, Long> getRealtimeCaptureLatency() {
+ return null;
+ };
+
+ /**
* Callback for notifying the status of {@link #startCapture(CaptureCallback)} and
* {@link #startRepeating(CaptureCallback)}.
*/
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
index f93db30..066282a 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
@@ -30,6 +30,7 @@
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
+import android.util.Pair
import android.util.Rational
import android.util.Size
import android.view.Surface
@@ -1634,6 +1635,40 @@
capturedImage_withHighResolutionEnabled(preview, imageAnalysis)
}
+ @Test
+ @SdkSuppress(minSdkVersion = 28)
+ fun getRealtimeCaptureLatencyEstimate_whenSessionProcessorSupportsRealtimeLatencyEstimate() =
+ runBlocking {
+ val expectedCaptureLatencyMillis = 1000L
+ val expectedProcessingLatencyMillis = 100L
+ val sessionProcessor = object : SessionProcessor by FakeSessionProcessor(
+ inputFormatPreview = null, // null means using the same output surface
+ inputFormatCapture = null
+ ) {
+ override fun getRealtimeCaptureLatency(): Pair<Long, Long> =
+ Pair(expectedCaptureLatencyMillis, expectedProcessingLatencyMillis)
+ }
+
+ val imageCapture = ImageCapture.Builder().build()
+ val preview = Preview.Builder().build()
+
+ withContext(Dispatchers.Main) {
+ preview.setSurfaceProvider(SurfaceTextureProvider.createSurfaceTextureProvider())
+ val cameraSelector =
+ getCameraSelectorWithSessionProcessor(BACK_SELECTOR, sessionProcessor)
+ cameraProvider.bindToLifecycle(
+ fakeLifecycleOwner, cameraSelector, imageCapture, preview
+ )
+ }
+
+ val latencyEstimate = imageCapture.realtimeCaptureLatencyEstimate
+ // Check the realtime latency estimate is correct.
+ assertThat(latencyEstimate.captureLatencyMillis).isEqualTo(expectedCaptureLatencyMillis)
+ assertThat(latencyEstimate.processingLatencyMillis).isEqualTo(
+ expectedProcessingLatencyMillis
+ )
+ }
+
private fun capturedImage_withHighResolutionEnabled(
preview: Preview? = null,
imageAnalysis: ImageAnalysis? = null