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 @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.CameraCharacteristics;
import android.hardware.camera2.CameraManager; import android.hardware.camera2.CameraManager;
import android.hardware.camera2.params.StreamConfigurationMap; import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.CamcorderProfile;
import android.media.ImageReader; import android.media.ImageReader;
import android.media.MediaRecorder; import android.media.MediaRecorder;
import android.os.Build; 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.Mode;
import com.otaliastudios.cameraview.controls.VideoCodec; import com.otaliastudios.cameraview.controls.VideoCodec;
import com.otaliastudios.cameraview.controls.WhiteBalance; import com.otaliastudios.cameraview.controls.WhiteBalance;
import com.otaliastudios.cameraview.internal.utils.CamcorderProfiles;
import com.otaliastudios.cameraview.size.AspectRatio; import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
@ -142,13 +144,14 @@ public class CameraOptions {
// Camera2Engine constructor. // Camera2Engine constructor.
@RequiresApi(Build.VERSION_CODES.LOLLIPOP) @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
@SuppressWarnings("deprecation") @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); Mapper mapper = Mapper.get(Engine.CAMERA2);
CameraCharacteristics cameraCharacteristics = manager.getCameraCharacteristics(cameraId);
// Facing // Facing
for (String cameraId : manager.getCameraIdList()) { for (String cameraId1 : manager.getCameraIdList()) {
CameraCharacteristics cameraCharacteristics = manager.getCameraCharacteristics(cameraId); CameraCharacteristics cameraCharacteristics1 = manager.getCameraCharacteristics(cameraId1);
Integer cameraFacing = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING); Integer cameraFacing = cameraCharacteristics1.get(CameraCharacteristics.LENS_FACING);
if (cameraFacing != null) { if (cameraFacing != null) {
Facing value = mapper.unmapFacing(cameraFacing); Facing value = mapper.unmapFacing(cameraFacing);
if (value != null) supportedFacing.add(value); if (value != null) supportedFacing.add(value);
@ -156,7 +159,7 @@ public class CameraOptions {
} }
// Picture Sizes // 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."); if (streamMap == null) throw new RuntimeException("StreamConfigurationMap is null. Should not happen.");
android.util.Size[] psizes = streamMap.getOutputSizes(ImageReader.class); android.util.Size[] psizes = streamMap.getOutputSizes(ImageReader.class);
for (android.util.Size size : psizes) { for (android.util.Size size : psizes) {
@ -167,14 +170,20 @@ public class CameraOptions {
} }
// Video Sizes // 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); android.util.Size[] vsizes = streamMap.getOutputSizes(MediaRecorder.class);
for (android.util.Size size : vsizes) { for (android.util.Size size : vsizes) {
if (size.getWidth() <= videoMaxSize.getWidth() && size.getHeight() <= videoMaxSize.getHeight()) {
int width = flipSizes ? size.getHeight() : size.getWidth(); int width = flipSizes ? size.getHeight() : size.getWidth();
int height = flipSizes ? size.getWidth() : size.getHeight(); int height = flipSizes ? size.getWidth() : size.getHeight();
supportedVideoSizes.add(new Size(width, height)); supportedVideoSizes.add(new Size(width, height));
supportedVideoAspectRatio.add(AspectRatio.of(width, height)); supportedVideoAspectRatio.add(AspectRatio.of(width, height));
} }
} }
}
/** /**
* Shorthand for getSupported*().contains(value). * 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.frame.Frame;
import com.otaliastudios.cameraview.PictureResult; import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.controls.Flash; import com.otaliastudios.cameraview.controls.Flash;
import com.otaliastudios.cameraview.gesture.Gesture; 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.CropHelper;
import com.otaliastudios.cameraview.internal.utils.Op; import com.otaliastudios.cameraview.internal.utils.Op;
import com.otaliastudios.cameraview.picture.FullPictureRecorder; import com.otaliastudios.cameraview.picture.FullPictureRecorder;
import com.otaliastudios.cameraview.picture.PictureRecorder;
import com.otaliastudios.cameraview.picture.Snapshot1PictureRecorder; import com.otaliastudios.cameraview.picture.Snapshot1PictureRecorder;
import com.otaliastudios.cameraview.picture.SnapshotGlPictureRecorder; import com.otaliastudios.cameraview.picture.SnapshotGlPictureRecorder;
import com.otaliastudios.cameraview.preview.GlCameraPreview; import com.otaliastudios.cameraview.preview.GlCameraPreview;
import com.otaliastudios.cameraview.size.AspectRatio; import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size; 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.SnapshotVideoRecorder;
import com.otaliastudios.cameraview.video.VideoRecorder;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -251,6 +247,7 @@ public class Camera1Engine extends CameraEngine implements Camera.PreviewCallbac
mCamera = null; mCamera = null;
mCameraOptions = null; mCameraOptions = null;
} }
mVideoRecorder = null;
mCameraOptions = null; mCameraOptions = null;
mCamera = null; mCamera = null;
LOG.w("onStopEngine:", "Clean up.", "Returning."); LOG.w("onStopEngine:", "Clean up.", "Returning.");
@ -308,19 +305,6 @@ public class Camera1Engine extends CameraEngine implements Camera.PreviewCallbac
throw new CameraException(runtime, reason); 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 @Override
public void setLocation(@Nullable Location location) { public void setLocation(@Nullable Location location) {
final Location oldLocation = mLocation; final Location oldLocation = mLocation;
@ -528,30 +512,9 @@ public class Camera1Engine extends CameraEngine implements Camera.PreviewCallbac
} }
@Override @Override
public void takeVideo(final @NonNull VideoResult.Stub stub, @NonNull final File videoFile) { protected void onTakeVideo(@NonNull VideoResult.Stub stub) {
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.rotation = offset(REF_SENSOR, REF_OUTPUT);
stub.size = flip(REF_SENSOR, REF_OUTPUT) ? mCaptureSize.flip() : mCaptureSize; 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. // Unlock the camera and start recording.
try { try {
mCamera.unlock(); mCamera.unlock();
@ -561,17 +524,16 @@ public class Camera1Engine extends CameraEngine implements Camera.PreviewCallbac
onVideoResult(null, e); onVideoResult(null, e);
return; return;
} }
mVideoRecorder = new FullVideoRecorder(stub, Camera1Engine.this, if (!(mVideoRecorder instanceof Full1VideoRecorder)) {
Camera1Engine.this, mCamera, mCameraId); mVideoRecorder = new Full1VideoRecorder(Camera1Engine.this, mCamera, mCameraId);
mVideoRecorder.start();
} }
}); mVideoRecorder.start(stub);
} }
@SuppressLint("NewApi") @SuppressLint("NewApi")
@WorkerThread @WorkerThread
@Override @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)) { if (!(mPreview instanceof GlCameraPreview)) {
throw new IllegalStateException("Video snapshots are only supported with 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. // Reset facing and start.
mFacing = realFacing; mFacing = realFacing;
mVideoRecorder = new SnapshotVideoRecorder(stub, Camera1Engine.this, glPreview); if (!(mVideoRecorder instanceof SnapshotVideoRecorder)) {
mVideoRecorder.start(); 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.CaptureRequest;
import android.hardware.camera2.params.StreamConfigurationMap; import android.hardware.camera2.params.StreamConfigurationMap;
import android.location.Location; import android.location.Location;
import android.media.MediaCodec;
import android.os.Build; import android.os.Build;
import android.view.Surface; import android.view.Surface;
import android.view.SurfaceHolder; import android.view.SurfaceHolder;
@ -25,7 +26,6 @@ import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.CameraOptions; import com.otaliastudios.cameraview.CameraOptions;
import com.otaliastudios.cameraview.PictureResult; import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.Engine; import com.otaliastudios.cameraview.controls.Engine;
import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.controls.Flash; 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.controls.WhiteBalance;
import com.otaliastudios.cameraview.gesture.Gesture; import com.otaliastudios.cameraview.gesture.Gesture;
import com.otaliastudios.cameraview.internal.utils.CropHelper; 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.picture.SnapshotGlPictureRecorder;
import com.otaliastudios.cameraview.preview.GlCameraPreview; import com.otaliastudios.cameraview.preview.GlCameraPreview;
import com.otaliastudios.cameraview.size.AspectRatio; import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
import com.otaliastudios.cameraview.video.Full2VideoRecorder;
import com.otaliastudios.cameraview.video.SnapshotVideoRecorder; import com.otaliastudios.cameraview.video.SnapshotVideoRecorder;
import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -56,7 +55,6 @@ import androidx.annotation.WorkerThread;
// TODO parameters // TODO parameters
// TODO pictures // TODO pictures
// TODO videos
@RequiresApi(Build.VERSION_CODES.LOLLIPOP) @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public class Camera2Engine extends CameraEngine { public class Camera2Engine extends CameraEngine {
@ -69,6 +67,13 @@ public class Camera2Engine extends CameraEngine {
private CameraCaptureSession mSession; private CameraCaptureSession mSession;
private CaptureRequest.Builder mPreviewStreamRequestBuilder; private CaptureRequest.Builder mPreviewStreamRequestBuilder;
private CaptureRequest mPreviewStreamRequest; 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) { public Camera2Engine(Callback callback) {
super(callback); super(callback);
@ -194,7 +199,7 @@ public class Camera2Engine extends CameraEngine {
try { try {
LOG.i("createCamera:", "Applying default parameters."); LOG.i("createCamera:", "Applying default parameters.");
CameraCharacteristics characteristics = mManager.getCameraCharacteristics(mCameraId); 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 // applyDefaultFocus(params); TODO
// applyFlash(params, Flash.OFF); // applyFlash(params, Flash.OFF);
// applyLocation(params, null); // 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 // 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. // the camera params object, we must resize our own surfaces.
final Object output = mPreview.getOutput(); final Object output = mPreview.getOutput();
Surface previewSurface;
if (output instanceof SurfaceHolder) { if (output instanceof SurfaceHolder) {
try { try {
// This must be called from the UI thread... // This must be called from the UI thread...
@ -259,37 +263,46 @@ public class Camera2Engine extends CameraEngine {
} catch (ExecutionException | InterruptedException e) { } catch (ExecutionException | InterruptedException e) {
throw new CameraException(e, CameraException.REASON_FAILED_TO_CONNECT); throw new CameraException(e, CameraException.REASON_FAILED_TO_CONNECT);
} }
previewSurface = ((SurfaceHolder) output).getSurface(); mPreviewStreamSurface = ((SurfaceHolder) output).getSurface();
} else if (output instanceof SurfaceTexture) { } else if (output instanceof SurfaceTexture) {
((SurfaceTexture) output).setDefaultBufferSize(mPreviewStreamSize.getWidth(), mPreviewStreamSize.getHeight()); ((SurfaceTexture) output).setDefaultBufferSize(mPreviewStreamSize.getWidth(), mPreviewStreamSize.getHeight());
previewSurface = new Surface((SurfaceTexture) output); mPreviewStreamSurface = new Surface((SurfaceTexture) output);
} else { } else {
throw new RuntimeException("Unknown CameraPreview output class."); throw new RuntimeException("Unknown CameraPreview output class.");
} }
// TODO: captureSize Surface captureSurface = null;
/* if (mMode == Mode.PICTURE) { if (mMode == Mode.PICTURE) {
params.setPictureSize(mCaptureSize.getWidth(), mCaptureSize.getHeight()); // <- allowed // TODO picture recorder
} else { } else {
// mCaptureSize in this case is a video size. The available video sizes are not necessarily if (Full2VideoRecorder.SUPPORTS_PERSISTENT_SURFACE) {
// a subset of the picture sizes, so we can't use the mCaptureSize value: it might crash. mFullVideoPersistentSurface = MediaCodec.createPersistentInputSurface();
// However, the setPictureSize() passed here is useless : we don't allow HQ pictures in video mode. captureSurface = mFullVideoPersistentSurface;
// While this might be lifted in the future, for now, just use a picture capture size. } else if (mFullVideoPendingStub != null) {
Size pictureSize = computeCaptureSize(Mode.PICTURE); Full2VideoRecorder recorder = new Full2VideoRecorder(this, mCameraId, null);
params.setPictureSize(pictureSize.getWidth(), pictureSize.getHeight()); try {
} */ captureSurface = recorder.createInputSurface(mFullVideoPendingStub);
} catch (Full2VideoRecorder.PrepareException e) {
//noinspection ArraysAsListWithZeroOrOneArgument throw new CameraException(e, CameraException.REASON_FAILED_TO_CONNECT);
List<Surface> outputSurfaces = Arrays.asList(previewSurface); }
mVideoRecorder = recorder;
}
}
List<Surface> outputSurfaces = new ArrayList<>();
outputSurfaces.add(mPreviewStreamSurface);
if (captureSurface != null) outputSurfaces.add(captureSurface);
try { try {
mPreviewStreamRequestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); mPreviewStreamRequestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
mPreviewStreamRequestBuilder.addTarget(previewSurface); mPreviewStreamRequestBuilder.addTarget(mPreviewStreamSurface);
// null handler means using the current looper which is totally ok. // null handler means using the current looper which is totally ok.
mCamera.createCaptureSession(outputSurfaces, new CameraCaptureSession.StateCallback() { mCamera.createCaptureSession(outputSurfaces, new CameraCaptureSession.StateCallback() {
@Override @Override
public void onConfigured(@NonNull CameraCaptureSession session) { public void onConfigured(@NonNull CameraCaptureSession session) {
mSession = session; mSession = session;
LOG.i("onStartBind:", "Completed");
task.trySetResult(null); task.trySetResult(null);
} }
@ -299,7 +312,6 @@ public class Camera2Engine extends CameraEngine {
String message = LOG.e("onConfigureFailed! Session", session); String message = LOG.e("onConfigureFailed! Session", session);
throw new RuntimeException(message); throw new RuntimeException(message);
} }
}, null); }, null);
} catch (CameraAccessException e) { } catch (CameraAccessException e) {
throw createCameraException(e); throw createCameraException(e);
@ -337,6 +349,12 @@ public class Camera2Engine extends CameraEngine {
throw new CameraException(e, CameraException.REASON_FAILED_TO_START_PREVIEW); throw new CameraException(e, CameraException.REASON_FAILED_TO_START_PREVIEW);
} }
LOG.i("onStartPreview", "Started 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); return Tasks.forResult(null);
} }
@ -372,6 +390,11 @@ public class Camera2Engine extends CameraEngine {
@NonNull @NonNull
@Override @Override
protected Task<Void> onStopBind() { protected Task<Void> onStopBind() {
if (mFullVideoPersistentSurface != null) {
mFullVideoPersistentSurface.release();
mFullVideoPersistentSurface = null;
}
mPreviewStreamSurface = null;
mPreviewStreamRequestBuilder = null; mPreviewStreamRequestBuilder = null;
mPreviewStreamSize = null; mPreviewStreamSize = null;
mCaptureSize = null; mCaptureSize = null;
@ -394,6 +417,7 @@ public class Camera2Engine extends CameraEngine {
} }
mCamera = null; mCamera = null;
mCameraOptions = null; mCameraOptions = null;
mVideoRecorder = null;
LOG.w("onStopEngine:", "Returning."); LOG.w("onStopEngine:", "Returning.");
return Tasks.forResult(null); return Tasks.forResult(null);
} }
@ -420,13 +444,58 @@ public class Camera2Engine extends CameraEngine {
//region Videos //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. * to read about the size and rotation computation.
*/ */
@WorkerThread @WorkerThread
@Override @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)) { if (!(mPreview instanceof GlCameraPreview)) {
throw new IllegalStateException("Video snapshots are only supported with GlCameraPreview."); throw new IllegalStateException("Video snapshots are only supported with GlCameraPreview.");
} }
@ -445,21 +514,32 @@ public class Camera2Engine extends CameraEngine {
// Reset facing and start. // Reset facing and start.
mFacing = realFacing; mFacing = realFacing;
mVideoRecorder = new SnapshotVideoRecorder(stub, this, glPreview); if (!(mVideoRecorder instanceof SnapshotVideoRecorder)) {
mVideoRecorder.start(); 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 @Override
@ -467,11 +547,6 @@ public class Camera2Engine extends CameraEngine {
} }
@Override
public void setMode(@NonNull Mode mode) {
}
@Override @Override
public void setZoom(float zoom, @Nullable PointF[] points, boolean notify) { 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 @Override
public void startAutoFocus(@Nullable Gesture gesture, @NonNull PointF point) { public void startAutoFocus(@Nullable Gesture gesture, @NonNull PointF point) {

@ -1,13 +1,10 @@
package com.otaliastudios.cameraview.engine; package com.otaliastudios.cameraview.engine;
import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.graphics.PointF; import android.graphics.PointF;
import android.graphics.Rect;
import android.location.Location; import android.location.Location;
import android.os.Build;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; 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.SuccessContinuation;
import com.google.android.gms.tasks.TaskCompletionSource; import com.google.android.gms.tasks.TaskCompletionSource;
import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
import com.otaliastudios.cameraview.CameraException; import com.otaliastudios.cameraview.CameraException;
import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.CameraOptions; import com.otaliastudios.cameraview.CameraOptions;
@ -25,12 +21,9 @@ import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.frame.Frame; import com.otaliastudios.cameraview.frame.Frame;
import com.otaliastudios.cameraview.frame.FrameManager; 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.Op;
import com.otaliastudios.cameraview.internal.utils.WorkerHandler; import com.otaliastudios.cameraview.internal.utils.WorkerHandler;
import com.otaliastudios.cameraview.picture.PictureRecorder; 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.preview.CameraPreview;
import com.otaliastudios.cameraview.controls.Audio; import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.Facing; 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.Mode;
import com.otaliastudios.cameraview.controls.VideoCodec; import com.otaliastudios.cameraview.controls.VideoCodec;
import com.otaliastudios.cameraview.controls.WhiteBalance; import com.otaliastudios.cameraview.controls.WhiteBalance;
import com.otaliastudios.cameraview.preview.GlCameraPreview;
import com.otaliastudios.cameraview.size.AspectRatio; import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
import com.otaliastudios.cameraview.size.SizeSelector; import com.otaliastudios.cameraview.size.SizeSelector;
import com.otaliastudios.cameraview.size.SizeSelectors; import com.otaliastudios.cameraview.size.SizeSelectors;
import com.otaliastudios.cameraview.video.SnapshotVideoRecorder;
import com.otaliastudios.cameraview.video.VideoRecorder; import com.otaliastudios.cameraview.video.VideoRecorder;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -59,7 +50,6 @@ import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor; 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 //endregion
//region Abstract setters and APIs //region Abstract setters and APIs
@ -885,9 +893,6 @@ public abstract class CameraEngine implements
*/ */
protected abstract boolean collectCameraInfo(@NonNull Facing facing); 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. // If closed, no-op. If opened, check supported and apply.
public abstract void setZoom(float zoom, @Nullable PointF[] points, boolean notify); 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 * @param stub a video stub
@ -984,7 +1014,7 @@ public abstract class CameraEngine implements
stub.audio = mAudio; stub.audio = mAudio;
stub.maxSize = mVideoMaxSize; stub.maxSize = mVideoMaxSize;
stub.maxDuration = mVideoMaxDuration; stub.maxDuration = mVideoMaxDuration;
onTakeVideoSnapshot(stub, file, viewAspectRatio); onTakeVideoSnapshot(stub, viewAspectRatio);
mStartVideoOp.end(null); mStartVideoOp.end(null);
} }
}); });
@ -995,10 +1025,9 @@ public abstract class CameraEngine implements
mHandler.run(new Runnable() { mHandler.run(new Runnable() {
@Override @Override
public void run() { public void run() {
LOG.i("stopVideo", "executing.", "has recorder?", mVideoRecorder != null); LOG.i("stopVideo", "executing.", "isTakingVideo?", isTakingVideo());
if (mVideoRecorder != null) { if (mVideoRecorder != null) {
mVideoRecorder.stop(); mVideoRecorder.stop();
mVideoRecorder = null;
} }
} }
}); });
@ -1019,7 +1048,10 @@ public abstract class CameraEngine implements
protected abstract void onTakePictureSnapshot(@NonNull PictureResult.Stub stub, @NonNull AspectRatio viewAspectRatio); protected abstract void onTakePictureSnapshot(@NonNull PictureResult.Stub stub, @NonNull AspectRatio viewAspectRatio);
@WorkerThread @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 //endregion
@ -1122,7 +1154,7 @@ public abstract class CameraEngine implements
} }
public final boolean isTakingVideo() { public final boolean isTakingVideo() {
return mVideoRecorder != null; return mVideoRecorder != null && mVideoRecorder.isRecording();
} }
public final boolean isTakingPicture() { 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, * 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). * to ensure we get acceptable video/audio parameters for MediaRecorders (most notably the bitrate).
@ -45,13 +65,13 @@ public class CamcorderProfiles {
*/ */
@NonNull @NonNull
public static CamcorderProfile get(int cameraId, @NonNull Size targetSize) { 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()); List<Size> sizes = new ArrayList<>(sizeToProfileMap.keySet());
Collections.sort(sizes, new Comparator<Size>() { Collections.sort(sizes, new Comparator<Size>() {
@Override @Override
public int compare(Size s1, Size s2) { public int compare(Size s1, Size s2) {
int a1 = Math.abs(s1.getWidth() * s1.getHeight() - targetArea); long a1 = Math.abs(s1.getWidth() * s1.getHeight() - targetArea);
int a2 = Math.abs(s2.getWidth() * s2.getHeight() - targetArea); long a2 = Math.abs(s2.getWidth() * s2.getHeight() - targetArea);
//noinspection UseCompareMethod //noinspection UseCompareMethod
return (a1 < a2) ? -1 : ((a1 == a2) ? 0 : 1); return (a1 < a2) ? -1 : ((a1 == a2) ? 0 : 1);
} }

@ -4,11 +4,15 @@ import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.os.Looper; 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 com.otaliastudios.cameraview.CameraLogger;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
@ -84,7 +88,23 @@ public class WorkerHandler {
if (Thread.currentThread() == getThread()) { if (Thread.currentThread() == getThread()) {
runnable.run(); runnable.run();
} else { } 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); 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. * Post an action on this handler.
* @param delay the delay in millis * @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; package com.otaliastudios.cameraview.video;
import android.hardware.Camera;
import android.media.CamcorderProfile; import android.media.CamcorderProfile;
import android.media.MediaRecorder; import android.media.MediaRecorder;
import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.controls.Audio; 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 com.otaliastudios.cameraview.size.Size;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -16,78 +13,84 @@ import androidx.annotation.Nullable;
/** /**
* A {@link VideoRecorder} that uses {@link android.media.MediaRecorder} APIs. * 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 String TAG = FullVideoRecorder.class.getSimpleName();
private static final CameraLogger LOG = CameraLogger.create(TAG); private static final CameraLogger LOG = CameraLogger.create(TAG);
private MediaRecorder mMediaRecorder; @SuppressWarnings("WeakerAccess") protected MediaRecorder mMediaRecorder;
private CamcorderProfile mProfile; @SuppressWarnings("WeakerAccess") protected CamcorderProfile mProfile;
private Camera1Engine mController; private boolean mMediaRecorderPrepared;
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. FullVideoRecorder(@Nullable VideoResultListener listener) {
mSize = mResult.rotation % 180 != 0 ? mResult.size.flip() : mResult.size; super(listener);
mProfile = CamcorderProfiles.get(cameraId, mSize);
} }
@Override @SuppressWarnings({"WeakerAccess", "UnusedReturnValue", "BooleanMethodIsAlwaysInverted"})
public void start() { protected boolean prepareMediaRecorder(@NonNull VideoResult.Stub stub) {
if (mResult.audio == Audio.ON) { 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. // Must be called before setOutputFormat.
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT); mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT);
} }
mMediaRecorder.setOutputFormat(mProfile.fileFormat); mMediaRecorder.setOutputFormat(mProfile.fileFormat);
if (mResult.videoFrameRate <= 0) { if (stub.videoFrameRate <= 0) {
mMediaRecorder.setVideoFrameRate(mProfile.videoFrameRate); mMediaRecorder.setVideoFrameRate(mProfile.videoFrameRate);
mResult.videoFrameRate = mProfile.videoFrameRate; stub.videoFrameRate = mProfile.videoFrameRate;
} else { } else {
mMediaRecorder.setVideoFrameRate(mResult.videoFrameRate); mMediaRecorder.setVideoFrameRate(stub.videoFrameRate);
} }
mMediaRecorder.setVideoSize(mSize.getWidth(), mSize.getHeight()); mMediaRecorder.setVideoSize(size.getWidth(), size.getHeight());
switch (mResult.videoCodec) { switch (stub.videoCodec) {
case H_263: mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H263); break; case H_263: mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H263); break;
case H_264: mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); break; case H_264: mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); break;
case DEVICE_DEFAULT: mMediaRecorder.setVideoEncoder(mProfile.videoCodec); break; case DEVICE_DEFAULT: mMediaRecorder.setVideoEncoder(mProfile.videoCodec); break;
} }
if (mResult.videoBitRate <= 0) { if (stub.videoBitRate <= 0) {
mMediaRecorder.setVideoEncodingBitRate(mProfile.videoBitRate); mMediaRecorder.setVideoEncodingBitRate(mProfile.videoBitRate);
mResult.videoBitRate = mProfile.videoBitRate; stub.videoBitRate = mProfile.videoBitRate;
} else { } else {
mMediaRecorder.setVideoEncodingBitRate(mResult.videoBitRate); mMediaRecorder.setVideoEncodingBitRate(stub.videoBitRate);
} }
if (mResult.audio == Audio.ON) { if (stub.audio == Audio.ON) {
mMediaRecorder.setAudioChannels(mProfile.audioChannels); mMediaRecorder.setAudioChannels(mProfile.audioChannels);
mMediaRecorder.setAudioSamplingRate(mProfile.audioSampleRate); mMediaRecorder.setAudioSamplingRate(mProfile.audioSampleRate);
mMediaRecorder.setAudioEncoder(mProfile.audioCodec); mMediaRecorder.setAudioEncoder(mProfile.audioCodec);
if (mResult.audioBitRate <= 0) { if (stub.audioBitRate <= 0) {
mMediaRecorder.setAudioEncodingBitRate(mProfile.audioBitRate); mMediaRecorder.setAudioEncodingBitRate(mProfile.audioBitRate);
mResult.audioBitRate = mProfile.audioBitRate; stub.audioBitRate = mProfile.audioBitRate;
} else { } else {
mMediaRecorder.setAudioEncodingBitRate(mResult.audioBitRate); mMediaRecorder.setAudioEncodingBitRate(stub.audioBitRate);
} }
} }
if (mResult.location != null) { if (stub.location != null) {
mMediaRecorder.setLocation( mMediaRecorder.setLocation(
(float) mResult.location.getLatitude(), (float) stub.location.getLatitude(),
(float) mResult.location.getLongitude()); (float) stub.location.getLongitude());
} }
mMediaRecorder.setOutputFile(mResult.file.getAbsolutePath()); mMediaRecorder.setOutputFile(stub.file.getAbsolutePath());
mMediaRecorder.setOrientationHint(mResult.rotation); mMediaRecorder.setOrientationHint(stub.rotation);
mMediaRecorder.setMaxFileSize(mResult.maxSize); mMediaRecorder.setMaxFileSize(stub.maxSize);
mMediaRecorder.setMaxDuration(mResult.maxDuration); mMediaRecorder.setMaxDuration(stub.maxDuration);
mMediaRecorder.setOnInfoListener(new MediaRecorder.OnInfoListener() { mMediaRecorder.setOnInfoListener(new MediaRecorder.OnInfoListener() {
@Override @Override
public void onInfo(MediaRecorder mediaRecorder, int what, int extra) { 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 { try {
mMediaRecorder.prepare(); 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(); mMediaRecorder.start();
} catch (Exception e) { } catch (Exception e) {
LOG.w("stop:", "Error while starting media recorder.", e); LOG.w("start:", "Error while starting media recorder.", e);
mResult = null; mResult = null;
mError = e; mError = e;
stop(); stop();
@ -117,7 +139,7 @@ public class FullVideoRecorder extends VideoRecorder {
} }
@Override @Override
public void stop() { protected void onStop() {
if (mMediaRecorder != null) { if (mMediaRecorder != null) {
try { try {
mMediaRecorder.stop(); mMediaRecorder.stop();
@ -130,15 +152,11 @@ public class FullVideoRecorder extends VideoRecorder {
if (mError == null) mError = e; if (mError == null) mError = e;
} }
mMediaRecorder.release(); mMediaRecorder.release();
if (mController != null) {
// Restore frame processing.
mCamera.setPreviewCallbackWithBuffer(mController);
}
} }
mProfile = null; mProfile = null;
mMediaRecorder = null; mMediaRecorder = null;
mCamera = null; mMediaRecorderPrepared = false;
mController = null;
dispatchResult(); dispatchResult();
} }
} }

@ -4,6 +4,7 @@ import android.graphics.SurfaceTexture;
import android.opengl.EGL14; import android.opengl.EGL14;
import android.os.Build; import android.os.Build;
import com.otaliastudios.cameraview.CameraException;
import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.controls.Audio; 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 static final int STATE_NOT_RECORDING = 1;
private MediaEncoderEngine mEncoderEngine; private MediaEncoderEngine mEncoderEngine;
private CameraEngine mEngine;
private GlCameraPreview mPreview; private GlCameraPreview mPreview;
private boolean mFlipped; private boolean mFlipped;
@ -46,22 +48,22 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
private int mDesiredState = STATE_NOT_RECORDING; private int mDesiredState = STATE_NOT_RECORDING;
private int mTextureId = 0; private int mTextureId = 0;
public SnapshotVideoRecorder(@NonNull VideoResult.Stub stub, public SnapshotVideoRecorder(@NonNull CameraEngine engine,
@NonNull CameraEngine engine,
@NonNull GlCameraPreview preview) { @NonNull GlCameraPreview preview) {
super(stub, engine); super(engine);
mPreview = preview; mPreview = preview;
mPreview.addRendererFrameCallback(this); mEngine = engine;
mFlipped = engine.flip(CameraEngine.REF_SENSOR, CameraEngine.REF_VIEW);
} }
@Override @Override
public void start() { protected void onStart() {
mPreview.addRendererFrameCallback(this);
mFlipped = mEngine.flip(CameraEngine.REF_SENSOR, CameraEngine.REF_VIEW);
mDesiredState = STATE_RECORDING; mDesiredState = STATE_RECORDING;
} }
@Override @Override
public void stop() { protected void onStop() {
mDesiredState = STATE_NOT_RECORDING; mDesiredState = STATE_NOT_RECORDING;
} }
@ -163,10 +165,7 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
// Cleanup // Cleanup
mCurrentState = STATE_NOT_RECORDING; mCurrentState = STATE_NOT_RECORDING;
mDesiredState = STATE_NOT_RECORDING; mDesiredState = STATE_NOT_RECORDING;
if (mPreview != null) {
mPreview.removeRendererFrameCallback(SnapshotVideoRecorder.this); mPreview.removeRendererFrameCallback(SnapshotVideoRecorder.this);
mPreview = null;
}
mEncoderEngine = null; mEncoderEngine = null;
dispatchResult(); dispatchResult();
} }

@ -2,6 +2,7 @@ package com.otaliastudios.cameraview.video;
import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.VideoResult;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
@ -9,7 +10,6 @@ import androidx.annotation.VisibleForTesting;
/** /**
* Interface for video recording. * Interface for video recording.
* Don't call start if already started. Don't call stop if already stopped. * Don't call start if already started. Don't call stop if already stopped.
* Don't reuse.
*/ */
public abstract class VideoRecorder { public abstract class VideoRecorder {
@ -27,40 +27,57 @@ public abstract class VideoRecorder {
} }
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) VideoResult.Stub mResult; @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) VideoResult.Stub mResult;
@VisibleForTesting VideoResultListener mListener; @VisibleForTesting final VideoResultListener mListener;
@SuppressWarnings("WeakerAccess")
protected Exception mError; protected Exception mError;
private boolean mIsRecording;
/** /**
* Creates a new video recorder. * Creates a new video recorder.
* @param stub a video stub
* @param listener a listener * @param listener a listener
*/ */
VideoRecorder(@NonNull VideoResult.Stub stub, @Nullable VideoResultListener listener) { VideoRecorder(@Nullable VideoResultListener listener) {
mResult = stub;
mListener = listener; mListener = listener;
} }
/** /**
* Starts recording a video. * Starts recording a video.
*/ */
public abstract void start(); public final void start(@NonNull VideoResult.Stub stub) {
mResult = stub;
mIsRecording = true;
onStart();
}
/** /**
* Stops recording. * 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, * Subclasses can call this to notify that the result was obtained,
* either with some error (null result) or with the actual stub, filled. * either with some error (null result) or with the actual stub, filled.
*/ */
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
@CallSuper
protected void dispatchResult() { protected void dispatchResult() {
if (mListener != null) { mIsRecording = false;
mListener.onVideoResult(mResult, mError); if (mListener != null) mListener.onVideoResult(mResult, mError);
mListener = null;
mResult = null; mResult = null;
mError = null; mError = null;
} }
}
} }

@ -20,6 +20,7 @@ import com.otaliastudios.cameraview.CameraView;
import com.otaliastudios.cameraview.PictureResult; import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.controls.Mode; import com.otaliastudios.cameraview.controls.Mode;
import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.size.SizeSelectors;
import java.io.File; import java.io.File;

Loading…
Cancel
Save