/* * Copyright (C) 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 android.hardware.camera2; import static com.android.internal.util.Preconditions.checkNotNull; import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.graphics.ImageFormat; import android.graphics.ImageFormat.Format; import android.hardware.HardwareBuffer; import android.hardware.HardwareBuffer.Usage; import android.hardware.camera2.params.MultiResolutionStreamInfo; import android.media.Image; import android.media.ImageReader; import android.util.Size; import android.view.Surface; import com.android.internal.camera.flags.Flags; import java.util.Collection; import java.util.concurrent.Executor; /** *

The MultiResolutionImageReader class wraps a group of {@link ImageReader ImageReaders} with * the same format and different sizes, source camera Id, or camera sensor modes.

* *

The main use case of this class is for a * {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA logical * multi-camera} or an ultra high resolution sensor camera to output variable-size images. For a * logical multi-camera which implements optical zoom, different physical cameras may have different * maximum resolutions. As a result, when the camera device switches between physical cameras * depending on zoom ratio, the maximum resolution for a particular format may change. For an * ultra high resolution sensor camera, the camera device may deem it better or worse to run in * maximum resolution mode / default mode depending on lighting conditions. So the application may * choose to let the camera device decide on its behalf.

* *

MultiResolutionImageReader should be used for a camera device only if the camera device * supports multi-resolution output stream by advertising the specified output format in {@link * CameraCharacteristics#SCALER_MULTI_RESOLUTION_STREAM_CONFIGURATION_MAP}.

* *

To acquire images from the MultiResolutionImageReader, the application must use the * {@link ImageReader} object passed by * {@link ImageReader.OnImageAvailableListener#onImageAvailable} callback to call * {@link ImageReader#acquireNextImage} or {@link ImageReader#acquireLatestImage}. The application * must not use the {@link ImageReader} passed by an {@link * ImageReader.OnImageAvailableListener#onImageAvailable} callback to acquire future images * because future images may originate from a different {@link ImageReader} contained within the * {@code MultiResolutionImageReader}.

* * * @see ImageReader * @see android.hardware.camera2.CameraCharacteristics#SCALER_MULTI_RESOLUTION_STREAM_CONFIGURATION_MAP */ public class MultiResolutionImageReader implements AutoCloseable { private static final String TAG = "MultiResolutionImageReader"; /** *

* Create a new multi-resolution reader based on a group of camera stream properties returned * by a camera device. *

*

* The valid size and formats depend on the camera characteristics. * {@code MultiResolutionImageReader} for an image format is supported by the camera device if * the format is in the supported multi-resolution output stream formats returned by * {@link android.hardware.camera2.params.MultiResolutionStreamConfigurationMap#getOutputFormats}. * If the image format is supported, the {@code MultiResolutionImageReader} object can be * created with the {@code streams} objects returned by * {@link android.hardware.camera2.params.MultiResolutionStreamConfigurationMap#getOutputInfo}. *

*

* The {@code maxImages} parameter determines the maximum number of * {@link Image} objects that can be acquired from each of the {@code ImageReader} * within the {@code MultiResolutionImageReader}. However, requesting more buffers will * use up more memory, so it is important to use only the minimum number necessary. The * application is strongly recommended to acquire no more than {@code maxImages} images * from all of the internal ImageReader objects combined. By keeping track of the number of * acquired images for the MultiResolutionImageReader, the application doesn't need to do the * bookkeeping for each internal ImageReader returned from {@link * ImageReader.OnImageAvailableListener#onImageAvailable onImageAvailable} callback. *

*

* Unlike the normal ImageReader, the MultiResolutionImageReader has a more complex * configuration sequence. Instead of passing the same surface to OutputConfiguration and * CaptureRequest, the * {@link android.hardware.camera2.params.OutputConfiguration#createInstancesForMultiResolutionOutput} * call needs to be used to create the OutputConfigurations for session creation, and then * {@link #getSurface} is used to get {@link CaptureRequest.Builder#addTarget the target for * CaptureRequest}. *

* @param streams The group of multi-resolution stream info, which is used to create * a multi-resolution reader containing a number of ImageReader objects. Each * ImageReader object represents a multi-resolution stream in the group. * @param format The format of the Image that this multi-resolution reader will produce. * This must be one of the {@link android.graphics.ImageFormat} or * {@link android.graphics.PixelFormat} constants. Note that not all formats are * supported, like ImageFormat.NV21. The supported multi-resolution * reader format can be queried by {@link * android.hardware.camera2.params.MultiResolutionStreamConfigurationMap#getOutputFormats}. * @param maxImages The maximum number of images the user will want to * access simultaneously. This should be as small as possible to * limit memory use. Once maxImages images are obtained by the * user from any given internal ImageReader, one of them has to be released before * a new Image will become available for access through the ImageReader's * {@link ImageReader#acquireLatestImage()} or * {@link ImageReader#acquireNextImage()}. Must be greater than 0. * @see Image * @see * android.hardware.camera2.CameraCharacteristics#SCALER_MULTI_RESOLUTION_STREAM_CONFIGURATION_MAP * @see * android.hardware.camera2.params.MultiResolutionStreamConfigurationMap */ public MultiResolutionImageReader( @NonNull Collection streams, @Format int format, @IntRange(from = 1) int maxImages) { mFormat = format; mMaxImages = maxImages; if (streams == null || streams.size() <= 1) { throw new IllegalArgumentException( "The streams info collection must contain at least 2 entries"); } if (mMaxImages < 1) { throw new IllegalArgumentException( "Maximum outstanding image count must be at least 1"); } if (format == ImageFormat.NV21) { throw new IllegalArgumentException( "NV21 format is not supported"); } int numImageReaders = streams.size(); mReaders = new ImageReader[numImageReaders]; mStreamInfo = new MultiResolutionStreamInfo[numImageReaders]; int index = 0; for (MultiResolutionStreamInfo streamInfo : streams) { mReaders[index] = ImageReader.newInstance(streamInfo.getWidth(), streamInfo.getHeight(), format, maxImages); mStreamInfo[index] = streamInfo; index++; } } /** *

* Create a new multi-resolution reader based on a group of camera stream properties returned * by a camera device, and the desired format, maximum buffer capacity and consumer usage flag. *

*

* The valid size and formats depend on the camera characteristics. * {@code MultiResolutionImageReader} for an image format is supported by the camera device if * the format is in the supported multi-resolution output stream formats returned by * {@link android.hardware.camera2.params.MultiResolutionStreamConfigurationMap#getOutputFormats}. * If the image format is supported, the {@code MultiResolutionImageReader} object can be * created with the {@code streams} objects returned by * {@link android.hardware.camera2.params.MultiResolutionStreamConfigurationMap#getOutputInfo}. *

*

* The {@code maxImages} parameter determines the maximum number of * {@link Image} objects that can be acquired from each of the {@code ImageReader} * within the {@code MultiResolutionImageReader}. However, requesting more buffers will * use up more memory, so it is important to use only the minimum number necessary. The * application is strongly recommended to acquire no more than {@code maxImages} images * from all of the internal ImageReader objects combined. By keeping track of the number of * acquired images for the MultiResolutionImageReader, the application doesn't need to do the * bookkeeping for each internal ImageReader returned from {@link * ImageReader.OnImageAvailableListener#onImageAvailable onImageAvailable} callback. *

*

* Unlike the normal ImageReader, the MultiResolutionImageReader has a more complex * configuration sequence. Instead of passing the same surface to OutputConfiguration and * CaptureRequest, the * {@link android.hardware.camera2.params.OutputConfiguration#createInstancesForMultiResolutionOutput} * call needs to be used to create the OutputConfigurations for session creation, and then * {@link #getSurface} is used to get {@link CaptureRequest.Builder#addTarget the target for * CaptureRequest}. *

* @param streams The group of multi-resolution stream info, which is used to create * a multi-resolution reader containing a number of ImageReader objects. Each * ImageReader object represents a multi-resolution stream in the group. * @param format The format of the Image that this multi-resolution reader will produce. * This must be one of the {@link android.graphics.ImageFormat} or * {@link android.graphics.PixelFormat} constants. Note that not all formats are * supported, like ImageFormat.NV21. The supported multi-resolution * reader format can be queried by {@link * android.hardware.camera2.params.MultiResolutionStreamConfigurationMap#getOutputFormats}. * @param maxImages The maximum number of images the user will want to * access simultaneously. This should be as small as possible to * limit memory use. Once maxImages images are obtained by the * user from any given internal ImageReader, one of them has to be released before * a new Image will become available for access through the ImageReader's * {@link ImageReader#acquireLatestImage()} or * {@link ImageReader#acquireNextImage()}. Must be greater than 0. * @param usage The intended usage of the images produced by the internal ImageReader. See the usages * on {@link HardwareBuffer} for a list of valid usage bits. See also * {@link HardwareBuffer#isSupported(int, int, int, int, long)} for checking * if a combination is supported. If it's not supported this will throw * an {@link IllegalArgumentException}. * @see Image * @see * android.hardware.camera2.CameraCharacteristics#SCALER_MULTI_RESOLUTION_STREAM_CONFIGURATION_MAP * @see * android.hardware.camera2.params.MultiResolutionStreamConfigurationMap * * @hide */ @FlaggedApi(Flags.FLAG_MULTIRESOLUTION_IMAGEREADER_USAGE_CONFIG) public MultiResolutionImageReader( @NonNull Collection streams, @Format int format, @IntRange(from = 1) int maxImages, @Usage long usage) { mFormat = format; mMaxImages = maxImages; if (streams == null || streams.size() <= 1) { throw new IllegalArgumentException( "The streams info collection must contain at least 2 entries"); } if (mMaxImages < 1) { throw new IllegalArgumentException( "Maximum outstanding image count must be at least 1"); } if (format == ImageFormat.NV21) { throw new IllegalArgumentException( "NV21 format is not supported"); } int numImageReaders = streams.size(); mReaders = new ImageReader[numImageReaders]; mStreamInfo = new MultiResolutionStreamInfo[numImageReaders]; int index = 0; for (MultiResolutionStreamInfo streamInfo : streams) { mReaders[index] = ImageReader.newInstance(streamInfo.getWidth(), streamInfo.getHeight(), format, maxImages, usage); mStreamInfo[index] = streamInfo; index++; } } /** * Set onImageAvailableListener callback. * *

This function sets the onImageAvailableListener for all the internal * {@link ImageReader} objects.

* *

For a multi-resolution ImageReader, the timestamps of images acquired in * onImageAvailable callback from different internal ImageReaders may become * out-of-order due to the asynchronous callbacks between the different resolution * image queues.

* * @param listener * The listener that will be run. * @param executor * The executor which will be used when invoking the callback. */ @SuppressLint({"ExecutorRegistration", "SamShouldBeLast"}) public void setOnImageAvailableListener( @Nullable ImageReader.OnImageAvailableListener listener, @Nullable @CallbackExecutor Executor executor) { for (int i = 0; i < mReaders.length; i++) { mReaders[i].setOnImageAvailableListenerWithExecutor(listener, executor); } } @Override public void close() { flush(); for (int i = 0; i < mReaders.length; i++) { mReaders[i].close(); } } @Override protected void finalize() { close(); } /** * Flush pending images from all internal ImageReaders * *

Acquire and close pending images from all internal ImageReaders. This has the same * effect as calling acquireLatestImage() on all internal ImageReaders, and closing all * latest images.

*/ public void flush() { flushOther(null); } /** * Flush pending images from other internal ImageReaders * *

Acquire and close pending images from all internal ImageReaders except for the * one specified.

* * @param reader The ImageReader object that won't be flushed. * * @hide */ public void flushOther(ImageReader reader) { for (int i = 0; i < mReaders.length; i++) { if (reader != null && reader == mReaders[i]) { continue; } while (true) { Image image = mReaders[i].acquireNextImageNoThrowISE(); if (image == null) { break; } else { image.close(); } } } } /** * Get the internal ImageReader objects * * @hide */ public @NonNull ImageReader[] getReaders() { return mReaders; } /** * Get the internal ImageReader surface based on configured size and physical camera Id. * *

The {@code configuredSize} and {@code physicalCameraId} parameters must match one of the * MultiResolutionStreamInfo used to create this {@link MultiResolutionImageReader}.

* *

The Surface returned from this function isn't meant to be used directly as part of a * {@link CaptureRequest}. It should instead be used for creating an OutputConfiguration * before session creation. See {@link OutputConfiguration#setSurfacesForMultiResolutionOutput} * for details. For {@link CaptureRequest}, use {@link #getSurface()} instead.

* *

Please note that holding on to the Surface objects returned by this method is not enough * to keep their parent MultiResolutionImageReaders from being reclaimed. In that sense, a * Surface acts like a {@link java.lang.ref.WeakReference weak reference} to the * MultiResolutionImageReader that provides it.

* * @param configuredSize The configured size corresponding to one of the internal ImageReader. * @param physicalCameraId The physical camera Id the internal ImageReader targets for. If * the ImageReader is not targeting a physical camera of a logical * multi-camera, this parameter is set to "". * * @return The {@link Surface} of the internal ImageReader corresponding to the provided * configured size and physical camera Id. * * @throws IllegalArgumentException If {@code configuredSize} is {@code null}, or the ({@code * configuredSize} and {@code physicalCameraId}) combo is not * part of this {@code MultiResolutionImageReader}. * @hide */ @FlaggedApi(Flags.FLAG_CAMERA_DEVICE_SETUP) public @NonNull Surface getSurface(@NonNull Size configuredSize, @NonNull String physicalCameraId) { checkNotNull(configuredSize, "configuredSize must not be null"); checkNotNull(physicalCameraId, "physicalCameraId must not be null"); for (int i = 0; i < mStreamInfo.length; i++) { if (mStreamInfo[i].getWidth() == configuredSize.getWidth() && mStreamInfo[i].getHeight() == configuredSize.getHeight() && physicalCameraId.equals(mStreamInfo[i].getPhysicalCameraId())) { return mReaders[i].getSurface(); } } throw new IllegalArgumentException("configuredSize and physicalCameraId don't match with " + "this MultiResolutionImageReader"); } /** * Get the surface that is used as a target for {@link CaptureRequest} * *

The application must use the surface returned by this function as a target for * {@link CaptureRequest}. The camera device makes the decision on which internal * {@code ImageReader} will receive the output image.

* *

Please note that holding on to the Surface objects returned by this method is not enough * to keep their parent MultiResolutionImageReaders from being reclaimed. In that sense, a * Surface acts like a {@link java.lang.ref.WeakReference weak reference} to the * MultiResolutionImageReader that provides it.

* * @return a {@link Surface} to use as the target for a capture request. */ public @NonNull Surface getSurface() { // Pick the surface of smallest size. This is necessary for an ultra high resolution // camera not to default to maximum resolution pixel mode. int minReaderSize = mReaders[0].getWidth() * mReaders[0].getHeight(); Surface candidateSurface = mReaders[0].getSurface(); for (int i = 1; i < mReaders.length; i++) { int readerSize = mReaders[i].getWidth() * mReaders[i].getHeight(); if (readerSize < minReaderSize) { minReaderSize = readerSize; candidateSurface = mReaders[i].getSurface(); } } return candidateSurface; } /** * Get the MultiResolutionStreamInfo describing the ImageReader an image originates from * *

An image from a {@code MultiResolutionImageReader} is produced from one of the underlying *{@code ImageReader}s. This function returns the {@link MultiResolutionStreamInfo} to describe *the property for that {@code ImageReader}, such as width, height, and physical camera Id.

* * @param reader An internal ImageReader within {@code MultiResolutionImageReader}. * * @return The stream info describing the internal {@code ImageReader}. */ public @NonNull MultiResolutionStreamInfo getStreamInfoForImageReader( @NonNull ImageReader reader) { for (int i = 0; i < mReaders.length; i++) { if (reader == mReaders[i]) { return mStreamInfo[i]; } } throw new IllegalArgumentException("ImageReader doesn't belong to this multi-resolution " + "imagereader"); } // mReaders and mStreamInfo has the same length, and their entries are 1:1 mapped. private final ImageReader[] mReaders; private final MultiResolutionStreamInfo[] mStreamInfo; private final int mFormat; private final int mMaxImages; }