From 7d87d4af61a98befcae86db428b3e36f7624fb85 Mon Sep 17 00:00:00 2001 From: Suneet Agrawal Date: Mon, 15 Jul 2019 22:19:38 +0700 Subject: [PATCH] Actual Video recording callback (#498) * added video recording callback * updated test * added annotation to dispatch function * updated review comments for video recording callback * reverted unwanted changes * Add tests and changelog * Fix video tests --- .../cameraview/CameraViewCallbacksTest.java | 9 + .../engine/CameraIntegrationTest.java | 188 +++++++++++------- .../cameraview/engine/MockCameraEngine.java | 5 + .../cameraview/video/VideoRecorderTest.java | 6 +- .../cameraview/CameraListener.java | 11 + .../otaliastudios/cameraview/CameraView.java | 13 ++ .../cameraview/engine/Camera2Engine.java | 3 + .../cameraview/engine/CameraEngine.java | 18 +- .../cameraview/video/FullVideoRecorder.java | 1 + .../video/SnapshotVideoRecorder.java | 1 + .../cameraview/video/VideoRecorder.java | 17 ++ docs/_posts/2018-12-20-camera-events.md | 2 + docs/_posts/2018-12-20-changelog.md | 11 +- 13 files changed, 201 insertions(+), 84 deletions(-) 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..f3300011 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; @@ -103,6 +96,8 @@ public abstract class CameraIntegrationTest extends BaseTest { rule.getActivity().inflate(camera); // Ensure that controller exceptions are thrown on this thread (not on the UI thread). + // TODO this makes debugging for wrong tests very hard, as we don't get the exception + // unless waitForUiException() is called. uiExceptionOp = new Op<>(true); WorkerHandler crashThread = WorkerHandler.get("CrashThread"); crashThread.getThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { @@ -130,7 +125,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 +140,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 +152,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 +191,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 +254,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 +293,7 @@ public abstract class CameraIntegrationTest extends BaseTest { @Test public void testStartInitializesOptions() { assertNull(camera.getCameraOptions()); - waitForOpen(true); + openSync(true); assertNotNull(camera.getCameraOptions()); } @@ -258,7 +304,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 +322,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 +343,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 +360,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 +377,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 +395,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 +413,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 +432,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 +442,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 +486,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 +497,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 +534,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 +551,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 +574,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 +594,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 +612,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 +620,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 +640,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 +672,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 +681,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 +695,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 +708,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 +722,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/androidTest/java/com/otaliastudios/cameraview/engine/MockCameraEngine.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/MockCameraEngine.java index 3c2be982..770d4b95 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/MockCameraEngine.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/MockCameraEngine.java @@ -177,4 +177,9 @@ public class MockCameraEngine extends CameraEngine { protected boolean collectCameraInfo(@NonNull Facing facing) { return true; } + + @Override + public void onVideoRecordingStart() { + + } } diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/video/VideoRecorderTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/video/VideoRecorderTest.java index 8e6420da..c8300f0c 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/video/VideoRecorderTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/video/VideoRecorderTest.java @@ -2,9 +2,7 @@ package com.otaliastudios.cameraview.video; import com.otaliastudios.cameraview.BaseTest; -import com.otaliastudios.cameraview.PictureResult; import com.otaliastudios.cameraview.VideoResult; -import com.otaliastudios.cameraview.video.VideoRecorder; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -29,7 +27,7 @@ public class VideoRecorderTest extends BaseTest { VideoRecorder.VideoResultListener listener = Mockito.mock(VideoRecorder.VideoResultListener.class); VideoRecorder recorder = new VideoRecorder(listener) { @Override - protected void onStart() { } + protected void onStart() { dispatchVideoRecordingStart(); } @Override protected void onStop() { @@ -37,6 +35,8 @@ public class VideoRecorderTest extends BaseTest { } }; recorder.start(result); + Mockito.verify(listener,Mockito.times(1) ) + .onVideoRecordingStart(); recorder.stop(); Mockito.verify(listener, Mockito.times(1)) .onVideoResult(result, null); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java index 8a51ef36..ab4ec2ce 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java @@ -127,4 +127,15 @@ public abstract class CameraListener { @UiThread public void onExposureCorrectionChanged(float newValue, @NonNull float[] bounds, @Nullable PointF[] fingers) { } + + /** + * Notifies that the actual video recording has started + * This is the time when actual frames recording starts. + * This can be used to show some indicator while the actual video recording. + */ + @UiThread + public void onVideoRecordingStart() { + + } + } \ No newline at end of file diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java index f7d5c08e..2c6292fa 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java @@ -2062,6 +2062,19 @@ public class CameraView extends FrameLayout implements LifecycleObserver { } }); } + + @Override + public void dispatchOnVideoRecordingStart() { + mLogger.i("dispatchOnVideoRecordingStart", "dispatchOnVideoRecordingStart"); + mUiHandler.post(new Runnable() { + @Override + public void run() { + for (CameraListener listener : mListeners) { + listener.onVideoRecordingStart(); + } + } + }); + } } //endregion diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java index 306954a3..964eb976 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java @@ -181,6 +181,9 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv mRepeatingRequestBuilder.addTarget(mFrameProcessingSurface); } for (Surface extraSurface : extraSurfaces) { + if (extraSurface == null) { + throw new IllegalArgumentException("Should not add a null surface."); + } mRepeatingRequestBuilder.addTarget(extraSurface); } } 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 4eea126a..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,6 +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 dispatchOnVideoRecordingStart(); } private static final String TAG = CameraEngine.class.getSimpleName(); @@ -198,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<>(); @@ -1120,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"); } @@ -1136,7 +1136,6 @@ public abstract class CameraEngine implements stub.videoBitRate = mVideoBitRate; stub.audioBitRate = mAudioBitRate; onTakeVideo(stub); - mStartVideoOp.end(null); } }); } @@ -1152,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; @@ -1165,7 +1164,6 @@ public abstract class CameraEngine implements stub.maxSize = mVideoMaxSize; stub.maxDuration = mVideoMaxDuration; onTakeVideoSnapshot(stub, viewAspectRatio); - mStartVideoOp.end(null); } }); } @@ -1196,6 +1194,12 @@ public abstract class CameraEngine implements } } + @Override + public void onVideoRecordingStart() { + mCallback.dispatchOnVideoRecordingStart(); + } + + @WorkerThread protected abstract void onTakePicture(@NonNull PictureResult.Stub stub); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java index b21d6b63..77fd4e19 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java @@ -130,6 +130,7 @@ public abstract class FullVideoRecorder extends VideoRecorder { try { mMediaRecorder.start(); + dispatchVideoRecordingStart(); } catch (Exception e) { LOG.w("start:", "Error while starting media recorder.", e); mResult = null; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java index 8a73aa76..83eab2dd 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java @@ -60,6 +60,7 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram mPreview.addRendererFrameCallback(this); mFlipped = mEngine.getAngles().flip(Reference.SENSOR, Reference.VIEW); mDesiredState = STATE_RECORDING; + dispatchVideoRecordingStart(); } @Override diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/VideoRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/VideoRecorder.java index 08f8870d..37c61be6 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/VideoRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/VideoRecorder.java @@ -24,6 +24,11 @@ public abstract class VideoRecorder { * @param exception the error or null if everything went fine */ void onVideoResult(@Nullable VideoResult.Stub result, @Nullable Exception exception); + + /** + * The callback for the actual video recording starting. + */ + void onVideoRecordingStart(); } @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) VideoResult.Stub mResult; @@ -84,4 +89,16 @@ public abstract class VideoRecorder { mResult = null; mError = null; } + + /** + * Subclasses can call this to notify that the video recording has started, + * this will be called when camera is prepared and started. + */ + @SuppressWarnings("WeakerAccess") + @CallSuper + protected void dispatchVideoRecordingStart(){ + if(mListener != null){ + mListener.onVideoRecordingStart(); + } + } } diff --git a/docs/_posts/2018-12-20-camera-events.md b/docs/_posts/2018-12-20-camera-events.md index f0ec6147..0f83728f 100644 --- a/docs/_posts/2018-12-20-camera-events.md +++ b/docs/_posts/2018-12-20-camera-events.md @@ -37,6 +37,8 @@ camera.addCameraListener(new CameraListener() { public void onZoomChanged(float newValue, float[] bounds, PointF[] fingers) {} public void onExposureCorrectionChanged(float newValue, float[] bounds, PointF[] fingers) {} + + public void onVideoRecordingStart() {} }); ``` 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