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
pull/513/head
Suneet Agrawal 5 years ago committed by Mattia Iavarone
parent 389da0467f
commit 7d87d4af61
  1. 9
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java
  2. 188
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java
  3. 5
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/MockCameraEngine.java
  4. 6
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/video/VideoRecorderTest.java
  5. 11
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java
  6. 13
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  7. 3
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java
  8. 18
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java
  9. 1
      cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java
  10. 1
      cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java
  11. 17
      cameraview/src/main/java/com/otaliastudios/cameraview/video/VideoRecorder.java
  12. 2
      docs/_posts/2018-12-20-camera-events.md
  13. 11
      docs/_posts/2018-12-20-changelog.md

@ -155,6 +155,15 @@ public class CameraViewCallbacksTest extends BaseTest {
verify(listener, times(1)).onCameraClosed(); 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 @Test
public void testDispatchOnVideoTaken() { public void testDispatchOnVideoTaken() {
VideoResult.Stub stub = new VideoResult.Stub(); VideoResult.Stub stub = new VideoResult.Stub();

@ -5,7 +5,6 @@ import android.graphics.Bitmap;
import android.graphics.PointF; import android.graphics.PointF;
import android.hardware.Camera; import android.hardware.Camera;
import android.os.Build; import android.os.Build;
import android.util.Log;
import com.otaliastudios.cameraview.BaseTest; import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.CameraException; import com.otaliastudios.cameraview.CameraException;
@ -13,7 +12,6 @@ import com.otaliastudios.cameraview.CameraListener;
import com.otaliastudios.cameraview.CameraOptions; import com.otaliastudios.cameraview.CameraOptions;
import com.otaliastudios.cameraview.CameraUtils; import com.otaliastudios.cameraview.CameraUtils;
import com.otaliastudios.cameraview.CameraView; import com.otaliastudios.cameraview.CameraView;
import com.otaliastudios.cameraview.DoNotRunOnTravis;
import com.otaliastudios.cameraview.PictureResult; import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.TestActivity; import com.otaliastudios.cameraview.TestActivity;
import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.VideoResult;
@ -30,19 +28,14 @@ import com.otaliastudios.cameraview.internal.utils.WorkerHandler;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.annotation.Nullable;
import androidx.test.filters.LargeTest;
import androidx.test.filters.MediumTest;
import androidx.test.filters.SmallTest;
import androidx.test.rule.ActivityTestRule; import androidx.test.rule.ActivityTestRule;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatcher; import org.mockito.ArgumentMatcher;
import java.io.File; import java.io.File;
@ -103,6 +96,8 @@ public abstract class CameraIntegrationTest extends BaseTest {
rule.getActivity().inflate(camera); rule.getActivity().inflate(camera);
// Ensure that controller exceptions are thrown on this thread (not on the UI thread). // 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); uiExceptionOp = new Op<>(true);
WorkerHandler crashThread = WorkerHandler.get("CrashThread"); WorkerHandler crashThread = WorkerHandler.get("CrashThread");
crashThread.getThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { 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(); camera.open();
final Op<CameraOptions> open = new Op<>(true); final Op<CameraOptions> open = new Op<>(true);
doEndTask(open, 0).when(listener).onCameraOpened(any(CameraOptions.class)); doEndTask(open, 0).when(listener).onCameraOpened(any(CameraOptions.class));
@ -145,7 +140,7 @@ public abstract class CameraIntegrationTest extends BaseTest {
return result; return result;
} }
private void waitForClose(boolean expectSuccess) { private void closeSync(boolean expectSuccess) {
camera.close(); camera.close();
final Op<Boolean> close = new Op<>(true); final Op<Boolean> close = new Op<>(true);
doEndTask(close, true).when(listener).onCameraClosed(); doEndTask(close, true).when(listener).onCameraClosed();
@ -157,25 +152,28 @@ public abstract class CameraIntegrationTest extends BaseTest {
} }
} }
private void waitForVideoEnd(boolean expectSuccess) { @SuppressWarnings("UnusedReturnValue")
final Op<Boolean> video = new Op<>(true); @Nullable
doEndTask(video, true).when(listener).onVideoTaken(any(VideoResult.class)); private VideoResult waitForVideoResult(boolean expectSuccess) {
doEndTask(video, false).when(listener).onCameraError(argThat(new ArgumentMatcher<CameraException>() { final Op<VideoResult> video = new Op<>(true);
doEndTask(video, 0).when(listener).onVideoTaken(any(VideoResult.class));
doEndTask(video, null).when(listener).onCameraError(argThat(new ArgumentMatcher<CameraException>() {
@Override @Override
public boolean matches(CameraException argument) { public boolean matches(CameraException argument) {
return argument.getReason() == CameraException.REASON_VIDEO_FAILED; return argument.getReason() == CameraException.REASON_VIDEO_FAILED;
} }
})); }));
Boolean result = video.await(DELAY); VideoResult result = video.await(DELAY);
if (expectSuccess) { if (expectSuccess) {
assertNotNull("Should end video", result); assertNotNull("Should end video", result);
assertTrue("Should end video without errors", result);
} else { } else {
assertNull("Should not end video", result); assertNull("Should not end video", result);
} }
return result;
} }
private PictureResult waitForPicture(boolean expectSuccess) { @Nullable
private PictureResult waitForPictureResult(boolean expectSuccess) {
final Op<PictureResult> pic = new Op<>(true); final Op<PictureResult> pic = new Op<>(true);
doEndTask(pic, 0).when(listener).onPictureTaken(any(PictureResult.class)); doEndTask(pic, 0).when(listener).onPictureTaken(any(PictureResult.class));
doEndTask(pic, null).when(listener).onCameraError(argThat(new ArgumentMatcher<CameraException>() { doEndTask(pic, null).when(listener).onCameraError(argThat(new ArgumentMatcher<CameraException>() {
@ -193,12 +191,60 @@ public abstract class CameraIntegrationTest extends BaseTest {
return result; return result;
} }
private void waitForVideoStart() { private void takeVideoSync(boolean expectSuccess) {
controller.mStartVideoOp.listen(); takeVideoSync(expectSuccess,0);
}
private void takeVideoSync(boolean expectSuccess, int duration) {
final Op<Boolean> op = new Op<>(true);
doEndTask(op, true).when(listener).onVideoRecordingStart();
doEndTask(op, false).when(listener).onCameraError(argThat(new ArgumentMatcher<CameraException>() {
@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<Boolean> op = new Op<>(true);
doEndTask(op, true).when(listener).onVideoRecordingStart();
doEndTask(op, false).when(listener).onCameraError(argThat(new ArgumentMatcher<CameraException>() {
@Override
public boolean matches(CameraException argument) {
return argument.getReason() == CameraException.REASON_VIDEO_FAILED;
}
}));
File file = new File(context().getFilesDir(), "video.mp4"); File file = new File(context().getFilesDir(), "video.mp4");
camera.takeVideo(file); if (duration > 0) {
controller.mStartVideoOp.await(DELAY); camera.takeVideoSnapshot(file, duration);
// TODO assert! } 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 //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. // Starting and stopping are hard to get since they happen on another thread.
assertEquals(controller.getEngineState(), CameraEngine.STATE_STOPPED); assertEquals(controller.getEngineState(), CameraEngine.STATE_STOPPED);
waitForOpen(true); openSync(true);
assertEquals(controller.getEngineState(), CameraEngine.STATE_STARTED); assertEquals(controller.getEngineState(), CameraEngine.STATE_STARTED);
waitForClose(true); closeSync(true);
assertEquals(controller.getEngineState(), CameraEngine.STATE_STOPPED); assertEquals(controller.getEngineState(), CameraEngine.STATE_STOPPED);
} }
@Test @Test
public void testOpenTwice() { public void testOpenTwice() {
waitForOpen(true); openSync(true);
waitForOpen(false); openSync(false);
} }
@Test @Test
public void testCloseTwice() { public void testCloseTwice() {
waitForClose(false); closeSync(false);
} }
@Test @Test
@ -247,7 +293,7 @@ public abstract class CameraIntegrationTest extends BaseTest {
@Test @Test
public void testStartInitializesOptions() { public void testStartInitializesOptions() {
assertNull(camera.getCameraOptions()); assertNull(camera.getCameraOptions());
waitForOpen(true); openSync(true);
assertNotNull(camera.getCameraOptions()); assertNotNull(camera.getCameraOptions());
} }
@ -258,7 +304,7 @@ public abstract class CameraIntegrationTest extends BaseTest {
@Test @Test
public void testSetFacing() throws Exception { public void testSetFacing() throws Exception {
CameraOptions o = waitForOpen(true); CameraOptions o = openSync(true);
int size = o.getSupportedFacing().size(); int size = o.getSupportedFacing().size();
if (size > 1) { if (size > 1) {
// set facing should call stop and start again. // set facing should call stop and start again.
@ -276,7 +322,7 @@ public abstract class CameraIntegrationTest extends BaseTest {
@Test @Test
public void testSetMode() throws Exception { public void testSetMode() throws Exception {
camera.setMode(Mode.PICTURE); camera.setMode(Mode.PICTURE);
waitForOpen(true); openSync(true);
// set session type should call stop and start again. // set session type should call stop and start again.
final CountDownLatch latch = new CountDownLatch(2); final CountDownLatch latch = new CountDownLatch(2);
@ -297,7 +343,7 @@ public abstract class CameraIntegrationTest extends BaseTest {
@Test @Test
public void testSetZoom() { public void testSetZoom() {
CameraOptions options = waitForOpen(true); CameraOptions options = openSync(true);
controller.mZoomOp.listen(); controller.mZoomOp.listen();
float oldValue = camera.getZoom(); float oldValue = camera.getZoom();
@ -314,7 +360,7 @@ public abstract class CameraIntegrationTest extends BaseTest {
@Test @Test
public void testSetExposureCorrection() { public void testSetExposureCorrection() {
CameraOptions options = waitForOpen(true); CameraOptions options = openSync(true);
controller.mExposureCorrectionOp.listen(); controller.mExposureCorrectionOp.listen();
float oldValue = camera.getExposureCorrection(); float oldValue = camera.getExposureCorrection();
@ -331,7 +377,7 @@ public abstract class CameraIntegrationTest extends BaseTest {
@Test @Test
public void testSetFlash() { public void testSetFlash() {
CameraOptions options = waitForOpen(true); CameraOptions options = openSync(true);
Flash[] values = Flash.values(); Flash[] values = Flash.values();
Flash oldValue = camera.getFlash(); Flash oldValue = camera.getFlash();
for (Flash value : values) { for (Flash value : values) {
@ -349,7 +395,7 @@ public abstract class CameraIntegrationTest extends BaseTest {
@Test @Test
public void testSetWhiteBalance() { public void testSetWhiteBalance() {
CameraOptions options = waitForOpen(true); CameraOptions options = openSync(true);
WhiteBalance[] values = WhiteBalance.values(); WhiteBalance[] values = WhiteBalance.values();
WhiteBalance oldValue = camera.getWhiteBalance(); WhiteBalance oldValue = camera.getWhiteBalance();
for (WhiteBalance value : values) { for (WhiteBalance value : values) {
@ -367,7 +413,7 @@ public abstract class CameraIntegrationTest extends BaseTest {
@Test @Test
public void testSetHdr() { public void testSetHdr() {
CameraOptions options = waitForOpen(true); CameraOptions options = openSync(true);
Hdr[] values = Hdr.values(); Hdr[] values = Hdr.values();
Hdr oldValue = camera.getHdr(); Hdr oldValue = camera.getHdr();
for (Hdr value : values) { for (Hdr value : values) {
@ -386,7 +432,7 @@ public abstract class CameraIntegrationTest extends BaseTest {
@Test @Test
public void testSetAudio() { public void testSetAudio() {
// TODO: when permissions are managed, check that Audio.ON triggers the audio permission // TODO: when permissions are managed, check that Audio.ON triggers the audio permission
waitForOpen(true); openSync(true);
Audio[] values = Audio.values(); Audio[] values = Audio.values();
for (Audio value : values) { for (Audio value : values) {
camera.setAudio(value); camera.setAudio(value);
@ -396,7 +442,7 @@ public abstract class CameraIntegrationTest extends BaseTest {
@Test @Test
public void testSetLocation() { public void testSetLocation() {
waitForOpen(true); openSync(true);
controller.mLocationOp.listen(); controller.mLocationOp.listen();
camera.setLocation(10d, 2d); camera.setLocation(10d, 2d);
controller.mLocationOp.await(300); controller.mLocationOp.await(300);
@ -440,8 +486,8 @@ public abstract class CameraIntegrationTest extends BaseTest {
// Error while starting MediaRecorder. java.lang.RuntimeException: start failed. // Error while starting MediaRecorder. java.lang.RuntimeException: start failed.
// as documented. This works locally though. // as documented. This works locally though.
camera.setMode(Mode.PICTURE); camera.setMode(Mode.PICTURE);
waitForOpen(true); openSync(true);
waitForVideoStart(); takeVideoSync(false);
waitForUiException(); waitForUiException();
} }
@ -451,35 +497,35 @@ public abstract class CameraIntegrationTest extends BaseTest {
// Error while starting MediaRecorder. java.lang.RuntimeException: start failed. // Error while starting MediaRecorder. java.lang.RuntimeException: start failed.
// as documented. This works locally though. // as documented. This works locally though.
camera.setMode(Mode.VIDEO); camera.setMode(Mode.VIDEO);
waitForOpen(true); openSync(true);
camera.takeVideo(new File(context().getFilesDir(), "video.mp4"), 4000); takeVideoSync(true, 4000);
waitForVideoEnd(true); waitForVideoResult(true);
} }
@Test @Test
public void testEndVideo_withoutStarting() { public void testEndVideo_withoutStarting() {
camera.setMode(Mode.VIDEO); camera.setMode(Mode.VIDEO);
waitForOpen(true); openSync(true);
camera.stopVideo(); camera.stopVideo();
waitForVideoEnd(false); waitForVideoResult(false);
} }
@Test @Test
public void testEndVideo_withMaxSize() { public void testEndVideo_withMaxSize() {
camera.setMode(Mode.VIDEO); camera.setMode(Mode.VIDEO);
camera.setVideoMaxSize(3000*1000); // Less is risky camera.setVideoMaxSize(3000*1000); // Less is risky
waitForOpen(true); openSync(true);
waitForVideoStart(); takeVideoSync(true);
waitForVideoEnd(true); waitForVideoResult(true);
} }
@Test @Test
public void testEndVideo_withMaxDuration() { public void testEndVideo_withMaxDuration() {
camera.setMode(Mode.VIDEO); camera.setMode(Mode.VIDEO);
camera.setVideoMaxDuration(4000); camera.setVideoMaxDuration(4000);
waitForOpen(true); openSync(true);
waitForVideoStart(); takeVideoSync(true);
waitForVideoEnd(true); waitForVideoResult(true);
} }
//endregion //endregion
@ -488,7 +534,7 @@ public abstract class CameraIntegrationTest extends BaseTest {
@Test @Test
public void testStartAutoFocus() { public void testStartAutoFocus() {
CameraOptions o = waitForOpen(true); CameraOptions o = openSync(true);
final Op<PointF> focus = new Op<>(true); final Op<PointF> focus = new Op<>(true);
doEndTask(focus, 0).when(listener).onAutoFocusStart(any(PointF.class)); doEndTask(focus, 0).when(listener).onAutoFocusStart(any(PointF.class));
@ -505,7 +551,7 @@ public abstract class CameraIntegrationTest extends BaseTest {
@Test @Test
public void testStopAutoFocus() { public void testStopAutoFocus() {
CameraOptions o = waitForOpen(true); CameraOptions o = openSync(true);
final Op<PointF> focus = new Op<>(true); final Op<PointF> focus = new Op<>(true);
doEndTask(focus, 1).when(listener).onAutoFocusEnd(anyBoolean(), any(PointF.class)); doEndTask(focus, 1).when(listener).onAutoFocusEnd(anyBoolean(), any(PointF.class));
@ -528,13 +574,13 @@ public abstract class CameraIntegrationTest extends BaseTest {
@Test @Test
public void testCapturePicture_beforeStarted() { public void testCapturePicture_beforeStarted() {
camera.takePicture(); camera.takePicture();
waitForPicture(false); waitForPictureResult(false);
} }
@Test @Test
public void testCapturePicture_concurrentCalls() throws Exception { public void testCapturePicture_concurrentCalls() throws Exception {
// Second take should fail. // Second take should fail.
waitForOpen(true); openSync(true);
CountDownLatch latch = new CountDownLatch(2); CountDownLatch latch = new CountDownLatch(2);
doCountDown(latch).when(listener).onPictureTaken(any(PictureResult.class)); doCountDown(latch).when(listener).onPictureTaken(any(PictureResult.class));
@ -548,12 +594,12 @@ public abstract class CameraIntegrationTest extends BaseTest {
@Test @Test
public void testCapturePicture_size() throws Exception { public void testCapturePicture_size() throws Exception {
waitForOpen(true); openSync(true);
// PictureSize can still be null after opened. // PictureSize can still be null after opened.
while (camera.getPictureSize() == null) {} while (camera.getPictureSize() == null) {}
Size size = camera.getPictureSize(); Size size = camera.getPictureSize();
camera.takePicture(); camera.takePicture();
PictureResult result = waitForPicture(true); PictureResult result = waitForPictureResult(true);
Bitmap bitmap = CameraUtils.decodeBitmap(result.getData(), Integer.MAX_VALUE, Integer.MAX_VALUE); Bitmap bitmap = CameraUtils.decodeBitmap(result.getData(), Integer.MAX_VALUE, Integer.MAX_VALUE);
assertEquals(result.getSize(), size); assertEquals(result.getSize(), size);
assertEquals(bitmap.getWidth(), size.getWidth()); assertEquals(bitmap.getWidth(), size.getWidth());
@ -566,7 +612,7 @@ public abstract class CameraIntegrationTest extends BaseTest {
@Test(expected = RuntimeException.class) @Test(expected = RuntimeException.class)
public void testCapturePicture_whileInVideoMode() throws Throwable { public void testCapturePicture_whileInVideoMode() throws Throwable {
camera.setMode(Mode.VIDEO); camera.setMode(Mode.VIDEO);
waitForOpen(true); openSync(true);
camera.takePicture(); camera.takePicture();
waitForUiException(); waitForUiException();
} }
@ -574,13 +620,13 @@ public abstract class CameraIntegrationTest extends BaseTest {
@Test @Test
public void testCaptureSnapshot_beforeStarted() { public void testCaptureSnapshot_beforeStarted() {
camera.takePictureSnapshot(); camera.takePictureSnapshot();
waitForPicture(false); waitForPictureResult(false);
} }
@Test @Test
public void testCaptureSnapshot_concurrentCalls() throws Exception { public void testCaptureSnapshot_concurrentCalls() throws Exception {
// Second take should fail. // Second take should fail.
waitForOpen(true); openSync(true);
CountDownLatch latch = new CountDownLatch(2); CountDownLatch latch = new CountDownLatch(2);
doCountDown(latch).when(listener).onPictureTaken(any(PictureResult.class)); doCountDown(latch).when(listener).onPictureTaken(any(PictureResult.class));
@ -594,13 +640,13 @@ public abstract class CameraIntegrationTest extends BaseTest {
@Test @Test
public void testCaptureSnapshot_size() throws Exception { public void testCaptureSnapshot_size() throws Exception {
waitForOpen(true); openSync(true);
// SnapshotSize can still be null after opened. // SnapshotSize can still be null after opened.
while (camera.getSnapshotSize() == null) {} while (camera.getSnapshotSize() == null) {}
Size size = camera.getSnapshotSize(); Size size = camera.getSnapshotSize();
camera.takePictureSnapshot(); camera.takePictureSnapshot();
PictureResult result = waitForPicture(true); PictureResult result = waitForPictureResult(true);
Bitmap bitmap = CameraUtils.decodeBitmap(result.getData(), Integer.MAX_VALUE, Integer.MAX_VALUE); Bitmap bitmap = CameraUtils.decodeBitmap(result.getData(), Integer.MAX_VALUE, Integer.MAX_VALUE);
assertEquals(result.getSize(), size); assertEquals(result.getSize(), size);
assertEquals(bitmap.getWidth(), size.getWidth()); assertEquals(bitmap.getWidth(), size.getWidth());
@ -626,7 +672,7 @@ public abstract class CameraIntegrationTest extends BaseTest {
public void testFrameProcessing_simple() throws Exception { public void testFrameProcessing_simple() throws Exception {
FrameProcessor processor = mock(FrameProcessor.class); FrameProcessor processor = mock(FrameProcessor.class);
camera.addFrameProcessor(processor); camera.addFrameProcessor(processor);
waitForOpen(true); openSync(true);
assert30Frames(processor); assert30Frames(processor);
} }
@ -635,12 +681,12 @@ public abstract class CameraIntegrationTest extends BaseTest {
public void testFrameProcessing_afterSnapshot() throws Exception { public void testFrameProcessing_afterSnapshot() throws Exception {
FrameProcessor processor = mock(FrameProcessor.class); FrameProcessor processor = mock(FrameProcessor.class);
camera.addFrameProcessor(processor); camera.addFrameProcessor(processor);
waitForOpen(true); openSync(true);
// In Camera1, snapshots will clear the preview callback // In Camera1, snapshots will clear the preview callback
// Ensure we restore correctly // Ensure we restore correctly
camera.takePictureSnapshot(); camera.takePictureSnapshot();
waitForPicture(true); waitForPictureResult(true);
assert30Frames(processor); assert30Frames(processor);
} }
@ -649,9 +695,9 @@ public abstract class CameraIntegrationTest extends BaseTest {
public void testFrameProcessing_afterRestart() throws Exception { public void testFrameProcessing_afterRestart() throws Exception {
FrameProcessor processor = mock(FrameProcessor.class); FrameProcessor processor = mock(FrameProcessor.class);
camera.addFrameProcessor(processor); camera.addFrameProcessor(processor);
waitForOpen(true); openSync(true);
waitForClose(true); closeSync(true);
waitForOpen(true); openSync(true);
assert30Frames(processor); assert30Frames(processor);
} }
@ -662,9 +708,9 @@ public abstract class CameraIntegrationTest extends BaseTest {
FrameProcessor processor = mock(FrameProcessor.class); FrameProcessor processor = mock(FrameProcessor.class);
camera.addFrameProcessor(processor); camera.addFrameProcessor(processor);
camera.setMode(Mode.VIDEO); camera.setMode(Mode.VIDEO);
waitForOpen(true); openSync(true);
camera.takeVideo(new File(context().getFilesDir(), "video.mp4"), 4000); takeVideoSync(true,4000);
waitForVideoEnd(true); waitForVideoResult(true);
assert30Frames(processor); assert30Frames(processor);
} }
@ -676,7 +722,7 @@ public abstract class CameraIntegrationTest extends BaseTest {
FrameProcessor source = new FreezeReleaseFrameProcessor(); FrameProcessor source = new FreezeReleaseFrameProcessor();
FrameProcessor processor = spy(source); FrameProcessor processor = spy(source);
camera.addFrameProcessor(processor); camera.addFrameProcessor(processor);
waitForOpen(true); openSync(true);
assert30Frames(processor); assert30Frames(processor);
} }

@ -177,4 +177,9 @@ public class MockCameraEngine extends CameraEngine {
protected boolean collectCameraInfo(@NonNull Facing facing) { protected boolean collectCameraInfo(@NonNull Facing facing) {
return true; return true;
} }
@Override
public void onVideoRecordingStart() {
}
} }

@ -2,9 +2,7 @@ package com.otaliastudios.cameraview.video;
import com.otaliastudios.cameraview.BaseTest; import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.video.VideoRecorder;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest; import androidx.test.filters.SmallTest;
@ -29,7 +27,7 @@ public class VideoRecorderTest extends BaseTest {
VideoRecorder.VideoResultListener listener = Mockito.mock(VideoRecorder.VideoResultListener.class); VideoRecorder.VideoResultListener listener = Mockito.mock(VideoRecorder.VideoResultListener.class);
VideoRecorder recorder = new VideoRecorder(listener) { VideoRecorder recorder = new VideoRecorder(listener) {
@Override @Override
protected void onStart() { } protected void onStart() { dispatchVideoRecordingStart(); }
@Override @Override
protected void onStop() { protected void onStop() {
@ -37,6 +35,8 @@ public class VideoRecorderTest extends BaseTest {
} }
}; };
recorder.start(result); recorder.start(result);
Mockito.verify(listener,Mockito.times(1) )
.onVideoRecordingStart();
recorder.stop(); recorder.stop();
Mockito.verify(listener, Mockito.times(1)) Mockito.verify(listener, Mockito.times(1))
.onVideoResult(result, null); .onVideoResult(result, null);

@ -127,4 +127,15 @@ public abstract class CameraListener {
@UiThread @UiThread
public void onExposureCorrectionChanged(float newValue, @NonNull float[] bounds, @Nullable PointF[] fingers) { } 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() {
}
} }

@ -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 //endregion

@ -181,6 +181,9 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
mRepeatingRequestBuilder.addTarget(mFrameProcessingSurface); mRepeatingRequestBuilder.addTarget(mFrameProcessingSurface);
} }
for (Surface extraSurface : extraSurfaces) { for (Surface extraSurface : extraSurfaces) {
if (extraSurface == null) {
throw new IllegalArgumentException("Should not add a null surface.");
}
mRepeatingRequestBuilder.addTarget(extraSurface); mRepeatingRequestBuilder.addTarget(extraSurface);
} }
} }

@ -134,6 +134,7 @@ public abstract class CameraEngine implements
void dispatchOnExposureCorrectionChanged(float newValue, @NonNull float[] bounds, @Nullable PointF[] fingers); void dispatchOnExposureCorrectionChanged(float newValue, @NonNull float[] bounds, @Nullable PointF[] fingers);
void dispatchFrame(Frame frame); void dispatchFrame(Frame frame);
void dispatchError(CameraException exception); void dispatchError(CameraException exception);
void dispatchOnVideoRecordingStart();
} }
private static final String TAG = CameraEngine.class.getSimpleName(); private static final String TAG = CameraEngine.class.getSimpleName();
@ -198,7 +199,6 @@ public abstract class CameraEngine implements
private Step mAllStep = new Step("all", mStepCallback); private Step mAllStep = new Step("all", mStepCallback);
// Ops used for testing. // Ops used for testing.
@VisibleForTesting Op<Void> mStartVideoOp = new Op<>();
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op<Void> mZoomOp = new Op<>(); @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op<Void> mZoomOp = new Op<>();
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op<Void> mExposureCorrectionOp = new Op<>(); @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op<Void> mExposureCorrectionOp = new Op<>();
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op<Void> mFlashOp = new Op<>(); @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Op<Void> mFlashOp = new Op<>();
@ -1120,8 +1120,8 @@ public abstract class CameraEngine implements
@Override @Override
public void run() { public void run() {
LOG.v("takeVideo", "performing. BindState:", getBindState(), "isTakingVideo:", isTakingVideo()); LOG.v("takeVideo", "performing. BindState:", getBindState(), "isTakingVideo:", isTakingVideo());
if (getBindState() < STATE_STARTED) { mStartVideoOp.end(null); return; } if (getBindState() < STATE_STARTED) return;
if (isTakingVideo()) { mStartVideoOp.end(null); return; } if (isTakingVideo()) return;
if (mMode == Mode.PICTURE) { if (mMode == Mode.PICTURE) {
throw new IllegalStateException("Can't record video while in PICTURE mode"); throw new IllegalStateException("Can't record video while in PICTURE mode");
} }
@ -1136,7 +1136,6 @@ public abstract class CameraEngine implements
stub.videoBitRate = mVideoBitRate; stub.videoBitRate = mVideoBitRate;
stub.audioBitRate = mAudioBitRate; stub.audioBitRate = mAudioBitRate;
onTakeVideo(stub); onTakeVideo(stub);
mStartVideoOp.end(null);
} }
}); });
} }
@ -1152,8 +1151,8 @@ public abstract class CameraEngine implements
@Override @Override
public void run() { public void run() {
LOG.v("takeVideoSnapshot", "performing. BindState:", getBindState(), "isTakingVideo:", isTakingVideo()); LOG.v("takeVideoSnapshot", "performing. BindState:", getBindState(), "isTakingVideo:", isTakingVideo());
if (getBindState() < STATE_STARTED) { mStartVideoOp.end(null); return; } if (getBindState() < STATE_STARTED) return;
if (isTakingVideo()) { mStartVideoOp.end(null); return; } if (isTakingVideo()) return;
stub.file = file; stub.file = file;
stub.isSnapshot = true; stub.isSnapshot = true;
stub.videoCodec = mVideoCodec; stub.videoCodec = mVideoCodec;
@ -1165,7 +1164,6 @@ public abstract class CameraEngine implements
stub.maxSize = mVideoMaxSize; stub.maxSize = mVideoMaxSize;
stub.maxDuration = mVideoMaxDuration; stub.maxDuration = mVideoMaxDuration;
onTakeVideoSnapshot(stub, viewAspectRatio); onTakeVideoSnapshot(stub, viewAspectRatio);
mStartVideoOp.end(null);
} }
}); });
} }
@ -1196,6 +1194,12 @@ public abstract class CameraEngine implements
} }
} }
@Override
public void onVideoRecordingStart() {
mCallback.dispatchOnVideoRecordingStart();
}
@WorkerThread @WorkerThread
protected abstract void onTakePicture(@NonNull PictureResult.Stub stub); protected abstract void onTakePicture(@NonNull PictureResult.Stub stub);

@ -130,6 +130,7 @@ public abstract class FullVideoRecorder extends VideoRecorder {
try { try {
mMediaRecorder.start(); mMediaRecorder.start();
dispatchVideoRecordingStart();
} catch (Exception e) { } catch (Exception e) {
LOG.w("start:", "Error while starting media recorder.", e); LOG.w("start:", "Error while starting media recorder.", e);
mResult = null; mResult = null;

@ -60,6 +60,7 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
mPreview.addRendererFrameCallback(this); mPreview.addRendererFrameCallback(this);
mFlipped = mEngine.getAngles().flip(Reference.SENSOR, Reference.VIEW); mFlipped = mEngine.getAngles().flip(Reference.SENSOR, Reference.VIEW);
mDesiredState = STATE_RECORDING; mDesiredState = STATE_RECORDING;
dispatchVideoRecordingStart();
} }
@Override @Override

@ -24,6 +24,11 @@ public abstract class VideoRecorder {
* @param exception the error or null if everything went fine * @param exception the error or null if everything went fine
*/ */
void onVideoResult(@Nullable VideoResult.Stub result, @Nullable Exception exception); 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; @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) VideoResult.Stub mResult;
@ -84,4 +89,16 @@ public abstract class VideoRecorder {
mResult = null; mResult = null;
mError = 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();
}
}
} }

@ -37,6 +37,8 @@ camera.addCameraListener(new CameraListener() {
public void onZoomChanged(float newValue, float[] bounds, PointF[] fingers) {} public void onZoomChanged(float newValue, float[] bounds, PointF[] fingers) {}
public void onExposureCorrectionChanged(float newValue, float[] bounds, PointF[] fingers) {} public void onExposureCorrectionChanged(float newValue, float[] bounds, PointF[] fingers) {}
public void onVideoRecordingStart() {}
}); });
``` ```

@ -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. 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 ### 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). 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 [cneuwirt]: https://github.com/cneuwirt
[agrawalsuneet]: https://github.com/agrawalsuneet
[356]: https://github.com/natario1/CameraView/pull/356 [356]: https://github.com/natario1/CameraView/pull/356
[360]: https://github.com/natario1/CameraView/pull/360 [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 [482]: https://github.com/natario1/CameraView/pull/482
[484]: https://github.com/natario1/CameraView/pull/484 [484]: https://github.com/natario1/CameraView/pull/484
[490]: https://github.com/natario1/CameraView/pull/490 [490]: https://github.com/natario1/CameraView/pull/490
[497]: https://github.com/natario1/CameraView/pull/497 [497]: https://github.com/natario1/CameraView/pull/497
[498]: https://github.com/natario1/CameraView/pull/498
[501]: https://github.com/natario1/CameraView/pull/501

Loading…
Cancel
Save