pull/696/head
Mattia Iavarone 6 years ago
parent 6fa7c4589c
commit 2f45585f05
  1. 2
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
  2. 44
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java
  3. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  4. 22
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java
  5. 85
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java
  6. 18
      cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java

@ -135,7 +135,7 @@ public class CameraViewTest extends BaseTest {
public void testDestroy() { public void testDestroy() {
cameraView.destroy(); cameraView.destroy();
verify(mockPreview, times(1)).onDestroy(); verify(mockPreview, times(1)).onDestroy();
verify(mockController, times(1)).destroy(); verify(mockController, times(1)).destroy(true);
} }
//region testDefaults //region testDefaults

@ -630,17 +630,26 @@ public abstract class CameraIntegrationTest<E extends CameraEngine> extends Base
@Test @Test
public void testEndVideo_withMaxSize() { public void testEndVideo_withMaxSize() {
camera.setMode(Mode.VIDEO); camera.setMode(Mode.VIDEO);
camera.setVideoSize(SizeSelectors.smallest()); camera.setVideoSize(SizeSelectors.maxArea(480 * 360));
camera.setVideoMaxSize(1000*1000);
openSync(true); openSync(true);
// Assuming video frame rate is 12...
//noinspection ConstantConditions
camera.setVideoBitRate((int) estimateVideoBitRate(camera.getVideoSize(), 12));
camera.setVideoMaxSize(estimateVideoBytes(camera.getVideoBitRate(), 4000));
takeVideoSync(true); takeVideoSync(true);
waitForVideoResult(true); waitForVideoResult(true);
} }
@Test @Test
public void testEndVideoSnapshot_withMaxSize() { public void testEndVideoSnapshot_withMaxSize() {
camera.setVideoMaxSize(1000*1000);
openSync(true); openSync(true);
camera.setSnapshotMaxWidth(480);
camera.setSnapshotMaxHeight(480);
camera.setPreviewFrameRate(12F);
//noinspection ConstantConditions
camera.setVideoBitRate((int) estimateVideoBitRate(camera.getSnapshotSize(),
(int) camera.getPreviewFrameRate()));
camera.setVideoMaxSize(estimateVideoBytes(camera.getVideoBitRate(), 4000));
takeVideoSnapshotSync(true); takeVideoSnapshotSync(true);
waitForVideoResult(true); waitForVideoResult(true);
} }
@ -865,11 +874,11 @@ public abstract class CameraIntegrationTest<E extends CameraEngine> extends Base
//region Frame Processing //region Frame Processing
private void assert30Frames(@NonNull FrameProcessor mock) throws Exception { private void assert15Frames(@NonNull FrameProcessor mock) throws Exception {
// Expect 30 frames // Expect 15 frames. Time is very high because currently Camera2 keeps a very low FPS.
CountDownLatch latch = new CountDownLatch(30); CountDownLatch latch = new CountDownLatch(15);
doCountDown(latch).when(mock).process(any(Frame.class)); doCountDown(latch).when(mock).process(any(Frame.class));
boolean did = latch.await(15, TimeUnit.SECONDS); boolean did = latch.await(30, TimeUnit.SECONDS);
assertTrue("Latch count should be 0: " + latch.getCount(), did); assertTrue("Latch count should be 0: " + latch.getCount(), did);
} }
@ -879,7 +888,7 @@ public abstract class CameraIntegrationTest<E extends CameraEngine> extends Base
camera.addFrameProcessor(processor); camera.addFrameProcessor(processor);
openSync(true); openSync(true);
assert30Frames(processor); assert15Frames(processor);
} }
@Test @Test
@ -893,7 +902,7 @@ public abstract class CameraIntegrationTest<E extends CameraEngine> extends Base
camera.takePictureSnapshot(); camera.takePictureSnapshot();
waitForPictureResult(true); waitForPictureResult(true);
assert30Frames(processor); assert15Frames(processor);
} }
@Test @Test
@ -904,7 +913,7 @@ public abstract class CameraIntegrationTest<E extends CameraEngine> extends Base
closeSync(true); closeSync(true);
openSync(true); openSync(true);
assert30Frames(processor); assert15Frames(processor);
} }
@ -917,7 +926,7 @@ public abstract class CameraIntegrationTest<E extends CameraEngine> extends Base
takeVideoSync(true,4000); takeVideoSync(true,4000);
waitForVideoResult(true); waitForVideoResult(true);
assert30Frames(processor); assert15Frames(processor);
} }
@Test @Test
@ -930,7 +939,7 @@ public abstract class CameraIntegrationTest<E extends CameraEngine> extends Base
camera.addFrameProcessor(processor); camera.addFrameProcessor(processor);
openSync(true); openSync(true);
assert30Frames(processor); assert15Frames(processor);
} }
public class FreezeReleaseFrameProcessor implements FrameProcessor { public class FreezeReleaseFrameProcessor implements FrameProcessor {
@ -969,4 +978,15 @@ public abstract class CameraIntegrationTest<E extends CameraEngine> extends Base
} }
//endregion //endregion
@SuppressWarnings("SameParameterValue")
private static long estimateVideoBitRate(@NonNull Size size, int frameRate) {
// Nasty estimate for a LQ video
return Math.round(0.05D * size.getWidth() * size.getHeight() * frameRate);
}
@SuppressWarnings("SameParameterValue")
private static long estimateVideoBytes(long bitRate, long millis) {
return Math.round(bitRate * (millis / 1000D) / 8D);
}
} }

@ -808,7 +808,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
if (mInEditor) return; if (mInEditor) return;
clearCameraListeners(); clearCameraListeners();
clearFrameProcessors(); clearFrameProcessors();
mCameraEngine.destroy(); mCameraEngine.destroy(true);
if (mCameraPreview != null) mCameraPreview.onDestroy(); if (mCameraPreview != null) mCameraPreview.onDestroy();
} }

@ -457,7 +457,7 @@ 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;
mFlashTask = mOrchestrator.scheduleStateful("flash", mFlashTask = mOrchestrator.scheduleStateful("flash (" + flash + ")",
CameraState.ENGINE, CameraState.ENGINE,
new Runnable() { new Runnable() {
@Override @Override
@ -508,7 +508,8 @@ 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;
mWhiteBalanceTask = mOrchestrator.scheduleStateful("white balance", mWhiteBalanceTask = mOrchestrator.scheduleStateful(
"white balance (" + whiteBalance + ")",
CameraState.ENGINE, CameraState.ENGINE,
new Runnable() { new Runnable() {
@Override @Override
@ -522,7 +523,11 @@ public class Camera1Engine extends CameraEngine implements
private boolean applyWhiteBalance(@NonNull Camera.Parameters params, private boolean applyWhiteBalance(@NonNull Camera.Parameters params,
@NonNull WhiteBalance oldWhiteBalance) { @NonNull WhiteBalance oldWhiteBalance) {
if (mCameraOptions.supports(mWhiteBalance)) { if (mCameraOptions.supports(mWhiteBalance)) {
// If this lock key is present, the engine can throw when applying the
// parameters, not sure why. Since we never lock it, this should be
// harmless for the rest of the engine.
params.setWhiteBalance(mMapper.mapWhiteBalance(mWhiteBalance)); params.setWhiteBalance(mMapper.mapWhiteBalance(mWhiteBalance));
params.remove("auto-whitebalance-lock");
return true; return true;
} }
mWhiteBalance = oldWhiteBalance; mWhiteBalance = oldWhiteBalance;
@ -533,7 +538,7 @@ 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;
mHdrTask = mOrchestrator.scheduleStateful("hdr", mHdrTask = mOrchestrator.scheduleStateful("hdr (" + hdr + ")",
CameraState.ENGINE, CameraState.ENGINE,
new Runnable() { new Runnable() {
@Override @Override
@ -557,7 +562,7 @@ 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;
mZoomTask = mOrchestrator.scheduleStateful("zoom", mZoomTask = mOrchestrator.scheduleStateful("zoom (" + zoom + ")",
CameraState.ENGINE, CameraState.ENGINE,
new Runnable() { new Runnable() {
@Override @Override
@ -589,7 +594,8 @@ 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;
mExposureCorrectionTask = mOrchestrator.scheduleStateful("exposure correction", mExposureCorrectionTask = mOrchestrator.scheduleStateful(
"exposure correction (" + EVvalue + ")",
CameraState.ENGINE, CameraState.ENGINE,
new Runnable() { new Runnable() {
@Override @Override
@ -629,7 +635,8 @@ 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;
mPlaySoundsTask = mOrchestrator.scheduleStateful("play sounds", mPlaySoundsTask = mOrchestrator.scheduleStateful(
"play sounds (" + playSounds + ")",
CameraState.ENGINE, CameraState.ENGINE,
new Runnable() { new Runnable() {
@Override @Override
@ -665,7 +672,8 @@ 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;
mPreviewFrameRateTask = mOrchestrator.scheduleStateful("preview fps", mPreviewFrameRateTask = mOrchestrator.scheduleStateful(
"preview fps (" + previewFrameRate + ")",
CameraState.ENGINE, CameraState.ENGINE,
new Runnable() { new Runnable() {
@Override @Override

@ -85,8 +85,9 @@ import java.util.concurrent.TimeUnit;
* S3 and S4 are also performed. * S3 and S4 are also performed.
* - {@link #stop(boolean)}: 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(boolean)} that will go on no matter what, * - {@link #destroy(boolean)}: SYNC - performs a {@link #stop(boolean)} that will go on no matter
* without throwing. Makes the engine unusable and clears resources. * what, without throwing. Makes the engine unusable and clears
* resources.
* *
* 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}.
@ -268,48 +269,45 @@ public abstract class CameraEngine implements
* @param throwable the throwable * @param throwable the throwable
* @param fromExceptionHandler true if coming from exception handler * @param fromExceptionHandler true if coming from exception handler
*/ */
private void handleException(@NonNull Thread thread, private void handleException(@NonNull final Thread thread,
final @NonNull Throwable throwable, final @NonNull Throwable throwable,
final boolean fromExceptionHandler) { final boolean fromExceptionHandler) {
if (!(throwable instanceof CameraException)) { // 1. If this comes from the exception handler, the thread has crashed. Replace it.
// This is unexpected, either a bug or something the developer should know. // I'm not sure if this can even be called now that all actions are wrapped into Tasks.
// Release and crash the UI thread so we get bug reports.
LOG.e("EXCEPTION:", "Unexpected exception! Scheduling destroy...");
mCrashHandler.post(new Runnable() {
@Override
public void run() {
LOG.e("EXCEPTION:", "Unexpected exception! Executing destroy...");
destroy();
// Throws an unchecked exception without unnecessary wrapping.
if (throwable instanceof RuntimeException) {
throw (RuntimeException) throwable;
} else {
throw new RuntimeException(throwable);
}
}
});
return;
}
final CameraException cameraException = (CameraException) throwable;
LOG.e("EXCEPTION:", "Received CameraException.",
"Engine state:", getState(), "Current thread:", Thread.currentThread());
if (fromExceptionHandler) { if (fromExceptionHandler) {
// Got to restart the handler.
LOG.e("EXCEPTION:", "Handler thread is gone. Replacing."); LOG.e("EXCEPTION:", "Handler thread is gone. Replacing.");
thread.interrupt(); thread.interrupt();
mHandler = WorkerHandler.get("CameraViewEngine"); mHandler = WorkerHandler.get("CameraViewEngine");
mHandler.getThread().setUncaughtExceptionHandler(new CrashExceptionHandler()); mHandler.getThread().setUncaughtExceptionHandler(new CrashExceptionHandler());
} }
LOG.e("EXCEPTION:", "Dispatching it to CameraListener."); // 2. Depending on the exception, we must destroy(false|true) to release resources, and
mCallback.dispatchError(cameraException); // notify the outside, either with the callback or by crashing the app.
if (cameraException.isUnrecoverable()) { LOG.e("EXCEPTION:", "Scheduling on the crash handler...");
LOG.e("EXCEPTION:", "Since exception is unrecoverable, we stop(true)", mCrashHandler.post(new Runnable() {
"to release resources and make it easy to retry."); @Override
// Stop everything (if needed) without notifying teardown errors. public void run() {
stop(true); if (throwable instanceof CameraException) {
} CameraException exception = (CameraException) throwable;
if (exception.isUnrecoverable()) {
LOG.e("EXCEPTION:", "Got CameraException. " +
"Since it is unrecoverable, executing destroy(false).");
destroy(false);
}
LOG.e("EXCEPTION:", "Got CameraException. Dispatching to callback.");
mCallback.dispatchError(exception);
} else {
LOG.e("EXCEPTION:", "Unexpected error! Executing destroy(true).");
destroy(true);
LOG.e("EXCEPTION:", "Unexpected error! Throwing.");
if (throwable instanceof RuntimeException) {
throw (RuntimeException) throwable;
} else {
throw new RuntimeException(throwable);
}
}
}
});
} }
//endregion //endregion
@ -334,14 +332,21 @@ public abstract class CameraEngine implements
* Calls {@link #stop(boolean)} and waits for it. * Calls {@link #stop(boolean)} and waits for it.
* Not final due to mockito requirements. * Not final due to mockito requirements.
* *
* If forever is true, this also releases resources and the engine will not be in a
* functional state after. If forever is false, this really is just a synchronous stop.
*
* NOTE: Should not be called on the orchestrator thread! This would cause deadlocks due to us * NOTE: Should not be called on the orchestrator thread! This would cause deadlocks due to us
* awaiting for {@link #stop(boolean)} to return. * awaiting for {@link #stop(boolean)} to return.
*/ */
public void destroy() { public void destroy(boolean forever) {
LOG.i("DESTROY:", "state:", getState(), "thread:", Thread.currentThread()); LOG.i("DESTROY:", "state:", getState(),
// Prevent CameraEngine leaks. Don't set to null, or exceptions "thread:", Thread.currentThread(),
// inside the standard stop() method might crash the main thread. "forever:", forever);
mHandler.getThread().setUncaughtExceptionHandler(new NoOpExceptionHandler()); if (forever) {
// 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. // Stop if needed, synchronously and silently.
// Cannot use Tasks.await() because we might be on the UI thread. // Cannot use Tasks.await() because we might be on the UI thread.
final CountDownLatch latch = new CountDownLatch(1); final CountDownLatch latch = new CountDownLatch(1);

@ -76,6 +76,7 @@ public abstract class FullVideoRecorder extends VideoRecorder {
@SuppressWarnings("SameParameterValue") @SuppressWarnings("SameParameterValue")
private boolean prepareMediaRecorder(@NonNull VideoResult.Stub stub, private boolean prepareMediaRecorder(@NonNull VideoResult.Stub stub,
boolean applyEncodersConstraints) { boolean applyEncodersConstraints) {
LOG.i("prepareMediaRecorder:", "Preparing on thread", Thread.currentThread());
// 1. Create reference and ask for the CamcorderProfile // 1. Create reference and ask for the CamcorderProfile
mMediaRecorder = new MediaRecorder(); mMediaRecorder = new MediaRecorder();
mProfile = getCamcorderProfile(stub); mProfile = getCamcorderProfile(stub);
@ -210,10 +211,14 @@ public abstract class FullVideoRecorder extends VideoRecorder {
// Would do this with max duration as well but there's no such callback. // Would do this with max duration as well but there's no such callback.
mMediaRecorder.setMaxFileSize(stub.maxSize <= 0 ? stub.maxSize mMediaRecorder.setMaxFileSize(stub.maxSize <= 0 ? stub.maxSize
: Math.round(stub.maxSize / 0.9D)); : Math.round(stub.maxSize / 0.9D));
LOG.i("prepareMediaRecorder:", "Increased max size from", stub.maxSize, "to",
Math.round(stub.maxSize / 0.9D));
mMediaRecorder.setMaxDuration(stub.maxDuration); mMediaRecorder.setMaxDuration(stub.maxDuration);
mMediaRecorder.setOnInfoListener(new MediaRecorder.OnInfoListener() { mMediaRecorder.setOnInfoListener(new MediaRecorder.OnInfoListener() {
@Override @Override
public void onInfo(MediaRecorder mediaRecorder, int what, int extra) { public void onInfo(MediaRecorder mediaRecorder, int what, int extra) {
LOG.i("OnInfoListener:", "Received info", what, extra,
"Thread: ", Thread.currentThread());
switch (what) { switch (what) {
case MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED: case MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED:
mResult.endReason = VideoResult.REASON_MAX_DURATION_REACHED; mResult.endReason = VideoResult.REASON_MAX_DURATION_REACHED;
@ -226,6 +231,15 @@ public abstract class FullVideoRecorder extends VideoRecorder {
} }
} }
}); });
mMediaRecorder.setOnErrorListener(new MediaRecorder.OnErrorListener() {
@Override
public void onError(MediaRecorder mr, int what, int extra) {
LOG.e("OnErrorListener: got error", what, extra, ". Stopping.");
mResult = null;
mError = new RuntimeException("MediaRecorder error: " + what + " " + extra);
stop(false);
}
});
// 8. Prepare the Recorder // 8. Prepare the Recorder
try { try {
@ -265,7 +279,9 @@ public abstract class FullVideoRecorder extends VideoRecorder {
if (mMediaRecorder != null) { if (mMediaRecorder != null) {
dispatchVideoRecordingEnd(); dispatchVideoRecordingEnd();
try { try {
LOG.i("stop:", "Stopping MediaRecorder...");
mMediaRecorder.stop(); mMediaRecorder.stop();
LOG.i("stop:", "Stopped MediaRecorder.");
} catch (Exception e) { } catch (Exception e) {
// This can happen if stopVideo() is called right after takeVideo() // This can happen if stopVideo() is called right after takeVideo()
// (in which case we don't care). Or when prepare()/start() have failed for // (in which case we don't care). Or when prepare()/start() have failed for
@ -278,7 +294,9 @@ public abstract class FullVideoRecorder extends VideoRecorder {
} }
} }
try { try {
LOG.i("stop:", "Releasing MediaRecorder...");
mMediaRecorder.release(); mMediaRecorder.release();
LOG.i("stop:", "Released MediaRecorder.");
} catch (Exception e) { } catch (Exception e) {
mResult = null; mResult = null;
if (mError == null) { if (mError == null) {

Loading…
Cancel
Save