diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java index cb69d51a..b6258ca7 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java @@ -155,6 +155,15 @@ public class CameraViewCallbacksTest extends BaseTest { verify(listener, times(1)).onCameraClosed(); } + @Test + public void testDispatchOnVideoRecordingStart() { + completeTask().when(listener).onVideoRecordingStart(); + camera.mCameraCallbacks.dispatchOnVideoRecordingStart(); + + assertNotNull(op.await(500)); + verify(listener, times(1)).onVideoRecordingStart(); + } + @Test public void testDispatchOnVideoTaken() { VideoResult.Stub stub = new VideoResult.Stub(); 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 bc3f39e9..f3140ac7 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java @@ -5,7 +5,6 @@ import android.graphics.Bitmap; import android.graphics.PointF; import android.hardware.Camera; import android.os.Build; -import android.util.Log; import com.otaliastudios.cameraview.BaseTest; import com.otaliastudios.cameraview.CameraException; @@ -13,7 +12,6 @@ import com.otaliastudios.cameraview.CameraListener; import com.otaliastudios.cameraview.CameraOptions; import com.otaliastudios.cameraview.CameraUtils; import com.otaliastudios.cameraview.CameraView; -import com.otaliastudios.cameraview.DoNotRunOnTravis; import com.otaliastudios.cameraview.PictureResult; import com.otaliastudios.cameraview.TestActivity; import com.otaliastudios.cameraview.VideoResult; @@ -30,19 +28,14 @@ import com.otaliastudios.cameraview.internal.utils.WorkerHandler; import com.otaliastudios.cameraview.size.Size; import androidx.annotation.NonNull; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.LargeTest; -import androidx.test.filters.MediumTest; -import androidx.test.filters.SmallTest; +import androidx.annotation.Nullable; import androidx.test.rule.ActivityTestRule; import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; -import org.junit.runner.RunWith; import org.mockito.ArgumentMatcher; import java.io.File; @@ -130,7 +123,7 @@ public abstract class CameraIntegrationTest extends BaseTest { } } - private CameraOptions waitForOpen(boolean expectSuccess) { + private CameraOptions openSync(boolean expectSuccess) { camera.open(); final Op open = new Op<>(true); doEndTask(open, 0).when(listener).onCameraOpened(any(CameraOptions.class)); @@ -145,7 +138,7 @@ public abstract class CameraIntegrationTest extends BaseTest { return result; } - private void waitForClose(boolean expectSuccess) { + private void closeSync(boolean expectSuccess) { camera.close(); final Op close = new Op<>(true); doEndTask(close, true).when(listener).onCameraClosed(); @@ -157,25 +150,28 @@ public abstract class CameraIntegrationTest extends BaseTest { } } - private void waitForVideoEnd(boolean expectSuccess) { - final Op video = new Op<>(true); - doEndTask(video, true).when(listener).onVideoTaken(any(VideoResult.class)); - doEndTask(video, false).when(listener).onCameraError(argThat(new ArgumentMatcher() { + @SuppressWarnings("UnusedReturnValue") + @Nullable + private VideoResult waitForVideoResult(boolean expectSuccess) { + final Op video = new Op<>(true); + doEndTask(video, 0).when(listener).onVideoTaken(any(VideoResult.class)); + doEndTask(video, null).when(listener).onCameraError(argThat(new ArgumentMatcher() { @Override public boolean matches(CameraException argument) { return argument.getReason() == CameraException.REASON_VIDEO_FAILED; } })); - Boolean result = video.await(DELAY); + VideoResult result = video.await(DELAY); if (expectSuccess) { assertNotNull("Should end video", result); - assertTrue("Should end video without errors", result); } else { assertNull("Should not end video", result); } + return result; } - private PictureResult waitForPicture(boolean expectSuccess) { + @Nullable + private PictureResult waitForPictureResult(boolean expectSuccess) { final Op pic = new Op<>(true); doEndTask(pic, 0).when(listener).onPictureTaken(any(PictureResult.class)); doEndTask(pic, null).when(listener).onCameraError(argThat(new ArgumentMatcher() { @@ -193,12 +189,60 @@ public abstract class CameraIntegrationTest extends BaseTest { return result; } - private void waitForVideoStart() { - controller.mStartVideoOp.listen(); + private void takeVideoSync(boolean expectSuccess) { + takeVideoSync(expectSuccess,0); + } + + private void takeVideoSync(boolean expectSuccess, int duration) { + final Op op = new Op<>(true); + doEndTask(op, true).when(listener).onVideoRecordingStart(); + doEndTask(op, false).when(listener).onCameraError(argThat(new ArgumentMatcher() { + @Override + public boolean matches(CameraException argument) { + return argument.getReason() == CameraException.REASON_VIDEO_FAILED; + } + })); + File file = new File(context().getFilesDir(), "video.mp4"); + if (duration > 0) { + camera.takeVideo(file, duration); + } else { + camera.takeVideo(file); + } + Boolean result = op.await(DELAY); + if (expectSuccess) { + assertNotNull("should start video recording or get CameraError", result); + assertTrue("should start video recording successfully", result); + } else { + assertTrue("should not start video recording", result == null || !result); + } + } + + private void takeVideoSnapshotSync(boolean expectSuccess) { + takeVideoSnapshotSync(expectSuccess,0); + } + + private void takeVideoSnapshotSync(boolean expectSuccess, int duration) { + final Op op = new Op<>(true); + doEndTask(op, true).when(listener).onVideoRecordingStart(); + doEndTask(op, false).when(listener).onCameraError(argThat(new ArgumentMatcher() { + @Override + public boolean matches(CameraException argument) { + return argument.getReason() == CameraException.REASON_VIDEO_FAILED; + } + })); File file = new File(context().getFilesDir(), "video.mp4"); - camera.takeVideo(file); - controller.mStartVideoOp.await(DELAY); - // TODO assert! + if (duration > 0) { + camera.takeVideoSnapshot(file, duration); + } else { + camera.takeVideoSnapshot(file); + } + Boolean result = op.await(DELAY); + if (expectSuccess) { + assertNotNull("should start video recording or get CameraError", result); + assertTrue("should start video recording successfully", result); + } else { + assertTrue("should not start video recording", result == null || !result); + } } //region test open/close @@ -208,22 +252,22 @@ public abstract class CameraIntegrationTest extends BaseTest { // Starting and stopping are hard to get since they happen on another thread. assertEquals(controller.getEngineState(), CameraEngine.STATE_STOPPED); - waitForOpen(true); + openSync(true); assertEquals(controller.getEngineState(), CameraEngine.STATE_STARTED); - waitForClose(true); + closeSync(true); assertEquals(controller.getEngineState(), CameraEngine.STATE_STOPPED); } @Test public void testOpenTwice() { - waitForOpen(true); - waitForOpen(false); + openSync(true); + openSync(false); } @Test public void testCloseTwice() { - waitForClose(false); + closeSync(false); } @Test @@ -247,7 +291,7 @@ public abstract class CameraIntegrationTest extends BaseTest { @Test public void testStartInitializesOptions() { assertNull(camera.getCameraOptions()); - waitForOpen(true); + openSync(true); assertNotNull(camera.getCameraOptions()); } @@ -258,7 +302,7 @@ public abstract class CameraIntegrationTest extends BaseTest { @Test public void testSetFacing() throws Exception { - CameraOptions o = waitForOpen(true); + CameraOptions o = openSync(true); int size = o.getSupportedFacing().size(); if (size > 1) { // set facing should call stop and start again. @@ -276,7 +320,7 @@ public abstract class CameraIntegrationTest extends BaseTest { @Test public void testSetMode() throws Exception { camera.setMode(Mode.PICTURE); - waitForOpen(true); + openSync(true); // set session type should call stop and start again. final CountDownLatch latch = new CountDownLatch(2); @@ -297,7 +341,7 @@ public abstract class CameraIntegrationTest extends BaseTest { @Test public void testSetZoom() { - CameraOptions options = waitForOpen(true); + CameraOptions options = openSync(true); controller.mZoomOp.listen(); float oldValue = camera.getZoom(); @@ -314,7 +358,7 @@ public abstract class CameraIntegrationTest extends BaseTest { @Test public void testSetExposureCorrection() { - CameraOptions options = waitForOpen(true); + CameraOptions options = openSync(true); controller.mExposureCorrectionOp.listen(); float oldValue = camera.getExposureCorrection(); @@ -331,7 +375,7 @@ public abstract class CameraIntegrationTest extends BaseTest { @Test public void testSetFlash() { - CameraOptions options = waitForOpen(true); + CameraOptions options = openSync(true); Flash[] values = Flash.values(); Flash oldValue = camera.getFlash(); for (Flash value : values) { @@ -349,7 +393,7 @@ public abstract class CameraIntegrationTest extends BaseTest { @Test public void testSetWhiteBalance() { - CameraOptions options = waitForOpen(true); + CameraOptions options = openSync(true); WhiteBalance[] values = WhiteBalance.values(); WhiteBalance oldValue = camera.getWhiteBalance(); for (WhiteBalance value : values) { @@ -367,7 +411,7 @@ public abstract class CameraIntegrationTest extends BaseTest { @Test public void testSetHdr() { - CameraOptions options = waitForOpen(true); + CameraOptions options = openSync(true); Hdr[] values = Hdr.values(); Hdr oldValue = camera.getHdr(); for (Hdr value : values) { @@ -386,7 +430,7 @@ public abstract class CameraIntegrationTest extends BaseTest { @Test public void testSetAudio() { // TODO: when permissions are managed, check that Audio.ON triggers the audio permission - waitForOpen(true); + openSync(true); Audio[] values = Audio.values(); for (Audio value : values) { camera.setAudio(value); @@ -396,7 +440,7 @@ public abstract class CameraIntegrationTest extends BaseTest { @Test public void testSetLocation() { - waitForOpen(true); + openSync(true); controller.mLocationOp.listen(); camera.setLocation(10d, 2d); controller.mLocationOp.await(300); @@ -440,8 +484,8 @@ public abstract class CameraIntegrationTest extends BaseTest { // Error while starting MediaRecorder. java.lang.RuntimeException: start failed. // as documented. This works locally though. camera.setMode(Mode.PICTURE); - waitForOpen(true); - waitForVideoStart(); + openSync(true); + takeVideoSync(false); waitForUiException(); } @@ -451,35 +495,35 @@ public abstract class CameraIntegrationTest extends BaseTest { // Error while starting MediaRecorder. java.lang.RuntimeException: start failed. // as documented. This works locally though. camera.setMode(Mode.VIDEO); - waitForOpen(true); - camera.takeVideo(new File(context().getFilesDir(), "video.mp4"), 4000); - waitForVideoEnd(true); + openSync(true); + takeVideoSync(true, 4000); + waitForVideoResult(true); } @Test public void testEndVideo_withoutStarting() { camera.setMode(Mode.VIDEO); - waitForOpen(true); + openSync(true); camera.stopVideo(); - waitForVideoEnd(false); + waitForVideoResult(false); } @Test public void testEndVideo_withMaxSize() { camera.setMode(Mode.VIDEO); camera.setVideoMaxSize(3000*1000); // Less is risky - waitForOpen(true); - waitForVideoStart(); - waitForVideoEnd(true); + openSync(true); + takeVideoSync(true); + waitForVideoResult(true); } @Test public void testEndVideo_withMaxDuration() { camera.setMode(Mode.VIDEO); camera.setVideoMaxDuration(4000); - waitForOpen(true); - waitForVideoStart(); - waitForVideoEnd(true); + openSync(true); + takeVideoSync(true); + waitForVideoResult(true); } //endregion @@ -488,7 +532,7 @@ public abstract class CameraIntegrationTest extends BaseTest { @Test public void testStartAutoFocus() { - CameraOptions o = waitForOpen(true); + CameraOptions o = openSync(true); final Op focus = new Op<>(true); doEndTask(focus, 0).when(listener).onAutoFocusStart(any(PointF.class)); @@ -505,7 +549,7 @@ public abstract class CameraIntegrationTest extends BaseTest { @Test public void testStopAutoFocus() { - CameraOptions o = waitForOpen(true); + CameraOptions o = openSync(true); final Op focus = new Op<>(true); doEndTask(focus, 1).when(listener).onAutoFocusEnd(anyBoolean(), any(PointF.class)); @@ -528,13 +572,13 @@ public abstract class CameraIntegrationTest extends BaseTest { @Test public void testCapturePicture_beforeStarted() { camera.takePicture(); - waitForPicture(false); + waitForPictureResult(false); } @Test public void testCapturePicture_concurrentCalls() throws Exception { // Second take should fail. - waitForOpen(true); + openSync(true); CountDownLatch latch = new CountDownLatch(2); doCountDown(latch).when(listener).onPictureTaken(any(PictureResult.class)); @@ -548,12 +592,12 @@ public abstract class CameraIntegrationTest extends BaseTest { @Test public void testCapturePicture_size() throws Exception { - waitForOpen(true); + openSync(true); // PictureSize can still be null after opened. while (camera.getPictureSize() == null) {} Size size = camera.getPictureSize(); camera.takePicture(); - PictureResult result = waitForPicture(true); + PictureResult result = waitForPictureResult(true); Bitmap bitmap = CameraUtils.decodeBitmap(result.getData(), Integer.MAX_VALUE, Integer.MAX_VALUE); assertEquals(result.getSize(), size); assertEquals(bitmap.getWidth(), size.getWidth()); @@ -566,7 +610,7 @@ public abstract class CameraIntegrationTest extends BaseTest { @Test(expected = RuntimeException.class) public void testCapturePicture_whileInVideoMode() throws Throwable { camera.setMode(Mode.VIDEO); - waitForOpen(true); + openSync(true); camera.takePicture(); waitForUiException(); } @@ -574,13 +618,13 @@ public abstract class CameraIntegrationTest extends BaseTest { @Test public void testCaptureSnapshot_beforeStarted() { camera.takePictureSnapshot(); - waitForPicture(false); + waitForPictureResult(false); } @Test public void testCaptureSnapshot_concurrentCalls() throws Exception { // Second take should fail. - waitForOpen(true); + openSync(true); CountDownLatch latch = new CountDownLatch(2); doCountDown(latch).when(listener).onPictureTaken(any(PictureResult.class)); @@ -594,13 +638,13 @@ public abstract class CameraIntegrationTest extends BaseTest { @Test public void testCaptureSnapshot_size() throws Exception { - waitForOpen(true); + openSync(true); // SnapshotSize can still be null after opened. while (camera.getSnapshotSize() == null) {} Size size = camera.getSnapshotSize(); camera.takePictureSnapshot(); - PictureResult result = waitForPicture(true); + PictureResult result = waitForPictureResult(true); Bitmap bitmap = CameraUtils.decodeBitmap(result.getData(), Integer.MAX_VALUE, Integer.MAX_VALUE); assertEquals(result.getSize(), size); assertEquals(bitmap.getWidth(), size.getWidth()); @@ -626,7 +670,7 @@ public abstract class CameraIntegrationTest extends BaseTest { public void testFrameProcessing_simple() throws Exception { FrameProcessor processor = mock(FrameProcessor.class); camera.addFrameProcessor(processor); - waitForOpen(true); + openSync(true); assert30Frames(processor); } @@ -635,12 +679,12 @@ public abstract class CameraIntegrationTest extends BaseTest { public void testFrameProcessing_afterSnapshot() throws Exception { FrameProcessor processor = mock(FrameProcessor.class); camera.addFrameProcessor(processor); - waitForOpen(true); + openSync(true); // In Camera1, snapshots will clear the preview callback // Ensure we restore correctly camera.takePictureSnapshot(); - waitForPicture(true); + waitForPictureResult(true); assert30Frames(processor); } @@ -649,9 +693,9 @@ public abstract class CameraIntegrationTest extends BaseTest { public void testFrameProcessing_afterRestart() throws Exception { FrameProcessor processor = mock(FrameProcessor.class); camera.addFrameProcessor(processor); - waitForOpen(true); - waitForClose(true); - waitForOpen(true); + openSync(true); + closeSync(true); + openSync(true); assert30Frames(processor); } @@ -662,9 +706,9 @@ public abstract class CameraIntegrationTest extends BaseTest { FrameProcessor processor = mock(FrameProcessor.class); camera.addFrameProcessor(processor); camera.setMode(Mode.VIDEO); - waitForOpen(true); - camera.takeVideo(new File(context().getFilesDir(), "video.mp4"), 4000); - waitForVideoEnd(true); + openSync(true); + takeVideoSync(true,4000); + waitForVideoResult(true); assert30Frames(processor); } @@ -676,7 +720,7 @@ public abstract class CameraIntegrationTest extends BaseTest { FrameProcessor source = new FreezeReleaseFrameProcessor(); FrameProcessor processor = spy(source); camera.addFrameProcessor(processor); - waitForOpen(true); + openSync(true); assert30Frames(processor); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java index f364bff2..2c6292fa 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java @@ -2064,8 +2064,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver { } @Override - public void onVideoRecordingStart() { - mLogger.i("onVideoRecordingStart", "onVideoRecordingStart"); + public void dispatchOnVideoRecordingStart() { + mLogger.i("dispatchOnVideoRecordingStart", "dispatchOnVideoRecordingStart"); mUiHandler.post(new Runnable() { @Override public void run() { 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 04ca01a0..9de9388c 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java @@ -134,7 +134,7 @@ public abstract class CameraEngine implements void dispatchOnExposureCorrectionChanged(float newValue, @NonNull float[] bounds, @Nullable PointF[] fingers); void dispatchFrame(Frame frame); void dispatchError(CameraException exception); - void onVideoRecordingStart(); + void dispatchOnVideoRecordingStart(); } private static final String TAG = CameraEngine.class.getSimpleName(); @@ -199,7 +199,6 @@ public abstract class CameraEngine implements private Step mAllStep = new Step("all", mStepCallback); // Ops used for testing. - @VisibleForTesting Op mStartVideoOp = new Op<>(); @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op mZoomOp = new Op<>(); @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op mExposureCorrectionOp = new Op<>(); @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op mFlashOp = new Op<>(); @@ -1121,8 +1120,8 @@ public abstract class CameraEngine implements @Override public void run() { LOG.v("takeVideo", "performing. BindState:", getBindState(), "isTakingVideo:", isTakingVideo()); - if (getBindState() < STATE_STARTED) { mStartVideoOp.end(null); return; } - if (isTakingVideo()) { mStartVideoOp.end(null); return; } + if (getBindState() < STATE_STARTED) return; + if (isTakingVideo()) return; if (mMode == Mode.PICTURE) { throw new IllegalStateException("Can't record video while in PICTURE mode"); } @@ -1137,7 +1136,6 @@ public abstract class CameraEngine implements stub.videoBitRate = mVideoBitRate; stub.audioBitRate = mAudioBitRate; onTakeVideo(stub); - mStartVideoOp.end(null); } }); } @@ -1153,8 +1151,8 @@ public abstract class CameraEngine implements @Override public void run() { LOG.v("takeVideoSnapshot", "performing. BindState:", getBindState(), "isTakingVideo:", isTakingVideo()); - if (getBindState() < STATE_STARTED) { mStartVideoOp.end(null); return; } - if (isTakingVideo()) { mStartVideoOp.end(null); return; } + if (getBindState() < STATE_STARTED) return; + if (isTakingVideo()) return; stub.file = file; stub.isSnapshot = true; stub.videoCodec = mVideoCodec; @@ -1166,7 +1164,6 @@ public abstract class CameraEngine implements stub.maxSize = mVideoMaxSize; stub.maxDuration = mVideoMaxDuration; onTakeVideoSnapshot(stub, viewAspectRatio); - mStartVideoOp.end(null); } }); } @@ -1197,6 +1194,12 @@ public abstract class CameraEngine implements } } + @Override + public void onVideoRecordingStart() { + mCallback.dispatchOnVideoRecordingStart(); + } + + @WorkerThread protected abstract void onTakePicture(@NonNull PictureResult.Stub stub); @@ -1383,13 +1386,4 @@ public abstract class CameraEngine implements } //endregion - - //region callbacks - - @Override - public void onVideoRecordingStart() { - mCallback.onVideoRecordingStart(); - } - - //endregion } diff --git a/docs/_posts/2018-12-20-changelog.md b/docs/_posts/2018-12-20-changelog.md index dfa5f24e..9e356ec8 100644 --- a/docs/_posts/2018-12-20-changelog.md +++ b/docs/_posts/2018-12-20-changelog.md @@ -8,9 +8,11 @@ order: 3 New versions are released through GitHub, so the reference page is the [GitHub Releases](https://github.com/natario1/CameraView/releases) page. -### v2.0.0-*** (to be released) +### v2.0.0 (to be released) -- New: `cameraUseDeviceOrientation` XML attribute and `setUseDeviceOrientation()` method to disable considering the device orientation for outputs. ([#497][497]) +- New: added `onVideoRecordingStart()` to be notified when video recording starts, thanks to [@agrawalsuneet][agrawalsuneet] ([#498][498]) +- New: added `cameraUseDeviceOrientation` to choose whether picture and video outputs should consider the device orientation or not ([#497][497]) +- Improvement: improved Camera2 stability and various bugs fixed (e.g. [#501][501]) ### v2.0.0-beta06 @@ -55,6 +57,7 @@ If you were using `focusWithMarker`, you can [add back the old marker](../docs/m This is the first beta release. For changes with respect to v1, please take a look at the [migration guide](../extra/v1-migration-guide.html). [cneuwirt]: https://github.com/cneuwirt +[agrawalsuneet]: https://github.com/agrawalsuneet [356]: https://github.com/natario1/CameraView/pull/356 [360]: https://github.com/natario1/CameraView/pull/360 @@ -69,4 +72,6 @@ This is the first beta release. For changes with respect to v1, please take a lo [482]: https://github.com/natario1/CameraView/pull/482 [484]: https://github.com/natario1/CameraView/pull/484 [490]: https://github.com/natario1/CameraView/pull/490 -[497]: https://github.com/natario1/CameraView/pull/497 \ No newline at end of file +[497]: https://github.com/natario1/CameraView/pull/497 +[498]: https://github.com/natario1/CameraView/pull/498 +[501]: https://github.com/natario1/CameraView/pull/501