* Use CopyOnWriteArrayList for Camera2 action list. Fixes #649

* Listen to suspicious display rotation events. Fixes #623

* Correctly release GL objects on activity pause. Fixes #648

* Add synchronization in SnapshotVideoRecorder. Fixes #642

* Make Holder.getLastResult nullable. Fixes #621

* Fix tests
pull/659/head
Mattia Iavarone 5 years ago committed by GitHub
parent 063c7a76fb
commit a63cd84726
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 111
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/utils/OrientationHelperTest.java
  2. 21
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  3. 28
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java
  4. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/LogAction.java
  5. 3
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/ActionHolder.java
  6. 14
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/lock/ExposureLock.java
  7. 29
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/lock/FocusLock.java
  8. 15
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/lock/WhiteBalanceLock.java
  9. 14
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/ExposureMeter.java
  10. 5
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/ExposureReset.java
  11. 18
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/FocusMeter.java
  12. 4
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/FocusReset.java
  13. 15
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/WhiteBalanceMeter.java
  14. 100
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/OrientationHelper.java
  15. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/picture/Snapshot2PictureRecorder.java
  16. 39
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java
  17. 56
      cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java
  18. 42
      cameraview/src/main/java/com/otaliastudios/cameraview/video/VideoRecorder.java
  19. 4
      demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java

@ -41,61 +41,68 @@ public class OrientationHelperTest extends BaseTest {
@Test @Test
public void testEnable() { public void testEnable() {
assertNotNull(helper.mListener); // On some API levels, enable() needs to be run on the UI thread.
assertEquals(helper.getDisplayOffset(), -1); uiSync(new Runnable() {
assertEquals(helper.getDeviceOrientation(), -1); @Override
public void run() {
helper.enable(getContext()); assertEquals(helper.getLastDisplayOffset(), -1);
assertNotNull(helper.mListener); assertEquals(helper.getLastDeviceOrientation(), -1);
assertNotEquals(helper.getDisplayOffset(), -1); // Don't know about device orientation.
helper.enable();
// Ensure nothing bad if called twice. assertNotEquals(helper.getLastDisplayOffset(), -1); // Don't know about device orientation.
helper.enable(getContext());
assertNotNull(helper.mListener); // Ensure nothing bad if called twice.
assertNotEquals(helper.getDisplayOffset(), -1); helper.enable();
assertNotEquals(helper.getLastDisplayOffset(), -1);
helper.disable();
assertNotNull(helper.mListener); helper.disable();
assertEquals(helper.getDisplayOffset(), -1); assertEquals(helper.getLastDisplayOffset(), -1);
assertEquals(helper.getDeviceOrientation(), -1); assertEquals(helper.getLastDeviceOrientation(), -1);
}
});
} }
@Test @Test
public void testRotation() { public void testRotation() {
// On some API levels, enable() needs to be run on the UI thread.
// Sometimes (on some APIs) the helper will trigger an update to 0 uiSync(new Runnable() {
// right after enabling. But that's fine for us, times(1) will be OK either way. @Override
helper.enable(getContext()); public void run() {
helper.mListener.onOrientationChanged(OrientationEventListener.ORIENTATION_UNKNOWN); // Sometimes (on some APIs) the helper will trigger an update to 0
assertEquals(helper.getDeviceOrientation(), 0); // right after enabling. But that's fine for us, times(1) will be OK either way.
helper.mListener.onOrientationChanged(10); helper.enable();
assertEquals(helper.getDeviceOrientation(), 0); helper.mDeviceOrientationListener.onOrientationChanged(OrientationEventListener.ORIENTATION_UNKNOWN);
helper.mListener.onOrientationChanged(-10); assertEquals(helper.getLastDeviceOrientation(), 0);
assertEquals(helper.getDeviceOrientation(), 0); helper.mDeviceOrientationListener.onOrientationChanged(10);
helper.mListener.onOrientationChanged(44); assertEquals(helper.getLastDeviceOrientation(), 0);
assertEquals(helper.getDeviceOrientation(), 0); helper.mDeviceOrientationListener.onOrientationChanged(-10);
helper.mListener.onOrientationChanged(360); assertEquals(helper.getLastDeviceOrientation(), 0);
assertEquals(helper.getDeviceOrientation(), 0); helper.mDeviceOrientationListener.onOrientationChanged(44);
assertEquals(helper.getLastDeviceOrientation(), 0);
// Callback called just once. helper.mDeviceOrientationListener.onOrientationChanged(360);
verify(callback, times(1)).onDeviceOrientationChanged(0); assertEquals(helper.getLastDeviceOrientation(), 0);
helper.mListener.onOrientationChanged(90); // Callback called just once.
helper.mListener.onOrientationChanged(91); verify(callback, times(1)).onDeviceOrientationChanged(0);
assertEquals(helper.getDeviceOrientation(), 90);
verify(callback, times(1)).onDeviceOrientationChanged(90); helper.mDeviceOrientationListener.onOrientationChanged(90);
helper.mDeviceOrientationListener.onOrientationChanged(91);
helper.mListener.onOrientationChanged(180); assertEquals(helper.getLastDeviceOrientation(), 90);
assertEquals(helper.getDeviceOrientation(), 180); verify(callback, times(1)).onDeviceOrientationChanged(90);
verify(callback, times(1)).onDeviceOrientationChanged(180);
helper.mDeviceOrientationListener.onOrientationChanged(180);
helper.mListener.onOrientationChanged(270); assertEquals(helper.getLastDeviceOrientation(), 180);
assertEquals(helper.getDeviceOrientation(), 270); verify(callback, times(1)).onDeviceOrientationChanged(180);
verify(callback, times(1)).onDeviceOrientationChanged(270);
helper.mDeviceOrientationListener.onOrientationChanged(270);
// It is still 270 after ORIENTATION_UNKNOWN assertEquals(helper.getLastDeviceOrientation(), 270);
helper.mListener.onOrientationChanged(OrientationEventListener.ORIENTATION_UNKNOWN); verify(callback, times(1)).onDeviceOrientationChanged(270);
assertEquals(helper.getDeviceOrientation(), 270);
verify(callback, times(1)).onDeviceOrientationChanged(270); // It is still 270 after ORIENTATION_UNKNOWN
helper.mDeviceOrientationListener.onOrientationChanged(OrientationEventListener.ORIENTATION_UNKNOWN);
assertEquals(helper.getLastDeviceOrientation(), 270);
verify(callback, times(1)).onDeviceOrientationChanged(270);
}
});
} }
} }

@ -356,7 +356,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
// attached. That's why we instantiate the preview here. // attached. That's why we instantiate the preview here.
doInstantiatePreview(); doInstantiatePreview();
} }
mOrientationHelper.enable(getContext()); mOrientationHelper.enable();
} }
@Override @Override
@ -722,8 +722,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
if (mCameraPreview != null) mCameraPreview.onResume(); if (mCameraPreview != null) mCameraPreview.onResume();
if (checkPermissions(getAudio())) { if (checkPermissions(getAudio())) {
// Update display orientation for current CameraEngine // Update display orientation for current CameraEngine
mOrientationHelper.enable(getContext()); mOrientationHelper.enable();
mCameraEngine.getAngles().setDisplayOffset(mOrientationHelper.getDisplayOffset()); mCameraEngine.getAngles().setDisplayOffset(mOrientationHelper.getLastDisplayOffset());
mCameraEngine.start(); mCameraEngine.start();
} }
} }
@ -2073,7 +2073,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
@Override @Override
public void onDeviceOrientationChanged(int deviceOrientation) { public void onDeviceOrientationChanged(int deviceOrientation) {
mLogger.i("onDeviceOrientationChanged", deviceOrientation); mLogger.i("onDeviceOrientationChanged", deviceOrientation);
int displayOffset = mOrientationHelper.getDisplayOffset(); int displayOffset = mOrientationHelper.getLastDisplayOffset();
if (!mUseDeviceOrientation) { if (!mUseDeviceOrientation) {
// To fool the engine to return outputs in the VIEW reference system, // To fool the engine to return outputs in the VIEW reference system,
// The device orientation should be set to -displayOffset. // The device orientation should be set to -displayOffset.
@ -2093,6 +2093,19 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
}); });
} }
@Override
public void onDisplayOffsetChanged(int displayOffset, boolean willRecreate) {
mLogger.i("onDisplayOffsetChanged", displayOffset, "recreate:", willRecreate);
if (isOpened() && !willRecreate) {
// Display offset changes when the device rotation lock is off and the activity
// is free to rotate. However, some changes will NOT recreate the activity, namely
// 180 degrees flips. In this case, we must restart the camera manually.
mLogger.w("onDisplayOffsetChanged", "restarting the camera.");
close();
open();
}
}
@Override @Override
public void dispatchOnZoomChanged(final float newValue, @Nullable final PointF[] fingers) { public void dispatchOnZoomChanged(final float newValue, @Nullable final PointF[] fingers) {
mLogger.i("dispatchOnZoomChanged", newValue); mLogger.i("dispatchOnZoomChanged", newValue);

@ -70,6 +70,7 @@ import com.otaliastudios.cameraview.video.SnapshotVideoRecorder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@RequiresApi(Build.VERSION_CODES.LOLLIPOP) @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
@ -111,7 +112,8 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
private final boolean mPictureCaptureStopsPreview = false; // can be configurable at some point private final boolean mPictureCaptureStopsPreview = false; // can be configurable at some point
// Actions // Actions
private final List<Action> mActions = new ArrayList<>(); // Use COW to properly synchronize the list. We'll iterate much more than mutate
private final List<Action> mActions = new CopyOnWriteArrayList<>();
private MeterAction mMeterAction; private MeterAction mMeterAction;
public Camera2Engine(Callback callback) { public Camera2Engine(Callback callback) {
@ -1428,28 +1430,14 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
@Override @Override
public void addAction(final @NonNull Action action) { public void addAction(final @NonNull Action action) {
// This is likely to be called during a Capture callback while we iterate if (!mActions.contains(action)) {
// on the actions list, or worse, from other threads. We must use mHandler.post. mActions.add(action);
mHandler.post(new Runnable() { }
@Override
public void run() {
if (!mActions.contains(action)) {
mActions.add(action);
}
}
});
} }
@Override @Override
public void removeAction(final @NonNull Action action) { public void removeAction(final @NonNull Action action) {
// This is likely to be called during a Capture callback while we iterate mActions.remove(action);
// on the actions list, or worse, from other threads. We must use mHandler.post.
mHandler.post(new Runnable() {
@Override
public void run() {
mActions.remove(action);
}
});
} }
@NonNull @NonNull
@ -1458,7 +1446,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
return mCameraCharacteristics; return mCameraCharacteristics;
} }
@NonNull @Nullable
@Override @Override
public TotalCaptureResult getLastResult(@NonNull Action action) { public TotalCaptureResult getLastResult(@NonNull Action action) {
return mLastRepeatingResult; return mLastRepeatingResult;

@ -36,7 +36,7 @@ class LogAction extends BaseAction {
" afState: " + afState + " afTriggerState: " + afTriggerState; " afState: " + afState + " afTriggerState: " + afTriggerState;
if (!log.equals(lastLog)) { if (!log.equals(lastLog)) {
lastLog = log; lastLog = log;
LOG.i(log); LOG.v(log);
} }
} }

@ -8,6 +8,7 @@ import android.hardware.camera2.TotalCaptureResult;
import android.os.Build; import android.os.Build;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
/** /**
@ -51,7 +52,7 @@ public interface ActionHolder {
* @param action action * @param action action
* @return last result * @return last result
*/ */
@NonNull @Nullable
TotalCaptureResult getLastResult(@NonNull Action action); TotalCaptureResult getLastResult(@NonNull Action action);
/** /**

@ -40,10 +40,16 @@ public class ExposureLock extends BaseLock {
@Override @Override
protected boolean checkShouldSkip(@NonNull ActionHolder holder) { protected boolean checkShouldSkip(@NonNull ActionHolder holder) {
Integer aeState = holder.getLastResult(this).get(CaptureResult.CONTROL_AE_STATE); CaptureResult lastResult = holder.getLastResult(this);
boolean result = aeState != null && aeState == CaptureResult.CONTROL_AE_STATE_LOCKED; if (lastResult != null) {
LOG.i("checkShouldSkip:", result); Integer aeState = lastResult.get(CaptureResult.CONTROL_AE_STATE);
return result; boolean result = aeState != null && aeState == CaptureResult.CONTROL_AE_STATE_LOCKED;
LOG.i("checkShouldSkip:", result);
return result;
} else {
LOG.i("checkShouldSkip: false - lastResult is null.");
return false;
}
} }
@Override @Override

@ -35,18 +35,23 @@ public class FocusLock extends BaseLock {
@Override @Override
protected boolean checkShouldSkip(@NonNull ActionHolder holder) { protected boolean checkShouldSkip(@NonNull ActionHolder holder) {
CaptureResult lastResult = holder.getLastResult(this); CaptureResult lastResult = holder.getLastResult(this);
Integer afState = lastResult.get(CaptureResult.CONTROL_AF_STATE); if (lastResult != null) {
boolean afStateOk = afState != null && Integer afState = lastResult.get(CaptureResult.CONTROL_AF_STATE);
(afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED boolean afStateOk = afState != null &&
|| afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED
|| afState == CaptureResult.CONTROL_AF_STATE_INACTIVE || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED
|| afState == CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED || afState == CaptureResult.CONTROL_AF_STATE_INACTIVE
|| afState == CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED); || afState == CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED
Integer afMode = lastResult.get(CaptureResult.CONTROL_AF_MODE); || afState == CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED);
boolean afModeOk = afMode != null && afMode == CaptureResult.CONTROL_AF_MODE_AUTO; Integer afMode = lastResult.get(CaptureResult.CONTROL_AF_MODE);
boolean result = afStateOk && afModeOk; boolean afModeOk = afMode != null && afMode == CaptureResult.CONTROL_AF_MODE_AUTO;
LOG.i("checkShouldSkip:", result); boolean result = afStateOk && afModeOk;
return result; LOG.i("checkShouldSkip:", result);
return result;
} else {
LOG.i("checkShouldSkip: false - lastResult is null.");
return false;
}
} }
@Override @Override

@ -33,10 +33,17 @@ public class WhiteBalanceLock extends BaseLock {
@Override @Override
protected boolean checkShouldSkip(@NonNull ActionHolder holder) { protected boolean checkShouldSkip(@NonNull ActionHolder holder) {
Integer awbState = holder.getLastResult(this).get(CaptureResult.CONTROL_AWB_STATE); CaptureResult lastResult = holder.getLastResult(this);
boolean result = awbState != null && awbState == CaptureRequest.CONTROL_AWB_STATE_LOCKED; if (lastResult != null) {
LOG.i("checkShouldSkip:", result); Integer awbState = lastResult.get(CaptureResult.CONTROL_AWB_STATE);
return result; boolean result = awbState != null
&& awbState == CaptureRequest.CONTROL_AWB_STATE_LOCKED;
LOG.i("checkShouldSkip:", result);
return result;
} else {
LOG.i("checkShouldSkip: false - lastResult is null.");
return false;
}
} }
@Override @Override

@ -50,10 +50,16 @@ public class ExposureMeter extends BaseMeter {
@Override @Override
protected boolean checkShouldSkip(@NonNull ActionHolder holder) { protected boolean checkShouldSkip(@NonNull ActionHolder holder) {
Integer aeState = holder.getLastResult(this).get(CaptureResult.CONTROL_AE_STATE); CaptureResult lastResult = holder.getLastResult(this);
boolean result = aeState != null && aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED; if (lastResult != null) {
LOG.i("checkShouldSkip:", result); Integer aeState = lastResult.get(CaptureResult.CONTROL_AE_STATE);
return result; boolean result = aeState != null && aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED;
LOG.i("checkShouldSkip:", result);
return result;
} else {
LOG.i("checkShouldSkip: false - lastResult is null.");
return false;
}
} }
@Override @Override

@ -37,8 +37,9 @@ public class ExposureReset extends BaseReset {
} }
// NOTE: precapture might not be supported, in which case I think it will be ignored. // NOTE: precapture might not be supported, in which case I think it will be ignored.
Integer trigger = holder.getLastResult(this) CaptureResult lastResult = holder.getLastResult(this);
.get(CaptureResult.CONTROL_AE_PRECAPTURE_TRIGGER); Integer trigger = lastResult == null ? null
: lastResult.get(CaptureResult.CONTROL_AE_PRECAPTURE_TRIGGER);
LOG.i("onStarted:", "last precapture trigger is", trigger); LOG.i("onStarted:", "last precapture trigger is", trigger);
if (trigger != null && trigger == CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START) { if (trigger != null && trigger == CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START) {
LOG.i("onStarted:", "canceling precapture."); LOG.i("onStarted:", "canceling precapture.");

@ -40,12 +40,18 @@ public class FocusMeter extends BaseMeter {
@Override @Override
protected boolean checkShouldSkip(@NonNull ActionHolder holder) { protected boolean checkShouldSkip(@NonNull ActionHolder holder) {
Integer afState = holder.getLastResult(this).get(CaptureResult.CONTROL_AF_STATE); CaptureResult lastResult = holder.getLastResult(this);
boolean result = afState != null && if (lastResult != null) {
(afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || Integer afState = lastResult.get(CaptureResult.CONTROL_AF_STATE);
afState == CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED); boolean result = afState != null &&
LOG.i("checkShouldSkip:", result); (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED ||
return result; afState == CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED);
LOG.i("checkShouldSkip:", result);
return result;
} else {
LOG.i("checkShouldSkip: false - lastResult is null.");
return false;
}
} }
@Override @Override

@ -36,7 +36,9 @@ public class FocusReset extends BaseReset {
} }
// NOTE: trigger might not be supported, in which case I think it will be ignored. // NOTE: trigger might not be supported, in which case I think it will be ignored.
Integer trigger = holder.getLastResult(this).get(CaptureResult.CONTROL_AF_TRIGGER); CaptureResult lastResult = holder.getLastResult(this);
Integer trigger = lastResult == null ? null
: lastResult.get(CaptureResult.CONTROL_AF_TRIGGER);
LOG.w("onStarted:", "last focus trigger is", trigger); LOG.w("onStarted:", "last focus trigger is", trigger);
if (trigger != null && trigger == CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START) { if (trigger != null && trigger == CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START) {
holder.getBuilder(this).set(CaptureRequest.CONTROL_AF_TRIGGER, holder.getBuilder(this).set(CaptureRequest.CONTROL_AF_TRIGGER,

@ -40,10 +40,17 @@ public class WhiteBalanceMeter extends BaseMeter {
@Override @Override
protected boolean checkShouldSkip(@NonNull ActionHolder holder) { protected boolean checkShouldSkip(@NonNull ActionHolder holder) {
Integer awbState = holder.getLastResult(this).get(CaptureResult.CONTROL_AWB_STATE); CaptureResult lastResult = holder.getLastResult(this);
boolean result = awbState != null && awbState == CaptureRequest.CONTROL_AWB_STATE_CONVERGED; if (lastResult != null) {
LOG.i("checkShouldSkip:", result); Integer awbState = lastResult.get(CaptureResult.CONTROL_AWB_STATE);
return result; boolean result = awbState != null
&& awbState == CaptureRequest.CONTROL_AWB_STATE_CONVERGED;
LOG.i("checkShouldSkip:", result);
return result;
} else {
LOG.i("checkShouldSkip: false - lastResult is null.");
return false;
}
} }
@Override @Override

@ -5,6 +5,8 @@ import android.hardware.SensorManager;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import android.hardware.display.DisplayManager;
import android.os.Build;
import android.view.Display; import android.view.Display;
import android.view.OrientationEventListener; import android.view.OrientationEventListener;
import android.view.Surface; import android.view.Surface;
@ -12,21 +14,28 @@ import android.view.WindowManager;
/** /**
* Helps with keeping track of both device orientation (which changes when device is rotated) * Helps with keeping track of both device orientation (which changes when device is rotated)
* and the display offset (which depends on the activity orientation * and the display offset (which depends on the activity orientation wrt the device default
* wrt the device default orientation). * orientation).
*/ */
public class OrientationHelper { public class OrientationHelper {
/** /**
* Receives callback about the device orientation changes. * Receives callback about the orientation changes.
*/ */
public interface Callback { public interface Callback {
void onDeviceOrientationChanged(int deviceOrientation); void onDeviceOrientationChanged(int deviceOrientation);
void onDisplayOffsetChanged(int displayOffset, boolean willRecreate);
} }
@VisibleForTesting final OrientationEventListener mListener; private final Context mContext;
private final Callback mCallback; private final Callback mCallback;
@VisibleForTesting
final OrientationEventListener mDeviceOrientationListener;
private int mDeviceOrientation = -1; private int mDeviceOrientation = -1;
@VisibleForTesting
final DisplayManager.DisplayListener mDisplayOffsetListener;
private int mDisplayOffset = -1; private int mDisplayOffset = -1;
/** /**
@ -35,57 +44,78 @@ public class OrientationHelper {
* @param callback a {@link Callback} * @param callback a {@link Callback}
*/ */
public OrientationHelper(@NonNull Context context, @NonNull Callback callback) { public OrientationHelper(@NonNull Context context, @NonNull Callback callback) {
mContext = context;
mCallback = callback; mCallback = callback;
mListener = new OrientationEventListener(context.getApplicationContext(), mDeviceOrientationListener = new OrientationEventListener(context.getApplicationContext(),
SensorManager.SENSOR_DELAY_NORMAL) { SensorManager.SENSOR_DELAY_NORMAL) {
@SuppressWarnings("ConstantConditions") @SuppressWarnings("ConstantConditions")
@Override @Override
public void onOrientationChanged(int orientation) { public void onOrientationChanged(int orientation) {
int or = 0; int deviceOrientation = 0;
if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN) { if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN) {
or = mDeviceOrientation != -1 ? mDeviceOrientation : 0; deviceOrientation = mDeviceOrientation != -1 ? mDeviceOrientation : 0;
} else if (orientation >= 315 || orientation < 45) { } else if (orientation >= 315 || orientation < 45) {
or = 0; deviceOrientation = 0;
} else if (orientation >= 45 && orientation < 135) { } else if (orientation >= 45 && orientation < 135) {
or = 90; deviceOrientation = 90;
} else if (orientation >= 135 && orientation < 225) { } else if (orientation >= 135 && orientation < 225) {
or = 180; deviceOrientation = 180;
} else if (orientation >= 225 && orientation < 315) { } else if (orientation >= 225 && orientation < 315) {
or = 270; deviceOrientation = 270;
} }
if (or != mDeviceOrientation) { if (deviceOrientation != mDeviceOrientation) {
mDeviceOrientation = or; mDeviceOrientation = deviceOrientation;
mCallback.onDeviceOrientationChanged(mDeviceOrientation); mCallback.onDeviceOrientationChanged(mDeviceOrientation);
} }
} }
}; };
if (Build.VERSION.SDK_INT >= 17) {
mDisplayOffsetListener = new DisplayManager.DisplayListener() {
public void onDisplayAdded(int displayId) { }
public void onDisplayRemoved(int displayId) { }
@Override
public void onDisplayChanged(int displayId) {
int oldDisplayOffset = mDisplayOffset;
int newDisplayOffset = findDisplayOffset();
if (newDisplayOffset != oldDisplayOffset) {
mDisplayOffset = newDisplayOffset;
// With 180 degrees flips, the activity is not recreated.
boolean willRecreate = Math.abs(newDisplayOffset - oldDisplayOffset) != 180;
mCallback.onDisplayOffsetChanged(newDisplayOffset, willRecreate);
}
}
};
} else {
mDisplayOffsetListener = null;
}
} }
/** /**
* Enables this listener. * Enables this listener.
* @param context a context
*/ */
public void enable(@NonNull Context context) { public void enable() {
Display display = ((WindowManager) context mDisplayOffset = findDisplayOffset();
.getSystemService(Context.WINDOW_SERVICE)) if (Build.VERSION.SDK_INT >= 17) {
.getDefaultDisplay(); DisplayManager manager = (DisplayManager)
switch (display.getRotation()) { mContext.getSystemService(Context.DISPLAY_SERVICE);
case Surface.ROTATION_0: mDisplayOffset = 0; break; manager.registerDisplayListener(mDisplayOffsetListener, null);
case Surface.ROTATION_90: mDisplayOffset = 90; break;
case Surface.ROTATION_180: mDisplayOffset = 180; break;
case Surface.ROTATION_270: mDisplayOffset = 270; break;
default: mDisplayOffset = 0; break;
} }
mListener.enable(); mDeviceOrientationListener.enable();
} }
/** /**
* Disables this listener. * Disables this listener.
*/ */
public void disable() { public void disable() {
mListener.disable(); mDeviceOrientationListener.disable();
if (Build.VERSION.SDK_INT >= 17) {
DisplayManager manager = (DisplayManager)
mContext.getSystemService(Context.DISPLAY_SERVICE);
manager.unregisterDisplayListener(mDisplayOffsetListener);
}
mDisplayOffset = -1; mDisplayOffset = -1;
mDeviceOrientation = -1; mDeviceOrientation = -1;
} }
@ -94,7 +124,8 @@ public class OrientationHelper {
* Returns the current device orientation. * Returns the current device orientation.
* @return device orientation * @return device orientation
*/ */
public int getDeviceOrientation() { @SuppressWarnings("WeakerAccess")
public int getLastDeviceOrientation() {
return mDeviceOrientation; return mDeviceOrientation;
} }
@ -102,7 +133,20 @@ public class OrientationHelper {
* Returns the current display offset. * Returns the current display offset.
* @return display offset * @return display offset
*/ */
public int getDisplayOffset() { public int getLastDisplayOffset() {
return mDisplayOffset; return mDisplayOffset;
} }
private int findDisplayOffset() {
Display display = ((WindowManager) mContext
.getSystemService(Context.WINDOW_SERVICE))
.getDefaultDisplay();
switch (display.getRotation()) {
case Surface.ROTATION_0: return 0;
case Surface.ROTATION_90: return 90;
case Surface.ROTATION_180: return 180;
case Surface.ROTATION_270: return 270;
default: return 0;
}
}
} }

@ -106,7 +106,13 @@ public class Snapshot2PictureRecorder extends SnapshotGlPictureRecorder {
} }
}); });
Integer aeState = mHolder.getLastResult(mAction).get(CaptureResult.CONTROL_AE_STATE); CaptureResult lastResult = mHolder.getLastResult(mAction);
if (lastResult == null) {
LOG.w("Picture snapshot requested very early, before the first preview frame.",
"Metering might not work as intended.");
}
Integer aeState = lastResult == null ? null
: lastResult.get(CaptureResult.CONTROL_AE_STATE);
mActionNeeded = engine.getPictureSnapshotMetering() mActionNeeded = engine.getPictureSnapshotMetering()
&& aeState != null && aeState != null
&& aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED; && aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED;

@ -2,7 +2,6 @@ package com.otaliastudios.cameraview.preview;
import android.content.Context; import android.content.Context;
import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture;
import android.hardware.camera2.CaptureResult;
import android.opengl.GLSurfaceView; import android.opengl.GLSurfaceView;
import android.opengl.Matrix; import android.opengl.Matrix;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -20,8 +19,6 @@ import com.otaliastudios.cameraview.filter.Filter;
import com.otaliastudios.cameraview.filter.NoFilter; import com.otaliastudios.cameraview.filter.NoFilter;
import com.otaliastudios.cameraview.size.AspectRatio; import com.otaliastudios.cameraview.size.AspectRatio;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CopyOnWriteArraySet;
@ -87,9 +84,10 @@ public class GlCameraPreview extends FilterCameraPreview<GLSurfaceView, SurfaceT
protected GLSurfaceView onCreateView(@NonNull Context context, @NonNull ViewGroup parent) { protected GLSurfaceView onCreateView(@NonNull Context context, @NonNull ViewGroup parent) {
ViewGroup root = (ViewGroup) LayoutInflater.from(context) ViewGroup root = (ViewGroup) LayoutInflater.from(context)
.inflate(R.layout.cameraview_gl_view, parent, false); .inflate(R.layout.cameraview_gl_view, parent, false);
GLSurfaceView glView = root.findViewById(R.id.gl_surface_view); final GLSurfaceView glView = root.findViewById(R.id.gl_surface_view);
final Renderer renderer = instantiateRenderer();
glView.setEGLContextClientVersion(2); glView.setEGLContextClientVersion(2);
glView.setRenderer(instantiateRenderer()); glView.setRenderer(renderer);
glView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); glView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
glView.getHolder().addCallback(new SurfaceHolder.Callback() { glView.getHolder().addCallback(new SurfaceHolder.Callback() {
public void surfaceCreated(SurfaceHolder holder) {} public void surfaceCreated(SurfaceHolder holder) {}
@ -97,6 +95,12 @@ public class GlCameraPreview extends FilterCameraPreview<GLSurfaceView, SurfaceT
@Override @Override
public void surfaceDestroyed(SurfaceHolder holder) { public void surfaceDestroyed(SurfaceHolder holder) {
glView.queueEvent(new Runnable() {
@Override
public void run() {
renderer.onSurfaceDestroyed();
}
});
dispatchOnSurfaceDestroyed(); dispatchOnSurfaceDestroyed();
mDispatched = false; mDispatched = false;
} }
@ -129,16 +133,6 @@ public class GlCameraPreview extends FilterCameraPreview<GLSurfaceView, SurfaceT
super.onDestroy(); super.onDestroy();
// View is gone, so EGL context is gone: callbacks make no sense anymore. // View is gone, so EGL context is gone: callbacks make no sense anymore.
mRendererFrameCallbacks.clear(); mRendererFrameCallbacks.clear();
if (mInputSurfaceTexture != null) {
mInputSurfaceTexture.setOnFrameAvailableListener(null);
mInputSurfaceTexture.release();
mInputSurfaceTexture = null;
}
mOutputTextureId = 0;
if (mOutputViewport != null) {
mOutputViewport.release();
mOutputViewport = null;
}
} }
/** /**
@ -174,6 +168,21 @@ public class GlCameraPreview extends FilterCameraPreview<GLSurfaceView, SurfaceT
}); });
} }
@SuppressWarnings("WeakerAccess")
@RendererThread
public void onSurfaceDestroyed() {
if (mInputSurfaceTexture != null) {
mInputSurfaceTexture.setOnFrameAvailableListener(null);
mInputSurfaceTexture.release();
mInputSurfaceTexture = null;
}
mOutputTextureId = 0;
if (mOutputViewport != null) {
mOutputViewport.release();
mOutputViewport = null;
}
}
@RendererThread @RendererThread
@Override @Override
public void onSurfaceChanged(GL10 gl, final int width, final int height) { public void onSurfaceChanged(GL10 gl, final int width, final int height) {

@ -51,6 +51,7 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
private static final int STATE_NOT_RECORDING = 1; private static final int STATE_NOT_RECORDING = 1;
private MediaEncoderEngine mEncoderEngine; private MediaEncoderEngine mEncoderEngine;
private final Object mEncoderEngineLock = new Object();
private GlCameraPreview mPreview; private GlCameraPreview mPreview;
private int mCurrentState = STATE_NOT_RECORDING; private int mCurrentState = STATE_NOT_RECORDING;
@ -82,14 +83,21 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
dispatchVideoRecordingStart(); dispatchVideoRecordingStart();
} }
// Can be called different threads
@Override @Override
protected void onStop(boolean isCameraShutdown) { protected void onStop(boolean isCameraShutdown) {
if (isCameraShutdown) { if (isCameraShutdown) {
// The renderer callback might never be called. From my tests, it's not. // The renderer callback might never be called. From my tests, it's not,
// so we can't wait for that callback to stop the encoder engine.
LOG.i("Stopping the encoder engine from isCameraShutdown."); LOG.i("Stopping the encoder engine from isCameraShutdown.");
mDesiredState = STATE_NOT_RECORDING; mDesiredState = STATE_NOT_RECORDING;
mCurrentState = STATE_NOT_RECORDING; mCurrentState = STATE_NOT_RECORDING;
mEncoderEngine.stop(); synchronized (mEncoderEngineLock) {
if (mEncoderEngine != null) {
mEncoderEngine.stop();
mEncoderEngine = null;
}
}
} else { } else {
mDesiredState = STATE_NOT_RECORDING; mDesiredState = STATE_NOT_RECORDING;
} }
@ -104,12 +112,15 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
} }
} }
@RendererThread
@Override @Override
public void onRendererFilterChanged(@NonNull Filter filter) { public void onRendererFilterChanged(@NonNull Filter filter) {
mCurrentFilter = filter.copy(); mCurrentFilter = filter.copy();
if (mEncoderEngine != null) { mCurrentFilter.setSize(mResult.size.getWidth(), mResult.size.getHeight());
mCurrentFilter.setSize(mResult.size.getWidth(), mResult.size.getHeight()); synchronized (mEncoderEngineLock) {
mEncoderEngine.notify(TextureMediaEncoder.FILTER_EVENT, mCurrentFilter); if (mEncoderEngine != null) {
mEncoderEngine.notify(TextureMediaEncoder.FILTER_EVENT, mCurrentFilter);
}
} }
} }
@ -206,14 +217,16 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
} }
// Engine // Engine
mEncoderEngine = new MediaEncoderEngine(mResult.file, synchronized (mEncoderEngineLock) {
videoEncoder, mEncoderEngine = new MediaEncoderEngine(mResult.file,
audioEncoder, videoEncoder,
mResult.maxDuration, audioEncoder,
mResult.maxSize, mResult.maxDuration,
SnapshotVideoRecorder.this); mResult.maxSize,
mEncoderEngine.notify(TextureMediaEncoder.FILTER_EVENT, mCurrentFilter); SnapshotVideoRecorder.this);
mEncoderEngine.start(); mEncoderEngine.notify(TextureMediaEncoder.FILTER_EVENT, mCurrentFilter);
mEncoderEngine.start();
}
mCurrentState = STATE_RECORDING; mCurrentState = STATE_RECORDING;
} }
@ -226,15 +239,22 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
// NOTE: this is an approximation but it seems to work: // NOTE: this is an approximation but it seems to work:
frame.timestampMillis = System.currentTimeMillis(); frame.timestampMillis = System.currentTimeMillis();
surfaceTexture.getTransformMatrix(frame.transform); surfaceTexture.getTransformMatrix(frame.transform);
if (mEncoderEngine != null) { // Can happen on teardown. At least it used to. synchronized (mEncoderEngineLock) {
mEncoderEngine.notify(TextureMediaEncoder.FRAME_EVENT, frame); if (mEncoderEngine != null) { // Can be null on teardown.
mEncoderEngine.notify(TextureMediaEncoder.FRAME_EVENT, frame);
}
} }
} }
if (mCurrentState == STATE_RECORDING && mDesiredState == STATE_NOT_RECORDING) { if (mCurrentState == STATE_RECORDING && mDesiredState == STATE_NOT_RECORDING) {
LOG.i("Stopping the encoder engine."); LOG.i("Stopping the encoder engine.");
mCurrentState = STATE_NOT_RECORDING; mCurrentState = STATE_NOT_RECORDING;
mEncoderEngine.stop(); synchronized (mEncoderEngineLock) {
if (mEncoderEngine != null) {
mEncoderEngine.stop();
mEncoderEngine = null;
}
}
} }
} }
@ -280,7 +300,9 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
mOverlayDrawer.release(); mOverlayDrawer.release();
mOverlayDrawer = null; mOverlayDrawer = null;
} }
mEncoderEngine = null; synchronized (mEncoderEngineLock) {
mEncoderEngine = null;
}
dispatchResult(); dispatchResult();
} }
} }

@ -50,6 +50,7 @@ public abstract class VideoRecorder {
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
protected Exception mError; protected Exception mError;
private int mState; private int mState;
private final Object mStateLock = new Object();
/** /**
* Creates a new video recorder. * Creates a new video recorder.
@ -66,12 +67,14 @@ public abstract class VideoRecorder {
* @param stub the video stub * @param stub the video stub
*/ */
public final void start(@NonNull VideoResult.Stub stub) { public final void start(@NonNull VideoResult.Stub stub) {
if (mState != STATE_IDLE) { synchronized (mStateLock) {
LOG.e("start:", "called twice, or while stopping! " + if (mState != STATE_IDLE) {
"Ignoring. state:", mState); LOG.e("start:", "called twice, or while stopping! " +
return; "Ignoring. state:", mState);
return;
}
mState = STATE_RECORDING;
} }
mState = STATE_RECORDING;
mResult = stub; mResult = stub;
onStart(); onStart();
} }
@ -81,12 +84,15 @@ public abstract class VideoRecorder {
* @param isCameraShutdown whether this is a full shutdown, camera is being closed * @param isCameraShutdown whether this is a full shutdown, camera is being closed
*/ */
public final void stop(boolean isCameraShutdown) { public final void stop(boolean isCameraShutdown) {
if (mState == STATE_IDLE) { synchronized (mStateLock) {
LOG.e("stop:", "called twice, or called before start! " + if (mState == STATE_IDLE) {
"Ignoring. isCameraShutdown:", isCameraShutdown); // Do not check for STOPPING! See onStop().
return; LOG.e("stop:", "called twice, or called before start! " +
"Ignoring. isCameraShutdown:", isCameraShutdown);
return;
}
mState = STATE_STOPPING;
} }
mState = STATE_STOPPING;
onStop(isCameraShutdown); onStop(isCameraShutdown);
} }
@ -96,11 +102,19 @@ public abstract class VideoRecorder {
*/ */
public boolean isRecording() { public boolean isRecording() {
// true if not idle. // true if not idle.
return mState != STATE_IDLE; synchronized (mStateLock) {
return mState != STATE_IDLE;
}
} }
protected abstract void onStart(); protected abstract void onStart();
/**
* Should stop recording as fast as possible. This can be called twice because the
* shutdown boolean might be different.
*
* @param isCameraShutdown whether camera is shutting down
*/
protected abstract void onStop(boolean isCameraShutdown); protected abstract void onStop(boolean isCameraShutdown);
/** /**
@ -108,8 +122,10 @@ public abstract class VideoRecorder {
* either with some error (null result) or with the actual stub, filled. * either with some error (null result) or with the actual stub, filled.
*/ */
protected final void dispatchResult() { protected final void dispatchResult() {
if (!isRecording()) return; synchronized (mStateLock) {
mState = STATE_IDLE; if (!isRecording()) return;
mState = STATE_IDLE;
}
onDispatchResult(); onDispatchResult();
if (mListener != null) { if (mListener != null) {
mListener.onVideoResult(mResult, mError); mListener.onVideoResult(mResult, mError);

@ -351,9 +351,11 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis
mCurrentFilter = 0; mCurrentFilter = 0;
} }
Filters filter = mAllFilters[mCurrentFilter]; Filters filter = mAllFilters[mCurrentFilter];
camera.setFilter(filter.newInstance());
message(filter.toString(), false); message(filter.toString(), false);
// Normal behavior:
camera.setFilter(filter.newInstance());
// To test MultiFilter: // To test MultiFilter:
// DuotoneFilter duotone = new DuotoneFilter(); // DuotoneFilter duotone = new DuotoneFilter();
// duotone.setFirstColor(Color.RED); // duotone.setFirstColor(Color.RED);

Loading…
Cancel
Save