* 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
public void testEnable() {
assertNotNull(helper.mListener);
assertEquals(helper.getDisplayOffset(), -1);
assertEquals(helper.getDeviceOrientation(), -1);
helper.enable(getContext());
assertNotNull(helper.mListener);
assertNotEquals(helper.getDisplayOffset(), -1); // Don't know about device orientation.
// Ensure nothing bad if called twice.
helper.enable(getContext());
assertNotNull(helper.mListener);
assertNotEquals(helper.getDisplayOffset(), -1);
helper.disable();
assertNotNull(helper.mListener);
assertEquals(helper.getDisplayOffset(), -1);
assertEquals(helper.getDeviceOrientation(), -1);
// On some API levels, enable() needs to be run on the UI thread.
uiSync(new Runnable() {
@Override
public void run() {
assertEquals(helper.getLastDisplayOffset(), -1);
assertEquals(helper.getLastDeviceOrientation(), -1);
helper.enable();
assertNotEquals(helper.getLastDisplayOffset(), -1); // Don't know about device orientation.
// Ensure nothing bad if called twice.
helper.enable();
assertNotEquals(helper.getLastDisplayOffset(), -1);
helper.disable();
assertEquals(helper.getLastDisplayOffset(), -1);
assertEquals(helper.getLastDeviceOrientation(), -1);
}
});
}
@Test
public void testRotation() {
// Sometimes (on some APIs) the helper will trigger an update to 0
// right after enabling. But that's fine for us, times(1) will be OK either way.
helper.enable(getContext());
helper.mListener.onOrientationChanged(OrientationEventListener.ORIENTATION_UNKNOWN);
assertEquals(helper.getDeviceOrientation(), 0);
helper.mListener.onOrientationChanged(10);
assertEquals(helper.getDeviceOrientation(), 0);
helper.mListener.onOrientationChanged(-10);
assertEquals(helper.getDeviceOrientation(), 0);
helper.mListener.onOrientationChanged(44);
assertEquals(helper.getDeviceOrientation(), 0);
helper.mListener.onOrientationChanged(360);
assertEquals(helper.getDeviceOrientation(), 0);
// Callback called just once.
verify(callback, times(1)).onDeviceOrientationChanged(0);
helper.mListener.onOrientationChanged(90);
helper.mListener.onOrientationChanged(91);
assertEquals(helper.getDeviceOrientation(), 90);
verify(callback, times(1)).onDeviceOrientationChanged(90);
helper.mListener.onOrientationChanged(180);
assertEquals(helper.getDeviceOrientation(), 180);
verify(callback, times(1)).onDeviceOrientationChanged(180);
helper.mListener.onOrientationChanged(270);
assertEquals(helper.getDeviceOrientation(), 270);
verify(callback, times(1)).onDeviceOrientationChanged(270);
// It is still 270 after ORIENTATION_UNKNOWN
helper.mListener.onOrientationChanged(OrientationEventListener.ORIENTATION_UNKNOWN);
assertEquals(helper.getDeviceOrientation(), 270);
verify(callback, times(1)).onDeviceOrientationChanged(270);
// On some API levels, enable() needs to be run on the UI thread.
uiSync(new Runnable() {
@Override
public void run() {
// Sometimes (on some APIs) the helper will trigger an update to 0
// right after enabling. But that's fine for us, times(1) will be OK either way.
helper.enable();
helper.mDeviceOrientationListener.onOrientationChanged(OrientationEventListener.ORIENTATION_UNKNOWN);
assertEquals(helper.getLastDeviceOrientation(), 0);
helper.mDeviceOrientationListener.onOrientationChanged(10);
assertEquals(helper.getLastDeviceOrientation(), 0);
helper.mDeviceOrientationListener.onOrientationChanged(-10);
assertEquals(helper.getLastDeviceOrientation(), 0);
helper.mDeviceOrientationListener.onOrientationChanged(44);
assertEquals(helper.getLastDeviceOrientation(), 0);
helper.mDeviceOrientationListener.onOrientationChanged(360);
assertEquals(helper.getLastDeviceOrientation(), 0);
// Callback called just once.
verify(callback, times(1)).onDeviceOrientationChanged(0);
helper.mDeviceOrientationListener.onOrientationChanged(90);
helper.mDeviceOrientationListener.onOrientationChanged(91);
assertEquals(helper.getLastDeviceOrientation(), 90);
verify(callback, times(1)).onDeviceOrientationChanged(90);
helper.mDeviceOrientationListener.onOrientationChanged(180);
assertEquals(helper.getLastDeviceOrientation(), 180);
verify(callback, times(1)).onDeviceOrientationChanged(180);
helper.mDeviceOrientationListener.onOrientationChanged(270);
assertEquals(helper.getLastDeviceOrientation(), 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.
doInstantiatePreview();
}
mOrientationHelper.enable(getContext());
mOrientationHelper.enable();
}
@Override
@ -722,8 +722,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
if (mCameraPreview != null) mCameraPreview.onResume();
if (checkPermissions(getAudio())) {
// Update display orientation for current CameraEngine
mOrientationHelper.enable(getContext());
mCameraEngine.getAngles().setDisplayOffset(mOrientationHelper.getDisplayOffset());
mOrientationHelper.enable();
mCameraEngine.getAngles().setDisplayOffset(mOrientationHelper.getLastDisplayOffset());
mCameraEngine.start();
}
}
@ -2073,7 +2073,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
@Override
public void onDeviceOrientationChanged(int deviceOrientation) {
mLogger.i("onDeviceOrientationChanged", deviceOrientation);
int displayOffset = mOrientationHelper.getDisplayOffset();
int displayOffset = mOrientationHelper.getLastDisplayOffset();
if (!mUseDeviceOrientation) {
// To fool the engine to return outputs in the VIEW reference system,
// 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
public void dispatchOnZoomChanged(final float newValue, @Nullable final PointF[] fingers) {
mLogger.i("dispatchOnZoomChanged", newValue);

@ -70,6 +70,7 @@ import com.otaliastudios.cameraview.video.SnapshotVideoRecorder;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException;
@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
// 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;
public Camera2Engine(Callback callback) {
@ -1428,28 +1430,14 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
@Override
public void addAction(final @NonNull Action action) {
// This is likely to be called during a Capture callback while we iterate
// on the actions list, or worse, from other threads. We must use mHandler.post.
mHandler.post(new Runnable() {
@Override
public void run() {
if (!mActions.contains(action)) {
mActions.add(action);
}
}
});
if (!mActions.contains(action)) {
mActions.add(action);
}
}
@Override
public void removeAction(final @NonNull Action action) {
// This is likely to be called during a Capture callback while we iterate
// 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);
}
});
mActions.remove(action);
}
@NonNull
@ -1458,7 +1446,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
return mCameraCharacteristics;
}
@NonNull
@Nullable
@Override
public TotalCaptureResult getLastResult(@NonNull Action action) {
return mLastRepeatingResult;

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

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

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

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

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

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

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

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

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

@ -5,6 +5,8 @@ import android.hardware.SensorManager;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import android.hardware.display.DisplayManager;
import android.os.Build;
import android.view.Display;
import android.view.OrientationEventListener;
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)
* and the display offset (which depends on the activity orientation
* wrt the device default orientation).
* and the display offset (which depends on the activity orientation wrt the device default
* orientation).
*/
public class OrientationHelper {
/**
* Receives callback about the device orientation changes.
* Receives callback about the orientation changes.
*/
public interface Callback {
void onDeviceOrientationChanged(int deviceOrientation);
void onDisplayOffsetChanged(int displayOffset, boolean willRecreate);
}
@VisibleForTesting final OrientationEventListener mListener;
private final Context mContext;
private final Callback mCallback;
@VisibleForTesting
final OrientationEventListener mDeviceOrientationListener;
private int mDeviceOrientation = -1;
@VisibleForTesting
final DisplayManager.DisplayListener mDisplayOffsetListener;
private int mDisplayOffset = -1;
/**
@ -35,57 +44,78 @@ public class OrientationHelper {
* @param callback a {@link Callback}
*/
public OrientationHelper(@NonNull Context context, @NonNull Callback callback) {
mContext = context;
mCallback = callback;
mListener = new OrientationEventListener(context.getApplicationContext(),
mDeviceOrientationListener = new OrientationEventListener(context.getApplicationContext(),
SensorManager.SENSOR_DELAY_NORMAL) {
@SuppressWarnings("ConstantConditions")
@Override
public void onOrientationChanged(int orientation) {
int or = 0;
int deviceOrientation = 0;
if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN) {
or = mDeviceOrientation != -1 ? mDeviceOrientation : 0;
deviceOrientation = mDeviceOrientation != -1 ? mDeviceOrientation : 0;
} else if (orientation >= 315 || orientation < 45) {
or = 0;
deviceOrientation = 0;
} else if (orientation >= 45 && orientation < 135) {
or = 90;
deviceOrientation = 90;
} else if (orientation >= 135 && orientation < 225) {
or = 180;
deviceOrientation = 180;
} else if (orientation >= 225 && orientation < 315) {
or = 270;
deviceOrientation = 270;
}
if (or != mDeviceOrientation) {
mDeviceOrientation = or;
if (deviceOrientation != mDeviceOrientation) {
mDeviceOrientation = deviceOrientation;
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.
* @param context a context
*/
public void enable(@NonNull Context context) {
Display display = ((WindowManager) context
.getSystemService(Context.WINDOW_SERVICE))
.getDefaultDisplay();
switch (display.getRotation()) {
case Surface.ROTATION_0: mDisplayOffset = 0; break;
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;
public void enable() {
mDisplayOffset = findDisplayOffset();
if (Build.VERSION.SDK_INT >= 17) {
DisplayManager manager = (DisplayManager)
mContext.getSystemService(Context.DISPLAY_SERVICE);
manager.registerDisplayListener(mDisplayOffsetListener, null);
}
mListener.enable();
mDeviceOrientationListener.enable();
}
/**
* Disables this listener.
*/
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;
mDeviceOrientation = -1;
}
@ -94,7 +124,8 @@ public class OrientationHelper {
* Returns the current device orientation.
* @return device orientation
*/
public int getDeviceOrientation() {
@SuppressWarnings("WeakerAccess")
public int getLastDeviceOrientation() {
return mDeviceOrientation;
}
@ -102,7 +133,20 @@ public class OrientationHelper {
* Returns the current display offset.
* @return display offset
*/
public int getDisplayOffset() {
public int getLastDisplayOffset() {
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()
&& aeState != null
&& aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED;

@ -2,7 +2,6 @@ package com.otaliastudios.cameraview.preview;
import android.content.Context;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.CaptureResult;
import android.opengl.GLSurfaceView;
import android.opengl.Matrix;
import androidx.annotation.NonNull;
@ -20,8 +19,6 @@ import com.otaliastudios.cameraview.filter.Filter;
import com.otaliastudios.cameraview.filter.NoFilter;
import com.otaliastudios.cameraview.size.AspectRatio;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
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) {
ViewGroup root = (ViewGroup) LayoutInflater.from(context)
.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.setRenderer(instantiateRenderer());
glView.setRenderer(renderer);
glView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
glView.getHolder().addCallback(new SurfaceHolder.Callback() {
public void surfaceCreated(SurfaceHolder holder) {}
@ -97,6 +95,12 @@ public class GlCameraPreview extends FilterCameraPreview<GLSurfaceView, SurfaceT
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
glView.queueEvent(new Runnable() {
@Override
public void run() {
renderer.onSurfaceDestroyed();
}
});
dispatchOnSurfaceDestroyed();
mDispatched = false;
}
@ -129,16 +133,6 @@ public class GlCameraPreview extends FilterCameraPreview<GLSurfaceView, SurfaceT
super.onDestroy();
// View is gone, so EGL context is gone: callbacks make no sense anymore.
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
@Override
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 MediaEncoderEngine mEncoderEngine;
private final Object mEncoderEngineLock = new Object();
private GlCameraPreview mPreview;
private int mCurrentState = STATE_NOT_RECORDING;
@ -82,14 +83,21 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
dispatchVideoRecordingStart();
}
// Can be called different threads
@Override
protected void onStop(boolean 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.");
mDesiredState = STATE_NOT_RECORDING;
mCurrentState = STATE_NOT_RECORDING;
mEncoderEngine.stop();
synchronized (mEncoderEngineLock) {
if (mEncoderEngine != null) {
mEncoderEngine.stop();
mEncoderEngine = null;
}
}
} else {
mDesiredState = STATE_NOT_RECORDING;
}
@ -104,12 +112,15 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
}
}
@RendererThread
@Override
public void onRendererFilterChanged(@NonNull Filter filter) {
mCurrentFilter = filter.copy();
if (mEncoderEngine != null) {
mCurrentFilter.setSize(mResult.size.getWidth(), mResult.size.getHeight());
mEncoderEngine.notify(TextureMediaEncoder.FILTER_EVENT, mCurrentFilter);
mCurrentFilter.setSize(mResult.size.getWidth(), mResult.size.getHeight());
synchronized (mEncoderEngineLock) {
if (mEncoderEngine != null) {
mEncoderEngine.notify(TextureMediaEncoder.FILTER_EVENT, mCurrentFilter);
}
}
}
@ -206,14 +217,16 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
}
// Engine
mEncoderEngine = new MediaEncoderEngine(mResult.file,
videoEncoder,
audioEncoder,
mResult.maxDuration,
mResult.maxSize,
SnapshotVideoRecorder.this);
mEncoderEngine.notify(TextureMediaEncoder.FILTER_EVENT, mCurrentFilter);
mEncoderEngine.start();
synchronized (mEncoderEngineLock) {
mEncoderEngine = new MediaEncoderEngine(mResult.file,
videoEncoder,
audioEncoder,
mResult.maxDuration,
mResult.maxSize,
SnapshotVideoRecorder.this);
mEncoderEngine.notify(TextureMediaEncoder.FILTER_EVENT, mCurrentFilter);
mEncoderEngine.start();
}
mCurrentState = STATE_RECORDING;
}
@ -226,15 +239,22 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
// NOTE: this is an approximation but it seems to work:
frame.timestampMillis = System.currentTimeMillis();
surfaceTexture.getTransformMatrix(frame.transform);
if (mEncoderEngine != null) { // Can happen on teardown. At least it used to.
mEncoderEngine.notify(TextureMediaEncoder.FRAME_EVENT, frame);
synchronized (mEncoderEngineLock) {
if (mEncoderEngine != null) { // Can be null on teardown.
mEncoderEngine.notify(TextureMediaEncoder.FRAME_EVENT, frame);
}
}
}
if (mCurrentState == STATE_RECORDING && mDesiredState == STATE_NOT_RECORDING) {
LOG.i("Stopping the encoder engine.");
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 = null;
}
mEncoderEngine = null;
synchronized (mEncoderEngineLock) {
mEncoderEngine = null;
}
dispatchResult();
}
}

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

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

Loading…
Cancel
Save