/*
* Copyright 2021 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.camera2.internal;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CaptureRequest;
import android.util.Size;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
import androidx.camera.camera2.impl.Camera2ImplConfig;
import androidx.camera.camera2.interop.CaptureRequestOptions;
import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.Logger;
import androidx.camera.core.Preview;
import androidx.camera.core.impl.CameraCaptureCallback;
import androidx.camera.core.impl.CameraCaptureFailure;
import androidx.camera.core.impl.CameraCaptureResult;
import androidx.camera.core.impl.CaptureConfig;
import androidx.camera.core.impl.Config;
import androidx.camera.core.impl.DeferrableSurface;
import androidx.camera.core.impl.DeferrableSurfaces;
import androidx.camera.core.impl.OutputSurface;
import androidx.camera.core.impl.SessionConfig;
import androidx.camera.core.impl.SessionProcessor;
import androidx.camera.core.impl.SessionProcessorSurface;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.impl.utils.futures.FutureCallback;
import androidx.camera.core.impl.utils.futures.FutureChain;
import androidx.camera.core.impl.utils.futures.Futures;
import androidx.core.util.Preconditions;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
/**
* A CaptureSession that uses a {@link SessionProcessor} to process/transform the stream
* configuration, repeating request and still-capture request.
*
* <p>This ProcessingCaptureSession works just like a normal {@link CaptureSession} excepts that
* there are some following restrictions:
* <pre>
* (1) Target surfaces specified in {@link #setSessionConfig} and
* {@link #issueCaptureRequests(List)} are ignored. Target surfaces can only be set by
* {@link SessionProcessor}.
* (2) {@link #issueCaptureRequests(List)} can only execute {@link CaptureConfig} with
* CameraDevice.TEMPLATE_STILL_CAPTURE. Others captureConfigs will be cancelled immediately.
* {@link CaptureConfig#getCameraCaptureCallbacks()} will be invoked but the
* {@link CameraCaptureResult} doesn't contain camera2
* {@link android.hardware.camera2.CaptureResult}.
* </pre>
* <p>This class is not thread-safe. All methods must be executed sequentially.
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
@OptIn(markerClass = ExperimentalCamera2Interop.class)
final class ProcessingCaptureSession implements CaptureSessionInterface {
private static final String TAG = "ProcessingCaptureSession";
private final SessionProcessor mSessionProcessor;
private final Camera2CameraInfoImpl mCamera2CameraInfoImpl;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
final Executor mExecutor;
private final ScheduledExecutorService mScheduledExecutorService;
private final CaptureSession mCaptureSession;
private List<DeferrableSurface> mOutputSurfaces = new ArrayList<>();
@Nullable
private SessionConfig mSessionConfig;
@Nullable
private Camera2RequestProcessor mRequestProcessor;
@Nullable
private SessionConfig mProcessorSessionConfig;
private boolean mIsRepeatingRequestStarted = false;
private static final long TIMEOUT_GET_SURFACE_IN_MS = 5000L;
private ProcessorState mProcessorState;
private static List<DeferrableSurface> sHeldProcessorSurfaces = new ArrayList<>();
@Nullable
private volatile CaptureConfig mPendingCaptureConfig = null;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
volatile boolean mIsExecutingStillCaptureRequest = false;
private final SessionProcessorCaptureCallback mSessionProcessorCaptureCallback;
private CaptureRequestOptions mSessionOptions = new CaptureRequestOptions.Builder().build();
private CaptureRequestOptions mStillCaptureOptions =
new CaptureRequestOptions.Builder().build();
private enum ProcessorState {
UNINITIALIZED,
SESSION_INITIALIZED,
ON_CAPTURE_SESSION_STARTED,
ON_CAPTURE_SESSION_ENDED,
CLOSED
}
// For debugging only
private static int sNextInstanceId = 0;
private int mInstanceId = 0;
ProcessingCaptureSession(@NonNull SessionProcessor sessionProcessor,
@NonNull Camera2CameraInfoImpl camera2CameraInfoImpl, @NonNull Executor executor,
@NonNull ScheduledExecutorService scheduledExecutorService) {
mCaptureSession = new CaptureSession();
mSessionProcessor = sessionProcessor;
mCamera2CameraInfoImpl = camera2CameraInfoImpl;
mExecutor = executor;
mScheduledExecutorService = scheduledExecutorService;
mProcessorState = ProcessorState.UNINITIALIZED;
mSessionProcessorCaptureCallback = new SessionProcessorCaptureCallback(mExecutor);
mInstanceId = sNextInstanceId++;
Logger.d(TAG, "New ProcessingCaptureSession (id=" + mInstanceId + ")");
}
@NonNull
@Override
public ListenableFuture<Void> open(@NonNull SessionConfig sessionConfig,
@NonNull CameraDevice cameraDevice, @NonNull SynchronizedCaptureSessionOpener opener) {
Preconditions.checkArgument(mProcessorState == ProcessorState.UNINITIALIZED,
"Invalid state state:" + mProcessorState);
Preconditions.checkArgument(!sessionConfig.getSurfaces().isEmpty(),
"SessionConfig contains no surfaces");
Logger.d(TAG, "open (id=" + mInstanceId + ")");
mOutputSurfaces = sessionConfig.getSurfaces();
ListenableFuture<Void> future =
FutureChain.from(DeferrableSurfaces.surfaceListWithTimeout(
mOutputSurfaces, false,
TIMEOUT_GET_SURFACE_IN_MS, mExecutor, mScheduledExecutorService))
.transformAsync(surfaceList -> {
Logger.d(TAG,
"-- getSurfaces done, start init (id=" + mInstanceId + ")");
if (mProcessorState == ProcessorState.CLOSED) {
return Futures.immediateFailedFuture(new IllegalStateException(
"SessionProcessorCaptureSession is closed."));
}
// Containing null means some DeferrableSurface was closed and we
// need to propagate the SurfaceClosedException in order to recreate
// the surfaces.
if (surfaceList.contains(null)) {
DeferrableSurface deferrableSurface =
sessionConfig.getSurfaces().get(surfaceList.indexOf(null));
return Futures.immediateFailedFuture(
new DeferrableSurface.SurfaceClosedException(
"Surface closed", deferrableSurface));
}
try {
DeferrableSurfaces.incrementAll(mOutputSurfaces);
} catch (DeferrableSurface.SurfaceClosedException e) {
return Futures.immediateFailedFuture(e);
}
OutputSurface previewOutputSurface = null;
OutputSurface captureOutputSurface = null;
OutputSurface analysisOutputSurface = null;
for (int i = 0; i < sessionConfig.getSurfaces().size(); i++) {
DeferrableSurface dSurface = sessionConfig.getSurfaces().get(i);
if (Objects.equals(dSurface.getContainerClass(),
Preview.class)) {
previewOutputSurface = OutputSurface.create(
dSurface.getSurface().get(),
new Size(dSurface.getPrescribedSize().getWidth(),
dSurface.getPrescribedSize().getHeight()),
dSurface.getPrescribedStreamFormat());
} else if (Objects.equals(dSurface.getContainerClass(),
ImageCapture.class)) {
captureOutputSurface = OutputSurface.create(
dSurface.getSurface().get(),
new Size(dSurface.getPrescribedSize().getWidth(),
dSurface.getPrescribedSize().getHeight()),
dSurface.getPrescribedStreamFormat());
} else if (Objects.equals(dSurface.getContainerClass(),
ImageAnalysis.class)) {
analysisOutputSurface = OutputSurface.create(
dSurface.getSurface().get(),
new Size(dSurface.getPrescribedSize().getWidth(),
dSurface.getPrescribedSize().getHeight()),
dSurface.getPrescribedStreamFormat());
}
}
mProcessorState = ProcessorState.SESSION_INITIALIZED;
Logger.w(TAG, "== initSession (id=" + mInstanceId + ")");
mProcessorSessionConfig = mSessionProcessor.initSession(
mCamera2CameraInfoImpl,
previewOutputSurface,
captureOutputSurface,
analysisOutputSurface
);
// DecrementAll the output surfaces when ProcessorSurface
// terminates.
mProcessorSessionConfig.getSurfaces().get(0).getTerminationFuture()
.addListener(() -> {
DeferrableSurfaces.decrementAll(mOutputSurfaces);
}, CameraXExecutors.directExecutor());
// Holding the Processor surfaces in case they are GCed
for (DeferrableSurface surface :
mProcessorSessionConfig.getSurfaces()) {
sHeldProcessorSurfaces.add(surface);
surface.getTerminationFuture().addListener(() -> {
sHeldProcessorSurfaces.remove(surface);
}, mExecutor);
}
SessionConfig.ValidatingBuilder validatingBuilder =
new SessionConfig.ValidatingBuilder();
validatingBuilder.add(sessionConfig);
validatingBuilder.clearSurfaces(); // remove origin surfaces.
validatingBuilder.add(mProcessorSessionConfig);
Preconditions.checkArgument(validatingBuilder.isValid(),
"Cannot transform the SessionConfig");
SessionConfig transformedConfig = validatingBuilder.build();
ListenableFuture<Void> openSessionFuture =
mCaptureSession.open(transformedConfig,
Preconditions.checkNotNull(cameraDevice),
opener);
Futures.addCallback(openSessionFuture, new FutureCallback<Void>() {
@Override
public void onSuccess(@Nullable Void result) {
// do nothing
}
@Override
public void onFailure(Throwable t) {
// Close() will invoke appropriate SessionProcessor methods
// to clear up and mark this session as CLOSED.
Logger.e(TAG, "open session failed ", t);
close();
}
}, mExecutor);
return openSessionFuture;
}, mExecutor)
.transform(v -> {
// Using transform instead of addListener because we want to ensure
// SessionProcessor#onCaptureSessionStarted is called when the future
// completes. Using future.addListener cannot guarantee that.
onConfigured(mCaptureSession);
return null;
}, mExecutor);
return future;
}
private static void cancelRequests(@NonNull List<CaptureConfig> captureConfigs) {
for (CaptureConfig captureConfig : captureConfigs) {
for (CameraCaptureCallback cameraCaptureCallback :
captureConfig.getCameraCaptureCallbacks()) {
cameraCaptureCallback.onCaptureCancelled();
}
}
}
private boolean isStillCapture(@NonNull List<CaptureConfig> captureConfigs) {
if (captureConfigs.isEmpty()) {
return false;
}
for (CaptureConfig captureConfig : captureConfigs) {
// Don't need to consider TEMPLATE_VIDEO_SNAPSHOT case since extensions does not
// support Video Capture yet
if (captureConfig.getTemplateType() != CameraDevice.TEMPLATE_STILL_CAPTURE) {
return false;
}
}
return true;
}
/**
* Submit a still capture request via
* {@link SessionProcessor#startCapture(SessionProcessor.CaptureCallback)}.
*
* <p>The method is more restrictive than {@link CaptureSession#issueCaptureRequests(List)}.
* Only one @link CaptureConfig} with {@link CameraDevice#TEMPLATE_STILL_CAPTURE} template is
* allowed. If the captureConfigs contain multiple {@link CaptureConfig}s or the contained
* {@link CaptureConfig} does not use {@link CameraDevice#TEMPLATE_STILL_CAPTURE}, all
* captureConfigs will be cancelled immediately.
*
* <p>Camera2 capture options in {@link CaptureConfig#getImplementationOptions()} will be
* merged with the options in {@link SessionConfig#getImplementationOptions()} set by
* {@link #setSessionConfig(SessionConfig)}. The merged parameters set will be passed to
* {@link SessionProcessor#setParameters(Config)} but it is up to the implementation of the
* {@link SessionProcessor} to determine which options to apply.
*
* <p>{@link CaptureConfig#getCameraCaptureCallbacks()} ()} will be invoked but it is unable
* to invoke callbacks of {@link CaptureCallbackContainer} type due to lack of the access to
* the camera2 {@link android.hardware.camera2.CameraCaptureSession.CaptureCallback}.
*
* <p>Still capture requests are expected to arrive one at a time sequentially by upper layer.
* Capture requests will be cancelled if previous request have not finished.
*/
@Override
public void issueCaptureRequests(@NonNull List<CaptureConfig> captureConfigs) {
if (captureConfigs.isEmpty()) {
return;
}
if (captureConfigs.size() > 1 || !isStillCapture(captureConfigs)) {
cancelRequests(captureConfigs);
return;
}
// Only allows one capture config at a time.
if (mPendingCaptureConfig != null || mIsExecutingStillCaptureRequest) {
cancelRequests(captureConfigs);
return;
}
// captureConfigs should contain exactly one CaptureConfig.
CaptureConfig captureConfig = captureConfigs.get(0);
Logger.d(TAG, "issueCaptureRequests (id=" + mInstanceId + ") + state =" + mProcessorState);
switch (mProcessorState) {
case UNINITIALIZED:
case SESSION_INITIALIZED:
mPendingCaptureConfig = captureConfig;
break;
case ON_CAPTURE_SESSION_STARTED:
mIsExecutingStillCaptureRequest = true;
CaptureRequestOptions.Builder builder =
CaptureRequestOptions.Builder.from(
captureConfig.getImplementationOptions());
if (captureConfig.getImplementationOptions().containsOption(
CaptureConfig.OPTION_ROTATION)) {
builder.setCaptureRequestOption(CaptureRequest.JPEG_ORIENTATION,
captureConfig.getImplementationOptions().retrieveOption(
CaptureConfig.OPTION_ROTATION));
}
if (captureConfig.getImplementationOptions().containsOption(
CaptureConfig.OPTION_JPEG_QUALITY)) {
builder.setCaptureRequestOption(CaptureRequest.JPEG_QUALITY,
captureConfig.getImplementationOptions().retrieveOption(
CaptureConfig.OPTION_JPEG_QUALITY).byteValue());
}
mStillCaptureOptions = builder.build();
updateParameters(mSessionOptions, mStillCaptureOptions);
mSessionProcessor.startCapture(new SessionProcessor.CaptureCallback() {
@Override
public void onCaptureStarted(
int captureSequenceId, long timestamp) {
}
@Override
public void onCaptureProcessStarted(
int captureSequenceId) {
}
@Override
public void onCaptureFailed(
int captureSequenceId) {
mExecutor.execute(() -> {
for (CameraCaptureCallback cameraCaptureCallback :
captureConfig.getCameraCaptureCallbacks()) {
cameraCaptureCallback.onCaptureFailed(new CameraCaptureFailure(
CameraCaptureFailure.Reason.ERROR));
}
mIsExecutingStillCaptureRequest = false;
});
}
@Override
public void onCaptureSequenceCompleted(int captureSequenceId) {
mExecutor.execute(() -> {
for (CameraCaptureCallback cameraCaptureCallback :
captureConfig.getCameraCaptureCallbacks()) {
cameraCaptureCallback.onCaptureCompleted(
new CameraCaptureResult.EmptyCameraCaptureResult());
}
mIsExecutingStillCaptureRequest = false;
});
}
@Override
public void onCaptureSequenceAborted(int captureSequenceId) {
}
});
break;
case ON_CAPTURE_SESSION_ENDED:
case CLOSED:
Logger.d(TAG, "Run issueCaptureRequests in wrong state, state = "
+ mProcessorState);
cancelRequests(captureConfigs);
break;
}
}
/**
* {@inheritDoc}
*/
@Override
@NonNull
public ListenableFuture<Void> release(boolean abortInFlightCaptures) {
Preconditions.checkState(mProcessorState == ProcessorState.CLOSED,
"release() can only be called in CLOSED state");
Logger.d(TAG, "release (id=" + mInstanceId + ")");
return mCaptureSession.release(abortInFlightCaptures);
}
private static List<SessionProcessorSurface> getSessionProcessorSurfaceList(
List<DeferrableSurface> deferrableSurfaceList) {
ArrayList<SessionProcessorSurface> outputSurfaceList = new ArrayList<>();
for (DeferrableSurface deferrableSurface : deferrableSurfaceList) {
Preconditions.checkArgument(deferrableSurface instanceof SessionProcessorSurface,
"Surface must be SessionProcessorSurface");
outputSurfaceList.add((SessionProcessorSurface) deferrableSurface);
}
return outputSurfaceList;
}
void onConfigured(@NonNull CaptureSession captureSession) {
Preconditions.checkArgument(mProcessorState == ProcessorState.SESSION_INITIALIZED,
"Invalid state state:" + mProcessorState);
mRequestProcessor = new Camera2RequestProcessor(captureSession,
getSessionProcessorSurfaceList(mProcessorSessionConfig.getSurfaces()));
mSessionProcessor.onCaptureSessionStart(mRequestProcessor);
mProcessorState = ProcessorState.ON_CAPTURE_SESSION_STARTED;
if (mSessionConfig != null) {
setSessionConfig(mSessionConfig);
}
if (mPendingCaptureConfig != null) {
List<CaptureConfig> pendingCaptureConfigList = Arrays.asList(mPendingCaptureConfig);
mPendingCaptureConfig = null;
issueCaptureRequests(pendingCaptureConfigList);
}
}
/**
* {@inheritDoc}
*/
@Nullable
@Override
public SessionConfig getSessionConfig() {
return mSessionConfig;
}
/**
* {@inheritDoc}
*/
@NonNull
@Override
public List<CaptureConfig> getCaptureConfigs() {
return mPendingCaptureConfig != null ? Arrays.asList(mPendingCaptureConfig)
: Collections.emptyList();
}
/**
* {@inheritDoc}
*/
@Override
public void cancelIssuedCaptureRequests() {
Logger.d(TAG, "cancelIssuedCaptureRequests (id=" + mInstanceId + ")");
if (mPendingCaptureConfig != null) {
for (CameraCaptureCallback cameraCaptureCallback :
mPendingCaptureConfig.getCameraCaptureCallbacks()) {
cameraCaptureCallback.onCaptureCancelled();
}
mPendingCaptureConfig = null;
}
}
/**
* Invokes appropriate SessionProcessor methods to clear up and mark this session as CLOSED.
*/
@Override
public void close() {
Logger.d(TAG, "close (id=" + mInstanceId + ") state=" + mProcessorState);
switch (mProcessorState) {
case ON_CAPTURE_SESSION_STARTED:
mSessionProcessor.onCaptureSessionEnd();
if (mRequestProcessor != null) {
mRequestProcessor.close();
}
mProcessorState = ProcessorState.ON_CAPTURE_SESSION_ENDED;
// fall through
case ON_CAPTURE_SESSION_ENDED:
case SESSION_INITIALIZED:
mSessionProcessor.deInitSession();
break;
case UNINITIALIZED:
// do nothing
break;
case CLOSED:
return; // already closed, no need to close again.
}
mProcessorState = ProcessorState.CLOSED;
mCaptureSession.close();
}
/**
* Set active session config for repeating request.
*
* <p> Surfaces contained in the {@link SessionConfig} will be ignored since the target
* surface of repeating request is determined by {@link SessionProcessor}.
* {@link SessionProcessor#setParameters(Config)} will be called to update the request
* parameters retrieved from {@link SessionConfig#getImplementationOptions()}. It will also
* invoke {@link SessionProcessor#startRepeating(SessionProcessor.CaptureCallback)} if it is not
* started yet. {@link SessionConfig#getRepeatingCameraCaptureCallbacks()} will be invoked
* but it is unable to invoke callbacks of {@link CaptureCallbackContainer} type.
*
* @param sessionConfig has the configuration that will currently active in issuing capture
* request.
*/
@Override
public void setSessionConfig(@Nullable SessionConfig sessionConfig) {
Logger.d(TAG, "setSessionConfig (id=" + mInstanceId + ")");
mSessionConfig = sessionConfig;
if (sessionConfig == null) {
return;
}
if (mRequestProcessor != null) {
mRequestProcessor.updateSessionConfig(sessionConfig);
}
if (mProcessorState == ProcessorState.ON_CAPTURE_SESSION_STARTED) {
mSessionOptions =
CaptureRequestOptions.Builder.from(sessionConfig.getImplementationOptions())
.build();
updateParameters(mSessionOptions, mStillCaptureOptions);
if (!mIsRepeatingRequestStarted) {
mSessionProcessor.startRepeating(mSessionProcessorCaptureCallback);
mIsRepeatingRequestStarted = true;
}
}
}
private void updateParameters(@NonNull CaptureRequestOptions sessionOptions,
@NonNull CaptureRequestOptions stillCaptureOptions) {
Camera2ImplConfig.Builder builder = new Camera2ImplConfig.Builder();
builder.insertAllOptions(sessionOptions);
builder.insertAllOptions(stillCaptureOptions);
mSessionProcessor.setParameters(builder.build());
}
private static class SessionProcessorCaptureCallback
implements SessionProcessor.CaptureCallback {
private List<CameraCaptureCallback> mCameraCaptureCallbacks = Collections.emptyList();
private final Executor mExecutor;
SessionProcessorCaptureCallback(@NonNull Executor executor) {
mExecutor = executor;
}
public void setCameraCaptureCallbacks(
@NonNull List<CameraCaptureCallback> cameraCaptureCallbacks) {
mCameraCaptureCallbacks = cameraCaptureCallbacks;
}
@Override
public void onCaptureStarted(int captureSequenceId, long timestamp) {
}
@Override
public void onCaptureProcessStarted(int captureSequenceId) {
}
@Override
public void onCaptureFailed(int captureSequenceId) {
mExecutor.execute(() -> {
for (CameraCaptureCallback cameraCaptureCallback : mCameraCaptureCallbacks) {
cameraCaptureCallback.onCaptureFailed(new CameraCaptureFailure(
CameraCaptureFailure.Reason.ERROR));
}
});
}
@Override
public void onCaptureSequenceCompleted(int captureSequenceId) {
mExecutor.execute(() -> {
for (CameraCaptureCallback cameraCaptureCallback : mCameraCaptureCallbacks) {
cameraCaptureCallback.onCaptureCompleted(
CameraCaptureResult.EmptyCameraCaptureResult.create());
}
});
}
@Override
public void onCaptureSequenceAborted(int captureSequenceId) {
}
}
}