blob: f93271b657b455ce606015357438fdc42d1d3fa8 [file] [log] [blame]
/*
* 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.extensions.impl.advanced;
import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.TotalCaptureResult;
import android.media.Image;
import android.util.LongSparseArray;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class CaptureResultImageMatcher {
private static final String TAG = "CaptureResultImageReader";
private final Object mLock = new Object();
@GuardedBy("mLock")
private boolean mClosed = false;
/** ImageInfos haven't been matched with Image. */
@GuardedBy("mLock")
private final LongSparseArray<TotalCaptureResult> mPendingImageInfos = new LongSparseArray<>();
Map<TotalCaptureResult, Integer> mCaptureStageIdMap = new HashMap<>();
/** Images haven't been matched with ImageInfo. */
@GuardedBy("mLock")
private final LongSparseArray<ImageReferenceImpl> mPendingImages = new LongSparseArray<>();
ImageReferenceListener mImageReferenceListener;
public CaptureResultImageMatcher() {
}
public void clear() {
synchronized (mLock) {
mPendingImageInfos.clear();
for (int i = 0; i < mPendingImages.size(); i++) {
long key = mPendingImages.keyAt(i);
mPendingImages.get(key).decrement();
}
mPendingImages.clear();
mCaptureStageIdMap.clear();
mClosed = false;
}
}
public void setImageReferenceListener(
@NonNull ImageReferenceListener imageReferenceImplListener) {
synchronized (mLock) {
mImageReferenceListener = imageReferenceImplListener;
}
}
public void setInputImage(@NonNull ImageReferenceImpl imageReferenceImpl) {
synchronized (mLock) {
if (mClosed) {
return;
}
Image image = imageReferenceImpl.get();
// Add the incoming Image to pending list and do the matching logic.
mPendingImages.put(image.getTimestamp(), imageReferenceImpl);
matchImages();
}
}
public void setCameraCaptureCallback(@NonNull TotalCaptureResult captureResult) {
setCameraCaptureCallback(captureResult, 0);
}
public void setCameraCaptureCallback(@NonNull TotalCaptureResult captureResult,
int captureStageId) {
synchronized (mLock) {
if (mClosed) {
return;
}
long timestamp = getTimeStampFromCaptureResult(captureResult);
// Add the incoming CameraCaptureResult to pending list and do the matching logic.
mPendingImageInfos.put(timestamp, captureResult);
mCaptureStageIdMap.put(captureResult, captureStageId);
matchImages();
}
}
private long getTimeStampFromCaptureResult(TotalCaptureResult captureResult) {
Long timestamp = captureResult.get(CaptureResult.SENSOR_TIMESTAMP);
long timestampValue = -1;
if (timestamp != null) {
timestampValue = timestamp;
}
return timestampValue;
}
private void notifyImage(ImageReferenceImpl imageReferenceImpl,
TotalCaptureResult totalCaptureResult) {
synchronized (mLock) {
if (mImageReferenceListener != null) {
mImageReferenceListener.onImageReferenceIncoming(imageReferenceImpl,
totalCaptureResult, mCaptureStageIdMap.get(totalCaptureResult));
} else {
imageReferenceImpl.decrement();
}
}
}
// Remove the stale {@link ImageProxy} and {@link ImageInfo} from the pending queue if there are
// any missing which can happen if the camera is momentarily shut off.
// The ImageProxy and ImageInfo timestamps are assumed to be monotonically increasing. This
// means any ImageProxy or ImageInfo which has a timestamp older (smaller in value) than the
// oldest timestamp in the other queue will never get matched, so they should be removed.
//
// This should only be called at the end of matchImages(). The assumption is that there are no
// matching timestamps.
private void removeStaleData() {
synchronized (mLock) {
// No stale data to remove
if (mPendingImages.size() == 0 || mPendingImageInfos.size() == 0) {
return;
}
Long minImageProxyTimestamp = mPendingImages.keyAt(0);
Long minImageInfoTimestamp = mPendingImageInfos.keyAt(0);
// If timestamps are equal then matchImages did not correctly match up the ImageInfo
// and ImageProxy
if (minImageInfoTimestamp.equals(minImageProxyTimestamp)) {
throw new IllegalArgumentException();
}
if (minImageInfoTimestamp > minImageProxyTimestamp) {
for (int i = mPendingImages.size() - 1; i >= 0; i--) {
if (mPendingImages.keyAt(i) < minImageInfoTimestamp) {
ImageReferenceImpl imageReferenceImpl = mPendingImages.valueAt(i);
imageReferenceImpl.decrement();
mPendingImages.removeAt(i);
}
}
} else {
for (int i = mPendingImageInfos.size() - 1; i >= 0; i--) {
if (mPendingImageInfos.keyAt(i) < minImageProxyTimestamp) {
mPendingImageInfos.removeAt(i);
}
}
}
}
}
// Match incoming Image from the ImageReader with the corresponding ImageInfo.
private void matchImages() {
synchronized (mLock) {
// Iterate in reverse order so that ImageInfo can be removed in place
for (int i = mPendingImageInfos.size() - 1; i >= 0; i--) {
TotalCaptureResult captureResult = mPendingImageInfos.valueAt(i);
long timestamp = getTimeStampFromCaptureResult(captureResult);
ImageReferenceImpl imageReferenceImpl = mPendingImages.get(timestamp);
if (imageReferenceImpl != null) {
mPendingImages.remove(timestamp);
mPendingImageInfos.removeAt(i);
// Got a match. Add the ImageProxy to matched list and invoke
// onImageAvailableListener.
notifyImage(imageReferenceImpl, captureResult);
}
}
removeStaleData();
}
}
public interface ImageReferenceListener {
void onImageReferenceIncoming(@NonNull ImageReferenceImpl imageReferenceImpl,
@NonNull TotalCaptureResult totalCaptureResult, int captureStageId);
}
}