Create orchestrator, replacing Step class

pull/697/head
Mattia Iavarone 6 years ago
parent fd17a8339e
commit 57a4cfba80
  1. 5
      cameraview/build.gradle
  2. 2
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
  3. 17
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  4. 123
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java
  5. 166
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java
  6. 573
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java
  7. 178
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Step.java
  8. 5
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/LogAction.java
  9. 180
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/orchestrator/CameraOrchestrator.java
  10. 17
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/orchestrator/CameraState.java
  11. 117
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/orchestrator/CameraStateOrchestrator.java

@ -42,8 +42,8 @@ dependencies {
androidTestImplementation 'org.mockito:mockito-android:2.28.2' androidTestImplementation 'org.mockito:mockito-android:2.28.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
api 'androidx.exifinterface:exifinterface:1.0.0' api 'androidx.exifinterface:exifinterface:1.1.0'
api 'androidx.lifecycle:lifecycle-common:2.1.0-alpha01' api 'androidx.lifecycle:lifecycle-common:2.1.0'
api 'com.google.android.gms:play-services-tasks:17.0.0' api 'com.google.android.gms:play-services-tasks:17.0.0'
implementation 'androidx.annotation:annotation:1.1.0' implementation 'androidx.annotation:annotation:1.1.0'
} }
@ -250,6 +250,7 @@ task mergeCoverageReports(type: JacocoReport) {
classFilter.add('**/com/otaliastudios/cameraview/picture/**.*') classFilter.add('**/com/otaliastudios/cameraview/picture/**.*')
classFilter.add('**/com/otaliastudios/cameraview/video/**.*') classFilter.add('**/com/otaliastudios/cameraview/video/**.*')
// TODO these below could be easily testable ALSO outside of the integration tests // TODO these below could be easily testable ALSO outside of the integration tests
classFilter.add('**/com/otaliastudios/cameraview/orchestrator/**.*')
classFilter.add('**/com/otaliastudios/cameraview/video/encoding/**.*') classFilter.add('**/com/otaliastudios/cameraview/video/encoding/**.*')
} }
// We don't test OpenGL filters. // We don't test OpenGL filters.

@ -128,7 +128,7 @@ public class CameraViewTest extends BaseTest {
public void testClose() { public void testClose() {
cameraView.close(); cameraView.close();
verify(mockPreview, times(1)).onPause(); verify(mockPreview, times(1)).onPause();
verify(mockController, times(1)).stop(); verify(mockController, times(1)).stop(false);
} }
@Test @Test

@ -48,6 +48,7 @@ import com.otaliastudios.cameraview.engine.Camera1Engine;
import com.otaliastudios.cameraview.engine.Camera2Engine; import com.otaliastudios.cameraview.engine.Camera2Engine;
import com.otaliastudios.cameraview.engine.CameraEngine; import com.otaliastudios.cameraview.engine.CameraEngine;
import com.otaliastudios.cameraview.engine.offset.Reference; import com.otaliastudios.cameraview.engine.offset.Reference;
import com.otaliastudios.cameraview.engine.orchestrator.CameraState;
import com.otaliastudios.cameraview.filter.Filter; import com.otaliastudios.cameraview.filter.Filter;
import com.otaliastudios.cameraview.filter.FilterParser; import com.otaliastudios.cameraview.filter.FilterParser;
import com.otaliastudios.cameraview.filter.Filters; import com.otaliastudios.cameraview.filter.Filters;
@ -693,15 +694,17 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
//region Lifecycle APIs //region Lifecycle APIs
/** /**
* Returns whether the camera has started showing its preview. * Returns whether the camera engine has started.
* @return whether the camera has started * @return whether the camera has started
*/ */
public boolean isOpened() { public boolean isOpened() {
return mCameraEngine.getEngineState() >= CameraEngine.STATE_STARTED; return mCameraEngine.getState().isAtLeast(CameraState.ENGINE)
&& mCameraEngine.getTargetState().isAtLeast(CameraState.ENGINE);
} }
private boolean isClosed() { private boolean isClosed() {
return mCameraEngine.getEngineState() == CameraEngine.STATE_STOPPED; return mCameraEngine.getState() == CameraState.OFF
&& !mCameraEngine.isChangingState();
} }
/** /**
@ -792,7 +795,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
public void close() { public void close() {
if (mInEditor) return; if (mInEditor) return;
mCameraEngine.stop(); mCameraEngine.stop(false);
if (mCameraPreview != null) mCameraPreview.onPause(); if (mCameraPreview != null) mCameraPreview.onPause();
} }
@ -2006,7 +2009,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
} }
@Override @Override
public void dispatchOnCameraOpened(final CameraOptions options) { public void dispatchOnCameraOpened(@NonNull final CameraOptions options) {
mLogger.i("dispatchOnCameraOpened", options); mLogger.i("dispatchOnCameraOpened", options);
mUiHandler.post(new Runnable() { mUiHandler.post(new Runnable() {
@Override @Override
@ -2054,7 +2057,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
} }
@Override @Override
public void dispatchOnPictureTaken(final PictureResult.Stub stub) { public void dispatchOnPictureTaken(@NonNull final PictureResult.Stub stub) {
mLogger.i("dispatchOnPictureTaken", stub); mLogger.i("dispatchOnPictureTaken", stub);
mUiHandler.post(new Runnable() { mUiHandler.post(new Runnable() {
@Override @Override
@ -2068,7 +2071,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
} }
@Override @Override
public void dispatchOnVideoTaken(final VideoResult.Stub stub) { public void dispatchOnVideoTaken(@NonNull final VideoResult.Stub stub) {
mLogger.i("dispatchOnVideoTaken", stub); mLogger.i("dispatchOnVideoTaken", stub);
mUiHandler.post(new Runnable() { mUiHandler.post(new Runnable() {
@Override @Override

@ -24,6 +24,7 @@ import com.otaliastudios.cameraview.engine.mappers.Camera1Mapper;
import com.otaliastudios.cameraview.engine.offset.Axis; import com.otaliastudios.cameraview.engine.offset.Axis;
import com.otaliastudios.cameraview.engine.offset.Reference; import com.otaliastudios.cameraview.engine.offset.Reference;
import com.otaliastudios.cameraview.engine.options.Camera1Options; import com.otaliastudios.cameraview.engine.options.Camera1Options;
import com.otaliastudios.cameraview.engine.orchestrator.CameraState;
import com.otaliastudios.cameraview.frame.Frame; import com.otaliastudios.cameraview.frame.Frame;
import com.otaliastudios.cameraview.PictureResult; import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.VideoResult;
@ -49,7 +50,6 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
@SuppressWarnings("deprecation")
public class Camera1Engine extends CameraEngine implements public class Camera1Engine extends CameraEngine implements
Camera.PreviewCallback, Camera.PreviewCallback,
Camera.ErrorCallback, Camera.ErrorCallback,
@ -58,13 +58,15 @@ public class Camera1Engine extends CameraEngine implements
private static final String TAG = Camera1Engine.class.getSimpleName(); private static final String TAG = Camera1Engine.class.getSimpleName();
private static final CameraLogger LOG = CameraLogger.create(TAG); private static final CameraLogger LOG = CameraLogger.create(TAG);
private static final String JOB_FOCUS_RESET = "focus reset";
private static final String JOB_FOCUS_END = "focus end";
private static final int PREVIEW_FORMAT = ImageFormat.NV21; private static final int PREVIEW_FORMAT = ImageFormat.NV21;
@VisibleForTesting static final int AUTOFOCUS_END_DELAY_MILLIS = 2500; @VisibleForTesting static final int AUTOFOCUS_END_DELAY_MILLIS = 2500;
private final Camera1Mapper mMapper = Camera1Mapper.get(); private final Camera1Mapper mMapper = Camera1Mapper.get();
private Camera mCamera; private Camera mCamera;
@VisibleForTesting int mCameraId; @VisibleForTesting int mCameraId;
private Runnable mFocusEndRunnable;
public Camera1Engine(@NonNull Callback callback) { public Camera1Engine(@NonNull Callback callback) {
super(callback); super(callback);
@ -286,10 +288,8 @@ public class Camera1Engine extends CameraEngine implements
@Override @Override
protected Task<Void> onStopEngine() { protected Task<Void> onStopEngine() {
LOG.i("onStopEngine:", "About to clean up."); LOG.i("onStopEngine:", "About to clean up.");
mHandler.remove(mFocusResetRunnable); mOrchestrator.remove(JOB_FOCUS_RESET);
if (mFocusEndRunnable != null) { mOrchestrator.remove(JOB_FOCUS_END);
mHandler.remove(mFocusEndRunnable);
}
if (mCamera != null) { if (mCamera != null) {
try { try {
LOG.i("onStopEngine:", "Clean up.", "Releasing camera."); LOG.i("onStopEngine:", "Clean up.", "Releasing camera.");
@ -457,15 +457,14 @@ public class Camera1Engine extends CameraEngine implements
public void setFlash(@NonNull Flash flash) { public void setFlash(@NonNull Flash flash) {
final Flash old = mFlash; final Flash old = mFlash;
mFlash = flash; mFlash = flash;
mHandler.run(new Runnable() { mFlashTask = mOrchestrator.scheduleStateful("flash",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) {
Camera.Parameters params = mCamera.getParameters(); Camera.Parameters params = mCamera.getParameters();
if (applyFlash(params, old)) mCamera.setParameters(params); if (applyFlash(params, old)) mCamera.setParameters(params);
} }
mFlashOp.end(null);
}
}); });
} }
@ -482,15 +481,14 @@ public class Camera1Engine extends CameraEngine implements
public void setLocation(@Nullable Location location) { public void setLocation(@Nullable Location location) {
final Location oldLocation = mLocation; final Location oldLocation = mLocation;
mLocation = location; mLocation = location;
mHandler.run(new Runnable() { mLocationTask = mOrchestrator.scheduleStateful("location",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) {
Camera.Parameters params = mCamera.getParameters(); Camera.Parameters params = mCamera.getParameters();
if (applyLocation(params, oldLocation)) mCamera.setParameters(params); if (applyLocation(params, oldLocation)) mCamera.setParameters(params);
} }
mLocationOp.end(null);
}
}); });
} }
@ -510,15 +508,14 @@ public class Camera1Engine extends CameraEngine implements
public void setWhiteBalance(@NonNull WhiteBalance whiteBalance) { public void setWhiteBalance(@NonNull WhiteBalance whiteBalance) {
final WhiteBalance old = mWhiteBalance; final WhiteBalance old = mWhiteBalance;
mWhiteBalance = whiteBalance; mWhiteBalance = whiteBalance;
mHandler.run(new Runnable() { mWhiteBalanceTask = mOrchestrator.scheduleStateful("white balance",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) {
Camera.Parameters params = mCamera.getParameters(); Camera.Parameters params = mCamera.getParameters();
if (applyWhiteBalance(params, old)) mCamera.setParameters(params); if (applyWhiteBalance(params, old)) mCamera.setParameters(params);
} }
mWhiteBalanceOp.end(null);
}
}); });
} }
@ -536,15 +533,14 @@ public class Camera1Engine extends CameraEngine implements
public void setHdr(@NonNull Hdr hdr) { public void setHdr(@NonNull Hdr hdr) {
final Hdr old = mHdr; final Hdr old = mHdr;
mHdr = hdr; mHdr = hdr;
mHandler.run(new Runnable() { mHdrTask = mOrchestrator.scheduleStateful("hdr",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) {
Camera.Parameters params = mCamera.getParameters(); Camera.Parameters params = mCamera.getParameters();
if (applyHdr(params, old)) mCamera.setParameters(params); if (applyHdr(params, old)) mCamera.setParameters(params);
} }
mHdrOp.end(null);
}
}); });
} }
@ -561,10 +557,11 @@ public class Camera1Engine extends CameraEngine implements
public void setZoom(final float zoom, @Nullable final PointF[] points, final boolean notify) { public void setZoom(final float zoom, @Nullable final PointF[] points, final boolean notify) {
final float old = mZoomValue; final float old = mZoomValue;
mZoomValue = zoom; mZoomValue = zoom;
mHandler.run(new Runnable() { mZoomTask = mOrchestrator.scheduleStateful("zoom",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) {
Camera.Parameters params = mCamera.getParameters(); Camera.Parameters params = mCamera.getParameters();
if (applyZoom(params, old)) { if (applyZoom(params, old)) {
mCamera.setParameters(params); mCamera.setParameters(params);
@ -573,8 +570,6 @@ public class Camera1Engine extends CameraEngine implements
} }
} }
} }
mZoomOp.end(null);
}
}); });
} }
@ -594,10 +589,11 @@ public class Camera1Engine extends CameraEngine implements
@Nullable final PointF[] points, final boolean notify) { @Nullable final PointF[] points, final boolean notify) {
final float old = mExposureCorrectionValue; final float old = mExposureCorrectionValue;
mExposureCorrectionValue = EVvalue; mExposureCorrectionValue = EVvalue;
mHandler.run(new Runnable() { mExposureCorrectionTask = mOrchestrator.scheduleStateful("exposure correction",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) {
Camera.Parameters params = mCamera.getParameters(); Camera.Parameters params = mCamera.getParameters();
if (applyExposureCorrection(params, old)) { if (applyExposureCorrection(params, old)) {
mCamera.setParameters(params); mCamera.setParameters(params);
@ -607,8 +603,6 @@ public class Camera1Engine extends CameraEngine implements
} }
} }
} }
mExposureCorrectionOp.end(null);
}
}); });
} }
@ -635,14 +629,13 @@ public class Camera1Engine extends CameraEngine implements
public void setPlaySounds(boolean playSounds) { public void setPlaySounds(boolean playSounds) {
final boolean old = mPlaySounds; final boolean old = mPlaySounds;
mPlaySounds = playSounds; mPlaySounds = playSounds;
mHandler.run(new Runnable() { mPlaySoundsTask = mOrchestrator.scheduleStateful("play sounds",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) {
applyPlaySounds(old); applyPlaySounds(old);
} }
mPlaySoundsOp.end(null);
}
}); });
} }
@ -672,15 +665,14 @@ public class Camera1Engine extends CameraEngine implements
public void setPreviewFrameRate(float previewFrameRate) { public void setPreviewFrameRate(float previewFrameRate) {
final float old = previewFrameRate; final float old = previewFrameRate;
mPreviewFrameRate = previewFrameRate; mPreviewFrameRate = previewFrameRate;
mHandler.run(new Runnable() { mPreviewFrameRateTask = mOrchestrator.scheduleStateful("preview fps",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) {
Camera.Parameters params = mCamera.getParameters(); Camera.Parameters params = mCamera.getParameters();
if (applyPreviewFrameRate(params, old)) mCamera.setParameters(params); if (applyPreviewFrameRate(params, old)) mCamera.setParameters(params);
} }
mPreviewFrameRateOp.end(null);
}
}); });
} }
@ -737,7 +729,8 @@ public class Camera1Engine extends CameraEngine implements
@Override @Override
public void onBufferAvailable(@NonNull byte[] buffer) { public void onBufferAvailable(@NonNull byte[] buffer) {
if (getEngineState() == STATE_STARTED) { if (getState().isAtLeast(CameraState.ENGINE)
&& getTargetState().isAtLeast(CameraState.ENGINE)) {
mCamera.addCallbackBuffer(buffer); mCamera.addCallbackBuffer(buffer);
} }
} }
@ -770,10 +763,9 @@ public class Camera1Engine extends CameraEngine implements
} }
final int viewWidthF = viewWidth; final int viewWidthF = viewWidth;
final int viewHeightF = viewHeight; final int viewHeightF = viewHeight;
mHandler.run(new Runnable() { mOrchestrator.scheduleStateful("auto focus", CameraState.ENGINE, new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() < STATE_STARTED) return;
if (!mCameraOptions.isAutoFocusSupported()) return; if (!mCameraOptions.isAutoFocusSupported()) return;
final PointF p = new PointF(point.x, point.y); // copy. final PointF p = new PointF(point.x, point.y); // copy.
int offset = getAngles().offset(Reference.SENSOR, Reference.VIEW, Axis.ABSOLUTE); int offset = getAngles().offset(Reference.SENSOR, Reference.VIEW, Axis.ABSOLUTE);
@ -794,14 +786,14 @@ public class Camera1Engine extends CameraEngine implements
// The auto focus callback is not guaranteed to be called, but we really want it // The auto focus callback is not guaranteed to be called, but we really want it
// to be. So we remove the old runnable if still present and post a new one. // to be. So we remove the old runnable if still present and post a new one.
if (mFocusEndRunnable != null) mHandler.remove(mFocusEndRunnable); mOrchestrator.remove(JOB_FOCUS_END);
mFocusEndRunnable = new Runnable() { mOrchestrator.scheduleDelayed(JOB_FOCUS_END, AUTOFOCUS_END_DELAY_MILLIS,
new Runnable() {
@Override @Override
public void run() { public void run() {
mCallback.dispatchOnFocusEnd(gesture, false, p); mCallback.dispatchOnFocusEnd(gesture, false, p);
} }
}; });
mHandler.post(AUTOFOCUS_END_DELAY_MILLIS, mFocusEndRunnable);
// Wrapping autoFocus in a try catch to handle some device specific exceptions, // Wrapping autoFocus in a try catch to handle some device specific exceptions,
// see See https://github.com/natario1/CameraView/issues/181. // see See https://github.com/natario1/CameraView/issues/181.
@ -809,14 +801,27 @@ public class Camera1Engine extends CameraEngine implements
mCamera.autoFocus(new Camera.AutoFocusCallback() { mCamera.autoFocus(new Camera.AutoFocusCallback() {
@Override @Override
public void onAutoFocus(boolean success, Camera camera) { public void onAutoFocus(boolean success, Camera camera) {
if (mFocusEndRunnable != null) { mOrchestrator.remove(JOB_FOCUS_END);
mHandler.remove(mFocusEndRunnable); mOrchestrator.remove(JOB_FOCUS_RESET);
mFocusEndRunnable = null;
}
mCallback.dispatchOnFocusEnd(gesture, success, p); mCallback.dispatchOnFocusEnd(gesture, success, p);
mHandler.remove(mFocusResetRunnable);
if (shouldResetAutoFocus()) { if (shouldResetAutoFocus()) {
mHandler.post(getAutoFocusResetDelay(), mFocusResetRunnable); mOrchestrator.scheduleStatefulDelayed(
JOB_FOCUS_RESET,
CameraState.ENGINE,
getAutoFocusResetDelay(),
new Runnable() {
@Override
public void run() {
mCamera.cancelAutoFocus();
Camera.Parameters params = mCamera.getParameters();
int maxAF = params.getMaxNumFocusAreas();
int maxAE = params.getMaxNumMeteringAreas();
if (maxAF > 0) params.setFocusAreas(null);
if (maxAE > 0) params.setMeteringAreas(null);
applyDefaultFocus(params); // Revert to internal focus.
mCamera.setParameters(params);
}
});
} }
} }
}); });
@ -825,7 +830,6 @@ public class Camera1Engine extends CameraEngine implements
// Let the mFocusEndRunnable do its job. (could remove it and quickly dispatch // Let the mFocusEndRunnable do its job. (could remove it and quickly dispatch
// onFocusEnd here, but let's make it simpler). // onFocusEnd here, but let's make it simpler).
} }
} }
}); });
} }
@ -876,21 +880,6 @@ public class Camera1Engine extends CameraEngine implements
return new Rect(left, top, right, bottom); return new Rect(left, top, right, bottom);
} }
private final Runnable mFocusResetRunnable = new Runnable() {
@Override
public void run() {
if (getEngineState() < STATE_STARTED) return;
mCamera.cancelAutoFocus();
Camera.Parameters params = mCamera.getParameters();
int maxAF = params.getMaxNumFocusAreas();
int maxAE = params.getMaxNumMeteringAreas();
if (maxAF > 0) params.setFocusAreas(null);
if (maxAE > 0) params.setMeteringAreas(null);
applyDefaultFocus(params); // Revert to internal focus.
mCamera.setParameters(params);
}
};
//endregion //endregion
} }

@ -48,12 +48,14 @@ import com.otaliastudios.cameraview.engine.action.ActionHolder;
import com.otaliastudios.cameraview.engine.action.Actions; import com.otaliastudios.cameraview.engine.action.Actions;
import com.otaliastudios.cameraview.engine.action.BaseAction; import com.otaliastudios.cameraview.engine.action.BaseAction;
import com.otaliastudios.cameraview.engine.action.CompletionCallback; import com.otaliastudios.cameraview.engine.action.CompletionCallback;
import com.otaliastudios.cameraview.engine.action.LogAction;
import com.otaliastudios.cameraview.engine.mappers.Camera2Mapper; import com.otaliastudios.cameraview.engine.mappers.Camera2Mapper;
import com.otaliastudios.cameraview.engine.meter.MeterAction; import com.otaliastudios.cameraview.engine.meter.MeterAction;
import com.otaliastudios.cameraview.engine.meter.MeterResetAction; import com.otaliastudios.cameraview.engine.meter.MeterResetAction;
import com.otaliastudios.cameraview.engine.offset.Axis; import com.otaliastudios.cameraview.engine.offset.Axis;
import com.otaliastudios.cameraview.engine.offset.Reference; import com.otaliastudios.cameraview.engine.offset.Reference;
import com.otaliastudios.cameraview.engine.options.Camera2Options; import com.otaliastudios.cameraview.engine.options.Camera2Options;
import com.otaliastudios.cameraview.engine.orchestrator.CameraState;
import com.otaliastudios.cameraview.frame.Frame; import com.otaliastudios.cameraview.frame.Frame;
import com.otaliastudios.cameraview.frame.FrameManager; import com.otaliastudios.cameraview.frame.FrameManager;
import com.otaliastudios.cameraview.gesture.Gesture; import com.otaliastudios.cameraview.gesture.Gesture;
@ -243,7 +245,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
} }
private void applyRepeatingRequestBuilder(boolean checkStarted, int errorReason) { private void applyRepeatingRequestBuilder(boolean checkStarted, int errorReason) {
if (getPreviewState() == STATE_STARTED || !checkStarted) { if ((getState() == CameraState.PREVIEW && !isChangingState()) || !checkStarted) {
try { try {
mSession.setRepeatingRequest(mRepeatingRequestBuilder.build(), mSession.setRepeatingRequest(mRepeatingRequestBuilder.build(),
mRepeatingRequestCallback, null); mRepeatingRequestCallback, null);
@ -256,9 +258,8 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
LOG.e("applyRepeatingRequestBuilder: session is invalid!", e, LOG.e("applyRepeatingRequestBuilder: session is invalid!", e,
"checkStarted:", checkStarted, "checkStarted:", checkStarted,
"currentThread:", Thread.currentThread().getName(), "currentThread:", Thread.currentThread().getName(),
"previewState:", getPreviewState(), "state:", getState(),
"bindState:", getBindState(), "targetState:", getTargetState());
"engineState:", getEngineState());
throw new CameraException(CameraException.REASON_DISCONNECTED); throw new CameraException(CameraException.REASON_DISCONNECTED);
} }
} }
@ -588,13 +589,12 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
if (mFullVideoPendingStub != null) { if (mFullVideoPendingStub != null) {
// Do not call takeVideo/onTakeVideo. It will reset some stub parameters that // Do not call takeVideo/onTakeVideo. It will reset some stub parameters that
// the recorder sets. Also we are posting so that doTakeVideo sees a started preview. // the recorder sets. Also we are posting so that doTakeVideo sees a started preview.
LOG.i("onStartPreview", "Posting doTakeVideo call.");
final VideoResult.Stub stub = mFullVideoPendingStub; final VideoResult.Stub stub = mFullVideoPendingStub;
mFullVideoPendingStub = null; mFullVideoPendingStub = null;
mHandler.post(new Runnable() { mOrchestrator.scheduleStateful("do take video", CameraState.PREVIEW,
new Runnable() {
@Override @Override
public void run() { public void run() {
LOG.i("onStartPreview", "Executing doTakeVideo call.");
doTakeVideo(stub); doTakeVideo(stub);
} }
}); });
@ -895,10 +895,10 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
// SnapshotRecorder will invoke this on its own thread, so let's post in our own thread // SnapshotRecorder will invoke this on its own thread, so let's post in our own thread
// and check camera state before trying to restore the preview. Engine might have been // and check camera state before trying to restore the preview. Engine might have been
// torn down in the engine thread while this was still being called. // torn down in the engine thread while this was still being called.
mHandler.run(new Runnable() { mOrchestrator.scheduleStateful("restore preview template", CameraState.BIND,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getBindState() < STATE_STARTED) return;
maybeRestorePreviewTemplateAfterVideo(); maybeRestorePreviewTemplateAfterVideo();
} }
}); });
@ -1028,12 +1028,13 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
public void setFlash(@NonNull final Flash flash) { public void setFlash(@NonNull final Flash flash) {
final Flash old = mFlash; final Flash old = mFlash;
mFlash = flash; mFlash = flash;
mHandler.run(new Runnable() { mFlashTask = mOrchestrator.scheduleStateful("flash (" + flash + ")",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) {
boolean shouldApply = applyFlash(mRepeatingRequestBuilder, old); boolean shouldApply = applyFlash(mRepeatingRequestBuilder, old);
boolean needsWorkaround = getPreviewState() == STATE_STARTED; boolean needsWorkaround = getState() == CameraState.PREVIEW;
if (needsWorkaround) { if (needsWorkaround) {
// Runtime changes to the flash value are not correctly handled by the // Runtime changes to the flash value are not correctly handled by the
// driver. See https://stackoverflow.com/q/53003383/4288782 for example. // driver. See https://stackoverflow.com/q/53003383/4288782 for example.
@ -1054,8 +1055,6 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
applyRepeatingRequestBuilder(); applyRepeatingRequestBuilder();
} }
} }
mFlashOp.end(null);
}
}); });
} }
@ -1104,16 +1103,15 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
public void setLocation(@Nullable Location location) { public void setLocation(@Nullable Location location) {
final Location old = mLocation; final Location old = mLocation;
mLocation = location; mLocation = location;
mHandler.run(new Runnable() { mLocationTask = mOrchestrator.scheduleStateful("location",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) {
if (applyLocation(mRepeatingRequestBuilder, old)) { if (applyLocation(mRepeatingRequestBuilder, old)) {
applyRepeatingRequestBuilder(); applyRepeatingRequestBuilder();
} }
} }
mLocationOp.end(null);
}
}); });
} }
@ -1130,16 +1128,16 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
public void setWhiteBalance(@NonNull WhiteBalance whiteBalance) { public void setWhiteBalance(@NonNull WhiteBalance whiteBalance) {
final WhiteBalance old = mWhiteBalance; final WhiteBalance old = mWhiteBalance;
mWhiteBalance = whiteBalance; mWhiteBalance = whiteBalance;
mHandler.run(new Runnable() { mWhiteBalanceTask = mOrchestrator.scheduleStateful(
"white balance (" + whiteBalance + ")",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) {
if (applyWhiteBalance(mRepeatingRequestBuilder, old)) { if (applyWhiteBalance(mRepeatingRequestBuilder, old)) {
applyRepeatingRequestBuilder(); applyRepeatingRequestBuilder();
} }
} }
mWhiteBalanceOp.end(null);
}
}); });
} }
@ -1159,16 +1157,15 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
public void setHdr(@NonNull Hdr hdr) { public void setHdr(@NonNull Hdr hdr) {
final Hdr old = mHdr; final Hdr old = mHdr;
mHdr = hdr; mHdr = hdr;
mHandler.run(new Runnable() { mHdrTask = mOrchestrator.scheduleStateful("hdr (" + hdr + ")",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) {
if (applyHdr(mRepeatingRequestBuilder, old)) { if (applyHdr(mRepeatingRequestBuilder, old)) {
applyRepeatingRequestBuilder(); applyRepeatingRequestBuilder();
} }
} }
mHdrOp.end(null);
}
}); });
} }
@ -1187,10 +1184,12 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
public void setZoom(final float zoom, final @Nullable PointF[] points, final boolean notify) { public void setZoom(final float zoom, final @Nullable PointF[] points, final boolean notify) {
final float old = mZoomValue; final float old = mZoomValue;
mZoomValue = zoom; mZoomValue = zoom;
mHandler.run(new Runnable() { mZoomTask = mOrchestrator.scheduleStateful(
"zoom (" + zoom + ")",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) {
if (applyZoom(mRepeatingRequestBuilder, old)) { if (applyZoom(mRepeatingRequestBuilder, old)) {
applyRepeatingRequestBuilder(); applyRepeatingRequestBuilder();
if (notify) { if (notify) {
@ -1198,8 +1197,6 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
} }
} }
} }
mZoomOp.end(null);
}
}); });
} }
@ -1243,10 +1240,12 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
final boolean notify) { final boolean notify) {
final float old = mExposureCorrectionValue; final float old = mExposureCorrectionValue;
mExposureCorrectionValue = EVvalue; mExposureCorrectionValue = EVvalue;
mHandler.run(new Runnable() { mExposureCorrectionTask = mOrchestrator.scheduleStateful(
"exposure correction (" + EVvalue + ")",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) {
if (applyExposureCorrection(mRepeatingRequestBuilder, old)) { if (applyExposureCorrection(mRepeatingRequestBuilder, old)) {
applyRepeatingRequestBuilder(); applyRepeatingRequestBuilder();
if (notify) { if (notify) {
@ -1254,8 +1253,6 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
} }
} }
} }
mExposureCorrectionOp.end(null);
}
}); });
} }
@ -1278,22 +1275,23 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
@Override @Override
public void setPlaySounds(boolean playSounds) { public void setPlaySounds(boolean playSounds) {
mPlaySounds = playSounds; mPlaySounds = playSounds;
mPlaySoundsOp.end(null); mPlaySoundsTask = Tasks.forResult(null);
} }
@Override public void setPreviewFrameRate(float previewFrameRate) { @Override
public void setPreviewFrameRate(float previewFrameRate) {
final float oldPreviewFrameRate = mPreviewFrameRate; final float oldPreviewFrameRate = mPreviewFrameRate;
mPreviewFrameRate = previewFrameRate; mPreviewFrameRate = previewFrameRate;
mHandler.run(new Runnable() { mPreviewFrameRateTask = mOrchestrator.scheduleStateful(
"preview fps (" + previewFrameRate + ")",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) {
if (applyPreviewFrameRate(mRepeatingRequestBuilder, oldPreviewFrameRate)) { if (applyPreviewFrameRate(mRepeatingRequestBuilder, oldPreviewFrameRate)) {
applyRepeatingRequestBuilder(); applyRepeatingRequestBuilder();
} }
} }
mPreviewFrameRateOp.end(null);
}
}); });
} }
@ -1334,20 +1332,13 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
public void setPictureFormat(final @NonNull PictureFormat pictureFormat) { public void setPictureFormat(final @NonNull PictureFormat pictureFormat) {
if (pictureFormat != mPictureFormat) { if (pictureFormat != mPictureFormat) {
mPictureFormat = pictureFormat; mPictureFormat = pictureFormat;
LOG.i("setPictureFormat", "changing to", pictureFormat, "posting."); mOrchestrator.scheduleStateful("picture format (" + pictureFormat + ")",
mHandler.run(new Runnable() { CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
LOG.i("setPictureFormat", "changing to", pictureFormat,
"executing. EngineState:", getEngineState(),
"BindState:", getBindState());
if (getEngineState() == STATE_STOPPED) {
LOG.i("setPictureFormat", "not started so won't restart.");
} else {
LOG.i("setPictureFormat", "started or starting. Calling restart()");
restart(); restart();
} }
}
}); });
} }
} }
@ -1391,7 +1382,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
return; return;
} }
image.close(); image.close();
if (getPreviewState() == STATE_STARTED) { if (getState() == CameraState.PREVIEW && !isChangingState()) {
// After preview, the frame manager is correctly set up // After preview, the frame manager is correctly set up
Frame frame = getFrameManager().getFrame(data, Frame frame = getFrameManager().getFrame(data,
System.currentTimeMillis(), System.currentTimeMillis(),
@ -1405,35 +1396,23 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
@Override @Override
public void setHasFrameProcessors(final boolean hasFrameProcessors) { public void setHasFrameProcessors(final boolean hasFrameProcessors) {
LOG.i("setHasFrameProcessors", "changing to", hasFrameProcessors, "posting."); // Frame processing is set up partially when binding and partially when starting
Camera2Engine.super.setHasFrameProcessors(hasFrameProcessors); // the preview. If the value is changed between the two, the preview step can crash.
mHandler.run(new Runnable() { mOrchestrator.schedule("has frame processors (" + hasFrameProcessors + ")",
true, new Runnable() {
@Override @Override
public void run() { public void run() {
LOG.i("setHasFrameProcessors", "changing to", hasFrameProcessors, if (getState().isAtLeast(CameraState.BIND) && isChangingState()) {
"executing. BindState:", getBindState(), // Extremely rare case in which this was called in between startBind and
"PreviewState:", getPreviewState()); // startPreview. This can cause issues. Try later.
setHasFrameProcessors(hasFrameProcessors);
// Frame processing is set up partially when binding and partially when starting } else if (getState().isAtLeast(CameraState.BIND)) {
// the preview. We don't want to only check bind state or startPreview can fail. // Apply and restart.
if (getBindState() == STATE_STOPPED) { Camera2Engine.super.setHasFrameProcessors(hasFrameProcessors);
LOG.i("setHasFrameProcessors", "not bound so won't restart.");
} else if (getPreviewState() == STATE_STARTED) {
// This needs a restartBind(). NOTE: if taking video, this stops it.
LOG.i("setHasFrameProcessors", "bound with preview.",
"Calling restartBind().");
restartBind(); restartBind();
} else { } else {
// Bind+Preview is not completely started yet not completely stopped. // Just apply.
// This can happen if the user adds a frame processor in onCameraOpened(). Camera2Engine.super.setHasFrameProcessors(hasFrameProcessors);
// Supporting this would add lot of complexity to this class, and
// this should be discouraged anyway since changing the frame processor number
// at this time requires restarting the camera when it was just opened.
// For these reasons, let's throw.
throw new IllegalStateException("Added/removed a FrameProcessor at illegal " +
"time. These operations should be done before opening the camera, or " +
"before closing it - NOT when it just opened, for example during the " +
"onCameraOpened() callback.");
} }
} }
}); });
@ -1445,16 +1424,14 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
@Override @Override
public void startAutoFocus(@Nullable final Gesture gesture, @NonNull final PointF point) { public void startAutoFocus(@Nullable final Gesture gesture, @NonNull final PointF point) {
LOG.i("startAutoFocus", "dispatching. Gesture:", gesture);
mHandler.run(new Runnable() {
@Override
public void run() {
LOG.i("startAutoFocus", "executing. Preview state:", getPreviewState());
// This will only work when we have a preview, since it launches the preview // This will only work when we have a preview, since it launches the preview
// in the end. Even without this it would need the bind state at least, // in the end. Even without this it would need the bind state at least,
// since we need the preview size. // since we need the preview size.
if (getPreviewState() < STATE_STARTED) return; mOrchestrator.scheduleStateful("autofocus (" + gesture + ")",
CameraState.PREVIEW,
new Runnable() {
@Override
public void run() {
// The camera options API still has the auto focus API but it really // The camera options API still has the auto focus API but it really
// refers to "3A metering to a specific point". Since we have a point, check. // refers to "3A metering to a specific point". Since we have a point, check.
if (!mCameraOptions.isAutoFocusSupported()) return; if (!mCameraOptions.isAutoFocusSupported()) return;
@ -1468,10 +1445,16 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
@Override @Override
protected void onActionCompleted(@NonNull Action a) { protected void onActionCompleted(@NonNull Action a) {
mCallback.dispatchOnFocusEnd(gesture, action.isSuccessful(), point); mCallback.dispatchOnFocusEnd(gesture, action.isSuccessful(), point);
mHandler.remove(mUnlockAndResetMeteringRunnable); mOrchestrator.remove("reset metering");
if (shouldResetAutoFocus()) { if (shouldResetAutoFocus()) {
mHandler.post(getAutoFocusResetDelay(), mOrchestrator.scheduleDelayed("reset metering",
mUnlockAndResetMeteringRunnable); getAutoFocusResetDelay(),
new Runnable() {
@Override
public void run() {
unlockAndResetMetering();
}
});
} }
} }
}); });
@ -1495,15 +1478,8 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
return mMeterAction; return mMeterAction;
} }
private final Runnable mUnlockAndResetMeteringRunnable = new Runnable() {
@Override
public void run() {
unlockAndResetMetering();
}
};
private void unlockAndResetMetering() { private void unlockAndResetMetering() {
if (getEngineState() == STATE_STARTED) { if (getState() == CameraState.PREVIEW && !isChangingState()) {
Actions.sequence( Actions.sequence(
new BaseAction() { new BaseAction() {
@Override @Override
@ -1566,7 +1542,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
@Override @Override
public void applyBuilder(@NonNull Action source, @NonNull CaptureRequest.Builder builder) public void applyBuilder(@NonNull Action source, @NonNull CaptureRequest.Builder builder)
throws CameraAccessException { throws CameraAccessException {
if (getPreviewState() == STATE_STARTED) { if (getState() == CameraState.PREVIEW && !isChangingState()) {
mSession.capture(builder.build(), mRepeatingRequestCallback, null); mSession.capture(builder.build(), mRepeatingRequestCallback, null);
} }
} }

@ -8,24 +8,24 @@ import android.location.Location;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import com.google.android.gms.tasks.Continuation;
import com.google.android.gms.tasks.OnCompleteListener; import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.OnFailureListener; import com.google.android.gms.tasks.OnSuccessListener;
import com.google.android.gms.tasks.SuccessContinuation;
import com.google.android.gms.tasks.TaskCompletionSource;
import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
import com.otaliastudios.cameraview.CameraException; import com.otaliastudios.cameraview.CameraException;
import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.CameraOptions; import com.otaliastudios.cameraview.CameraOptions;
import com.otaliastudios.cameraview.PictureResult; import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.controls.PictureFormat; import com.otaliastudios.cameraview.controls.PictureFormat;
import com.otaliastudios.cameraview.engine.orchestrator.CameraOrchestrator;
import com.otaliastudios.cameraview.engine.orchestrator.CameraState;
import com.otaliastudios.cameraview.engine.orchestrator.CameraStateOrchestrator;
import com.otaliastudios.cameraview.overlay.Overlay; import com.otaliastudios.cameraview.overlay.Overlay;
import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.engine.offset.Angles; import com.otaliastudios.cameraview.engine.offset.Angles;
import com.otaliastudios.cameraview.engine.offset.Reference; import com.otaliastudios.cameraview.engine.offset.Reference;
import com.otaliastudios.cameraview.frame.Frame; import com.otaliastudios.cameraview.frame.Frame;
import com.otaliastudios.cameraview.frame.FrameManager; import com.otaliastudios.cameraview.frame.FrameManager;
import com.otaliastudios.cameraview.internal.utils.Op;
import com.otaliastudios.cameraview.internal.utils.WorkerHandler; import com.otaliastudios.cameraview.internal.utils.WorkerHandler;
import com.otaliastudios.cameraview.picture.PictureRecorder; import com.otaliastudios.cameraview.picture.PictureRecorder;
import com.otaliastudios.cameraview.preview.CameraPreview; import com.otaliastudios.cameraview.preview.CameraPreview;
@ -54,7 +54,6 @@ import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -73,13 +72,8 @@ import java.util.concurrent.TimeUnit;
* So at the end of both step 1 and 2, the engine should check if both have * So at the end of both step 1 and 2, the engine should check if both have
* been performed and trigger the steps 3 and 4. * been performed and trigger the steps 3 and 4.
* *
* We use an abstraction for each step called {@link Step} that manages the state of
* each step and ensures that start and stop operations, for each step, are never called if the
* previous one has not ended.
*
*
* STATE * STATE
* We only expose generic {@link #start()} and {@link #stop()} calls to the outside. * We only expose generic {@link #start()} and {@link #stop(boolean)} calls to the outside.
* The external users of this class are most likely interested in whether we have completed step 2 * The external users of this class are most likely interested in whether we have completed step 2
* or not, since that tells us if we can act on the camera or not, rather than knowing about * or not, since that tells us if we can act on the camera or not, rather than knowing about
* steps 3 and 4. * steps 3 and 4.
@ -87,18 +81,11 @@ import java.util.concurrent.TimeUnit;
* So in the {@link CameraEngine} notation, * So in the {@link CameraEngine} notation,
* - {@link #start()}: ASYNC - starts the engine (S2). When possible, at a later time, * - {@link #start()}: ASYNC - starts the engine (S2). When possible, at a later time,
* S3 and S4 are also performed. * S3 and S4 are also performed.
* - {@link #stop()}: ASYNC - stops everything: undoes S4, then S3, then S2. * - {@link #stop(boolean)}: ASYNC - stops everything: undoes S4, then S3, then S2.
* - {@link #restart()}: ASYNC - completes a stop then a start. * - {@link #restart()}: ASYNC - completes a stop then a start.
* - {@link #destroy()}: SYNC - performs a {@link #stop()} that will go on no matter the exceptions, * - {@link #destroy()}: SYNC - performs a {@link #stop(boolean)} that will go on no matter what,
* without throwing. Makes the engine unusable and clears resources. * without throwing. Makes the engine unusable and clears resources.
* *
* For example, we expose the engine (S2) state through {@link #getEngineState()}. It will be:
* - {@link #STATE_STARTING} if we're into step 2
* - {@link #STATE_STARTED} if we've completed step 2. No clue about 3 or 4.
* - {@link #STATE_STOPPING} if we're undoing steps 4, 3 and 2.
* - {@link #STATE_STOPPED} if we have undone steps 4, 3 and 2 (or they never started at all).
*
*
* THREADING * THREADING
* Subclasses should always execute code on the thread given by {@link #mHandler}. * Subclasses should always execute code on the thread given by {@link #mHandler}.
* For convenience, all the setup and tear down methods are called on this engine thread: * For convenience, all the setup and tear down methods are called on this engine thread:
@ -127,17 +114,16 @@ public abstract class CameraEngine implements
public interface Callback { public interface Callback {
@NonNull Context getContext(); @NonNull Context getContext();
void dispatchOnCameraOpened(CameraOptions options); void dispatchOnCameraOpened(@NonNull CameraOptions options);
void dispatchOnCameraClosed(); void dispatchOnCameraClosed();
void onCameraPreviewStreamSizeChanged(); void onCameraPreviewStreamSizeChanged();
void onShutter(boolean shouldPlaySound); void onShutter(boolean shouldPlaySound);
void dispatchOnVideoTaken(VideoResult.Stub stub); void dispatchOnVideoTaken(@NonNull VideoResult.Stub stub);
void dispatchOnPictureTaken(PictureResult.Stub stub); void dispatchOnPictureTaken(@NonNull PictureResult.Stub stub);
void dispatchOnFocusStart(@Nullable Gesture trigger, @NonNull PointF where); void dispatchOnFocusStart(@Nullable Gesture trigger, @NonNull PointF where);
void dispatchOnFocusEnd(@Nullable Gesture trigger, boolean success, @NonNull PointF where); void dispatchOnFocusEnd(@Nullable Gesture trigger, boolean success, @NonNull PointF where);
void dispatchOnZoomChanged(final float newValue, @Nullable final PointF[] fingers); void dispatchOnZoomChanged(final float newValue, @Nullable final PointF[] fingers);
void dispatchOnExposureCorrectionChanged(float newValue, void dispatchOnExposureCorrectionChanged(float newValue, @NonNull float[] bounds,
@NonNull float[] bounds,
@Nullable PointF[] fingers); @Nullable PointF[] fingers);
void dispatchFrame(@NonNull Frame frame); void dispatchFrame(@NonNull Frame frame);
void dispatchError(CameraException exception); void dispatchError(CameraException exception);
@ -148,15 +134,7 @@ public abstract class CameraEngine implements
private static final String TAG = CameraEngine.class.getSimpleName(); private static final String TAG = CameraEngine.class.getSimpleName();
private static final CameraLogger LOG = CameraLogger.create(TAG); private static final CameraLogger LOG = CameraLogger.create(TAG);
@SuppressWarnings({"WeakerAccess", "unused"})
public static final int STATE_STOPPING = Step.STATE_STOPPING;
public static final int STATE_STOPPED = Step.STATE_STOPPED;
@SuppressWarnings({"WeakerAccess", "unused"})
public static final int STATE_STARTING = Step.STATE_STARTING;
public static final int STATE_STARTED = Step.STATE_STARTED;
// Need to be protected // Need to be protected
@SuppressWarnings("WeakerAccess") protected WorkerHandler mHandler;
@SuppressWarnings("WeakerAccess") protected final Callback mCallback; @SuppressWarnings("WeakerAccess") protected final Callback mCallback;
@SuppressWarnings("WeakerAccess") protected CameraPreview mPreview; @SuppressWarnings("WeakerAccess") protected CameraPreview mPreview;
@SuppressWarnings("WeakerAccess") protected CameraOptions mCameraOptions; @SuppressWarnings("WeakerAccess") protected CameraOptions mCameraOptions;
@ -177,7 +155,7 @@ public abstract class CameraEngine implements
@SuppressWarnings("WeakerAccess") protected boolean mPictureSnapshotMetering; @SuppressWarnings("WeakerAccess") protected boolean mPictureSnapshotMetering;
@SuppressWarnings("WeakerAccess") protected float mPreviewFrameRate; @SuppressWarnings("WeakerAccess") protected float mPreviewFrameRate;
// Can be private private WorkerHandler mHandler;
@VisibleForTesting Handler mCrashHandler; @VisibleForTesting Handler mCrashHandler;
private final FrameManager mFrameManager; private final FrameManager mFrameManager;
private final Angles mAngles; private final Angles mAngles;
@ -193,36 +171,42 @@ public abstract class CameraEngine implements
private int mAudioBitRate; private int mAudioBitRate;
private boolean mHasFrameProcessors; private boolean mHasFrameProcessors;
private long mAutoFocusResetDelayMillis; private long mAutoFocusResetDelayMillis;
// in REF_VIEW, for consistency with SizeSelectors private int mSnapshotMaxWidth = Integer.MAX_VALUE; // in REF_VIEW like SizeSelectors
private int mSnapshotMaxWidth = Integer.MAX_VALUE; private int mSnapshotMaxHeight = Integer.MAX_VALUE; // in REF_VIEW like SizeSelectors
// in REF_VIEW, for consistency with SizeSelectors
private int mSnapshotMaxHeight = Integer.MAX_VALUE;
private Overlay overlay; private Overlay overlay;
// Steps @SuppressWarnings("WeakerAccess")
private final Step.Callback mStepCallback = new Step.Callback() { protected final CameraStateOrchestrator mOrchestrator
@Override @NonNull public Executor getExecutor() { return mHandler.getExecutor(); } = new CameraStateOrchestrator(new CameraOrchestrator.Callback() {
@Override public void handleException(@NonNull Exception exception) { @Override
CameraEngine.this.handleException(Thread.currentThread(), exception, false); @NonNull
public WorkerHandler getJobWorker(@NonNull String job) {
return mHandler;
}
@Override
public void handleJobException(@NonNull String job, @NonNull Exception exception) {
handleException(Thread.currentThread(), exception, false);
} }
}; });
@VisibleForTesting
Step mEngineStep = new Step("engine", mStepCallback);
private Step mBindStep = new Step("bind", mStepCallback);
private Step mPreviewStep = new Step("preview", mStepCallback);
private Step mAllStep = new Step("all", mStepCallback);
// Ops used for testing. // Ops used for testing.
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op<Void> mZoomOp = new Op<>(); @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mZoomTask
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op<Void> mExposureCorrectionOp = Tasks.forResult(null);
= new Op<>(); @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mExposureCorrectionTask
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op<Void> mFlashOp = new Op<>(); = Tasks.forResult(null);
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op<Void> mWhiteBalanceOp @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mFlashTask
= new Op<>(); = Tasks.forResult(null);
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op<Void> mHdrOp = new Op<>(); @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mWhiteBalanceTask
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op<Void> mLocationOp = new Op<>(); = Tasks.forResult(null);
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op<Void> mPlaySoundsOp = new Op<>(); @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mHdrTask
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op<Void> mPreviewFrameRateOp = new Op<>(); = Tasks.forResult(null);
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mLocationTask
= Tasks.forResult(null);
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mPlaySoundsTask
= Tasks.forResult(null);
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mPreviewFrameRateTask
= Tasks.forResult(null);
protected CameraEngine(@NonNull Callback callback) { protected CameraEngine(@NonNull Callback callback) {
mCallback = callback; mCallback = callback;
@ -252,7 +236,7 @@ public abstract class CameraEngine implements
*/ */
private class CrashExceptionHandler implements Thread.UncaughtExceptionHandler { private class CrashExceptionHandler implements Thread.UncaughtExceptionHandler {
@Override @Override
public void uncaughtException(final Thread thread, final Throwable throwable) { public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
handleException(thread, throwable, true); handleException(thread, throwable, true);
} }
} }
@ -263,7 +247,7 @@ public abstract class CameraEngine implements
*/ */
private static class NoOpExceptionHandler implements Thread.UncaughtExceptionHandler { private static class NoOpExceptionHandler implements Thread.UncaughtExceptionHandler {
@Override @Override
public void uncaughtException(Thread t, Throwable e) { public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
// No-op. // No-op.
} }
} }
@ -305,7 +289,7 @@ public abstract class CameraEngine implements
final CameraException cameraException = (CameraException) throwable; final CameraException cameraException = (CameraException) throwable;
LOG.e("uncaughtException:", "Got CameraException:", cameraException, LOG.e("uncaughtException:", "Got CameraException:", cameraException,
"on engine state:", getEngineState()); "on state:", getState());
if (fromExceptionHandler) { if (fromExceptionHandler) {
// Got to restart the handler. // Got to restart the handler.
thread.interrupt(); thread.interrupt();
@ -322,49 +306,95 @@ public abstract class CameraEngine implements
//endregion //endregion
//region states and steps //region State management
public final int getEngineState() { @NonNull
return mEngineStep.getState(); public final CameraState getState() {
return mOrchestrator.getCurrentState();
} }
@SuppressWarnings("WeakerAccess") @NonNull
public final int getBindState() { public final CameraState getTargetState() {
return mBindStep.getState(); return mOrchestrator.getTargetState();
} }
@SuppressWarnings({"unused", "WeakerAccess"}) public boolean isChangingState() {
public final int getPreviewState() { return mOrchestrator.hasPendingStateChange();
return mPreviewStep.getState();
} }
private boolean canStartEngine() { /**
return mEngineStep.isStoppingOrStopped(); * Calls {@link #stop(boolean)} and waits for it.
* Not final due to mockito requirements.
*
* NOTE: Should not be called on the orchestrator thread! This would cause deadlocks due to us
* awaiting for {@link #stop(boolean)} to return.
*/
public void destroy() {
LOG.i("DESTROY:", "state:", getState(), "thread:", Thread.currentThread());
// Prevent CameraEngine leaks. Don't set to null, or exceptions
// inside the standard stop() method might crash the main thread.
mHandler.getThread().setUncaughtExceptionHandler(new NoOpExceptionHandler());
// Stop if needed, synchronously and silently.
// Cannot use Tasks.await() because we might be on the UI thread.
final CountDownLatch latch = new CountDownLatch(1);
stop(true).addOnCompleteListener(
WorkerHandler.get().getExecutor(),
new OnCompleteListener<Void>() {
@Override
public void onComplete(@NonNull Task<Void> task) {
latch.countDown();
}
});
try {
boolean success = latch.await(3, TimeUnit.SECONDS);
if (!success) {
LOG.e("Probably some deadlock in destroy.",
"Current thread:", Thread.currentThread(),
"Handler thread: ", mHandler.getThread());
}
} catch (InterruptedException ignore) {}
} }
private boolean needsStopEngine() { @SuppressWarnings("WeakerAccess")
return mEngineStep.isStartedOrStarting(); public void restart() {
LOG.i("RESTART:", "scheduled. State:", getState());
stop(false);
start();
} }
private boolean canStartBind() { @NonNull
return mEngineStep.isStarted() public Task<Void> start() {
&& mPreview != null LOG.i("START:", "scheduled. State:", getState());
&& mPreview.hasSurface() Task<Void> engine = startEngine();
&& mBindStep.isStoppingOrStopped(); startBind();
startPreview();
return engine;
} }
private boolean needsStopBind() { @NonNull
return mBindStep.isStartedOrStarting(); public Task<Void> stop(final boolean swallowExceptions) {
LOG.i("STOP:", "scheduled. State:", getState());
stopPreview(swallowExceptions);
stopBind(swallowExceptions);
return stopEngine(swallowExceptions);
} }
private boolean canStartPreview() { @SuppressWarnings("WeakerAccess")
return mEngineStep.isStarted() @NonNull
&& mBindStep.isStarted() protected Task<Void> restartBind() {
&& mPreviewStep.isStoppingOrStopped(); LOG.i("RESTART BIND:", "scheduled. State:", getState());
stopPreview(false);
stopBind(false);
startBind();
return startPreview();
} }
private boolean needsStopPreview() { @SuppressWarnings("WeakerAccess")
return mPreviewStep.isStartedOrStarting(); @NonNull
protected Task<Void> restartPreview() {
LOG.i("RESTART PREVIEW:", "scheduled. State:", getState());
stopPreview(false);
return startPreview();
} }
//endregion //endregion
@ -374,8 +404,9 @@ public abstract class CameraEngine implements
@NonNull @NonNull
@EngineThread @EngineThread
private Task<Void> startEngine() { private Task<Void> startEngine() {
if (canStartEngine()) { return mOrchestrator.scheduleStateChange(CameraState.OFF, CameraState.ENGINE,
mEngineStep.doStart(false, new Callable<Task<Void>>() { true,
new Callable<Task<Void>>() {
@Override @Override
public Task<Void> call() { public Task<Void> call() {
if (!collectCameraInfo(mFacing)) { if (!collectCameraInfo(mFacing)) {
@ -384,34 +415,31 @@ public abstract class CameraEngine implements
} }
return onStartEngine(); return onStartEngine();
} }
}, new Runnable() { }).addOnSuccessListener(new OnSuccessListener<Void>() {
@Override @Override
public void run() { public void onSuccess(Void aVoid) {
mCallback.dispatchOnCameraOpened(mCameraOptions); mCallback.dispatchOnCameraOpened(mCameraOptions);
} }
}); });
} }
return mEngineStep.getTask();
}
@NonNull @NonNull
@EngineThread @EngineThread
private Task<Void> stopEngine(boolean swallowExceptions) { private Task<Void> stopEngine(boolean swallowExceptions) {
if (needsStopEngine()) { return mOrchestrator.scheduleStateChange(CameraState.ENGINE, CameraState.OFF,
mEngineStep.doStop(swallowExceptions, new Callable<Task<Void>>() { !swallowExceptions,
new Callable<Task<Void>>() {
@Override @Override
public Task<Void> call() { public Task<Void> call() {
return onStopEngine(); return onStopEngine();
} }
}, new Runnable() { }).addOnSuccessListener(new OnSuccessListener<Void>() {
@Override @Override
public void run() { public void onSuccess(Void aVoid) {
mCallback.dispatchOnCameraClosed(); mCallback.dispatchOnCameraClosed();
} }
}); });
} }
return mEngineStep.getTask();
}
/** /**
* Starts the engine. * Starts the engine.
@ -438,30 +466,33 @@ public abstract class CameraEngine implements
@NonNull @NonNull
@EngineThread @EngineThread
private Task<Void> startBind() { private Task<Void> startBind() {
if (canStartBind()) { return mOrchestrator.scheduleStateChange(CameraState.ENGINE, CameraState.BIND,
mBindStep.doStart(false, new Callable<Task<Void>>() { true,
new Callable<Task<Void>>() {
@Override @Override
public Task<Void> call() { public Task<Void> call() {
if (mPreview != null && mPreview.hasSurface()) {
return onStartBind(); return onStartBind();
} else {
return Tasks.forCanceled();
} }
});
} }
return mBindStep.getTask(); });
} }
@SuppressWarnings("UnusedReturnValue")
@NonNull @NonNull
@EngineThread @EngineThread
private Task<Void> stopBind(boolean swallowExceptions) { private Task<Void> stopBind(boolean swallowExceptions) {
if (needsStopBind()) { return mOrchestrator.scheduleStateChange(CameraState.BIND, CameraState.ENGINE,
mBindStep.doStop(swallowExceptions, new Callable<Task<Void>>() { !swallowExceptions,
new Callable<Task<Void>>() {
@Override @Override
public Task<Void> call() { public Task<Void> call() {
return onStopBind(); return onStopBind();
} }
}); });
} }
return mBindStep.getTask();
}
/** /**
* Starts the binding process. * Starts the binding process.
@ -481,39 +512,6 @@ public abstract class CameraEngine implements
@EngineThread @EngineThread
protected abstract Task<Void> onStopBind(); protected abstract Task<Void> onStopBind();
@SuppressWarnings("WeakerAccess")
protected void restartBind() {
LOG.i("restartBind", "posting.");
mHandler.run(new Runnable() {
@Override
public void run() {
LOG.w("restartBind", "executing stopPreview.");
stopPreview(false).continueWithTask(mHandler.getExecutor(),
new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(@NonNull Task<Void> task) {
LOG.w("restartBind", "executing stopBind.");
return stopBind(false);
}
}).onSuccessTask(mHandler.getExecutor(), new SuccessContinuation<Void, Void>() {
@NonNull
@Override
public Task<Void> then(@Nullable Void aVoid) {
LOG.w("restartBind", "executing startBind.");
return startBind();
}
}).onSuccessTask(mHandler.getExecutor(), new SuccessContinuation<Void, Void>() {
@NonNull
@Override
public Task<Void> then(@Nullable Void aVoid) {
LOG.w("restartBind", "executing startPreview.");
return startPreview();
}
});
}
});
}
//endregion //endregion
//region Start & Stop preview //region Start & Stop preview
@ -521,47 +519,29 @@ public abstract class CameraEngine implements
@NonNull @NonNull
@EngineThread @EngineThread
private Task<Void> startPreview() { private Task<Void> startPreview() {
LOG.i("startPreview", "canStartPreview:", canStartPreview()); return mOrchestrator.scheduleStateChange(CameraState.BIND, CameraState.PREVIEW,
if (canStartPreview()) { true,
mPreviewStep.doStart(false, new Callable<Task<Void>>() { new Callable<Task<Void>>() {
@Override @Override
public Task<Void> call() { public Task<Void> call() {
return onStartPreview(); return onStartPreview();
} }
}); });
} }
return mPreviewStep.getTask();
}
@SuppressWarnings("UnusedReturnValue")
@NonNull @NonNull
@EngineThread @EngineThread
private Task<Void> stopPreview(boolean swallowExceptions) { private Task<Void> stopPreview(boolean swallowExceptions) {
LOG.i("stopPreview", return mOrchestrator.scheduleStateChange(CameraState.PREVIEW, CameraState.BIND,
"needsStopPreview:", needsStopPreview(), !swallowExceptions,
"swallowExceptions:", swallowExceptions); new Callable<Task<Void>>() {
if (needsStopPreview()) {
mPreviewStep.doStop(swallowExceptions, new Callable<Task<Void>>() {
@Override @Override
public Task<Void> call() { public Task<Void> call() {
return onStopPreview(); return onStopPreview();
} }
}); });
} }
return mPreviewStep.getTask();
}
@SuppressWarnings("WeakerAccess")
protected void restartPreview() {
LOG.i("restartPreview", "posting.");
mHandler.run(new Runnable() {
@Override
public void run() {
LOG.i("restartPreview", "executing.");
stopPreview(false);
startPreview();
}
});
}
/** /**
* Starts the preview streaming. * Starts the preview streaming.
@ -592,33 +572,24 @@ public abstract class CameraEngine implements
@Override @Override
public final void onSurfaceAvailable() { public final void onSurfaceAvailable() {
LOG.i("onSurfaceAvailable:", "Size is", getPreviewSurfaceSize(Reference.VIEW)); LOG.i("onSurfaceAvailable:", "Size is", getPreviewSurfaceSize(Reference.VIEW));
mHandler.run(new Runnable() { startBind();
@Override startPreview();
public void run() {
startBind().onSuccessTask(mHandler.getExecutor(), new SuccessContinuation<Void, Void>() {
@NonNull
@Override
public Task<Void> then(@Nullable Void aVoid) {
return startPreview();
}
});
} }
});
@Override
public final void onSurfaceDestroyed() {
LOG.i("onSurfaceDestroyed");
stopPreview(false);
stopBind(false);
} }
@Override @Override
public final void onSurfaceChanged() { public final void onSurfaceChanged() {
LOG.i("onSurfaceChanged:", "Size is", getPreviewSurfaceSize(Reference.VIEW), LOG.i("onSurfaceChanged:", "Size is", getPreviewSurfaceSize(Reference.VIEW));
"Posting."); mOrchestrator.scheduleStateful("surface changed", CameraState.BIND,
mHandler.run(new Runnable() { new Runnable() {
@Override @Override
public void run() { public void run() {
LOG.i("onSurfaceChanged:",
"Engine started?", mEngineStep.isStarted(),
"Bind started?", mBindStep.isStarted());
if (!mEngineStep.isStarted()) return; // Too early
if (!mBindStep.isStarted()) return; // Too early
// Compute a new camera preview size and apply. // Compute a new camera preview size and apply.
Size newSize = computePreviewStreamSize(); Size newSize = computePreviewStreamSize();
if (newSize.equals(mPreviewStreamSize)) { if (newSize.equals(mPreviewStreamSize)) {
@ -643,176 +614,6 @@ public abstract class CameraEngine implements
@EngineThread @EngineThread
protected abstract void onPreviewStreamSizeChanged(); protected abstract void onPreviewStreamSizeChanged();
@Override
public final void onSurfaceDestroyed() {
LOG.i("onSurfaceDestroyed");
mHandler.run(new Runnable() {
@Override
public void run() {
stopPreview(false).onSuccessTask(mHandler.getExecutor(),
new SuccessContinuation<Void, Void>() {
@NonNull
@Override
public Task<Void> then(@Nullable Void aVoid) {
return stopBind(false);
}
});
}
});
}
//endregion
//region Start & Stop all
/**
* Not final due to mockito requirements, but this is basically
* it, nothing more to do.
*
* NOTE: Should not be called on the {@link #mHandler} thread! I think
* that would cause deadlocks due to us awaiting for {@link #stop()} to return.
*/
public void destroy() {
LOG.i("destroy:", "state:", getEngineState(), "thread:", Thread.currentThread());
// Prevent CameraEngine leaks. Don't set to null, or exceptions
// inside the standard stop() method might crash the main thread.
mHandler.getThread().setUncaughtExceptionHandler(new NoOpExceptionHandler());
// Stop if needed, synchronously and silently.
// Cannot use Tasks.await() because we might be on the UI thread.
final CountDownLatch latch = new CountDownLatch(1);
stop(true).addOnCompleteListener(mHandler.getExecutor(),
new OnCompleteListener<Void>() {
@Override
public void onComplete(@NonNull Task<Void> task) {
latch.countDown();
}
});
try {
boolean success = latch.await(3, TimeUnit.SECONDS);
if (!success) {
// TODO seems like this is always the case?
LOG.e("Probably some deadlock in destroy.",
"Current thread:", Thread.currentThread(),
"Handler thread: ", mHandler.getThread());
}
} catch (InterruptedException ignore) {}
}
@SuppressWarnings("WeakerAccess")
protected final void restart() {
LOG.i("Restart:", "calling stop and start");
stop();
start();
}
@NonNull
public Task<Void> start() {
LOG.i("Start:", "posting runnable. State:", getEngineState());
final TaskCompletionSource<Void> outTask = new TaskCompletionSource<>();
mHandler.run(new Runnable() {
@Override
public void run() {
LOG.w("Start:", "executing runnable. AllState is", mAllStep.getState());
// It's better to schedule anyway. allStep might be STARTING and we might be
// tempted to early return here, but the truth is that there might be a stop
// already scheduled when the STARTING op ends.
// if (mAllStep.isStoppingOrStopped()) {
// LOG.i("Start:", "executing runnable. AllState is STOPPING or STOPPED,
// so we schedule a start.");
mAllStep.doStart(false, new Callable<Task<Void>>() {
@Override
public Task<Void> call() {
return startEngine().addOnFailureListener(mHandler.getExecutor(),
new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
outTask.trySetException(e);
}
}).onSuccessTask(mHandler.getExecutor(), new SuccessContinuation<Void, Void>() {
@NonNull
@Override
public Task<Void> then(@Nullable Void aVoid) {
outTask.trySetResult(null);
return startBind();
}
}).onSuccessTask(mHandler.getExecutor(), new SuccessContinuation<Void, Void>() {
@NonNull
@Override
public Task<Void> then(@Nullable Void aVoid) {
return startPreview();
}
});
}
});
// } else {
// // NOTE: this returns early if we were STARTING.
// LOG.i("Start:",
// "executing runnable. AllState is STARTING or STARTED, so we return early.");
// outTask.trySetResult(null);
// }
}
});
return outTask.getTask();
}
@NonNull
public Task<Void> stop() {
return stop(false);
}
@NonNull
private Task<Void> stop(final boolean swallowExceptions) {
LOG.i("Stop:", "posting runnable. State:", getEngineState());
final TaskCompletionSource<Void> outTask = new TaskCompletionSource<>();
mHandler.run(new Runnable() {
@Override
public void run() {
LOG.w("Stop:", "executing runnable. AllState is", mAllStep.getState());
// It's better to schedule anyway. allStep might be STOPPING and we might be
// tempted to early return here, but the truth is that there might be a start
// already scheduled when the STOPPING op ends.
// if (mAllStep.isStartedOrStarting()) {
// LOG.i("Stop:", "executing runnable. AllState is STARTING or STARTED,
// so we schedule a stop.");
mAllStep.doStop(swallowExceptions, new Callable<Task<Void>>() {
@Override
public Task<Void> call() {
return stopPreview(swallowExceptions).continueWithTask(
mHandler.getExecutor(), new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(@NonNull Task<Void> task) {
return stopBind(swallowExceptions);
}
}).continueWithTask(mHandler.getExecutor(), new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(@NonNull Task<Void> task) {
return stopEngine(swallowExceptions);
}
}).continueWithTask(mHandler.getExecutor(), new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(@NonNull Task<Void> task) {
if (task.isSuccessful()) {
outTask.trySetResult(null);
} else {
//noinspection ConstantConditions
outTask.trySetException(task.getException());
}
return task;
}
});
}
});
// } else {
// // NOTE: this returns early if we were STOPPING.
// LOG.i("Stop:", "executing runnable.
// AllState is STOPPING or STOPPED, so we return early.");
// outTask.trySetResult(null);
// }
}
});
return outTask.getTask();
}
//endregion //endregion
//region Final setters and getters //region Final setters and getters
@ -922,7 +723,7 @@ public abstract class CameraEngine implements
} }
/** /**
* Sets a new facing value. This will restart the session (if there's any) * Sets a new facing value. This will restart the engine session (if there's any)
* so that we can open the new facing camera. * so that we can open the new facing camera.
* @param facing facing * @param facing facing
*/ */
@ -930,10 +731,10 @@ public abstract class CameraEngine implements
final Facing old = mFacing; final Facing old = mFacing;
if (facing != old) { if (facing != old) {
mFacing = facing; mFacing = facing;
mHandler.run(new Runnable() { mOrchestrator.scheduleStateful("facing", CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() < STATE_STARTED) return;
if (collectCameraInfo(facing)) { if (collectCameraInfo(facing)) {
restart(); restart();
} else { } else {
@ -975,13 +776,12 @@ public abstract class CameraEngine implements
public final void setMode(@NonNull Mode mode) { public final void setMode(@NonNull Mode mode) {
if (mode != mMode) { if (mode != mMode) {
mMode = mode; mMode = mode;
mHandler.run(new Runnable() { mOrchestrator.scheduleStateful("mode", CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) {
restart(); restart();
} }
}
}); });
} }
} }
@ -1134,22 +934,22 @@ public abstract class CameraEngine implements
/* not final for tests */ /* not final for tests */
public void takePicture(final @NonNull PictureResult.Stub stub) { public void takePicture(final @NonNull PictureResult.Stub stub) {
LOG.i("takePicture", "scheduling"); // Save boolean before scheduling! See how Camera2Engine calls this with a temp value.
mHandler.run(new Runnable() { final boolean metering = mPictureMetering;
mOrchestrator.scheduleStateful("take picture", CameraState.BIND,
new Runnable() {
@Override @Override
public void run() { public void run() {
LOG.i("takePicture", "performing. BindState:", getBindState(), LOG.i("takePicture:", "running. isTakingPicture:", isTakingPicture());
"isTakingPicture:", isTakingPicture()); if (isTakingPicture()) return;
if (mMode == Mode.VIDEO) { if (mMode == Mode.VIDEO) {
throw new IllegalStateException("Can't take hq pictures while in VIDEO mode"); throw new IllegalStateException("Can't take hq pictures while in VIDEO mode");
} }
if (getBindState() < STATE_STARTED) return;
if (isTakingPicture()) return;
stub.isSnapshot = false; stub.isSnapshot = false;
stub.location = mLocation; stub.location = mLocation;
stub.facing = mFacing; stub.facing = mFacing;
stub.format = mPictureFormat; stub.format = mPictureFormat;
onTakePicture(stub, mPictureMetering); onTakePicture(stub, metering);
} }
}); });
} }
@ -1160,13 +960,13 @@ public abstract class CameraEngine implements
* @param stub a picture stub * @param stub a picture stub
*/ */
public final void takePictureSnapshot(final @NonNull PictureResult.Stub stub) { public final void takePictureSnapshot(final @NonNull PictureResult.Stub stub) {
LOG.i("takePictureSnapshot", "scheduling"); // Save boolean before scheduling! See how Camera2Engine calls this with a temp value.
mHandler.run(new Runnable() { final boolean metering = mPictureSnapshotMetering;
mOrchestrator.scheduleStateful("take picture snapshot", CameraState.BIND,
new Runnable() {
@Override @Override
public void run() { public void run() {
LOG.i("takePictureSnapshot", "performing. BindState:", LOG.i("takePictureSnapshot:", "running. isTakingPicture:", isTakingPicture());
getBindState(), "isTakingPicture:", isTakingPicture());
if (getBindState() < STATE_STARTED) return;
if (isTakingPicture()) return; if (isTakingPicture()) return;
stub.location = mLocation; stub.location = mLocation;
stub.isSnapshot = true; stub.isSnapshot = true;
@ -1175,7 +975,7 @@ public abstract class CameraEngine implements
// Leave the other parameters to subclasses. // Leave the other parameters to subclasses.
//noinspection ConstantConditions //noinspection ConstantConditions
AspectRatio ratio = AspectRatio.of(getPreviewSurfaceSize(Reference.OUTPUT)); AspectRatio ratio = AspectRatio.of(getPreviewSurfaceSize(Reference.OUTPUT));
onTakePictureSnapshot(stub, ratio, mPictureSnapshotMetering); onTakePictureSnapshot(stub, ratio, metering);
} }
}); });
} }
@ -1202,13 +1002,10 @@ public abstract class CameraEngine implements
} }
public final void takeVideo(final @NonNull VideoResult.Stub stub, final @NonNull File file) { public final void takeVideo(final @NonNull VideoResult.Stub stub, final @NonNull File file) {
LOG.i("takeVideo", "scheduling"); mOrchestrator.scheduleStateful("take video", CameraState.BIND, new Runnable() {
mHandler.run(new Runnable() {
@Override @Override
public void run() { public void run() {
LOG.i("takeVideo", "performing. BindState:", getBindState(), LOG.i("takeVideo:", "running. isTakingVideo:", isTakingVideo());
"isTakingVideo:", isTakingVideo());
if (getBindState() < STATE_STARTED) return;
if (isTakingVideo()) return; if (isTakingVideo()) return;
if (mMode == Mode.PICTURE) { if (mMode == Mode.PICTURE) {
throw new IllegalStateException("Can't record video while in PICTURE mode"); throw new IllegalStateException("Can't record video while in PICTURE mode");
@ -1234,14 +1031,11 @@ public abstract class CameraEngine implements
*/ */
public final void takeVideoSnapshot(@NonNull final VideoResult.Stub stub, public final void takeVideoSnapshot(@NonNull final VideoResult.Stub stub,
@NonNull final File file) { @NonNull final File file) {
LOG.i("takeVideoSnapshot", "scheduling"); mOrchestrator.scheduleStateful("take video snapshot", CameraState.BIND,
mHandler.run(new Runnable() { new Runnable() {
@Override @Override
public void run() { public void run() {
LOG.i("takeVideoSnapshot", "performing. BindState:", getBindState(), LOG.i("takeVideoSnapshot:", "running. isTakingVideo:", isTakingVideo());
"isTakingVideo:", isTakingVideo());
if (getBindState() < STATE_STARTED) return;
if (isTakingVideo()) return;
stub.file = file; stub.file = file;
stub.isSnapshot = true; stub.isSnapshot = true;
stub.videoCodec = mVideoCodec; stub.videoCodec = mVideoCodec;
@ -1260,11 +1054,10 @@ public abstract class CameraEngine implements
} }
public final void stopVideo() { public final void stopVideo() {
LOG.i("stopVideo", "posting"); mOrchestrator.schedule("stop video", true, new Runnable() {
mHandler.run(new Runnable() {
@Override @Override
public void run() { public void run() {
LOG.i("stopVideo", "executing.", "isTakingVideo?", isTakingVideo()); LOG.i("stopVideo", "running. isTakingVideo?", isTakingVideo());
onStopVideo(); onStopVideo();
} }
}); });

@ -1,178 +0,0 @@
package com.otaliastudios.cameraview.engine;
import com.google.android.gms.tasks.Continuation;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.SuccessContinuation;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
import com.otaliastudios.cameraview.CameraLogger;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
/**
* Represents one of the steps in the {@link CameraEngine} setup: for example, the engine step,
* the bind-to-surface step, and the preview step.
*
* A step is something that can be setup (started) or torn down (stopped), and
* steps can of course depend onto each other.
*
* The purpose of this class is to manage the step state (stopping, stopped, starting or started)
* and, more importantly, to perform START and STOP operations in such a way that they do not
* overlap. For example, if we're stopping, we're wait for stop to finish before starting again.
*
* This is an important condition for simplifying the engine code.
* Since Camera1, the only requirement was basically to use a single thread.
* Since Camera2, which has an asynchronous API, further care must be used.
*
* For this reason, we use Google's {@link Task} abstraction and only start new operations
* once the previous one has ended.
*
* <strong>This class is NOT thread safe!</string>
*/
class Step {
private static final String TAG = Step.class.getSimpleName();
private static final CameraLogger LOG = CameraLogger.create(TAG);
interface Callback {
@NonNull
Executor getExecutor();
void handleException(@NonNull Exception exception);
}
static final int STATE_STOPPING = -1;
static final int STATE_STOPPED = 0;
static final int STATE_STARTING = 1;
static final int STATE_STARTED = 2;
private int state = STATE_STOPPED;
// To avoid dirty scenarios (e.g. calling stopXXX while XXX is starting),
// and since every operation can be asynchronous, we use some tasks for each step.
private Task<Void> task = Tasks.forResult(null);
private final String name;
private final Callback callback;
Step(@NonNull String name, @NonNull Callback callback) {
this.name = name.toUpperCase();
this.callback = callback;
}
int getState() {
return state;
}
@VisibleForTesting void setState(int newState) {
state = newState;
}
@NonNull
String getStateName() {
switch (state) {
case STATE_STOPPING: return name + "_STATE_STOPPING";
case STATE_STOPPED: return name + "_STATE_STOPPED";
case STATE_STARTING: return name + "_STATE_STARTING";
case STATE_STARTED: return name + "_STATE_STARTED";
}
return "null";
}
boolean isStoppingOrStopped() {
return state == STATE_STOPPING || state == STATE_STOPPED;
}
boolean isStartedOrStarting() {
return state == STATE_STARTING || state == STATE_STARTED;
}
boolean isStarted() {
return state == STATE_STARTED;
}
@NonNull
Task<Void> getTask() {
return task;
}
@SuppressWarnings({"SameParameterValue", "UnusedReturnValue"})
Task<Void> doStart(final boolean swallowExceptions, final @NonNull Callable<Task<Void>> op) {
return doStart(swallowExceptions, op, null);
}
Task<Void> doStart(final boolean swallowExceptions,
final @NonNull Callable<Task<Void>> op,
final @Nullable Runnable onStarted) {
LOG.i(name, "doStart", "Called. Enqueuing.");
task = task.continueWithTask(callback.getExecutor(), new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(@NonNull Task<Void> task) throws Exception {
LOG.i(name, "doStart", "About to start. Setting state to STARTING");
setState(STATE_STARTING);
return op.call().addOnFailureListener(callback.getExecutor(),
new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
LOG.w(name, "doStart", "Failed with error", e,
"Setting state to STOPPED");
setState(STATE_STOPPED);
if (!swallowExceptions) callback.handleException(e);
}
});
}
}).onSuccessTask(callback.getExecutor(), new SuccessContinuation<Void, Void>() {
@NonNull
@Override
public Task<Void> then(@Nullable Void aVoid) {
LOG.i(name, "doStart", "Succeeded! Setting state to STARTED");
setState(STATE_STARTED);
if (onStarted != null) onStarted.run();
return Tasks.forResult(null);
}
});
return task;
}
@SuppressWarnings("UnusedReturnValue")
Task<Void> doStop(final boolean swallowExceptions, final @NonNull Callable<Task<Void>> op) {
return doStop(swallowExceptions, op, null);
}
Task<Void> doStop(final boolean swallowExceptions,
final @NonNull Callable<Task<Void>> op,
final @Nullable Runnable onStopped) {
LOG.i(name, "doStop", "Called. Enqueuing.");
task = task.continueWithTask(callback.getExecutor(), new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(@NonNull Task<Void> task) throws Exception {
LOG.i(name, "doStop", "About to stop. Setting state to STOPPING");
state = STATE_STOPPING;
return op.call().addOnFailureListener(callback.getExecutor(),
new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
LOG.w(name, "doStop", "Failed with error", e,
"Setting state to STOPPED");
state = STATE_STOPPED;
if (!swallowExceptions) callback.handleException(e);
}
});
}
}).onSuccessTask(callback.getExecutor(), new SuccessContinuation<Void, Void>() {
@NonNull
@Override
public Task<Void> then(@Nullable Void aVoid) {
LOG.i(name, "doStop", "Succeeded! Setting state to STOPPED");
state = STATE_STOPPED;
if (onStopped != null) onStopped.run();
return Tasks.forResult(null);
}
});
return task;
}
}

@ -1,4 +1,4 @@
package com.otaliastudios.cameraview.engine; package com.otaliastudios.cameraview.engine.action;
import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult; import android.hardware.camera2.CaptureResult;
@ -9,11 +9,12 @@ import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.engine.Camera2Engine;
import com.otaliastudios.cameraview.engine.action.ActionHolder; import com.otaliastudios.cameraview.engine.action.ActionHolder;
import com.otaliastudios.cameraview.engine.action.BaseAction; import com.otaliastudios.cameraview.engine.action.BaseAction;
@RequiresApi(Build.VERSION_CODES.LOLLIPOP) @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
class LogAction extends BaseAction { public class LogAction extends BaseAction {
private final static CameraLogger LOG private final static CameraLogger LOG
= CameraLogger.create(Camera2Engine.class.getSimpleName()); = CameraLogger.create(Camera2Engine.class.getSimpleName());

@ -0,0 +1,180 @@
package com.otaliastudios.cameraview.engine.orchestrator;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.TaskCompletionSource;
import com.google.android.gms.tasks.Tasks;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.internal.utils.WorkerHandler;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
/**
* Schedules {@link com.otaliastudios.cameraview.engine.CameraEngine} actions,
* so that they always run on the same thread.
*
* We need to be extra careful (not as easy as posting on a Handler) because the engine
* has different states, and some actions will modify the engine state - turn it on or
* tear it down. Other actions might need a specific state to be executed.
* And most importantly, some actions will finish asynchronously, so subsequent actions
* should wait for the previous to finish, but without blocking the thread.
*/
@SuppressWarnings("WeakerAccess")
public class CameraOrchestrator {
protected static final String TAG = CameraOrchestrator.class.getSimpleName();
protected static final CameraLogger LOG = CameraLogger.create(TAG);
public interface Callback {
@NonNull
WorkerHandler getJobWorker(@NonNull String job);
void handleJobException(@NonNull String job, @NonNull Exception exception);
}
protected static class Token {
public final String name;
public final Task<Void> task;
private Token(@NonNull String name, @NonNull Task<Void> task) {
this.name = name;
this.task = task;
}
@Override
public boolean equals(@Nullable Object obj) {
return obj instanceof Token && ((Token) obj).name.equals(name);
}
}
protected final Callback mCallback;
protected final ArrayDeque<Token> mJobs = new ArrayDeque<>();
protected final Object mLock = new Object();
private final Map<String, Runnable> mDelayedJobs = new HashMap<>();
public CameraOrchestrator(@NonNull Callback callback) {
mCallback = callback;
ensureToken();
}
@NonNull
public Task<Void> schedule(@NonNull String name,
boolean dispatchExceptions,
@NonNull final Runnable job) {
return schedule(name, dispatchExceptions, new Callable<Task<Void>>() {
@Override
public Task<Void> call() {
job.run();
return Tasks.forResult(null);
}
});
}
@NonNull
public Task<Void> schedule(@NonNull final String name,
final boolean dispatchExceptions,
@NonNull final Callable<Task<Void>> job) {
LOG.i(name.toUpperCase(), "- Scheduling.");
final TaskCompletionSource<Void> source = new TaskCompletionSource<>();
final WorkerHandler handler = mCallback.getJobWorker(name);
synchronized (mLock) {
applyCompletionListener(mJobs.getLast().task, handler,
new OnCompleteListener<Void>() {
@Override
public void onComplete(@NonNull Task<Void> task) {
synchronized (mLock) {
mJobs.removeFirst();
ensureToken();
}
try {
LOG.i(name.toUpperCase(), "- Executing.");
Task<Void> inner = job.call();
applyCompletionListener(inner, handler, new OnCompleteListener<Void>() {
@Override
public void onComplete(@NonNull Task<Void> task) {
Exception e = task.getException();
LOG.i(name.toUpperCase(), "- Finished.", e);
if (e != null) {
if (dispatchExceptions) {
mCallback.handleJobException(name, e);
}
source.trySetException(e);
} else {
source.trySetResult(null);
}
}
});
} catch (Exception e) {
LOG.i(name.toUpperCase(), "- Finished.", e);
if (dispatchExceptions) mCallback.handleJobException(name, e);
source.trySetException(e);
}
}
});
mJobs.addLast(new Token(name, source.getTask()));
}
return source.getTask();
}
public void scheduleDelayed(@NonNull final String name,
long minDelay,
@NonNull final Runnable runnable) {
Runnable wrapper = new Runnable() {
@Override
public void run() {
schedule(name, true, runnable);
synchronized (mLock) {
if (mDelayedJobs.containsValue(this)) {
mDelayedJobs.remove(name);
}
}
}
};
synchronized (mLock) {
mDelayedJobs.put(name, wrapper);
mCallback.getJobWorker(name).post(minDelay, wrapper);
}
}
public void remove(@NonNull String name) {
synchronized (mLock) {
if (mDelayedJobs.get(name) != null) {
//noinspection ConstantConditions
mCallback.getJobWorker(name).remove(mDelayedJobs.get(name));
mDelayedJobs.remove(name);
}
Token token = new Token(name, Tasks.<Void>forResult(null));
//noinspection StatementWithEmptyBody
while (mJobs.remove(token)) { /* do nothing */ }
ensureToken();
}
}
private void ensureToken() {
synchronized (mLock) {
if (mJobs.isEmpty()) {
mJobs.add(new Token("BASE", Tasks.<Void>forResult(null)));
}
}
}
private static void applyCompletionListener(@NonNull final Task<Void> task,
@NonNull WorkerHandler handler,
@NonNull final OnCompleteListener<Void> listener) {
if (task.isComplete()) {
handler.run(new Runnable() {
@Override
public void run() {
listener.onComplete(task);
}
});
} else {
task.addOnCompleteListener(handler.getExecutor(), listener);
}
}
}

@ -0,0 +1,17 @@
package com.otaliastudios.cameraview.engine.orchestrator;
import androidx.annotation.NonNull;
public enum CameraState {
OFF(0), ENGINE(1), BIND(2), PREVIEW(3);
private int mState;
CameraState(int state) {
mState = state;
}
public boolean isAtLeast(@NonNull CameraState reference) {
return mState >= reference.mState;
}
}

@ -0,0 +1,117 @@
package com.otaliastudios.cameraview.engine.orchestrator;
import androidx.annotation.NonNull;
import com.google.android.gms.tasks.Continuation;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
/**
* A special {@link CameraOrchestrator} with special methods that deal with the
* {@link CameraState}.
*/
public class CameraStateOrchestrator extends CameraOrchestrator {
private CameraState mCurrentState = CameraState.OFF;
private CameraState mTargetState = CameraState.OFF;
private int mStateChangeCount = 0;
public CameraStateOrchestrator(@NonNull Callback callback) {
super(callback);
}
@NonNull
public CameraState getCurrentState() {
return mCurrentState;
}
@NonNull
public CameraState getTargetState() {
return mTargetState;
}
public boolean hasPendingStateChange() {
synchronized (mLock) {
for (Token token : mJobs) {
if (token.name.contains(" > ") && !token.task.isComplete()) {
return true;
}
}
return false;
}
}
@NonNull
public Task<Void> scheduleStateChange(@NonNull final CameraState fromState,
@NonNull final CameraState toState,
boolean dispatchExceptions,
@NonNull final Callable<Task<Void>> stateChange) {
final int changeCount = ++mStateChangeCount;
mTargetState = toState;
final boolean isTearDown = !toState.isAtLeast(fromState);
final String changeName = fromState.name() + " > " + toState.name();
return schedule(changeName, dispatchExceptions, new Callable<Task<Void>>() {
@Override
public Task<Void> call() throws Exception {
if (getCurrentState() != fromState) {
LOG.w(changeName.toUpperCase(), "- State mismatch, aborting. current:",
getCurrentState(), "from:", fromState, "to:", toState);
return Tasks.forResult(null);
} else {
Executor executor = mCallback.getJobWorker(changeName).getExecutor();
return stateChange.call().continueWithTask(executor,
new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(@NonNull Task<Void> task) {
if (task.isSuccessful() || isTearDown) {
mCurrentState = toState;
}
return task;
}
});
}
}
}).addOnCompleteListener(new OnCompleteListener<Void>() {
@Override
public void onComplete(@NonNull Task<Void> task) {
if (changeCount == mStateChangeCount) {
mTargetState = mCurrentState;
}
}
});
}
@SuppressWarnings("UnusedReturnValue")
@NonNull
public Task<Void> scheduleStateful(@NonNull String name,
@NonNull final CameraState atLeast,
@NonNull final Runnable job) {
return schedule(name, true, new Runnable() {
@Override
public void run() {
if (getCurrentState().isAtLeast(atLeast)) {
job.run();
}
}
});
}
public void scheduleStatefulDelayed(@NonNull String name,
@NonNull final CameraState atLeast,
long delay,
@NonNull final Runnable job) {
scheduleDelayed(name, delay, new Runnable() {
@Override
public void run() {
if (getCurrentState().isAtLeast(atLeast)) {
job.run();
}
}
});
}
}
Loading…
Cancel
Save