/* * Copyright (C) 2014 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.legacy; import android.hardware.camera2.impl.CameraMetadataNative; import android.util.Log; import android.util.Pair; import java.util.ArrayDeque; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; /** * Collect timestamps and state for each {@link CaptureRequest} as it passes through * the Legacy camera pipeline. */ public class CaptureCollector { private static final String TAG = "CaptureCollector"; private static final boolean DEBUG = Log.isLoggable(LegacyCameraDevice.DEBUG_PROP, Log.DEBUG); private static final int FLAG_RECEIVED_JPEG = 1; private static final int FLAG_RECEIVED_JPEG_TS = 2; private static final int FLAG_RECEIVED_PREVIEW = 4; private static final int FLAG_RECEIVED_PREVIEW_TS = 8; private static final int FLAG_RECEIVED_ALL_JPEG = FLAG_RECEIVED_JPEG | FLAG_RECEIVED_JPEG_TS; private static final int FLAG_RECEIVED_ALL_PREVIEW = FLAG_RECEIVED_PREVIEW | FLAG_RECEIVED_PREVIEW_TS; private static final int MAX_JPEGS_IN_FLIGHT = 1; private class CaptureHolder { private final RequestHolder mRequest; private final LegacyRequest mLegacy; public final boolean needsJpeg; public final boolean needsPreview; private long mTimestamp = 0; private int mReceivedFlags = 0; private boolean mHasStarted = false; public CaptureHolder(RequestHolder request, LegacyRequest legacyHolder) { mRequest = request; mLegacy = legacyHolder; needsJpeg = request.hasJpegTargets(); needsPreview = request.hasPreviewTargets(); } public boolean isPreviewCompleted() { return (mReceivedFlags & FLAG_RECEIVED_ALL_PREVIEW) == FLAG_RECEIVED_ALL_PREVIEW; } public boolean isJpegCompleted() { return (mReceivedFlags & FLAG_RECEIVED_ALL_JPEG) == FLAG_RECEIVED_ALL_JPEG; } public boolean isCompleted() { return (needsJpeg == isJpegCompleted()) && (needsPreview == isPreviewCompleted()); } public void tryComplete() { if (isCompleted()) { if (needsPreview && isPreviewCompleted()) { CaptureCollector.this.onPreviewCompleted(); } CaptureCollector.this.onRequestCompleted(mRequest, mLegacy, mTimestamp); } } public void setJpegTimestamp(long timestamp) { if (DEBUG) { Log.d(TAG, "setJpegTimestamp - called for request " + mRequest.getRequestId()); } if (!needsJpeg) { throw new IllegalStateException( "setJpegTimestamp called for capture with no jpeg targets."); } if (isCompleted()) { throw new IllegalStateException( "setJpegTimestamp called on already completed request."); } mReceivedFlags |= FLAG_RECEIVED_JPEG_TS; if (mTimestamp == 0) { mTimestamp = timestamp; } if (!mHasStarted) { mHasStarted = true; CaptureCollector.this.mDeviceState.setCaptureStart(mRequest, mTimestamp); } tryComplete(); } public void setJpegProduced() { if (DEBUG) { Log.d(TAG, "setJpegProduced - called for request " + mRequest.getRequestId()); } if (!needsJpeg) { throw new IllegalStateException( "setJpegProduced called for capture with no jpeg targets."); } if (isCompleted()) { throw new IllegalStateException( "setJpegProduced called on already completed request."); } mReceivedFlags |= FLAG_RECEIVED_JPEG; tryComplete(); } public void setPreviewTimestamp(long timestamp) { if (DEBUG) { Log.d(TAG, "setPreviewTimestamp - called for request " + mRequest.getRequestId()); } if (!needsPreview) { throw new IllegalStateException( "setPreviewTimestamp called for capture with no preview targets."); } if (isCompleted()) { throw new IllegalStateException( "setPreviewTimestamp called on already completed request."); } mReceivedFlags |= FLAG_RECEIVED_PREVIEW_TS; if (mTimestamp == 0) { mTimestamp = timestamp; } if (!needsJpeg) { if (!mHasStarted) { mHasStarted = true; CaptureCollector.this.mDeviceState.setCaptureStart(mRequest, mTimestamp); } } tryComplete(); } public void setPreviewProduced() { if (DEBUG) { Log.d(TAG, "setPreviewProduced - called for request " + mRequest.getRequestId()); } if (!needsPreview) { throw new IllegalStateException( "setPreviewProduced called for capture with no preview targets."); } if (isCompleted()) { throw new IllegalStateException( "setPreviewProduced called on already completed request."); } mReceivedFlags |= FLAG_RECEIVED_PREVIEW; tryComplete(); } } private final ArrayDeque mJpegCaptureQueue; private final ArrayDeque mJpegProduceQueue; private final ArrayDeque mPreviewCaptureQueue; private final ArrayDeque mPreviewProduceQueue; private final ReentrantLock mLock = new ReentrantLock(); private final Condition mIsEmpty; private final Condition mPreviewsEmpty; private final Condition mNotFull; private final CameraDeviceState mDeviceState; private final LegacyResultMapper mMapper = new LegacyResultMapper(); private int mInFlight = 0; private int mInFlightPreviews = 0; private final int mMaxInFlight; /** * Create a new {@link CaptureCollector} that can modify the given {@link CameraDeviceState}. * * @param maxInFlight max allowed in-flight requests. * @param deviceState the {@link CameraDeviceState} to update as requests are processed. */ public CaptureCollector(int maxInFlight, CameraDeviceState deviceState) { mMaxInFlight = maxInFlight; mJpegCaptureQueue = new ArrayDeque<>(MAX_JPEGS_IN_FLIGHT); mJpegProduceQueue = new ArrayDeque<>(MAX_JPEGS_IN_FLIGHT); mPreviewCaptureQueue = new ArrayDeque<>(mMaxInFlight); mPreviewProduceQueue = new ArrayDeque<>(mMaxInFlight); mIsEmpty = mLock.newCondition(); mNotFull = mLock.newCondition(); mPreviewsEmpty = mLock.newCondition(); mDeviceState = deviceState; } /** * Queue a new request. * *

* For requests that use the Camera1 API preview output stream, this will block if there are * already {@code maxInFlight} requests in progress (until at least one prior request has * completed). For requests that use the Camera1 API jpeg callbacks, this will block until * all prior requests have been completed to avoid stopping preview for * {@link android.hardware.Camera#takePicture} before prior preview requests have been * completed. *

* @param holder the {@link RequestHolder} for this request. * @param legacy the {@link LegacyRequest} for this request; this will not be mutated. * @param timeout a timeout to use for this call. * @param unit the units to use for the timeout. * @return {@code false} if this method timed out. * @throws InterruptedException if this thread is interrupted. */ public boolean queueRequest(RequestHolder holder, LegacyRequest legacy, long timeout, TimeUnit unit) throws InterruptedException { CaptureHolder h = new CaptureHolder(holder, legacy); long nanos = unit.toNanos(timeout); final ReentrantLock lock = this.mLock; lock.lock(); try { if (DEBUG) { Log.d(TAG, "queueRequest for request " + holder.getRequestId() + " - " + mInFlight + " requests remain in flight."); } if (h.needsJpeg) { // Wait for all current requests to finish before queueing jpeg. while (mInFlight > 0) { if (nanos <= 0) { return false; } nanos = mIsEmpty.awaitNanos(nanos); } mJpegCaptureQueue.add(h); mJpegProduceQueue.add(h); } if (h.needsPreview) { while (mInFlight >= mMaxInFlight) { if (nanos <= 0) { return false; } nanos = mNotFull.awaitNanos(nanos); } mPreviewCaptureQueue.add(h); mPreviewProduceQueue.add(h); mInFlightPreviews++; } if (!(h.needsJpeg || h.needsPreview)) { throw new IllegalStateException("Request must target at least one output surface!"); } mInFlight++; return true; } finally { lock.unlock(); } } /** * Wait all queued requests to complete. * * @param timeout a timeout to use for this call. * @param unit the units to use for the timeout. * @return {@code false} if this method timed out. * @throws InterruptedException if this thread is interrupted. */ public boolean waitForEmpty(long timeout, TimeUnit unit) throws InterruptedException { long nanos = unit.toNanos(timeout); final ReentrantLock lock = this.mLock; lock.lock(); try { while (mInFlight > 0) { if (nanos <= 0) { return false; } nanos = mIsEmpty.awaitNanos(nanos); } return true; } finally { lock.unlock(); } } /** * Wait all queued requests that use the Camera1 API preview output to complete. * * @param timeout a timeout to use for this call. * @param unit the units to use for the timeout. * @return {@code false} if this method timed out. * @throws InterruptedException if this thread is interrupted. */ public boolean waitForPreviewsEmpty(long timeout, TimeUnit unit) throws InterruptedException { long nanos = unit.toNanos(timeout); final ReentrantLock lock = this.mLock; lock.lock(); try { while (mInFlightPreviews > 0) { if (nanos <= 0) { return false; } nanos = mPreviewsEmpty.awaitNanos(nanos); } return true; } finally { lock.unlock(); } } /** * Called to alert the {@link CaptureCollector} that the jpeg capture has begun. * * @param timestamp the time of the jpeg capture. * @return the {@link RequestHolder} for the request associated with this capture. */ public RequestHolder jpegCaptured(long timestamp) { final ReentrantLock lock = this.mLock; lock.lock(); try { CaptureHolder h = mJpegCaptureQueue.poll(); if (h == null) { Log.w(TAG, "jpegCaptured called with no jpeg request on queue!"); return null; } h.setJpegTimestamp(timestamp); return h.mRequest; } finally { lock.unlock(); } } /** * Called to alert the {@link CaptureCollector} that the jpeg capture has completed. * * @return a pair containing the {@link RequestHolder} and the timestamp of the capture. */ public Pair jpegProduced() { final ReentrantLock lock = this.mLock; lock.lock(); try { CaptureHolder h = mJpegProduceQueue.poll(); if (h == null) { Log.w(TAG, "jpegProduced called with no jpeg request on queue!"); return null; } h.setJpegProduced(); return new Pair<>(h.mRequest, h.mTimestamp); } finally { lock.unlock(); } } /** * Check if there are any pending capture requests that use the Camera1 API preview output. * * @return {@code true} if there are pending preview requests. */ public boolean hasPendingPreviewCaptures() { final ReentrantLock lock = this.mLock; lock.lock(); try { return !mPreviewCaptureQueue.isEmpty(); } finally { lock.unlock(); } } /** * Called to alert the {@link CaptureCollector} that the preview capture has begun. * * @param timestamp the time of the preview capture. * @return a pair containing the {@link RequestHolder} and the timestamp of the capture. */ public Pair previewCaptured(long timestamp) { final ReentrantLock lock = this.mLock; lock.lock(); try { CaptureHolder h = mPreviewCaptureQueue.poll(); if (h == null) { Log.w(TAG, "previewCaptured called with no preview request on queue!"); return null; } h.setPreviewTimestamp(timestamp); return new Pair<>(h.mRequest, h.mTimestamp); } finally { lock.unlock(); } } /** * Called to alert the {@link CaptureCollector} that the preview capture has completed. * * @return the {@link RequestHolder} for the request associated with this capture. */ public RequestHolder previewProduced() { final ReentrantLock lock = this.mLock; lock.lock(); try { CaptureHolder h = mPreviewProduceQueue.poll(); if (h == null) { Log.w(TAG, "previewProduced called with no preview request on queue!"); return null; } h.setPreviewProduced(); return h.mRequest; } finally { lock.unlock(); } } private void onPreviewCompleted() { mInFlightPreviews--; if (mInFlightPreviews < 0) { throw new IllegalStateException( "More preview captures completed than requests queued."); } if (mInFlightPreviews == 0) { mPreviewsEmpty.signalAll(); } } private void onRequestCompleted(RequestHolder request, LegacyRequest legacyHolder, long timestamp) { mInFlight--; if (DEBUG) { Log.d(TAG, "Completed request " + request.getRequestId() + ", " + mInFlight + " requests remain in flight."); } if (mInFlight < 0) { throw new IllegalStateException( "More captures completed than requests queued."); } mNotFull.signalAll(); if (mInFlight == 0) { mIsEmpty.signalAll(); } CameraMetadataNative result = mMapper.cachedConvertResultMetadata( legacyHolder, timestamp); mDeviceState.setCaptureResult(request, result); } }