diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java index b6258ca7..917fc9e7 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java @@ -164,6 +164,15 @@ public class CameraViewCallbacksTest extends BaseTest { verify(listener, times(1)).onVideoRecordingStart(); } + @Test + public void testDispatchOnVideoRecordingEnd() { + completeTask().when(listener).onVideoRecordingEnd(); + camera.mCameraCallbacks.dispatchOnVideoRecordingEnd(); + + assertNotNull(op.await(500)); + verify(listener, times(1)).onVideoRecordingEnd(); + } + @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 3b9fd5a2..76339f99 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java @@ -167,6 +167,11 @@ public abstract class CameraIntegrationTest extends BaseTest { @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<>(true); doEndTask(video, 0).when(listener).onVideoTaken(any(VideoResult.class)); doEndTask(video, null).when(listener).onCameraError(argThat(new ArgumentMatcher() { @@ -175,8 +180,11 @@ public abstract class CameraIntegrationTest extends BaseTest { return argument.getReason() == CameraException.REASON_VIDEO_FAILED; } })); + + // Wait for onVideoTaken and check. VideoResult result = video.await(VIDEO_DELAY); if (expectSuccess) { + assertEquals("Should call onVideoRecordingEnd", 0, onVideoRecordingEnd.getCount()); assertNotNull("Should end video", result); } else { assertNull("Should not end video", result); 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 770d4b95..3c2be982 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/MockCameraEngine.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/MockCameraEngine.java @@ -177,9 +177,4 @@ 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 c8300f0c..c3930c70 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/video/VideoRecorderTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/video/VideoRecorderTest.java @@ -27,10 +27,13 @@ public class VideoRecorderTest extends BaseTest { VideoRecorder.VideoResultListener listener = Mockito.mock(VideoRecorder.VideoResultListener.class); VideoRecorder recorder = new VideoRecorder(listener) { @Override - protected void onStart() { dispatchVideoRecordingStart(); } + protected void onStart() { + dispatchVideoRecordingStart(); + } @Override protected void onStop() { + dispatchVideoRecordingEnd(); dispatchResult(); } }; @@ -38,6 +41,8 @@ public class VideoRecorderTest extends BaseTest { Mockito.verify(listener,Mockito.times(1) ) .onVideoRecordingStart(); recorder.stop(); + Mockito.verify(listener, Mockito.times(1)) + .onVideoRecordingEnd(); 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 ab4ec2ce..0a1f774d 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java @@ -129,13 +129,30 @@ public abstract class CameraListener { /** - * Notifies that the actual video recording has started + * 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. + * + * This can be used to show some UI indicator for video recording or counting time. + * + * @see #onVideoRecordingEnd() */ @UiThread public void onVideoRecordingStart() { } + /** + * Notifies that the actual video recording has ended. + * At this point recording has ended, though the file might still be processed. + * The {@link #onVideoTaken(VideoResult)} callback will be called soon. + * + * This can be used to remove UI indicators for video recording. + * + * @see #onVideoRecordingStart() + */ + @UiThread + public void onVideoRecordingEnd() { + + } + } \ 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 e182b66f..d7eed69c 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java @@ -2080,7 +2080,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { @Override public void dispatchOnVideoRecordingStart() { - mLogger.i("dispatchOnVideoRecordingStart", "dispatchOnVideoRecordingStart"); + mLogger.i("dispatchOnVideoRecordingStart"); mUiHandler.post(new Runnable() { @Override public void run() { @@ -2090,6 +2090,19 @@ public class CameraView extends FrameLayout implements LifecycleObserver { } }); } + + @Override + public void dispatchOnVideoRecordingEnd() { + mLogger.i("dispatchOnVideoRecordingEnd"); + mUiHandler.post(new Runnable() { + @Override + public void run() { + for (CameraListener listener : mListeners) { + listener.onVideoRecordingEnd(); + } + } + }); + } } //endregion 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 3a82de46..2d2b1d60 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java @@ -136,6 +136,7 @@ public abstract class CameraEngine implements void dispatchFrame(Frame frame); void dispatchError(CameraException exception); void dispatchOnVideoRecordingStart(); + void dispatchOnVideoRecordingEnd(); } private static final String TAG = CameraEngine.class.getSimpleName(); @@ -1210,6 +1211,10 @@ public abstract class CameraEngine implements mCallback.dispatchOnVideoRecordingStart(); } + @Override + public void onVideoRecordingEnd() { + mCallback.dispatchOnVideoRecordingEnd(); + } @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 69bb3567..abd73fcd 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java @@ -148,6 +148,7 @@ public abstract class FullVideoRecorder extends VideoRecorder { @Override protected void onStop() { if (mMediaRecorder != null) { + dispatchVideoRecordingEnd(); try { mMediaRecorder.stop(); } catch (Exception e) { 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 5f73fe57..8031dad0 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java @@ -81,7 +81,6 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram protected void onStart() { mPreview.addRendererFrameCallback(this); mDesiredState = STATE_RECORDING; - dispatchVideoRecordingStart(); } @Override @@ -210,7 +209,12 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram @Override public void onEncodingStart() { - // Do nothing. + dispatchVideoRecordingStart(); + } + + @Override + public void onEncodingStop() { + dispatchVideoRecordingEnd(); } @EncoderThread 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 37c61be6..02b52876 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/VideoRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/VideoRecorder.java @@ -29,10 +29,16 @@ public abstract class VideoRecorder { * The callback for the actual video recording starting. */ void onVideoRecordingStart(); + + /** + * Video recording has ended. We will finish processing the file + * and soon {@link #onVideoResult(VideoResult.Stub, Exception)} will be called. + */ + void onVideoRecordingEnd(); } @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) VideoResult.Stub mResult; - @VisibleForTesting final VideoResultListener mListener; + private final VideoResultListener mListener; @SuppressWarnings("WeakerAccess") protected Exception mError; private boolean mIsRecording; @@ -96,9 +102,20 @@ public abstract class VideoRecorder { */ @SuppressWarnings("WeakerAccess") @CallSuper - protected void dispatchVideoRecordingStart(){ - if(mListener != null){ + protected void dispatchVideoRecordingStart() { + if (mListener != null) { mListener.onVideoRecordingStart(); } } + + /** + * Subclasses can call this to notify that the video recording has ended, + * although the video result might still be processed. + */ + @CallSuper + protected void dispatchVideoRecordingEnd() { + if (mListener != null) { + mListener.onVideoRecordingEnd(); + } + } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioMediaEncoder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioMediaEncoder.java index fc12420c..e01fe1d2 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioMediaEncoder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioMediaEncoder.java @@ -22,7 +22,6 @@ import java.util.concurrent.LinkedBlockingQueue; /** * Default implementation for audio encoding. */ -// TODO create onVideoRecordingEnd callback @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) public class AudioMediaEncoder extends MediaEncoder { diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoderEngine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoderEngine.java index 4e53dd35..904e668f 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoderEngine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoderEngine.java @@ -68,7 +68,17 @@ public class MediaEncoderEngine { void onEncodingStart(); /** - * Called when encoding stopped for some reason. + * Called when encoding stopped. At this point the mxuer might still be processing, + * but we have stopped receiving input (recording video and audio frames). + * + * The {@link #onEncodingEnd(int, Exception)} callback will soon be called + * with the results. + */ + @EncoderThread + void onEncodingStop(); + + /** + * Called when encoding ended for some reason. * If there's an exception, it failed. * @param reason the reason * @param e the error, if present @@ -353,6 +363,9 @@ public class MediaEncoderEngine { LOG.w("notifyStopped:", "Called for track", track); if (++mReleasedEncodersCount == mEncoders.size()) { LOG.w("requestStop:", "All encoders have been released. Stopping the muxer."); + if (mListener != null) { + mListener.onEncodingStop(); + } end(); } } diff --git a/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java b/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java index c9ce8220..1b81d370 100644 --- a/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java +++ b/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java @@ -35,6 +35,8 @@ import java.util.List; public class CameraActivity extends AppCompatActivity implements View.OnClickListener, OptionView.Callback { + private final static CameraLogger LOG = CameraLogger.create("DemoApp"); + private CameraView camera; private ViewGroup controlPanel; private long mCaptureTime; @@ -134,9 +136,14 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis animator.start(); } - private void message(String content, boolean important) { - int length = important ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT; - Toast.makeText(this, content, length).show(); + private void message(@NonNull String content, boolean important) { + if (important) { + LOG.w(content); + Toast.makeText(this, content, Toast.LENGTH_LONG).show(); + } else { + LOG.i(content); + Toast.makeText(this, content, Toast.LENGTH_SHORT).show(); + } } private class Listener extends CameraListener { @@ -170,7 +177,7 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis PicturePreviewActivity.setPictureResult(result); Intent intent = new Intent(CameraActivity.this, PicturePreviewActivity.class); intent.putExtra("delay", callbackTime - mCaptureTime); - Log.e("CameraActivity", "Picture delay: " + (callbackTime - mCaptureTime)); + LOG.w("Picture delay:", callbackTime - mCaptureTime); startActivity(intent); mCaptureTime = 0; } @@ -182,6 +189,12 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis Intent intent = new Intent(CameraActivity.this, VideoPreviewActivity.class); startActivity(intent); } + + @Override + public void onVideoRecordingStart() { + super.onVideoRecordingStart(); + LOG.w("onVideoRecordingStart!"); + } } @Override diff --git a/docs/_posts/2018-12-20-camera-events.md b/docs/_posts/2018-12-20-camera-events.md index 0f83728f..f8ffd87a 100644 --- a/docs/_posts/2018-12-20-camera-events.md +++ b/docs/_posts/2018-12-20-camera-events.md @@ -39,6 +39,8 @@ camera.addCameraListener(new CameraListener() { public void onExposureCorrectionChanged(float newValue, float[] bounds, PointF[] fingers) {} public void onVideoRecordingStart() {} + + public void onVideoRecordingEnd() {} }); ``` diff --git a/docs/_posts/2018-12-20-capturing-media.md b/docs/_posts/2018-12-20-capturing-media.md index 2138b383..70133745 100644 --- a/docs/_posts/2018-12-20-capturing-media.md +++ b/docs/_posts/2018-12-20-capturing-media.md @@ -59,7 +59,7 @@ Please note that the video snaphot features requires: This is allowed at the following conditions: - `takePictureSnapshot()` is used (no HQ pictures) -- the OpenGL preview is used (see [previews](previews.html)) +- the `GL_SURFACE` preview is used (see [previews](previews.html)) ### Related XML attributes @@ -68,6 +68,35 @@ This is allowed at the following conditions: app:cameraMode="picture|video"/> ``` +### Related callbacks + +```java +camera.addCameraListener(new CameraListener() { + + @Override + public void onPictureTaken(@NonNull PictureResult result) { + // A Picture was taken! + } + + @Override + public void onVideoTaken(@NonNull VideoResult result) { + // A Video was taken! + } + + @Override + public void onVideoRecordingStart() { + // Notifies that the actual video recording has started. + // Can be used to show some UI indicator for video recording or counting time. + } + + @Override + public void onVideoRecordingEnd() { + // Notifies that the actual video recording has ended. + // Can be used to remove UI indicators added in onVideoRecordingStart. + } +}) +``` + ### Related APIs |Method|Description| diff --git a/docs/_posts/2018-12-20-changelog.md b/docs/_posts/2018-12-20-changelog.md index 0c87a674..c977ee17 100644 --- a/docs/_posts/2018-12-20-changelog.md +++ b/docs/_posts/2018-12-20-changelog.md @@ -27,7 +27,7 @@ New versions are released through GitHub, so the reference page is the [GitHub R If you were using `focus`, just switch to `autoFocus`. -If you were using `focusWithMarker`, you can [add back the old marker](../docs/more-features.html#cameraautofocusmarker). +If you were using `focusWithMarker`, you can [add back the old marker](../docs/controls.html#cameraautofocusmarker). ### v2.0.0-beta05 diff --git a/docs/_posts/2018-12-20-controls.md b/docs/_posts/2018-12-20-controls.md index cafab804..6c75262a 100644 --- a/docs/_posts/2018-12-20-controls.md +++ b/docs/_posts/2018-12-20-controls.md @@ -143,7 +143,7 @@ cameraView.setVideoBitRate(0); cameraView.setVideoBitRate(4000000); ``` -### Manual Focus +### Auto Focus There are many ways to focus a CameraView engine: @@ -175,6 +175,44 @@ cameraView.addCameraListener(new CameraListener() { Auto focus is not guaranteed to be supported: check the `CameraOptions` to be sure. +```xml + +``` + +##### cameraAutoFocusMarker + +Lets you set a marker for drawing on screen in response to auto focus events. +In XML, you should pass the qualified class name of your marker. + +```java +cameraView.setAutoFocusMarker(null); +cameraView.setAutoFocusMarker(marker); +``` + +We offer a default marker (similar to the old `focusWithMarker` attribute in v1), +which you can set in XML using the `@string/cameraview_default_autofocus_marker` resource, +or programmatically: + +```java +cameraView.setAutoFocusMarker(new DefaultAutoFocusMarker()); +``` + +##### cameraAutoFocusResetDelay + +Lets you control how an auto-focus operation is reset after completed. +Setting a value <= 0 or == Long.MAX_VALUE will not reset the auto-focus. +This is useful for low end devices that have slow auto-focus capabilities. +Defaults to 3 seconds. + +```java +cameraView.setCameraAutoFocusResetDelay(1000); // 1 second +cameraView.setCameraAutoFocusResetDelay(0); // NO reset +cameraView.setCameraAutoFocusResetDelay(-1); // NO reset +cameraView.setCameraAutoFocusResetDelay(Long.MAX_VALUE); // NO reset +``` + ### Zoom There are two ways to control the zoom value: diff --git a/docs/_posts/2018-12-20-more-features.md b/docs/_posts/2018-12-20-more-features.md index 2b9c0a71..45f1cd67 100644 --- a/docs/_posts/2018-12-20-more-features.md +++ b/docs/_posts/2018-12-20-more-features.md @@ -58,38 +58,6 @@ cameraView.setGridColor(Color.WHITE); cameraView.setGridColor(Color.BLACK); ``` -##### cameraAutoFocusMarker - -Lets you set a marker for drawing on screen in response to auto focus events. -In XML, you should pass the qualified class name of your marker. - -```java -cameraView.setAutoFocusMarker(null); -cameraView.setAutoFocusMarker(marker); -``` - -We offer a default marker (similar to the old `focusWithMarker` attribute in v1), -which you can set in XML using the `@string/cameraview_default_autofocus_marker` resource, -or programmatically: - -```java -cameraView.setAutoFocusMarker(new DefaultAutoFocusMarker()); -``` - -##### cameraAutoFocusResetDelay - -Lets you control how an auto-focus operation is reset after completed. -Setting a value <= 0 or == Long.MAX_VALUE will not reset the auto-focus. -This is useful for low end devices that have slow auto-focus capabilities. -Defaults to 3 seconds. - -```java -cameraView.setCameraAutoFocusResetDelay(1000); // 1 second -cameraView.setCameraAutoFocusResetDelay(0); // NO reset -cameraView.setCameraAutoFocusResetDelay(-1); // NO reset -cameraView.setCameraAutoFocusResetDelay(Long.MAX_VALUE); // NO reset -``` - ##### cameraUseDeviceOrientation Controls whether we should consider the device orientation for picture and video outputs.