diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/VideoRecorderTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/VideoRecorderTest.java new file mode 100644 index 00000000..13fd0932 --- /dev/null +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/VideoRecorderTest.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); + } +} diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/VideoResultTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/VideoResultTest.java index 33f5e90a..dff186e5 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/VideoResultTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/VideoResultTest.java @@ -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); } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java index c654d430..7cf05202 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java @@ -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. diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java index 03be98b6..d0e327f1 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java @@ -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 mWhiteBalanceTask = new Task<>(); Task mHdrTask = new Task<>(); Task mLocationTask = new Task<>(); - Task mVideoQualityTask = new Task<>(); Task mStartVideoTask = new Task<>(); Task mPlaySoundsTask = new Task<>(); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/MediaRecorderVideoRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/MediaRecorderVideoRecorder.java new file mode 100644 index 00000000..9727fef9 --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/MediaRecorderVideoRecorder.java @@ -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; + } +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/VideoRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/VideoRecorder.java new file mode 100644 index 00000000..773191e2 --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/VideoRecorder.java @@ -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); + } +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/VideoResult.java b/cameraview/src/main/java/com/otaliastudios/cameraview/VideoResult.java index 62752c0c..21a1bb2d 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/VideoResult.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/VideoResult.java @@ -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; + } }