CaptureNode.java

/*
 * 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.core.imagecapture;

import static androidx.camera.core.impl.utils.Threads.checkMainThread;
import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
import static androidx.core.util.Preconditions.checkState;

import static java.util.Objects.requireNonNull;

import android.media.ImageReader;
import android.os.Build;
import android.util.Size;
import android.view.Surface;

import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.camera.core.ForwardingImageProxy;
import androidx.camera.core.ImageProxy;
import androidx.camera.core.MetadataImageReader;
import androidx.camera.core.SafeCloseImageReaderProxy;
import androidx.camera.core.impl.CameraCaptureCallback;
import androidx.camera.core.impl.DeferrableSurface;
import androidx.camera.core.impl.ImageReaderProxy;
import androidx.camera.core.impl.ImmediateSurface;
import androidx.camera.core.processing.Edge;
import androidx.camera.core.processing.Node;

import com.google.auto.value.AutoValue;

import java.util.HashSet;
import java.util.Set;

/**
 * A {@link Node} that calls back when all the images for one capture are received.
 *
 * <p>This {@link Node} waits for all the images in a {@link ProcessingRequest} are received
 * before invoking the {@link ProcessingRequest#onImageCaptured()} callback. Then it makes
 * sure that the {@link ImageProxy} are omitted after their corresponding {@link ProcessingRequest}.
 *
 * <p>It's also responsible for managing the {@link ImageReaderProxy}. It makes sure that the
 * queue is not overflowed.
 *
 */
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
class CaptureNode implements Node<CaptureNode.In, CaptureNode.Out> {

    // TODO: we might need to calculate this number dynamically based on on many frames are
    //  needed by the post-processing. e.g. night mode might need to merge 10+ frames. 4 images
    //  should be enough for now.
    @VisibleForTesting
    static final int MAX_IMAGES = 4;

    @NonNull
    private final Set<Integer> mPendingStageIds = new HashSet<>();
    private final Set<ImageProxy> mPendingImages = new HashSet<>();
    private ProcessingRequest mCurrentRequest = null;

    SafeCloseImageReaderProxy mSafeCloseImageReaderProxy;
    private Out mOutputEdge;
    private In mInputEdge;

    @NonNull
    @Override
    public Out transform(@NonNull In inputEdge) {
        mInputEdge = inputEdge;
        Size size = inputEdge.getSize();
        int format = inputEdge.getFormat();

        // Creates ImageReaders.
        MetadataImageReader metadataImageReader = new MetadataImageReader(size.getWidth(),
                size.getHeight(), format, MAX_IMAGES);
        mSafeCloseImageReaderProxy = new SafeCloseImageReaderProxy(metadataImageReader);
        inputEdge.setCameraCaptureCallback(metadataImageReader.getCameraCaptureCallback());
        inputEdge.setSurface(requireNonNull(metadataImageReader.getSurface()));

        // Listen to the input edges.
        metadataImageReader.setOnImageAvailableListener(imageReader -> onImageProxyAvailable(
                requireNonNull(imageReader.acquireNextImage())), mainThreadExecutor());
        inputEdge.getRequestEdge().setListener(this::onRequestAvailable);

        mOutputEdge = Out.of(inputEdge.getFormat());
        return mOutputEdge;
    }

    @VisibleForTesting
    @MainThread
    void onImageProxyAvailable(@NonNull ImageProxy imageProxy) {
        checkMainThread();
        if (mCurrentRequest == null) {
            // Request has not arrived yet. Track the image and match later.
            mPendingImages.add(imageProxy);
        } else {
            // Match image and send it downstream.
            matchAndPropagateImage(imageProxy);
        }
    }

    private void matchAndPropagateImage(@NonNull ImageProxy imageProxy) {
        // Check if the capture is complete.
        int stageId = (Integer) requireNonNull(
                imageProxy.getImageInfo().getTagBundle().getTag(mCurrentRequest.getTagBundleKey()));
        checkState(mPendingStageIds.contains(stageId),
                "Received an unexpected stage id" + stageId);
        mPendingStageIds.remove(stageId);

        if (mPendingStageIds.isEmpty()) {
            // The capture is complete. Let the pipeline know it can take another picture.
            mCurrentRequest.onImageCaptured();
            mCurrentRequest = null;
        }

        // Send the image downstream.
        mOutputEdge.getImageEdge().accept(imageProxy);
    }

    @VisibleForTesting
    @MainThread
    void onRequestAvailable(@NonNull ProcessingRequest request) {
        checkMainThread();
        // Unable to issue request if the queue has no capacity.
        checkState(getCapacity() > 0,
                "Too many acquire images. Close image to be able to process next.");
        // Check if there is already a current request. Only one concurrent request is allowed.
        checkState(mCurrentRequest == null || mPendingStageIds.isEmpty(),
                "The previous request is not complete");

        // Track the request and its stage IDs.
        mCurrentRequest = request;
        mPendingStageIds.addAll(request.getStageIds());

        // Send the request downstream.
        mOutputEdge.getRequestEdge().accept(request);

        // Match pending images and send them downstream.
        for (ImageProxy imageProxy : mPendingImages) {
            matchAndPropagateImage(imageProxy);
        }
        mPendingImages.clear();
    }

    @MainThread
    @Override
    public void release() {
        checkMainThread();
        if (mSafeCloseImageReaderProxy != null) {
            mSafeCloseImageReaderProxy.safeClose();
        }
        if (mInputEdge != null) {
            mInputEdge.closeSurface();
        }
    }

    @VisibleForTesting
    @NonNull
    In getInputEdge() {
        return mInputEdge;
    }

    @MainThread
    public int getCapacity() {
        checkMainThread();
        checkState(mSafeCloseImageReaderProxy != null,
                "The ImageReader is not initialized.");
        return mSafeCloseImageReaderProxy.getCapacity();
    }

    @MainThread
    public void setOnImageCloseListener(ForwardingImageProxy.OnImageCloseListener listener) {
        checkMainThread();
        checkState(mSafeCloseImageReaderProxy != null,
                "The ImageReader is not initialized.");
        mSafeCloseImageReaderProxy.setOnImageCloseListener(listener);
    }

    /**
     * Input edges of a {@link CaptureNode}.
     */
    @AutoValue
    abstract static class In {

        private CameraCaptureCallback mCameraCaptureCallback;

        private DeferrableSurface mSurface;

        /**
         * Size of the {@link ImageReader} buffer.
         */
        abstract Size getSize();

        /**
         * Size of the {@link ImageReader} format.
         */
        abstract int getFormat();

        /**
         * Edge that accepts {@link ProcessingRequest}.
         */
        @NonNull
        abstract Edge<ProcessingRequest> getRequestEdge();

        /**
         * Edge that accepts the image frames.
         *
         * <p>The value will be used in a capture request sent to the camera.
         */
        @NonNull
        DeferrableSurface getSurface() {
            return mSurface;
        }

        void setSurface(@NonNull Surface surface) {
            checkState(mSurface == null, "The surface is already set.");
            mSurface = new ImmediateSurface(surface);
        }

        void closeSurface() {
            mSurface.close();
        }

        /**
         * Edge that accepts image metadata.
         *
         * <p>The value will be used in a capture request sent to the camera.
         */
        CameraCaptureCallback getCameraCaptureCallback() {
            return mCameraCaptureCallback;
        }

        void setCameraCaptureCallback(@NonNull CameraCaptureCallback cameraCaptureCallback) {
            mCameraCaptureCallback = cameraCaptureCallback;
        }

        @NonNull
        static In of(Size size, int format) {
            return new AutoValue_CaptureNode_In(size, format, new Edge<>());
        }
    }

    /**
     * Output edges of a {@link CaptureNode}.
     */
    @AutoValue
    abstract static class Out {

        /**
         * Edge that omits {@link ImageProxy}s.
         *
         * <p>The frames will be closed by downstream nodes.
         */
        abstract Edge<ImageProxy> getImageEdge();

        /**
         * Edge that omits {@link ProcessingRequest}.
         */
        abstract Edge<ProcessingRequest> getRequestEdge();

        /**
         * Format of the {@link ImageProxy} in {@link #getImageEdge()}.
         */
        abstract int getFormat();

        static Out of(int format) {
            return new AutoValue_CaptureNode_Out(new Edge<>(), new Edge<>(), format);
        }
    }
}