Camera2 support for videos

pull/493/head
Mattia Iavarone 5 years ago
parent 1dabf4db01
commit c353ae808e
  1. 2
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/MockCameraEngine.java
  2. 19
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java
  3. 58
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java
  4. 167
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java
  5. 70
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java
  6. 26
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/CamcorderProfiles.java
  7. 41
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/WorkerHandler.java
  8. 52
      cameraview/src/main/java/com/otaliastudios/cameraview/video/Full1VideoRecorder.java
  9. 96
      cameraview/src/main/java/com/otaliastudios/cameraview/video/Full2VideoRecorder.java
  10. 120
      cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java
  11. 19
      cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java
  12. 39
      cameraview/src/main/java/com/otaliastudios/cameraview/video/VideoRecorder.java
  13. 1
      demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.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) {
}

@ -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,14 +170,20 @@ 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) {
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));
}
}
}
/**
* Shorthand for getSupported*().contains(value).

@ -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,30 +512,9 @@ 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;
protected void onTakeVideo(@NonNull VideoResult.Stub stub) {
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();
@ -561,17 +524,16 @@ public class Camera1Engine extends CameraEngine implements Camera.PreviewCallbac
onVideoResult(null, e);
return;
}
mVideoRecorder = new FullVideoRecorder(stub, Camera1Engine.this,
Camera1Engine.this, mCamera, mCameraId);
mVideoRecorder.start();
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);
}
// -----------------

@ -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<Surface> 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<Surface> 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<Void> 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) {

@ -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() {

@ -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<Size> sizes = new ArrayList<>(sizeToProfileMap.keySet());
Collections.sort(sizes, new Comparator<Size>() {
@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);
}

@ -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 <T> Task<T> run(@NonNull Callable<T> 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 <T> Task<T> post(@NonNull final Callable<T> callable) {
final TaskCompletionSource<T> 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

@ -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();
}
}

@ -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);
}
}
}

@ -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;
@SuppressWarnings("WeakerAccess") protected MediaRecorder mMediaRecorder;
@SuppressWarnings("WeakerAccess") protected CamcorderProfile mProfile;
private boolean mMediaRecorderPrepared;
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);
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();
}
}

@ -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;
}
mEncoderEngine = null;
dispatchResult();
}

@ -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;
mIsRecording = false;
if (mListener != null) mListener.onVideoResult(mResult, mError);
mResult = null;
mError = null;
}
}
}

@ -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;

Loading…
Cancel
Save