diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java index 6cd53d8f..2ba47b8e 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java @@ -193,22 +193,26 @@ public abstract class CameraIntegrationTest extends Base @SuppressWarnings("UnusedReturnValue") @Nullable private VideoResult waitForVideoResult(boolean expectSuccess) { - // CountDownLatch for onVideoRecordingEnd. - CountDownLatch onVideoRecordingEnd = new CountDownLatch(1); - doCountDown(onVideoRecordingEnd).when(listener).onVideoRecordingEnd(); - - // Op for onVideoTaken. - final Op video = new Op<>(); - doEndOp(video, 0).when(listener).onVideoTaken(any(VideoResult.class)); - doEndOp(video, null).when(listener).onCameraError(argThat(new ArgumentMatcher() { + Op wait1 = new Op<>(); + Op wait2 = new Op<>(); + doEndOp(wait1, true).when(listener).onVideoRecordingEnd(); + doEndOp(wait1, false).when(listener).onCameraError(argThat(new ArgumentMatcher() { + @Override + public boolean matches(CameraException argument) { + return argument.getReason() == CameraException.REASON_VIDEO_FAILED; + } + })); + doEndOp(wait2, 0).when(listener).onVideoTaken(any(VideoResult.class)); + doEndOp(wait2, null).when(listener).onCameraError(argThat(new ArgumentMatcher() { @Override public boolean matches(CameraException argument) { return argument.getReason() == CameraException.REASON_VIDEO_FAILED; } })); - // Wait for onVideoTaken and check. - VideoResult result = video.await(DELAY); + // First wait for onVideoRecordingEnd(). + LOG.i("[WAIT VIDEO]", "Waiting for onVideoRecordingEnd()..."); + Boolean wait1Result = wait1.await(DELAY); // It seems that when running all the tests together, the device can go in some // power saving mode which makes the CPU extremely slow. This is especially problematic @@ -217,24 +221,26 @@ public abstract class CameraIntegrationTest extends Base if (expectSuccess && camera.isTakingVideo()) { while (camera.isTakingVideo()) { LOG.w("[WAIT VIDEO]", "Waiting extra", DELAY, "milliseconds..."); - video.listen(); - result = video.await(DELAY); + wait1.listen(); + wait1Result = wait1.await(DELAY); } - // Sleep another 1000, because camera.isTakingVideo() might return false even - // if the result still has to be dispatched. Rare but could happen. - try { Thread.sleep(1000); } catch (InterruptedException ignore) {} } - // Now we should be OK. + // Now wait for onVideoResult(). + LOG.i("[WAIT VIDEO]", "Waiting for onVideoTaken()..."); + VideoResult wait2Result = wait2.await(DELAY); + + // Assert. if (expectSuccess) { - LOG.i("[WAIT VIDEO]", "Expecting success."); - assertEquals("Should call onVideoRecordingEnd", 0, onVideoRecordingEnd.getCount()); - assertNotNull("Should end video", result); + assertNotNull("Should call onVideoRecordingEnd", wait1Result); + assertTrue("Should call onVideoRecordingEnd", wait1Result); + assertNotNull("Should call onVideoTaken", wait2Result); } else { - LOG.i("[WAIT VIDEO]", "Expecting failure."); - assertNull("Should not end video", result); + assertTrue("Should not call onVideoRecordingEnd", + wait1Result == null || !wait1Result); + assertNull("Should not call onVideoTaken", wait2Result); } - return result; + return wait2Result; } @Nullable @@ -635,7 +641,7 @@ public abstract class CameraIntegrationTest extends Base // Assuming video frame rate is 20... //noinspection ConstantConditions camera.setVideoBitRate((int) estimateVideoBitRate(camera.getVideoSize(), 20)); - camera.setVideoMaxSize(estimateVideoBytes(camera.getVideoBitRate(), 6000)); + camera.setVideoMaxSize(estimateVideoBytes(camera.getVideoBitRate(), 5000)); takeVideoSync(true); waitForVideoResult(true); } @@ -651,7 +657,7 @@ public abstract class CameraIntegrationTest extends Base //noinspection ConstantConditions camera.setVideoBitRate((int) estimateVideoBitRate(camera.getSnapshotSize(), (int) camera.getPreviewFrameRate())); - camera.setVideoMaxSize(estimateVideoBytes(camera.getVideoBitRate(), 6000)); + camera.setVideoMaxSize(estimateVideoBytes(camera.getVideoBitRate(), 5000)); takeVideoSnapshotSync(true); waitForVideoResult(true); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java index 7e63f250..27a53dbb 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java @@ -277,9 +277,7 @@ public abstract class CameraEngine implements // (at least in Camera1, e.g. onError). if (fromExceptionHandler) { LOG.e("EXCEPTION:", "Handler thread is gone. Replacing."); - thread.interrupt(); - mHandler = WorkerHandler.get("CameraViewEngine"); - mHandler.getThread().setUncaughtExceptionHandler(new CrashExceptionHandler()); + recreateHandler(); } // 2. Depending on the exception, we must destroy(false|true) to release resources, and @@ -311,6 +309,12 @@ public abstract class CameraEngine implements }); } + private void recreateHandler() { + mHandler.getThread().interrupt(); + mHandler = WorkerHandler.get("CameraViewEngine"); + mHandler.getThread().setUncaughtExceptionHandler(new CrashExceptionHandler()); + } + //endregion //region State management @@ -348,10 +352,6 @@ public abstract class CameraEngine implements // inside the standard stop() method might crash the main thread. mHandler.getThread().setUncaughtExceptionHandler(new NoOpExceptionHandler()); } - // Instead of waiting forever, it's better to wait a fixed amount of time and loop. - // This frees the thread in between the loop, and possibly solves deadlock issues - // in the internal camera implementation (see Camera1Engine.onStopEngine() and - // Camera2Engine.onStopEngine()). // Cannot use Tasks.await() because we might be on the UI thread. final CountDownLatch latch = new CountDownLatch(1); stop(true).addOnCompleteListener( @@ -365,10 +365,14 @@ public abstract class CameraEngine implements try { boolean success = latch.await(6, TimeUnit.SECONDS); if (!success) { + // This thread is likely stuck. The reason might be deadlock issues in the internal + // camera implementation, at least in emulators: see Camera1Engine and Camera2Engine + // onStopEngine() implementation and comments. LOG.w("DESTROY: Could not destroy synchronously after 6 seconds.", "Current thread:", Thread.currentThread(), "Handler thread: ", mHandler.getThread(), - "Trying again..."); + "Trying again on a new thread..."); + recreateHandler(); destroy(unrecoverably); } } catch (InterruptedException ignore) {}