diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java index 50e0a0b7..653c8968 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java @@ -264,7 +264,6 @@ public class CameraViewCallbacksTest extends BaseTest { camera.setRight(viewDim[0]); // Ensure the image will (not) be cropped. - camera.setCropOutput(crop); mockPreview.setIsCropping(crop); // Create fake JPEG array and trigger the process. diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java index 3a3010e3..dc292230 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java @@ -76,7 +76,6 @@ public class CameraViewTest extends BaseTest { public void testNullBeforeStart() { assertFalse(cameraView.isStarted()); assertNull(cameraView.getCameraOptions()); - assertNull(cameraView.getExtraProperties()); assertNull(cameraView.getPreviewSize()); assertNull(cameraView.getPictureSize()); } diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java index 7011d9ba..1bba99cb 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java @@ -126,7 +126,7 @@ public class IntegrationTest extends BaseTest { private void waitForVideoEnd(boolean expectSuccess) { final Task video = new Task<>(true); - doEndTask(video, true).when(listener).onVideoTaken(any(File.class)); + doEndTask(video, true).when(listener).onVideoTaken(any(VideoResult.class)); Boolean result = video.await(8000); if (expectSuccess) { assertNotNull("Should end video", result); @@ -205,10 +205,8 @@ public class IntegrationTest extends BaseTest { @Test public void testStartInitializesOptions() { assertNull(camera.getCameraOptions()); - assertNull(camera.getExtraProperties()); waitForOpen(true); assertNotNull(camera.getCameraOptions()); - assertNotNull(camera.getExtraProperties()); } //endregion @@ -530,7 +528,7 @@ public class IntegrationTest extends BaseTest { @Test public void testCapturePicture_size() throws Exception { - camera.setCropOutput(false); + // TODO v2: might have to change this waitForOpen(true); Size size = camera.getPictureSize(); @@ -566,7 +564,7 @@ public class IntegrationTest extends BaseTest { @Test public void testCaptureSnapshot_size() throws Exception { - camera.setCropOutput(false); + // TODO v2: might have to change this waitForOpen(true); Size size = camera.getPreviewSize(); diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/VideoResultTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/VideoResultTest.java new file mode 100644 index 00000000..33f5e90a --- /dev/null +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/VideoResultTest.java @@ -0,0 +1,47 @@ +package com.otaliastudios.cameraview; + + +import android.hardware.Camera; +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; + + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class VideoResultTest extends BaseTest { + + private VideoResult result = new VideoResult(); + + @Test + public void testResult() { + File file = Mockito.mock(File.class); + int rotation = 90; + Size size = new Size(20, 120); + VideoCodec codec = VideoCodec.H_263; + Location location = Mockito.mock(Location.class); + boolean isSnapshot = true; + + result.file = file; + result.rotation = rotation; + result.size = size; + result.codec = codec; + result.location = location; + result.isSnapshot = isSnapshot; + + assertEquals(result.getFile(), file); + assertEquals(result.getRotation(), rotation); + assertEquals(result.getSize(), size); + assertEquals(result.getCodec(), codec); + assertEquals(result.getLocation(), location); + assertEquals(result.isSnapshot(), isSnapshot); + } +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java index d6181fbd..ba52a938 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java @@ -225,7 +225,6 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera LOG.w("onStop:", "Clean up.", "Exception while releasing camera.", e); } } - mExtraProperties = null; mCameraOptions = null; mCamera = null; mPreviewSize = null; @@ -636,15 +635,67 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera public void run() { if (mIsCapturingVideo) return; if (mSessionType == SessionType.VIDEO) { - mVideoFile = videoFile; mIsCapturingVideo = true; - initMediaRecorder(); + + // Create the video result + CamcorderProfile profile = getCamcorderProfile(); + Size videoSize = new Size(profile.videoFrameWidth, profile.videoFrameHeight); + mVideoResult = new VideoResult(); + mVideoResult.file = videoFile; + mVideoResult.isSnapshot = false; + mVideoResult.codec = mVideoCodec; + mVideoResult.location = mLocation; + mVideoResult.rotation = computeSensorToOutputOffset(); + mVideoResult.size = mVideoResult.rotation % 180 == 0 ? videoSize : videoSize.flip(); + + // 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); + } + mMediaRecorder.setOutputFormat(profile.fileFormat); + mMediaRecorder.setVideoFrameRate(profile.videoFrameRate); + mMediaRecorder.setVideoSize(profile.videoFrameWidth, profile.videoFrameHeight); + 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); - mVideoFile = null; + mVideoResult = null; mCamera.lock(); stopVideoImmediately(); } @@ -679,9 +730,9 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera mMediaRecorder.release(); mMediaRecorder = null; } - if (mVideoFile != null) { - mCameraCallbacks.dispatchOnVideoTaken(mVideoFile); - mVideoFile = null; + if (mVideoResult != null) { + mCameraCallbacks.dispatchOnVideoTaken(mVideoResult); + mVideoResult = null; } if (mCamera != null) { // This is needed to restore FrameProcessor. No re-allocation needed though. @@ -716,31 +767,6 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera mMediaRecorder.setAudioEncoder(profile.audioCodec); mMediaRecorder.setAudioEncodingBitRate(profile.audioBitRate); } - - if (mLocation != null) { - mMediaRecorder.setLocation( - (float) mLocation.getLatitude(), - (float) mLocation.getLongitude()); - } - - mMediaRecorder.setOutputFile(mVideoFile.getAbsolutePath()); - mMediaRecorder.setOrientationHint(computeSensorToOutputOffset()); - - 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()); } // ----------------- diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java index 168c641e..f80dea8c 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java @@ -54,7 +54,7 @@ abstract class CameraController implements protected FrameManager mFrameManager; protected SizeSelector mPictureSizeSelector; protected MediaRecorder mMediaRecorder; - protected File mVideoFile; + protected VideoResult mVideoResult; protected long mVideoMaxSize; protected int mVideoMaxDuration; protected Size mPictureSize; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java index 3f35d803..1766b552 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java @@ -4,8 +4,6 @@ import android.graphics.PointF; import android.support.annotation.NonNull; import android.support.annotation.UiThread; -import java.io.File; - public abstract class CameraListener { @@ -65,18 +63,13 @@ public abstract class CameraListener { /** - * Notifies that a video capture has just ended. The file parameter is the one that - * was passed to {@link CameraView#takeVideo(File)}, if any. - * If not, the camera fallsback to: - * - * new File(getContext().getExternalFilesDir(null), "video.mp4"); - * + * Notifies that a video capture has just ended. * - * @param video file hosting the mp4 video + * @param result the video result */ @UiThread - public void onVideoTaken(File video) { - // TODO v2: use a VideoResult. + public void onVideoTaken(VideoResult result) { + } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java index b6eade39..47489a08 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java @@ -1189,7 +1189,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { final int old = getVideoMaxDuration(); addCameraListener(new CameraListener() { @Override - public void onVideoTaken(File video) { + public void onVideoTaken(VideoResult result) { setVideoMaxDuration(old); removeCameraListener(this); } @@ -1204,7 +1204,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { /** * Stops capturing video, if there was a video record going on. - * This will fire {@link CameraListener#onVideoTaken(File)}. + * This will fire {@link CameraListener#onVideoTaken(VideoResult)}. */ public void stopVideo() { mCameraController.stopVideo(); @@ -1389,7 +1389,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { void onShutter(boolean shouldPlaySound); void processPicture(byte[] jpeg, boolean consistentWithView, boolean flipHorizontally); void processSnapshot(YuvImage image, boolean consistentWithView, boolean flipHorizontally); - void dispatchOnVideoTaken(File file); + void dispatchOnVideoTaken(VideoResult result); void dispatchOnFocusStart(@Nullable Gesture trigger, PointF where); void dispatchOnFocusEnd(@Nullable Gesture trigger, boolean success, PointF where); void dispatchOnZoomChanged(final float newValue, final PointF[] fingers); @@ -1515,7 +1515,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { } @Override - public void dispatchOnVideoTaken(final File video) { + public void dispatchOnVideoTaken(final VideoResult video) { mLogger.i("dispatchOnVideoTaken", video); mUiHandler.post(new Runnable() { @Override diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/VideoResult.java b/cameraview/src/main/java/com/otaliastudios/cameraview/VideoResult.java new file mode 100644 index 00000000..62752c0c --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/VideoResult.java @@ -0,0 +1,83 @@ +package com.otaliastudios.cameraview; + +import android.location.Location; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.io.File; + +/** + * Wraps the result of a video recording started by {@link CameraView#takeVideo(File)}. + */ +public class VideoResult { + + boolean isSnapshot; + Location location; + int rotation; + Size size; + File file; + VideoCodec codec; + + VideoResult() {} + + /** + * Returns whether this result comes from a snapshot. + * + * @return whether this is a snapshot + */ + public boolean isSnapshot() { + return isSnapshot; + } + + /** + * Returns geographic information for this video, if any. + * If it was set, it is also present in the file metadata. + * + * @return a nullable Location + */ + @Nullable + public Location getLocation() { + return location; + } + + /** + * Returns the clock-wise rotation that should be applied to the + * video frames before displaying. If it is non-zero, it is also present + * in the video metadata, so most reader will take care of it. + * + * @return the clock-wise rotation + */ + public int getRotation() { + return rotation; + } + + /** + * Returns the size of the frames after the rotation is applied. + * + * @return the Size of this video + */ + @NonNull + public Size getSize() { + return size; + } + + /** + * Returns the file where the video was saved. + * + * @return the File of this video + */ + @NonNull + public File getFile() { + return file; + } + + /** + * Returns the codec that was used to encode the video frames. + * + * @return the video codec + */ + @NonNull + public VideoCodec getCodec() { + return codec; + } +} diff --git a/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java b/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java index e5d395c0..1384a64b 100644 --- a/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java +++ b/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java @@ -19,6 +19,7 @@ import com.otaliastudios.cameraview.CameraOptions; import com.otaliastudios.cameraview.CameraView; import com.otaliastudios.cameraview.SessionType; import com.otaliastudios.cameraview.Size; +import com.otaliastudios.cameraview.VideoResult; import java.io.File; @@ -50,9 +51,9 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis public void onPictureTaken(byte[] jpeg) { onPicture(jpeg); } @Override - public void onVideoTaken(File video) { - super.onVideoTaken(video); - onVideo(video); + public void onVideoTaken(VideoResult result) { + super.onVideoTaken(result); + onVideo(result.getFile()); } }); diff --git a/demo/src/main/res/layout/activity_camera.xml b/demo/src/main/res/layout/activity_camera.xml index fd820df3..01e9b719 100644 --- a/demo/src/main/res/layout/activity_camera.xml +++ b/demo/src/main/res/layout/activity_camera.xml @@ -17,7 +17,6 @@ android:keepScreenOn="true" app:cameraPlaySounds="true" app:cameraGrid="off" - app:cameraCropOutput="false" app:cameraFacing="back" app:cameraFlash="off" app:cameraAudio="on" @@ -26,7 +25,6 @@ app:cameraGesturePinch="zoom" app:cameraGestureScrollHorizontal="exposureCorrection" app:cameraGestureScrollVertical="none" - app:cameraJpegQuality="100" app:cameraSessionType="picture" />