Add VideoRecorder interface and MediaRecorderVideoRecorder for standard videos

v2
Mattia Iavarone 7 years ago
parent 7411d55855
commit aa88eb08c6
  1. 35
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/VideoRecorderTest.java
  2. 6
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/VideoResultTest.java
  3. 134
      cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java
  4. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java
  5. 93
      cameraview/src/main/java/com/otaliastudios/cameraview/MediaRecorderVideoRecorder.java
  6. 33
      cameraview/src/main/java/com/otaliastudios/cameraview/VideoRecorder.java
  7. 33
      cameraview/src/main/java/com/otaliastudios/cameraview/VideoResult.java

@ -0,0 +1,35 @@
package com.otaliastudios.cameraview;
import android.location.Location;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import java.io.File;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class VideoRecorderTest extends BaseTest {
@Test
public void testRecorder() {
VideoResult result = new VideoResult();
VideoRecorder.VideoResultListener listener = Mockito.mock(VideoRecorder.VideoResultListener.class);
VideoRecorder recorder = new VideoRecorder(result, listener) {
void start() {}
};
recorder.start();
recorder.stop();
Mockito.verify(listener, Mockito.times(1)).onVideoResult(result);
assertNull(recorder.mListener);
assertNull(recorder.mResult);
}
}

@ -29,6 +29,8 @@ public class VideoResultTest extends BaseTest {
VideoCodec codec = VideoCodec.H_263;
Location location = Mockito.mock(Location.class);
boolean isSnapshot = true;
int maxDuration = 1234;
long maxFileSize = 500000;
result.file = file;
result.rotation = rotation;
@ -36,6 +38,8 @@ public class VideoResultTest extends BaseTest {
result.codec = codec;
result.location = location;
result.isSnapshot = isSnapshot;
result.maxDuration = maxDuration;
result.maxSize = maxFileSize;
assertEquals(result.getFile(), file);
assertEquals(result.getRotation(), rotation);
@ -43,5 +47,7 @@ public class VideoResultTest extends BaseTest {
assertEquals(result.getCodec(), codec);
assertEquals(result.getLocation(), location);
assertEquals(result.isSnapshot(), isSnapshot);
assertEquals(result.getMaxSize(), maxFileSize);
assertEquals(result.getMaxDuration(), maxDuration);
}
}

@ -27,7 +27,7 @@ import java.util.List;
@SuppressWarnings("deprecation")
class Camera1 extends CameraController implements Camera.PreviewCallback, Camera.ErrorCallback {
class Camera1 extends CameraController implements Camera.PreviewCallback, Camera.ErrorCallback, VideoRecorder.VideoResultListener {
private static final String TAG = Camera1.class.getSimpleName();
private static final CameraLogger LOG = CameraLogger.create(TAG);
@ -206,7 +206,10 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
if (mCamera != null) {
LOG.i("onStop:", "Clean up.", "Ending video.");
stopVideoImmediately();
if (mVideoRecorder != null) {
mVideoRecorder.stop();
mVideoRecorder = null;
}
try {
LOG.i("onStop:", "Clean up.", "Stopping preview.");
@ -230,7 +233,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
mPreviewSize = null;
mCaptureSize = null;
mIsBound = false;
mIsCapturingImage = false;
mIsTakingImage = false;
mIsTakingVideo = false;
LOG.w("onStop:", "Clean up.", "Returning.");
@ -316,11 +319,6 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
params.setGpsAltitude(mLocation.getAltitude());
params.setGpsTimestamp(mLocation.getTime());
params.setGpsProcessingMethod(mLocation.getProvider());
if (mIsTakingVideo && mMediaRecorder != null) {
mMediaRecorder.setLocation((float) mLocation.getLatitude(),
(float) mLocation.getLongitude());
}
}
return true;
}
@ -474,9 +472,9 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
throw new IllegalStateException("Can't take hq pictures while in VIDEO mode");
}
LOG.v("takePicture: performing.", mIsCapturingImage);
if (mIsCapturingImage) return;
mIsCapturingImage = true;
LOG.v("takePicture: performing.", mIsTakingImage);
if (mIsTakingImage) return;
mIsTakingImage = true;
final int sensorToOutput = offset(REF_SENSOR, REF_OUTPUT);
final Size outputSize = getPictureSize(REF_OUTPUT);
@ -495,7 +493,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, final Camera camera) {
mIsCapturingImage = false;
mIsTakingImage = false;
int exifRotation;
try {
ExifInterface exif = new ExifInterface(new ByteArrayInputStream(data));
@ -533,9 +531,9 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
return;
}
LOG.v("takePictureSnapshot: performing.", mIsCapturingImage);
if (mIsCapturingImage) return;
mIsCapturingImage = true;
LOG.v("takePictureSnapshot: performing.", mIsTakingImage);
if (mIsTakingImage) return;
mIsTakingImage = true;
mCamera.setOneShotPreviewCallback(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(final byte[] yuv, Camera camera) {
@ -571,7 +569,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
result.location = mLocation;
result.isSnapshot = true;
mCameraCallbacks.dispatchOnPictureTaken(result);
mIsCapturingImage = false;
mIsTakingImage = false;
}
});
@ -631,70 +629,22 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
if (mIsTakingVideo) return;
mIsTakingVideo = true;
// Create the video result
final Size videoSize = mCaptureSize;
mVideoResult = new VideoResult();
mVideoResult.file = videoFile;
mVideoResult.isSnapshot = false;
mVideoResult.codec = mVideoCodec;
mVideoResult.location = mLocation;
mVideoResult.rotation = offset(REF_SENSOR, REF_OUTPUT);
mVideoResult.size = flip(REF_SENSOR, REF_OUTPUT) ? videoSize.flip() : videoSize;
// Create the video result stub
VideoResult videoResult = new VideoResult();
videoResult.file = videoFile;
videoResult.isSnapshot = false;
videoResult.codec = mVideoCodec;
videoResult.location = mLocation;
videoResult.rotation = offset(REF_SENSOR, REF_OUTPUT);
videoResult.size = flip(REF_SENSOR, REF_OUTPUT) ? mCaptureSize.flip() : mCaptureSize;
videoResult.audio = mAudio;
videoResult.maxSize = mVideoMaxSize;
videoResult.maxDuration = mVideoMaxDuration;
// Initialize the media recorder
mCamera.unlock();
mMediaRecorder = new MediaRecorder();
mMediaRecorder.setCamera(mCamera);
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
if (mAudio == Audio.ON) {
// Must be called before setOutputFormat.
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT);
}
// TODO: should get a profile of a quality compatible with the chosen size.
final CamcorderProfile profile = CamcorderProfile.get(mCameraId, CamcorderProfile.QUALITY_HIGH);
mMediaRecorder.setOutputFormat(profile.fileFormat);
mMediaRecorder.setVideoFrameRate(profile.videoFrameRate);
mMediaRecorder.setVideoSize(videoSize.getWidth(), videoSize.getHeight());
mMediaRecorder.setVideoEncoder(mMapper.map(mVideoCodec));
mMediaRecorder.setVideoEncodingBitRate(profile.videoBitRate);
if (mAudio == Audio.ON) {
mMediaRecorder.setAudioChannels(profile.audioChannels);
mMediaRecorder.setAudioSamplingRate(profile.audioSampleRate);
mMediaRecorder.setAudioEncoder(profile.audioCodec);
mMediaRecorder.setAudioEncodingBitRate(profile.audioBitRate);
}
if (mLocation != null) {
mMediaRecorder.setLocation(
(float) mLocation.getLatitude(),
(float) mLocation.getLongitude());
}
mMediaRecorder.setOutputFile(mVideoResult.getFile().getAbsolutePath());
mMediaRecorder.setOrientationHint(mVideoResult.getRotation());
mMediaRecorder.setMaxFileSize(mVideoMaxSize);
mMediaRecorder.setMaxDuration(mVideoMaxDuration);
mMediaRecorder.setOnInfoListener(new MediaRecorder.OnInfoListener() {
@Override
public void onInfo(MediaRecorder mediaRecorder, int what, int extra) {
switch (what) {
case MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED:
case MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED:
stopVideoImmediately();
break;
}
}
});
// Not needed. mMediaRecorder.setPreviewDisplay(mPreview.getSurface());
try {
mMediaRecorder.prepare();
mMediaRecorder.start();
} catch (Exception e) {
LOG.e("Error while starting MediaRecorder. Swallowing.", e);
mVideoResult = null;
mCamera.lock();
stopVideoImmediately();
}
mVideoRecorder = new MediaRecorderVideoRecorder(videoResult, Camera1.this, mCamera, mCameraId);
mVideoRecorder.start();
}
});
}
@ -704,28 +654,22 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
schedule(null, false, new Runnable() {
@Override
public void run() {
stopVideoImmediately();
mIsTakingVideo = false;
if (mVideoRecorder != null) {
mVideoRecorder.stop();
mVideoRecorder = null;
}
}
});
}
@WorkerThread
private void stopVideoImmediately() {
LOG.i("stopVideoImmediately:", "is capturing:", mIsTakingVideo);
mIsTakingVideo = false;
if (mMediaRecorder != null) {
try {
mMediaRecorder.stop();
} catch (Exception e) {
// This can happen if stopVideo() is called right after takeVideo(). We don't care.
LOG.w("stopVideoImmediately:", "Error while closing media recorder. Swallowing", e);
}
mMediaRecorder.release();
mMediaRecorder = null;
}
if (mVideoResult != null) {
mCameraCallbacks.dispatchOnVideoTaken(mVideoResult);
mVideoResult = null;
@Override
public void onVideoResult(@Nullable VideoResult result) {
if (result != null) {
mCameraCallbacks.dispatchOnVideoTaken(result);
} else {
// Something went wrong, lock the camera again.
mCamera.lock();
}
if (mCamera != null) {
// This is needed to restore FrameProcessor. No re-allocation needed though.

@ -57,8 +57,9 @@ abstract class CameraController implements
protected FrameManager mFrameManager;
protected SizeSelector mPictureSizeSelector;
protected SizeSelector mVideoSizeSelector;
protected MediaRecorder mMediaRecorder;
protected VideoResult mVideoResult;
// protected MediaRecorder mMediaRecorder;
// protected VideoResult mVideoResult;
protected VideoRecorder mVideoRecorder;
protected long mVideoMaxSize;
protected int mVideoMaxDuration;
protected Size mCaptureSize;
@ -69,7 +70,7 @@ abstract class CameraController implements
private int mDisplayOffset;
private int mDeviceOrientation;
protected boolean mIsCapturingImage = false;
protected boolean mIsTakingImage = false;
protected boolean mIsTakingVideo = false;
protected int mState = STATE_STOPPED;
@ -81,7 +82,6 @@ abstract class CameraController implements
Task<Void> mWhiteBalanceTask = new Task<>();
Task<Void> mHdrTask = new Task<>();
Task<Void> mLocationTask = new Task<>();
Task<Void> mVideoQualityTask = new Task<>();
Task<Void> mStartVideoTask = new Task<>();
Task<Void> mPlaySoundsTask = new Task<>();

@ -0,0 +1,93 @@
package com.otaliastudios.cameraview;
import android.hardware.Camera;
import android.media.CamcorderProfile;
import android.media.MediaRecorder;
/**
* A {@link VideoRecorder} that uses {@link android.media.MediaRecorder} APIs.
*/
class MediaRecorderVideoRecorder extends VideoRecorder {
private static final String TAG = MediaRecorderVideoRecorder.class.getSimpleName();
private static final CameraLogger LOG = CameraLogger.create(TAG);
private MediaRecorder mMediaRecorder;
private CamcorderProfile mProfile;
MediaRecorderVideoRecorder(VideoResult stub, VideoResultListener listener, Camera camera, int cameraId) {
super(stub, listener);
mMediaRecorder = new MediaRecorder();
mMediaRecorder.setCamera(camera);
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
mProfile = CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_HIGH);
// TODO: should get a profile of a quality compatible with the chosen size.
}
@Override
void start() {
if (mResult.getAudio() == Audio.ON) {
// Must be called before setOutputFormat.
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT);
}
Size size = mResult.getRotation() % 180 != 0 ? mResult.getSize().flip() : mResult.getSize();
mMediaRecorder.setOutputFormat(mProfile.fileFormat);
mMediaRecorder.setVideoFrameRate(mProfile.videoFrameRate);
mMediaRecorder.setVideoSize(size.getWidth(), size.getHeight());
mMediaRecorder.setVideoEncoder(new Mapper1().map(mResult.getCodec()));
mMediaRecorder.setVideoEncodingBitRate(mProfile.videoBitRate);
if (mResult.getAudio() == Audio.ON) {
mMediaRecorder.setAudioChannels(mProfile.audioChannels);
mMediaRecorder.setAudioSamplingRate(mProfile.audioSampleRate);
mMediaRecorder.setAudioEncoder(mProfile.audioCodec);
mMediaRecorder.setAudioEncodingBitRate(mProfile.audioBitRate);
}
if (mResult.getLocation() != null) {
mMediaRecorder.setLocation(
(float) mResult.getLocation().getLatitude(),
(float) mResult.getLocation().getLongitude());
}
mMediaRecorder.setOutputFile(mResult.getFile().getAbsolutePath());
mMediaRecorder.setOrientationHint(mResult.getRotation());
mMediaRecorder.setMaxFileSize(mResult.getMaxSize());
mMediaRecorder.setMaxDuration(mResult.getMaxDuration());
mMediaRecorder.setOnInfoListener(new MediaRecorder.OnInfoListener() {
@Override
public void onInfo(MediaRecorder mediaRecorder, int what, int extra) {
switch (what) {
case MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED:
case MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED:
stop();
break;
}
}
});
// Not needed. mMediaRecorder.setPreviewDisplay(mPreview.getSurface());
try {
mMediaRecorder.prepare();
mMediaRecorder.start();
} catch (Exception e) {
mResult = null;
stop();
}
}
@Override
void stop() {
if (mMediaRecorder != null) {
try {
mMediaRecorder.stop();
} catch (Exception e) {
// This can happen if stopVideo() is called right after takeVideo(). We don't care.
LOG.w("stop:", "Error while closing media recorder. Swallowing", e);
}
mMediaRecorder.release();
}
super.stop();
mProfile = null;
mMediaRecorder = null;
}
}

@ -0,0 +1,33 @@
package com.otaliastudios.cameraview;
import android.support.annotation.Nullable;
import java.io.File;
/**
* Interface for video recording.
* Don't call start if already started. Don't call stop if already stopped.
* Don't reuse.
*/
abstract class VideoRecorder {
/* tests */ VideoResult mResult;
/* tests */ VideoResultListener mListener;
VideoRecorder(VideoResult stub, VideoResultListener listener) {
mResult = stub;
mListener = listener;
}
abstract void start();
void stop() {
mListener.onVideoResult(mResult);
mListener = null;
mResult = null;
}
interface VideoResultListener {
void onVideoResult(@Nullable VideoResult result);
}
}

@ -17,6 +17,9 @@ public class VideoResult {
Size size;
File file;
VideoCodec codec;
Audio audio;
long maxSize;
int maxDuration;
VideoResult() {}
@ -80,4 +83,34 @@ public class VideoResult {
public VideoCodec getCodec() {
return codec;
}
/**
* Returns the max file size in bytes that was set before recording,
* or 0 if no constraint was set.
*
* @return the max file size in bytes
*/
public long getMaxSize() {
return maxSize;
}
/**
* Returns the max video duration in milliseconds that was set before recording,
* or 0 if no constraint was set.
*
* @return the max duration in milliseconds
*/
public int getMaxDuration() {
return maxDuration;
}
/**
* Returns the {@link Audio} setting for this video.
*
* @return the audio setting for this video
*/
@NonNull
public Audio getAudio() {
return audio;
}
}

Loading…
Cancel
Save