From 1a88cd09f4aca1f77aed88df1071444e224cc007 Mon Sep 17 00:00:00 2001 From: Sewar Date: Tue, 7 Jan 2020 12:34:54 -0600 Subject: [PATCH] Add support to record video to FileDescriptor. (#732) --- .../cameraview/VideoResultTest.java | 63 ++++++++++++++++++- .../otaliastudios/cameraview/CameraView.java | 44 ++++++++++++- .../otaliastudios/cameraview/VideoResult.java | 24 ++++++- .../cameraview/engine/CameraBaseEngine.java | 13 +++- .../cameraview/engine/CameraEngine.java | 5 +- .../cameraview/video/FullVideoRecorder.java | 11 +++- docs/_docs/capturing-media.md | 2 + 7 files changed, 151 insertions(+), 11 deletions(-) diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/VideoResultTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/VideoResultTest.java index 2f470906..b4d9b6fd 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/VideoResultTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/VideoResultTest.java @@ -16,6 +16,7 @@ import org.junit.runner.RunWith; import org.mockito.Mockito; import java.io.File; +import java.io.FileDescriptor; import static org.junit.Assert.assertEquals; @@ -27,7 +28,7 @@ public class VideoResultTest extends BaseTest { private VideoResult.Stub stub = new VideoResult.Stub(); @Test - public void testResult() { + public void testResultWithFile() { File file = Mockito.mock(File.class); int rotation = 90; Size size = new Size(20, 120); @@ -73,6 +74,66 @@ public class VideoResultTest extends BaseTest { assertEquals(result.getAudioBitRate(), audioBitRate); assertEquals(result.getAudio(), audio); assertEquals(result.getFacing(), facing); + } + + @Test + public void testResultWithFileDescriptor() { + FileDescriptor fileDescriptor = FileDescriptor.in; + int rotation = 90; + Size size = new Size(20, 120); + VideoCodec codec = VideoCodec.H_263; + Location location = Mockito.mock(Location.class); + boolean isSnapshot = true; + int maxDuration = 1234; + long maxFileSize = 500000; + int reason = VideoResult.REASON_MAX_DURATION_REACHED; + int videoFrameRate = 30; + int videoBitRate = 300000; + int audioBitRate = 30000; + Audio audio = Audio.ON; + Facing facing = Facing.FRONT; + + stub.fileDescriptor = fileDescriptor; + stub.rotation = rotation; + stub.size = size; + stub.videoCodec = codec; + stub.location = location; + stub.isSnapshot = isSnapshot; + stub.maxDuration = maxDuration; + stub.maxSize = maxFileSize; + stub.endReason = reason; + stub.videoFrameRate = videoFrameRate; + stub.videoBitRate = videoBitRate; + stub.audioBitRate = audioBitRate; + stub.audio = audio; + stub.facing = facing; + VideoResult result = new VideoResult(stub); + assertEquals(result.getFileDescriptor(), fileDescriptor); + assertEquals(result.getRotation(), rotation); + assertEquals(result.getSize(), size); + assertEquals(result.getVideoCodec(), codec); + assertEquals(result.getLocation(), location); + assertEquals(result.isSnapshot(), isSnapshot); + assertEquals(result.getMaxSize(), maxFileSize); + assertEquals(result.getMaxDuration(), maxDuration); + assertEquals(result.getTerminationReason(), reason); + assertEquals(result.getVideoFrameRate(), videoFrameRate); + assertEquals(result.getVideoBitRate(), videoBitRate); + assertEquals(result.getAudioBitRate(), audioBitRate); + assertEquals(result.getAudio(), audio); + assertEquals(result.getFacing(), facing); + } + + @Test(expected = RuntimeException.class) + public void testResultWithNoFile() { + VideoResult result = new VideoResult(stub); + result.getFile(); + } + + @Test(expected = RuntimeException.class) + public void testResultWithNoFileDescriptor() { + VideoResult result = new VideoResult(stub); + result.getFileDescriptor(); } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java index 2812edd1..f5066a42 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java @@ -86,6 +86,7 @@ import com.otaliastudios.cameraview.size.SizeSelectorParser; import com.otaliastudios.cameraview.size.SizeSelectors; import java.io.File; +import java.io.FileDescriptor; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -1646,8 +1647,28 @@ public class CameraView extends FrameLayout implements LifecycleObserver { * @param file a file where the video will be saved */ public void takeVideo(@NonNull File file) { + takeVideo(file, null); + } + + /** + * Starts recording a video. Video will be written to the given file, + * so callers should ensure they have appropriate permissions to write to the file. + * + * @param fileDescriptor a file descriptor where the video will be saved + */ + public void takeVideo(@NonNull FileDescriptor fileDescriptor) { + takeVideo(null, fileDescriptor); + } + + private void takeVideo(@Nullable File file, @Nullable FileDescriptor fileDescriptor) { VideoResult.Stub stub = new VideoResult.Stub(); - mCameraEngine.takeVideo(stub, file); + if (file != null) { + mCameraEngine.takeVideo(stub, file, null); + } else if (fileDescriptor != null) { + mCameraEngine.takeVideo(stub, null, fileDescriptor); + } else { + throw new IllegalStateException("file and fileDescriptor are both null."); + } mUiHandler.post(new Runnable() { @Override public void run() { @@ -1686,9 +1707,26 @@ public class CameraView extends FrameLayout implements LifecycleObserver { * * @param file a file where the video will be saved * @param durationMillis recording max duration - * */ public void takeVideo(@NonNull File file, int durationMillis) { + takeVideo(file, null, durationMillis); + } + + /** + * Starts recording a video. Video will be written to the given file, + * so callers should ensure they have appropriate permissions to write to the file. + * Recording will be automatically stopped after the given duration, overriding + * temporarily any duration limit set by {@link #setVideoMaxDuration(int)}. + * + * @param fileDescriptor a file descriptor where the video will be saved + * @param durationMillis recording max duration + */ + public void takeVideo(@NonNull FileDescriptor fileDescriptor, int durationMillis) { + takeVideo(null, fileDescriptor, durationMillis); + } + + private void takeVideo(@Nullable File file, @Nullable FileDescriptor fileDescriptor, + int durationMillis) { final int old = getVideoMaxDuration(); addCameraListener(new CameraListener() { @Override @@ -1707,7 +1745,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { } }); setVideoMaxDuration(durationMillis); - takeVideo(file); + takeVideo(file, fileDescriptor); } /** diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/VideoResult.java b/cameraview/src/main/java/com/otaliastudios/cameraview/VideoResult.java index 725838d2..41dd4dcd 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/VideoResult.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/VideoResult.java @@ -7,11 +7,12 @@ import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.controls.VideoCodec; import com.otaliastudios.cameraview.size.Size; +import java.io.File; +import java.io.FileDescriptor; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import java.io.File; - /** * Wraps the result of a video recording started by {@link CameraView#takeVideo(File)}. */ @@ -30,6 +31,7 @@ public class VideoResult { public int rotation; public Size size; public File file; + public FileDescriptor fileDescriptor; public Facing facing; public VideoCodec videoCodec; public Audio audio; @@ -55,6 +57,7 @@ public class VideoResult { private final int rotation; private final Size size; private final File file; + private final FileDescriptor fileDescriptor; private final Facing facing; private final VideoCodec videoCodec; private final Audio audio; @@ -71,6 +74,7 @@ public class VideoResult { rotation = builder.rotation; size = builder.size; file = builder.file; + fileDescriptor = builder.fileDescriptor; facing = builder.facing; videoCodec = builder.videoCodec; audio = builder.audio; @@ -130,9 +134,25 @@ public class VideoResult { */ @NonNull public File getFile() { + if (file == null) { + throw new RuntimeException("File is only available when takeVideo(File) is used."); + } return file; } + /** + * Returns the file descriptor where the video was saved. + * + * @return the File Descriptor of this video + */ + @NonNull + public FileDescriptor getFileDescriptor() { + if (fileDescriptor == null) { + throw new RuntimeException("FileDescriptor is only available when takeVideo(FileDescriptor) is used."); + } + return fileDescriptor; + } + /** * Returns the facing value with which this video was recorded. * diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraBaseEngine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraBaseEngine.java index 4f60bab8..0f21ac54 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraBaseEngine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraBaseEngine.java @@ -38,6 +38,7 @@ import com.otaliastudios.cameraview.size.SizeSelectors; import com.otaliastudios.cameraview.video.VideoRecorder; import java.io.File; +import java.io.FileDescriptor; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -549,7 +550,9 @@ public abstract class CameraBaseEngine extends CameraEngine { } @Override - public final void takeVideo(final @NonNull VideoResult.Stub stub, final @NonNull File file) { + public final void takeVideo(final @NonNull VideoResult.Stub stub, + final @Nullable File file, + final @Nullable FileDescriptor fileDescriptor) { getOrchestrator().scheduleStateful("take video", CameraState.BIND, new Runnable() { @Override public void run() { @@ -558,7 +561,13 @@ public abstract class CameraBaseEngine extends CameraEngine { if (mMode == Mode.PICTURE) { throw new IllegalStateException("Can't record video while in PICTURE mode"); } - stub.file = file; + if (file != null) { + stub.file = file; + } else if (fileDescriptor != null) { + stub.fileDescriptor = fileDescriptor; + } else { + throw new IllegalStateException("file and fileDescriptor are both null."); + } stub.isSnapshot = false; stub.videoCodec = mVideoCodec; stub.location = mLocation; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java index d0b7911a..7b26330f 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java @@ -49,6 +49,7 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import java.io.File; +import java.io.FileDescriptor; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -712,7 +713,9 @@ public abstract class CameraEngine implements public abstract void takePictureSnapshot(final @NonNull PictureResult.Stub stub); public abstract boolean isTakingVideo(); - public abstract void takeVideo(@NonNull VideoResult.Stub stub, @NonNull File file); + public abstract void takeVideo(@NonNull VideoResult.Stub stub, + @Nullable File file, + @Nullable FileDescriptor fileDescriptor); public abstract void takeVideoSnapshot(@NonNull VideoResult.Stub stub, @NonNull File file); public abstract void stopVideo(); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java index 67b14825..b52f40ee 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java @@ -2,7 +2,6 @@ package com.otaliastudios.cameraview.video; import android.media.CamcorderProfile; import android.media.MediaRecorder; -import android.os.Handler; import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.VideoResult; @@ -220,7 +219,15 @@ public abstract class FullVideoRecorder extends VideoRecorder { (float) stub.location.getLatitude(), (float) stub.location.getLongitude()); } - mMediaRecorder.setOutputFile(stub.file.getAbsolutePath()); + + if (stub.file != null) { + mMediaRecorder.setOutputFile(stub.file.getAbsolutePath()); + } else if (stub.fileDescriptor != null) { + mMediaRecorder.setOutputFile(stub.fileDescriptor); + } else { + throw new IllegalStateException("file and fileDescriptor are both null."); + } + mMediaRecorder.setOrientationHint(stub.rotation); // When using MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED, the recorder might have stopped // before calling it. But this creates issues on Camera2 Legacy devices - they need a diff --git a/docs/_docs/capturing-media.md b/docs/_docs/capturing-media.md index e84b5cb0..fdde9828 100644 --- a/docs/_docs/capturing-media.md +++ b/docs/_docs/capturing-media.md @@ -102,7 +102,9 @@ camera.addCameraListener(new CameraListener() { |`isTakingPicture()`|Returns true if the camera is currently capturing a picture.| |`takePicture()`|Takes a high quality picture.| |`takeVideo(File)`|Takes a high quality video.| +|`takeVideo(FileDescriptor)`|Takes a high quality video.| |`takeVideo(File, long)`|Takes a high quality video, stopping after the given duration.| +|`takeVideo(FileDescriptor, long)`|Takes a high quality video, stopping after the given duration.| |`takePictureSnapshot()`|Takes a picture snapshot.| |`takeVideoSnapshot(File)`|Takes a video snapshot.| |`takeVideoSnapshot(File, long)`|Takes a video snapshot, stopping after the given duration.|