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 78ea055d..d0c4e95b 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/MockCameraEngine.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/MockCameraEngine.java @@ -169,7 +169,7 @@ public class MockCameraEngine extends CameraEngine { } @Override - protected void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub, @NonNull File file, @NonNull AspectRatio viewAspectRatio) { + protected void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub, @NonNull AspectRatio viewAspectRatio) { } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java index cb786671..00b8ebb2 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java @@ -7,6 +7,7 @@ import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraManager; import android.hardware.camera2.params.StreamConfigurationMap; +import android.media.CamcorderProfile; import android.media.ImageReader; import android.media.MediaRecorder; import android.os.Build; @@ -23,6 +24,7 @@ import com.otaliastudios.cameraview.controls.Hdr; import com.otaliastudios.cameraview.controls.Mode; import com.otaliastudios.cameraview.controls.VideoCodec; import com.otaliastudios.cameraview.controls.WhiteBalance; +import com.otaliastudios.cameraview.internal.utils.CamcorderProfiles; import com.otaliastudios.cameraview.size.AspectRatio; import com.otaliastudios.cameraview.size.Size; @@ -142,13 +144,14 @@ public class CameraOptions { // Camera2Engine constructor. @RequiresApi(Build.VERSION_CODES.LOLLIPOP) @SuppressWarnings("deprecation") - public CameraOptions(@NonNull CameraManager manager, @NonNull CameraCharacteristics characteristics, boolean flipSizes) throws CameraAccessException { + public CameraOptions(@NonNull CameraManager manager, @NonNull String cameraId, boolean flipSizes) throws CameraAccessException { Mapper mapper = Mapper.get(Engine.CAMERA2); + CameraCharacteristics cameraCharacteristics = manager.getCameraCharacteristics(cameraId); // Facing - for (String cameraId : manager.getCameraIdList()) { - CameraCharacteristics cameraCharacteristics = manager.getCameraCharacteristics(cameraId); - Integer cameraFacing = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING); + for (String cameraId1 : manager.getCameraIdList()) { + CameraCharacteristics cameraCharacteristics1 = manager.getCameraCharacteristics(cameraId1); + Integer cameraFacing = cameraCharacteristics1.get(CameraCharacteristics.LENS_FACING); if (cameraFacing != null) { Facing value = mapper.unmapFacing(cameraFacing); if (value != null) supportedFacing.add(value); @@ -156,7 +159,7 @@ public class CameraOptions { } // Picture Sizes - StreamConfigurationMap streamMap = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); + StreamConfigurationMap streamMap = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); if (streamMap == null) throw new RuntimeException("StreamConfigurationMap is null. Should not happen."); android.util.Size[] psizes = streamMap.getOutputSizes(ImageReader.class); for (android.util.Size size : psizes) { @@ -167,12 +170,18 @@ public class CameraOptions { } // Video Sizes + // As a safety measure, remove Sizes bigger than CamcorderProfile.highest + CamcorderProfile profile = CamcorderProfiles.get(cameraId, + new Size(Integer.MAX_VALUE, Integer.MAX_VALUE)); + Size videoMaxSize = new Size(profile.videoFrameWidth, profile.videoFrameHeight); android.util.Size[] vsizes = streamMap.getOutputSizes(MediaRecorder.class); for (android.util.Size size : vsizes) { - int width = flipSizes ? size.getHeight() : size.getWidth(); - int height = flipSizes ? size.getWidth() : size.getHeight(); - supportedVideoSizes.add(new Size(width, height)); - supportedVideoAspectRatio.add(AspectRatio.of(width, height)); + if (size.getWidth() <= videoMaxSize.getWidth() && size.getHeight() <= videoMaxSize.getHeight()) { + int width = flipSizes ? size.getHeight() : size.getWidth(); + int height = flipSizes ? size.getWidth() : size.getHeight(); + supportedVideoSizes.add(new Size(width, height)); + supportedVideoAspectRatio.add(AspectRatio.of(width, height)); + } } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java index 8e3de39e..3bdd26d2 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java @@ -24,7 +24,6 @@ import com.otaliastudios.cameraview.controls.Engine; import com.otaliastudios.cameraview.frame.Frame; import com.otaliastudios.cameraview.PictureResult; import com.otaliastudios.cameraview.VideoResult; -import com.otaliastudios.cameraview.controls.Audio; import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.controls.Flash; import com.otaliastudios.cameraview.gesture.Gesture; @@ -34,17 +33,14 @@ import com.otaliastudios.cameraview.controls.WhiteBalance; import com.otaliastudios.cameraview.internal.utils.CropHelper; import com.otaliastudios.cameraview.internal.utils.Op; import com.otaliastudios.cameraview.picture.FullPictureRecorder; -import com.otaliastudios.cameraview.picture.PictureRecorder; import com.otaliastudios.cameraview.picture.Snapshot1PictureRecorder; import com.otaliastudios.cameraview.picture.SnapshotGlPictureRecorder; import com.otaliastudios.cameraview.preview.GlCameraPreview; import com.otaliastudios.cameraview.size.AspectRatio; import com.otaliastudios.cameraview.size.Size; -import com.otaliastudios.cameraview.video.FullVideoRecorder; +import com.otaliastudios.cameraview.video.Full1VideoRecorder; import com.otaliastudios.cameraview.video.SnapshotVideoRecorder; -import com.otaliastudios.cameraview.video.VideoRecorder; -import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -251,6 +247,7 @@ public class Camera1Engine extends CameraEngine implements Camera.PreviewCallbac mCamera = null; mCameraOptions = null; } + mVideoRecorder = null; mCameraOptions = null; mCamera = null; LOG.w("onStopEngine:", "Clean up.", "Returning."); @@ -308,19 +305,6 @@ public class Camera1Engine extends CameraEngine implements Camera.PreviewCallbac throw new CameraException(runtime, reason); } - @Override - public void setMode(@NonNull Mode mode) { - if (mode != mMode) { - mMode = mode; - schedule(null, true, new Runnable() { - @Override - public void run() { - restart(); - } - }); - } - } - @Override public void setLocation(@Nullable Location location) { final Location oldLocation = mLocation; @@ -528,50 +512,28 @@ public class Camera1Engine extends CameraEngine implements Camera.PreviewCallbac } @Override - public void takeVideo(final @NonNull VideoResult.Stub stub, @NonNull final File videoFile) { - schedule(mStartVideoOp, true, new Runnable() { - @Override - public void run() { - if (mMode == Mode.PICTURE) { - throw new IllegalStateException("Can't record video while in PICTURE mode"); - } - - if (isTakingVideo()) return; - - // Create the video result stub - stub.file = videoFile; - stub.isSnapshot = false; - stub.videoCodec = mVideoCodec; - stub.location = mLocation; - stub.facing = mFacing; - stub.rotation = offset(REF_SENSOR, REF_OUTPUT); - stub.size = flip(REF_SENSOR, REF_OUTPUT) ? mCaptureSize.flip() : mCaptureSize; - stub.audio = mAudio; - stub.maxSize = mVideoMaxSize; - stub.maxDuration = mVideoMaxDuration; - stub.videoBitRate = mVideoBitRate; - stub.audioBitRate = mAudioBitRate; - - // Unlock the camera and start recording. - try { - mCamera.unlock(); - } catch (Exception e) { - // If this failed, we are unlikely able to record the video. - // Dispatch an error. - onVideoResult(null, e); - return; - } - mVideoRecorder = new FullVideoRecorder(stub, Camera1Engine.this, - Camera1Engine.this, mCamera, mCameraId); - mVideoRecorder.start(); - } - }); + protected void onTakeVideo(@NonNull VideoResult.Stub stub) { + stub.rotation = offset(REF_SENSOR, REF_OUTPUT); + stub.size = flip(REF_SENSOR, REF_OUTPUT) ? mCaptureSize.flip() : mCaptureSize; + // Unlock the camera and start recording. + try { + mCamera.unlock(); + } catch (Exception e) { + // If this failed, we are unlikely able to record the video. + // Dispatch an error. + onVideoResult(null, e); + return; + } + if (!(mVideoRecorder instanceof Full1VideoRecorder)) { + mVideoRecorder = new Full1VideoRecorder(Camera1Engine.this, mCamera, mCameraId); + } + mVideoRecorder.start(stub); } @SuppressLint("NewApi") @WorkerThread @Override - protected void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub, @NonNull File file, @NonNull AspectRatio viewAspectRatio) { + protected void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub, @NonNull AspectRatio viewAspectRatio) { if (!(mPreview instanceof GlCameraPreview)) { throw new IllegalStateException("Video snapshots are only supported with GlCameraPreview."); } @@ -627,8 +589,10 @@ public class Camera1Engine extends CameraEngine implements Camera.PreviewCallbac // Reset facing and start. mFacing = realFacing; - mVideoRecorder = new SnapshotVideoRecorder(stub, Camera1Engine.this, glPreview); - mVideoRecorder.start(); + if (!(mVideoRecorder instanceof SnapshotVideoRecorder)) { + mVideoRecorder = new SnapshotVideoRecorder(Camera1Engine.this, glPreview); + } + mVideoRecorder.start(stub); } // ----------------- 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 d0da47f0..522454c5 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java @@ -13,6 +13,7 @@ import android.hardware.camera2.CameraManager; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.params.StreamConfigurationMap; import android.location.Location; +import android.media.MediaCodec; import android.os.Build; import android.view.Surface; import android.view.SurfaceHolder; @@ -25,7 +26,6 @@ import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.CameraOptions; import com.otaliastudios.cameraview.PictureResult; import com.otaliastudios.cameraview.VideoResult; -import com.otaliastudios.cameraview.controls.Audio; import com.otaliastudios.cameraview.controls.Engine; import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.controls.Flash; @@ -34,20 +34,19 @@ import com.otaliastudios.cameraview.controls.Mode; import com.otaliastudios.cameraview.controls.WhiteBalance; import com.otaliastudios.cameraview.gesture.Gesture; import com.otaliastudios.cameraview.internal.utils.CropHelper; -import com.otaliastudios.cameraview.internal.utils.Op; -import com.otaliastudios.cameraview.picture.Snapshot1PictureRecorder; import com.otaliastudios.cameraview.picture.SnapshotGlPictureRecorder; import com.otaliastudios.cameraview.preview.GlCameraPreview; import com.otaliastudios.cameraview.size.AspectRatio; import com.otaliastudios.cameraview.size.Size; +import com.otaliastudios.cameraview.video.Full2VideoRecorder; import com.otaliastudios.cameraview.video.SnapshotVideoRecorder; -import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -56,7 +55,6 @@ import androidx.annotation.WorkerThread; // TODO parameters // TODO pictures -// TODO videos @RequiresApi(Build.VERSION_CODES.LOLLIPOP) public class Camera2Engine extends CameraEngine { @@ -69,6 +67,13 @@ public class Camera2Engine extends CameraEngine { private CameraCaptureSession mSession; private CaptureRequest.Builder mPreviewStreamRequestBuilder; private CaptureRequest mPreviewStreamRequest; + private Surface mPreviewStreamSurface; + + // API 23+ for full video recording. The surface is created before. + private Surface mFullVideoPersistentSurface; + + // API 21-22 for full video recording. When takeVideo is called, we have to reset the session. + private VideoResult.Stub mFullVideoPendingStub; public Camera2Engine(Callback callback) { super(callback); @@ -194,7 +199,7 @@ public class Camera2Engine extends CameraEngine { try { LOG.i("createCamera:", "Applying default parameters."); CameraCharacteristics characteristics = mManager.getCameraCharacteristics(mCameraId); - mCameraOptions = new CameraOptions(mManager, characteristics, flip(REF_SENSOR, REF_VIEW)); + mCameraOptions = new CameraOptions(mManager, mCameraId, flip(REF_SENSOR, REF_VIEW)); // applyDefaultFocus(params); TODO // applyFlash(params, Flash.OFF); // applyLocation(params, null); @@ -245,7 +250,6 @@ public class Camera2Engine extends CameraEngine { // Create a preview surface with the correct size.In Camera2, instead of applying it to // the camera params object, we must resize our own surfaces. final Object output = mPreview.getOutput(); - Surface previewSurface; if (output instanceof SurfaceHolder) { try { // This must be called from the UI thread... @@ -259,37 +263,46 @@ public class Camera2Engine extends CameraEngine { } catch (ExecutionException | InterruptedException e) { throw new CameraException(e, CameraException.REASON_FAILED_TO_CONNECT); } - previewSurface = ((SurfaceHolder) output).getSurface(); + mPreviewStreamSurface = ((SurfaceHolder) output).getSurface(); } else if (output instanceof SurfaceTexture) { ((SurfaceTexture) output).setDefaultBufferSize(mPreviewStreamSize.getWidth(), mPreviewStreamSize.getHeight()); - previewSurface = new Surface((SurfaceTexture) output); + mPreviewStreamSurface = new Surface((SurfaceTexture) output); } else { throw new RuntimeException("Unknown CameraPreview output class."); } - // TODO: captureSize - /* if (mMode == Mode.PICTURE) { - params.setPictureSize(mCaptureSize.getWidth(), mCaptureSize.getHeight()); // <- allowed + Surface captureSurface = null; + if (mMode == Mode.PICTURE) { + // TODO picture recorder } else { - // mCaptureSize in this case is a video size. The available video sizes are not necessarily - // a subset of the picture sizes, so we can't use the mCaptureSize value: it might crash. - // However, the setPictureSize() passed here is useless : we don't allow HQ pictures in video mode. - // While this might be lifted in the future, for now, just use a picture capture size. - Size pictureSize = computeCaptureSize(Mode.PICTURE); - params.setPictureSize(pictureSize.getWidth(), pictureSize.getHeight()); - } */ - - //noinspection ArraysAsListWithZeroOrOneArgument - List outputSurfaces = Arrays.asList(previewSurface); + if (Full2VideoRecorder.SUPPORTS_PERSISTENT_SURFACE) { + mFullVideoPersistentSurface = MediaCodec.createPersistentInputSurface(); + captureSurface = mFullVideoPersistentSurface; + } else if (mFullVideoPendingStub != null) { + Full2VideoRecorder recorder = new Full2VideoRecorder(this, mCameraId, null); + try { + captureSurface = recorder.createInputSurface(mFullVideoPendingStub); + } catch (Full2VideoRecorder.PrepareException e) { + throw new CameraException(e, CameraException.REASON_FAILED_TO_CONNECT); + } + mVideoRecorder = recorder; + } + } + + List outputSurfaces = new ArrayList<>(); + outputSurfaces.add(mPreviewStreamSurface); + if (captureSurface != null) outputSurfaces.add(captureSurface); + try { mPreviewStreamRequestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); - mPreviewStreamRequestBuilder.addTarget(previewSurface); + mPreviewStreamRequestBuilder.addTarget(mPreviewStreamSurface); // null handler means using the current looper which is totally ok. mCamera.createCaptureSession(outputSurfaces, new CameraCaptureSession.StateCallback() { @Override public void onConfigured(@NonNull CameraCaptureSession session) { mSession = session; + LOG.i("onStartBind:", "Completed"); task.trySetResult(null); } @@ -299,7 +312,6 @@ public class Camera2Engine extends CameraEngine { String message = LOG.e("onConfigureFailed! Session", session); throw new RuntimeException(message); } - }, null); } catch (CameraAccessException e) { throw createCameraException(e); @@ -337,6 +349,12 @@ public class Camera2Engine extends CameraEngine { throw new CameraException(e, CameraException.REASON_FAILED_TO_START_PREVIEW); } LOG.i("onStartPreview", "Started preview."); + + // Start delayed video if needed. + if (mFullVideoPendingStub != null) { + // Do not call takeVideo. It will reset some stub parameters that the recorder sets. + onTakeVideo(mFullVideoPendingStub); + } return Tasks.forResult(null); } @@ -372,6 +390,11 @@ public class Camera2Engine extends CameraEngine { @NonNull @Override protected Task onStopBind() { + if (mFullVideoPersistentSurface != null) { + mFullVideoPersistentSurface.release(); + mFullVideoPersistentSurface = null; + } + mPreviewStreamSurface = null; mPreviewStreamRequestBuilder = null; mPreviewStreamSize = null; mCaptureSize = null; @@ -394,6 +417,7 @@ public class Camera2Engine extends CameraEngine { } mCamera = null; mCameraOptions = null; + mVideoRecorder = null; LOG.w("onStopEngine:", "Returning."); return Tasks.forResult(null); } @@ -420,13 +444,58 @@ public class Camera2Engine extends CameraEngine { //region Videos + @WorkerThread + @Override + protected void onTakeVideo(@NonNull VideoResult.Stub stub) { + stub.rotation = offset(REF_SENSOR, REF_OUTPUT); + stub.size = flip(REF_SENSOR, REF_OUTPUT) ? mCaptureSize.flip() : mCaptureSize; + if (!Full2VideoRecorder.SUPPORTS_PERSISTENT_SURFACE) { + // On API 21 and 22, we must restart the session at each time. + if (stub == mFullVideoPendingStub) { + // We have restarted and are ready to take the video. + doTakeVideo(mFullVideoPendingStub); + mFullVideoPendingStub = null; + } else { + // Save the pending data and restart the session. + mFullVideoPendingStub = stub; + restartBind(); + } + } else { + doTakeVideo(stub); + } + } + + private void doTakeVideo(@NonNull final VideoResult.Stub stub) { + if (!(mVideoRecorder instanceof Full2VideoRecorder)) { + mVideoRecorder = new Full2VideoRecorder(this, mCameraId, mFullVideoPersistentSurface); + } + Full2VideoRecorder recorder = (Full2VideoRecorder) mVideoRecorder; + try { + CaptureRequest.Builder builder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_RECORD); + //noinspection ConstantConditions + builder.addTarget(recorder.getInputSurface()); + builder.addTarget(mPreviewStreamSurface); + + final AtomicBoolean started = new AtomicBoolean(false); + mSession.setRepeatingRequest(builder.build(), new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureStarted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, long timestamp, long frameNumber) { + if (started.compareAndSet(false, true)) mVideoRecorder.start(stub); + } + }, null); + } catch (CameraAccessException e) { + onVideoResult(null, e); + throw createCameraException(e); + } + } + /** - * See {@link Camera1Engine#onTakeVideoSnapshot(VideoResult.Stub, File, AspectRatio)} + * See {@link CameraEngine#onTakeVideoSnapshot(VideoResult.Stub, AspectRatio)} * to read about the size and rotation computation. */ @WorkerThread @Override - protected void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub, @NonNull File file, @NonNull AspectRatio viewAspectRatio) { + protected void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub, @NonNull AspectRatio viewAspectRatio) { if (!(mPreview instanceof GlCameraPreview)) { throw new IllegalStateException("Video snapshots are only supported with GlCameraPreview."); } @@ -445,21 +514,32 @@ public class Camera2Engine extends CameraEngine { // Reset facing and start. mFacing = realFacing; - mVideoRecorder = new SnapshotVideoRecorder(stub, this, glPreview); - mVideoRecorder.start(); + if (!(mVideoRecorder instanceof SnapshotVideoRecorder)) { + mVideoRecorder = new SnapshotVideoRecorder(this, glPreview); + } + mVideoRecorder.start(stub); } - //endregion - - - - - - - - - + @Override + public void onVideoResult(@Nullable VideoResult.Stub result, @Nullable Exception exception) { + if (mVideoRecorder instanceof Full2VideoRecorder) { + // We have to stop all repeating requests and restart them. + try { + if (getBindState() == STATE_STARTED) { + mSession.stopRepeating(); + } + if (getPreviewState() == STATE_STARTED) { + mPreviewStreamRequest = mPreviewStreamRequestBuilder.build(); + mSession.setRepeatingRequest(mPreviewStreamRequest, null, null); + } + } catch (CameraAccessException e) { + throw createCameraException(e); + } + } + super.onVideoResult(result, exception); + } + //endregion @Override @@ -467,11 +547,6 @@ public class Camera2Engine extends CameraEngine { } - @Override - public void setMode(@NonNull Mode mode) { - - } - @Override public void setZoom(float zoom, @Nullable PointF[] points, boolean notify) { @@ -507,12 +582,6 @@ public class Camera2Engine extends CameraEngine { } - - @Override - public void takeVideo(@NonNull VideoResult.Stub stub, @NonNull File file) { - - } - @Override public void startAutoFocus(@Nullable Gesture gesture, @NonNull PointF point) { 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 bc365248..f5806c86 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java @@ -1,13 +1,10 @@ package com.otaliastudios.cameraview.engine; -import android.annotation.SuppressLint; import android.content.Context; import android.graphics.PointF; -import android.graphics.Rect; import android.location.Location; -import android.os.Build; import android.os.Handler; import android.os.Looper; @@ -17,7 +14,6 @@ import com.google.android.gms.tasks.OnFailureListener; import com.google.android.gms.tasks.SuccessContinuation; import com.google.android.gms.tasks.TaskCompletionSource; import com.google.android.gms.tasks.Task; -import com.google.android.gms.tasks.Tasks; import com.otaliastudios.cameraview.CameraException; import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.CameraOptions; @@ -25,12 +21,9 @@ import com.otaliastudios.cameraview.PictureResult; import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.frame.Frame; import com.otaliastudios.cameraview.frame.FrameManager; -import com.otaliastudios.cameraview.internal.utils.CropHelper; import com.otaliastudios.cameraview.internal.utils.Op; import com.otaliastudios.cameraview.internal.utils.WorkerHandler; import com.otaliastudios.cameraview.picture.PictureRecorder; -import com.otaliastudios.cameraview.picture.Snapshot1PictureRecorder; -import com.otaliastudios.cameraview.picture.SnapshotGlPictureRecorder; import com.otaliastudios.cameraview.preview.CameraPreview; import com.otaliastudios.cameraview.controls.Audio; import com.otaliastudios.cameraview.controls.Facing; @@ -40,12 +33,10 @@ import com.otaliastudios.cameraview.controls.Hdr; import com.otaliastudios.cameraview.controls.Mode; import com.otaliastudios.cameraview.controls.VideoCodec; import com.otaliastudios.cameraview.controls.WhiteBalance; -import com.otaliastudios.cameraview.preview.GlCameraPreview; import com.otaliastudios.cameraview.size.AspectRatio; import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.SizeSelector; import com.otaliastudios.cameraview.size.SizeSelectors; -import com.otaliastudios.cameraview.video.SnapshotVideoRecorder; import com.otaliastudios.cameraview.video.VideoRecorder; import androidx.annotation.NonNull; @@ -59,7 +50,6 @@ import java.util.Collection; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; @@ -869,6 +859,24 @@ public abstract class CameraEngine implements } } + /** + * Sets the desired mode (either picture or video). + * @param mode desired mode. + */ + public final void setMode(@NonNull Mode mode) { + if (mode != mMode) { + mMode = mode; + mHandler.run(new Runnable() { + @Override + public void run() { + if (getEngineState() == STATE_STARTED) { + restart(); + } + } + }); + } + } + //endregion //region Abstract setters and APIs @@ -885,9 +893,6 @@ public abstract class CameraEngine implements */ protected abstract boolean collectCameraInfo(@NonNull Facing facing); - // Should restart the session if active. - public abstract void setMode(@NonNull Mode mode); - // If closed, no-op. If opened, check supported and apply. public abstract void setZoom(float zoom, @Nullable PointF[] points, boolean notify); @@ -959,7 +964,32 @@ public abstract class CameraEngine implements } } - public abstract void takeVideo(@NonNull VideoResult.Stub stub, @NonNull File file); + public final void takeVideo(final @NonNull VideoResult.Stub stub, final @NonNull File file) { + LOG.v("takeVideo", "scheduling"); + mHandler.run(new Runnable() { + @Override + public void run() { + LOG.v("takeVideo", "performing. Engine:", getEngineStateName(), "isTakingVideo:", isTakingVideo()); + if (getEngineState() < STATE_STARTED) { mStartVideoOp.end(null); return; } + if (isTakingVideo()) { mStartVideoOp.end(null); return; } + if (mMode == Mode.PICTURE) { + throw new IllegalStateException("Can't record video while in PICTURE mode"); + } + stub.file = file; + stub.isSnapshot = false; + stub.videoCodec = mVideoCodec; + stub.location = mLocation; + stub.facing = mFacing; + stub.audio = mAudio; + stub.maxSize = mVideoMaxSize; + stub.maxDuration = mVideoMaxDuration; + stub.videoBitRate = mVideoBitRate; + stub.audioBitRate = mAudioBitRate; + onTakeVideo(stub); + mStartVideoOp.end(null); + } + }); + } /** * @param stub a video stub @@ -984,7 +1014,7 @@ public abstract class CameraEngine implements stub.audio = mAudio; stub.maxSize = mVideoMaxSize; stub.maxDuration = mVideoMaxDuration; - onTakeVideoSnapshot(stub, file, viewAspectRatio); + onTakeVideoSnapshot(stub, viewAspectRatio); mStartVideoOp.end(null); } }); @@ -995,10 +1025,9 @@ public abstract class CameraEngine implements mHandler.run(new Runnable() { @Override public void run() { - LOG.i("stopVideo", "executing.", "has recorder?", mVideoRecorder != null); + LOG.i("stopVideo", "executing.", "isTakingVideo?", isTakingVideo()); if (mVideoRecorder != null) { mVideoRecorder.stop(); - mVideoRecorder = null; } } }); @@ -1019,7 +1048,10 @@ public abstract class CameraEngine implements protected abstract void onTakePictureSnapshot(@NonNull PictureResult.Stub stub, @NonNull AspectRatio viewAspectRatio); @WorkerThread - protected abstract void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub, @NonNull File file, @NonNull AspectRatio viewAspectRatio); + protected abstract void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub, @NonNull AspectRatio viewAspectRatio); + + @WorkerThread + protected abstract void onTakeVideo(@NonNull VideoResult.Stub stub); //endregion @@ -1122,7 +1154,7 @@ public abstract class CameraEngine implements } public final boolean isTakingVideo() { - return mVideoRecorder != null; + return mVideoRecorder != null && mVideoRecorder.isRecording(); } public final boolean isTakingPicture() { diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/CamcorderProfiles.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/CamcorderProfiles.java index 74349519..74d9c8f7 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/CamcorderProfiles.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/CamcorderProfiles.java @@ -35,6 +35,26 @@ public class CamcorderProfiles { } } + + /** + * Returns a CamcorderProfile that's somewhat coherent with the target size, + * to ensure we get acceptable video/audio parameters for MediaRecorders (most notably the bitrate). + * + * @param cameraId the camera2 id + * @param targetSize the target video size + * @return a profile + */ + @NonNull + public static CamcorderProfile get(@NonNull String cameraId, @NonNull Size targetSize) { + // It seems that the way to do this is to use Integer.parseInt(). + try { + int camera1Id = Integer.parseInt(cameraId); + return get(camera1Id, targetSize); + } catch (NumberFormatException e) { + return CamcorderProfile.get(CamcorderProfile.QUALITY_LOW); + } + } + /** * Returns a CamcorderProfile that's somewhat coherent with the target size, * to ensure we get acceptable video/audio parameters for MediaRecorders (most notably the bitrate). @@ -45,13 +65,13 @@ public class CamcorderProfiles { */ @NonNull public static CamcorderProfile get(int cameraId, @NonNull Size targetSize) { - final int targetArea = targetSize.getWidth() * targetSize.getHeight(); + final long targetArea = (long) targetSize.getWidth() * targetSize.getHeight(); List sizes = new ArrayList<>(sizeToProfileMap.keySet()); Collections.sort(sizes, new Comparator() { @Override public int compare(Size s1, Size s2) { - int a1 = Math.abs(s1.getWidth() * s1.getHeight() - targetArea); - int a2 = Math.abs(s2.getWidth() * s2.getHeight() - targetArea); + long a1 = Math.abs(s1.getWidth() * s1.getHeight() - targetArea); + long a2 = Math.abs(s2.getWidth() * s2.getHeight() - targetArea); //noinspection UseCompareMethod return (a1 < a2) ? -1 : ((a1 == a2) ? 0 : 1); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/WorkerHandler.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/WorkerHandler.java index fbf4caba..414b63b7 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/WorkerHandler.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/WorkerHandler.java @@ -4,11 +4,15 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.TaskCompletionSource; +import com.google.android.gms.tasks.Tasks; import com.otaliastudios.cameraview.CameraLogger; import androidx.annotation.NonNull; import java.lang.ref.WeakReference; +import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; @@ -84,7 +88,23 @@ public class WorkerHandler { if (Thread.currentThread() == getThread()) { runnable.run(); } else { - mHandler.post(runnable); + post(runnable); + } + } + + /** + * Post an action on this handler. + * @param callable the action + */ + public Task run(@NonNull Callable callable) { + if (Thread.currentThread() == getThread()) { + try { + return Tasks.forResult(callable.call()); + } catch (Exception e) { + return Tasks.forException(e); + } + } else { + return post(callable); } } @@ -96,6 +116,25 @@ public class WorkerHandler { mHandler.post(runnable); } + /** + * Post an action on this handler. + * @param callable the action + */ + public Task post(@NonNull final Callable callable) { + final TaskCompletionSource source = new TaskCompletionSource<>(); + post(new Runnable() { + @Override + public void run() { + try { + source.trySetResult(callable.call()); + } catch (Exception e) { + source.trySetException(e); + } + } + }); + return source.getTask(); + } + /** * Post an action on this handler. * @param delay the delay in millis diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/Full1VideoRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/Full1VideoRecorder.java new file mode 100644 index 00000000..b0dbfd12 --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/Full1VideoRecorder.java @@ -0,0 +1,52 @@ +package com.otaliastudios.cameraview.video; + +import android.hardware.Camera; +import android.media.MediaRecorder; + +import com.otaliastudios.cameraview.CameraLogger; +import com.otaliastudios.cameraview.VideoResult; +import com.otaliastudios.cameraview.engine.Camera1Engine; +import com.otaliastudios.cameraview.internal.utils.CamcorderProfiles; +import com.otaliastudios.cameraview.size.Size; + +import androidx.annotation.NonNull; + +/** + * A {@link VideoRecorder} that uses {@link MediaRecorder} APIs + * for the Camera1 engine. + */ +public class Full1VideoRecorder extends FullVideoRecorder { + + private static final String TAG = Full1VideoRecorder.class.getSimpleName(); + private static final CameraLogger LOG = CameraLogger.create(TAG); + + private final Camera1Engine mEngine; + private final Camera mCamera; + private final int mCameraId; + + public Full1VideoRecorder(@NonNull Camera1Engine engine, + @NonNull Camera camera, int cameraId) { + super(engine); + mCamera = camera; + mEngine = engine; + mCameraId = cameraId; + } + + @Override + protected boolean onPrepareMediaRecorder(@NonNull VideoResult.Stub stub, @NonNull MediaRecorder mediaRecorder) { + mediaRecorder.setCamera(mCamera); + mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); + + // Get a profile of quality compatible with the chosen size. + Size size = stub.rotation % 180 != 0 ? stub.size.flip() : stub.size; + mProfile = CamcorderProfiles.get(mCameraId, size); + return super.onPrepareMediaRecorder(stub, mediaRecorder); + } + + @Override + protected void onStop() { + // Restore frame processing. + mCamera.setPreviewCallbackWithBuffer(mEngine); + super.onStop(); + } +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/Full2VideoRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/Full2VideoRecorder.java new file mode 100644 index 00000000..7369a96e --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/Full2VideoRecorder.java @@ -0,0 +1,96 @@ +package com.otaliastudios.cameraview.video; + +import android.annotation.SuppressLint; +import android.graphics.SurfaceTexture; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CaptureFailure; +import android.hardware.camera2.CaptureRequest; +import android.media.Image; +import android.media.MediaActionSound; +import android.media.MediaRecorder; +import android.os.Build; +import android.view.Surface; + +import com.otaliastudios.cameraview.CameraLogger; +import com.otaliastudios.cameraview.VideoResult; +import com.otaliastudios.cameraview.engine.Camera2Engine; +import com.otaliastudios.cameraview.internal.utils.CamcorderProfiles; +import com.otaliastudios.cameraview.size.Size; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + +/** + * A {@link VideoRecorder} that uses {@link MediaRecorder} APIs + * for the Camera2 engine. + */ +@RequiresApi(Build.VERSION_CODES.LOLLIPOP) +public class Full2VideoRecorder extends FullVideoRecorder { + + private static final String TAG = Full2VideoRecorder.class.getSimpleName(); + private static final CameraLogger LOG = CameraLogger.create(TAG); + + // This actually didn't work as I expected, we're never using this. Should remove. + @SuppressWarnings("PointlessBooleanExpression") // TODO + public static final boolean SUPPORTS_PERSISTENT_SURFACE = false && Build.VERSION.SDK_INT >= 23; + + private final String mCameraId; + private Surface mInputSurface; + private final boolean mUseInputSurface; + + public Full2VideoRecorder(@NonNull Camera2Engine engine, + @NonNull String cameraId, + @Nullable Surface surface) { + super(engine); + mCameraId = cameraId; + mUseInputSurface = surface != null && SUPPORTS_PERSISTENT_SURFACE; + mInputSurface = mUseInputSurface ? surface : null; + } + + @SuppressLint("NewApi") + @Override + protected boolean onPrepareMediaRecorder(@NonNull VideoResult.Stub stub, @NonNull MediaRecorder mediaRecorder) { + mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); + Size size = stub.rotation % 180 != 0 ? stub.size.flip() : stub.size; + mProfile = CamcorderProfiles.get(mCameraId, size); + if (mUseInputSurface) { + //noinspection ConstantConditions + mediaRecorder.setInputSurface(mInputSurface); + } + return super.onPrepareMediaRecorder(stub, mediaRecorder); + } + + /** + * This method should be called just once. + * @return a surface + * @throws PrepareException if prepare went wrong + */ + @NonNull + public Surface createInputSurface(@NonNull VideoResult.Stub stub) throws PrepareException { + if (mUseInputSurface) { + throw new IllegalStateException("We are using the input surface (API23+), can't createInputSurface here."); + } + if (!prepareMediaRecorder(stub)) { + throw new PrepareException(mError); + } + mInputSurface = mMediaRecorder.getSurface(); + return mInputSurface; + } + + @Nullable + public Surface getInputSurface() { + return mInputSurface; + } + + + @SuppressWarnings("WeakerAccess") + public class PrepareException extends Exception { + private PrepareException(Throwable cause) { + super(cause); + } + } + +} 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 4c200690..b21d6b63 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java @@ -1,14 +1,11 @@ package com.otaliastudios.cameraview.video; -import android.hardware.Camera; import android.media.CamcorderProfile; import android.media.MediaRecorder; import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.controls.Audio; -import com.otaliastudios.cameraview.engine.Camera1Engine; -import com.otaliastudios.cameraview.internal.utils.CamcorderProfiles; import com.otaliastudios.cameraview.size.Size; import androidx.annotation.NonNull; @@ -16,78 +13,84 @@ import androidx.annotation.Nullable; /** * A {@link VideoRecorder} that uses {@link android.media.MediaRecorder} APIs. + * + * When started, the media recorder will be prepared in {@link #onPrepareMediaRecorder(VideoResult.Stub, MediaRecorder)}. + * Subclasses should override this method and, before calling super(), do two things: + * - set the media recorder VideoSource + * - define {@link #mProfile} + * + * Subclasses can also call {@link #prepareMediaRecorder(VideoResult.Stub)} before start happens, + * in which case it will not be prepared twice. This can be used for example to test some + * configurations. */ -public class FullVideoRecorder extends VideoRecorder { +public abstract class FullVideoRecorder extends VideoRecorder { private static final String TAG = FullVideoRecorder.class.getSimpleName(); private static final CameraLogger LOG = CameraLogger.create(TAG); - private MediaRecorder mMediaRecorder; - private CamcorderProfile mProfile; - private Camera1Engine mController; - private Camera mCamera; - private Size mSize; - - public FullVideoRecorder(@NonNull VideoResult.Stub stub, @Nullable VideoResultListener listener, - @NonNull Camera1Engine controller, @NonNull Camera camera, int cameraId) { - super(stub, listener); - mCamera = camera; - mController = controller; - mMediaRecorder = new MediaRecorder(); - mMediaRecorder.setCamera(camera); - mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); - - // Get a profile of quality compatible with the chosen size. - mSize = mResult.rotation % 180 != 0 ? mResult.size.flip() : mResult.size; - mProfile = CamcorderProfiles.get(cameraId, mSize); + @SuppressWarnings("WeakerAccess") protected MediaRecorder mMediaRecorder; + @SuppressWarnings("WeakerAccess") protected CamcorderProfile mProfile; + private boolean mMediaRecorderPrepared; + + + FullVideoRecorder(@Nullable VideoResultListener listener) { + super(listener); } - @Override - public void start() { - if (mResult.audio == Audio.ON) { + @SuppressWarnings({"WeakerAccess", "UnusedReturnValue", "BooleanMethodIsAlwaysInverted"}) + protected boolean prepareMediaRecorder(@NonNull VideoResult.Stub stub) { + if (mMediaRecorderPrepared) return true; + return onPrepareMediaRecorder(stub, new MediaRecorder()); + } + + @SuppressWarnings("WeakerAccess") + protected boolean onPrepareMediaRecorder(@NonNull VideoResult.Stub stub, @NonNull MediaRecorder mediaRecorder) { + mMediaRecorder = mediaRecorder; + Size size = stub.rotation % 180 != 0 ? stub.size.flip() : stub.size; + if (stub.audio == Audio.ON) { // Must be called before setOutputFormat. mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT); } mMediaRecorder.setOutputFormat(mProfile.fileFormat); - if (mResult.videoFrameRate <= 0) { + if (stub.videoFrameRate <= 0) { mMediaRecorder.setVideoFrameRate(mProfile.videoFrameRate); - mResult.videoFrameRate = mProfile.videoFrameRate; + stub.videoFrameRate = mProfile.videoFrameRate; } else { - mMediaRecorder.setVideoFrameRate(mResult.videoFrameRate); + mMediaRecorder.setVideoFrameRate(stub.videoFrameRate); } - mMediaRecorder.setVideoSize(mSize.getWidth(), mSize.getHeight()); - switch (mResult.videoCodec) { + mMediaRecorder.setVideoSize(size.getWidth(), size.getHeight()); + switch (stub.videoCodec) { case H_263: mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H263); break; case H_264: mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); break; case DEVICE_DEFAULT: mMediaRecorder.setVideoEncoder(mProfile.videoCodec); break; } - if (mResult.videoBitRate <= 0) { + if (stub.videoBitRate <= 0) { mMediaRecorder.setVideoEncodingBitRate(mProfile.videoBitRate); - mResult.videoBitRate = mProfile.videoBitRate; + stub.videoBitRate = mProfile.videoBitRate; } else { - mMediaRecorder.setVideoEncodingBitRate(mResult.videoBitRate); + mMediaRecorder.setVideoEncodingBitRate(stub.videoBitRate); } - if (mResult.audio == Audio.ON) { + if (stub.audio == Audio.ON) { mMediaRecorder.setAudioChannels(mProfile.audioChannels); mMediaRecorder.setAudioSamplingRate(mProfile.audioSampleRate); mMediaRecorder.setAudioEncoder(mProfile.audioCodec); - if (mResult.audioBitRate <= 0) { + if (stub.audioBitRate <= 0) { mMediaRecorder.setAudioEncodingBitRate(mProfile.audioBitRate); - mResult.audioBitRate = mProfile.audioBitRate; + stub.audioBitRate = mProfile.audioBitRate; } else { - mMediaRecorder.setAudioEncodingBitRate(mResult.audioBitRate); + mMediaRecorder.setAudioEncodingBitRate(stub.audioBitRate); } } - if (mResult.location != null) { + if (stub.location != null) { mMediaRecorder.setLocation( - (float) mResult.location.getLatitude(), - (float) mResult.location.getLongitude()); + (float) stub.location.getLatitude(), + (float) stub.location.getLongitude()); } - mMediaRecorder.setOutputFile(mResult.file.getAbsolutePath()); - mMediaRecorder.setOrientationHint(mResult.rotation); - mMediaRecorder.setMaxFileSize(mResult.maxSize); - mMediaRecorder.setMaxDuration(mResult.maxDuration); + mMediaRecorder.setOutputFile(stub.file.getAbsolutePath()); + mMediaRecorder.setOrientationHint(stub.rotation); + mMediaRecorder.setMaxFileSize(stub.maxSize); + mMediaRecorder.setMaxDuration(stub.maxDuration); mMediaRecorder.setOnInfoListener(new MediaRecorder.OnInfoListener() { @Override public void onInfo(MediaRecorder mediaRecorder, int what, int extra) { @@ -103,13 +106,32 @@ public class FullVideoRecorder extends VideoRecorder { } } }); - // Not needed. mMediaRecorder.setPreviewDisplay(mPreview.getSurface()); try { mMediaRecorder.prepare(); + mMediaRecorderPrepared = true; + mError = null; + return true; + } catch (Exception e) { + LOG.w("prepareMediaRecorder:", "Error while preparing media recorder.", e); + mMediaRecorderPrepared = false; + mError = e; + return false; + } + } + + @Override + protected void onStart() { + if (!prepareMediaRecorder(mResult)) { + mResult = null; + stop(); + return; + } + + try { mMediaRecorder.start(); } catch (Exception e) { - LOG.w("stop:", "Error while starting media recorder.", e); + LOG.w("start:", "Error while starting media recorder.", e); mResult = null; mError = e; stop(); @@ -117,7 +139,7 @@ public class FullVideoRecorder extends VideoRecorder { } @Override - public void stop() { + protected void onStop() { if (mMediaRecorder != null) { try { mMediaRecorder.stop(); @@ -130,15 +152,11 @@ public class FullVideoRecorder extends VideoRecorder { if (mError == null) mError = e; } mMediaRecorder.release(); - if (mController != null) { - // Restore frame processing. - mCamera.setPreviewCallbackWithBuffer(mController); - } } mProfile = null; mMediaRecorder = null; - mCamera = null; - mController = null; + mMediaRecorderPrepared = false; dispatchResult(); } + } 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 717b9737..4652ef48 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java @@ -4,6 +4,7 @@ import android.graphics.SurfaceTexture; import android.opengl.EGL14; import android.os.Build; +import com.otaliastudios.cameraview.CameraException; import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.controls.Audio; @@ -39,6 +40,7 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram private static final int STATE_NOT_RECORDING = 1; private MediaEncoderEngine mEncoderEngine; + private CameraEngine mEngine; private GlCameraPreview mPreview; private boolean mFlipped; @@ -46,22 +48,22 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram private int mDesiredState = STATE_NOT_RECORDING; private int mTextureId = 0; - public SnapshotVideoRecorder(@NonNull VideoResult.Stub stub, - @NonNull CameraEngine engine, + public SnapshotVideoRecorder(@NonNull CameraEngine engine, @NonNull GlCameraPreview preview) { - super(stub, engine); + super(engine); mPreview = preview; - mPreview.addRendererFrameCallback(this); - mFlipped = engine.flip(CameraEngine.REF_SENSOR, CameraEngine.REF_VIEW); + mEngine = engine; } @Override - public void start() { + protected void onStart() { + mPreview.addRendererFrameCallback(this); + mFlipped = mEngine.flip(CameraEngine.REF_SENSOR, CameraEngine.REF_VIEW); mDesiredState = STATE_RECORDING; } @Override - public void stop() { + protected void onStop() { mDesiredState = STATE_NOT_RECORDING; } @@ -163,10 +165,7 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram // Cleanup mCurrentState = STATE_NOT_RECORDING; mDesiredState = STATE_NOT_RECORDING; - if (mPreview != null) { - mPreview.removeRendererFrameCallback(SnapshotVideoRecorder.this); - mPreview = null; - } + mPreview.removeRendererFrameCallback(SnapshotVideoRecorder.this); mEncoderEngine = null; dispatchResult(); } 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 e855ebdf..e7c82099 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/VideoRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/VideoRecorder.java @@ -2,6 +2,7 @@ package com.otaliastudios.cameraview.video; import com.otaliastudios.cameraview.VideoResult; +import androidx.annotation.CallSuper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -9,7 +10,6 @@ import androidx.annotation.VisibleForTesting; /** * Interface for video recording. * Don't call start if already started. Don't call stop if already stopped. - * Don't reuse. */ public abstract class VideoRecorder { @@ -27,40 +27,57 @@ public abstract class VideoRecorder { } @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) VideoResult.Stub mResult; - @VisibleForTesting VideoResultListener mListener; + @VisibleForTesting final VideoResultListener mListener; + @SuppressWarnings("WeakerAccess") protected Exception mError; + private boolean mIsRecording; /** * Creates a new video recorder. - * @param stub a video stub * @param listener a listener */ - VideoRecorder(@NonNull VideoResult.Stub stub, @Nullable VideoResultListener listener) { - mResult = stub; + VideoRecorder(@Nullable VideoResultListener listener) { mListener = listener; } /** * Starts recording a video. */ - public abstract void start(); + public final void start(@NonNull VideoResult.Stub stub) { + mResult = stub; + mIsRecording = true; + onStart(); + } /** * Stops recording. */ - public abstract void stop(); + public final void stop() { + onStop(); + } + + /** + * Returns true if it is currently recording. + * @return true if recording + */ + public boolean isRecording() { + return mIsRecording; + } + + protected abstract void onStart(); + + protected abstract void onStop(); /** * Subclasses can call this to notify that the result was obtained, * either with some error (null result) or with the actual stub, filled. */ @SuppressWarnings("WeakerAccess") + @CallSuper protected void dispatchResult() { - if (mListener != null) { - mListener.onVideoResult(mResult, mError); - mListener = null; - mResult = null; - mError = null; - } + mIsRecording = false; + if (mListener != null) mListener.onVideoResult(mResult, mError); + mResult = null; + mError = null; } } 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 016a405a..d0c1e557 100644 --- a/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java +++ b/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java @@ -20,6 +20,7 @@ import com.otaliastudios.cameraview.CameraView; import com.otaliastudios.cameraview.PictureResult; import com.otaliastudios.cameraview.controls.Mode; import com.otaliastudios.cameraview.VideoResult; +import com.otaliastudios.cameraview.size.SizeSelectors; import java.io.File;