From 824abbc55a5ee323e6386932ea3bba043790a5e7 Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Mon, 7 Oct 2019 15:01:18 +0200 Subject: [PATCH] takeVideo improvements (#617) * takeVideo improvements * Try to fix tests * Reorder operations in FullVideoRecorder --- .../cameraview/CameraOptions1Test.java | 66 +++--- .../cameraview/CameraOptions.java | 28 ++- .../cameraview/engine/Camera1Engine.java | 4 +- .../cameraview/internal/DeviceEncoders.java | 10 +- .../internal/utils/CamcorderProfiles.java | 5 + .../cameraview/video/Full1VideoRecorder.java | 12 +- .../cameraview/video/Full2VideoRecorder.java | 16 +- .../cameraview/video/FullVideoRecorder.java | 211 +++++++++++------- 8 files changed, 212 insertions(+), 140 deletions(-) diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java index f6e303f0..25f218ab 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java @@ -41,7 +41,7 @@ public class CameraOptions1Test extends BaseTest { @Test public void testEmpty() { - CameraOptions o = new CameraOptions(mock(Camera.Parameters.class), false); + CameraOptions o = new CameraOptions(mock(Camera.Parameters.class), 0, false); assertTrue(o.getSupportedPictureAspectRatios().isEmpty()); assertTrue(o.getSupportedPictureSizes().isEmpty()); assertTrue(o.getSupportedWhiteBalance().isEmpty()); @@ -71,7 +71,7 @@ public class CameraOptions1Test extends BaseTest { ); Camera.Parameters params = mock(Camera.Parameters.class); when(params.getSupportedPictureSizes()).thenReturn(sizes); - CameraOptions o = new CameraOptions(params, false); + CameraOptions o = new CameraOptions(params, 0, false); Collection supportedSizes = o.getSupportedPictureSizes(); assertEquals(supportedSizes.size(), sizes.size()); for (Camera.Size size : sizes) { @@ -90,7 +90,7 @@ public class CameraOptions1Test extends BaseTest { ); Camera.Parameters params = mock(Camera.Parameters.class); when(params.getSupportedPictureSizes()).thenReturn(sizes); - CameraOptions o = new CameraOptions(params, true); + CameraOptions o = new CameraOptions(params, 0, true); Collection supportedSizes = o.getSupportedPictureSizes(); assertEquals(supportedSizes.size(), sizes.size()); for (Camera.Size size : sizes) { @@ -115,7 +115,7 @@ public class CameraOptions1Test extends BaseTest { Camera.Parameters params = mock(Camera.Parameters.class); when(params.getSupportedPictureSizes()).thenReturn(sizes); - CameraOptions o = new CameraOptions(params, false); + CameraOptions o = new CameraOptions(params, 0, false); Collection supportedRatios = o.getSupportedPictureAspectRatios(); assertEquals(supportedRatios.size(), expected.size()); for (AspectRatio ratio : expected) { @@ -126,15 +126,17 @@ public class CameraOptions1Test extends BaseTest { @Test public void testVideoSizes() { + // VideoSize are capped by CamcorderProfile.QUALITY_HIGH max size. + // This can be very small on an emulator, so use very small sizes to not hit that value. List sizes = Arrays.asList( - mockCameraSize(100, 200), - mockCameraSize(50, 50), - mockCameraSize(1600, 900), - mockCameraSize(1000, 2000) + mockCameraSize(10, 20), + mockCameraSize(5, 5), + mockCameraSize(16, 9), + mockCameraSize(20, 40) ); Camera.Parameters params = mock(Camera.Parameters.class); when(params.getSupportedVideoSizes()).thenReturn(sizes); - CameraOptions o = new CameraOptions(params, false); + CameraOptions o = new CameraOptions(params, 0, false); Collection supportedSizes = o.getSupportedVideoSizes(); assertEquals(supportedSizes.size(), sizes.size()); for (Camera.Size size : sizes) { @@ -147,15 +149,15 @@ public class CameraOptions1Test extends BaseTest { public void testVideoSizesNull() { // When videoSizes is null, we take the preview sizes. List sizes = Arrays.asList( - mockCameraSize(100, 200), - mockCameraSize(50, 50), - mockCameraSize(1600, 900), - mockCameraSize(1000, 2000) + mockCameraSize(10, 20), + mockCameraSize(5, 5), + mockCameraSize(16, 9), + mockCameraSize(20, 40) ); Camera.Parameters params = mock(Camera.Parameters.class); when(params.getSupportedVideoSizes()).thenReturn(null); when(params.getSupportedPreviewSizes()).thenReturn(sizes); - CameraOptions o = new CameraOptions(params, false); + CameraOptions o = new CameraOptions(params, 0, false); Collection supportedSizes = o.getSupportedVideoSizes(); assertEquals(supportedSizes.size(), sizes.size()); for (Camera.Size size : sizes) { @@ -167,14 +169,14 @@ public class CameraOptions1Test extends BaseTest { @Test public void testVideoSizesFlip() { List sizes = Arrays.asList( - mockCameraSize(100, 200), - mockCameraSize(50, 50), - mockCameraSize(1600, 900), - mockCameraSize(1000, 2000) + mockCameraSize(10, 20), + mockCameraSize(5, 5), + mockCameraSize(16, 9), + mockCameraSize(20, 40) ); Camera.Parameters params = mock(Camera.Parameters.class); when(params.getSupportedVideoSizes()).thenReturn(sizes); - CameraOptions o = new CameraOptions(params, true); + CameraOptions o = new CameraOptions(params, 0, true); Collection supportedSizes = o.getSupportedVideoSizes(); assertEquals(supportedSizes.size(), sizes.size()); for (Camera.Size size : sizes) { @@ -186,10 +188,10 @@ public class CameraOptions1Test extends BaseTest { @Test public void testVideoAspectRatio() { List sizes = Arrays.asList( - mockCameraSize(100, 200), - mockCameraSize(50, 50), - mockCameraSize(1600, 900), - mockCameraSize(1000, 2000) + mockCameraSize(10, 20), + mockCameraSize(5, 5), + mockCameraSize(16, 9), + mockCameraSize(20, 40) ); Set expected = new HashSet<>(); @@ -199,7 +201,7 @@ public class CameraOptions1Test extends BaseTest { Camera.Parameters params = mock(Camera.Parameters.class); when(params.getSupportedVideoSizes()).thenReturn(sizes); - CameraOptions o = new CameraOptions(params, false); + CameraOptions o = new CameraOptions(params, 0, false); Collection supportedRatios = o.getSupportedVideoAspectRatios(); assertEquals(supportedRatios.size(), expected.size()); for (AspectRatio ratio : expected) { @@ -215,7 +217,7 @@ public class CameraOptions1Test extends BaseTest { when(params.getMaxExposureCompensation()).thenReturn(0); when(params.getMinExposureCompensation()).thenReturn(0); - CameraOptions o = new CameraOptions(params, false); + CameraOptions o = new CameraOptions(params, 0, false); assertFalse(o.supports(GestureAction.AUTO_FOCUS)); assertTrue(o.supports(GestureAction.TAKE_PICTURE)); assertTrue(o.supports(GestureAction.NONE)); @@ -229,7 +231,7 @@ public class CameraOptions1Test extends BaseTest { public void testAlwaysSupportedControls() { // Grid, VideoQuality, SessionType and Audio are always supported. Camera.Parameters params = mock(Camera.Parameters.class); - CameraOptions o = new CameraOptions(params, false); + CameraOptions o = new CameraOptions(params, 0, false); Collection grids = o.getSupportedControls(Grid.class); Collection video = o.getSupportedControls(VideoCodec.class); @@ -250,7 +252,7 @@ public class CameraOptions1Test extends BaseTest { supported.add(cameraInfo.facing); } - CameraOptions o = new CameraOptions(mock(Camera.Parameters.class), false); + CameraOptions o = new CameraOptions(mock(Camera.Parameters.class), 0, false); Camera1Mapper m = Camera1Mapper.get(); Collection s = o.getSupportedControls(Facing.class); assertEquals(s.size(), supported.size()); @@ -269,7 +271,7 @@ public class CameraOptions1Test extends BaseTest { Camera.Parameters.WHITE_BALANCE_SHADE // Not supported )); - CameraOptions o = new CameraOptions(params, false); + CameraOptions o = new CameraOptions(params, 0, false); Collection w = o.getSupportedControls(WhiteBalance.class); assertEquals(w.size(), 2); assertTrue(w.contains(WhiteBalance.AUTO)); @@ -288,7 +290,7 @@ public class CameraOptions1Test extends BaseTest { Camera.Parameters.FLASH_MODE_RED_EYE // Not supported )); - CameraOptions o = new CameraOptions(params, false); + CameraOptions o = new CameraOptions(params, 0, false); Collection f = o.getSupportedControls(Flash.class); assertEquals(f.size(), 3); assertTrue(f.contains(Flash.OFF)); @@ -309,7 +311,7 @@ public class CameraOptions1Test extends BaseTest { Camera.Parameters.SCENE_MODE_FIREWORKS // Not supported )); - CameraOptions o = new CameraOptions(params, false); + CameraOptions o = new CameraOptions(params, 0, false); Collection h = o.getSupportedControls(Hdr.class); assertEquals(h.size(), 2); assertTrue(h.contains(Hdr.OFF)); @@ -326,7 +328,7 @@ public class CameraOptions1Test extends BaseTest { when(params.isZoomSupported()).thenReturn(true); //noinspection ArraysAsListWithZeroOrOneArgument when(params.getSupportedFocusModes()).thenReturn(Arrays.asList(Camera.Parameters.FOCUS_MODE_AUTO)); - CameraOptions o = new CameraOptions(params, false); + CameraOptions o = new CameraOptions(params, 0, false); assertTrue(o.isZoomSupported()); assertTrue(o.isAutoFocusSupported()); } @@ -337,7 +339,7 @@ public class CameraOptions1Test extends BaseTest { when(params.getMaxExposureCompensation()).thenReturn(10); when(params.getMinExposureCompensation()).thenReturn(-10); when(params.getExposureCompensationStep()).thenReturn(0.5f); - CameraOptions o = new CameraOptions(params, false); + CameraOptions o = new CameraOptions(params, 0, false); assertTrue(o.isExposureCorrectionSupported()); assertEquals(o.getExposureCorrectionMinValue(), -10f * 0.5f, 0f); assertEquals(o.getExposureCorrectionMaxValue(), 10f * 0.5f, 0f); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java index 4380a620..7568c66c 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java @@ -62,7 +62,7 @@ public class CameraOptions { private boolean autoFocusSupported; - public CameraOptions(@NonNull Camera.Parameters params, boolean flipSizes) { + public CameraOptions(@NonNull Camera.Parameters params, int cameraId, boolean flipSizes) { List strings; Camera1Mapper mapper = Camera1Mapper.get(); @@ -127,23 +127,33 @@ public class CameraOptions { } // 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); List vsizes = params.getSupportedVideoSizes(); if (vsizes != null) { for (Camera.Size size : vsizes) { - int width = flipSizes ? size.height : size.width; - int height = flipSizes ? size.width : size.height; - supportedVideoSizes.add(new Size(width, height)); - supportedVideoAspectRatio.add(AspectRatio.of(width, height)); + if (size.width <= videoMaxSize.getWidth() + && size.height <= videoMaxSize.getHeight()) { + int width = flipSizes ? size.height : size.width; + int height = flipSizes ? size.width : size.height; + supportedVideoSizes.add(new Size(width, height)); + supportedVideoAspectRatio.add(AspectRatio.of(width, height)); + } } } else { // StackOverflow threads seems to agree that if getSupportedVideoSizes is null, // previews can be used. List fallback = params.getSupportedPreviewSizes(); for (Camera.Size size : fallback) { - int width = flipSizes ? size.height : size.width; - int height = flipSizes ? size.width : size.height; - supportedVideoSizes.add(new Size(width, height)); - supportedVideoAspectRatio.add(AspectRatio.of(width, height)); + if (size.width <= videoMaxSize.getWidth() + && size.height <= videoMaxSize.getHeight()) { + int width = flipSizes ? size.height : size.width; + int height = flipSizes ? size.width : size.height; + supportedVideoSizes.add(new Size(width, height)); + supportedVideoAspectRatio.add(AspectRatio.of(width, height)); + } } } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java index dfb10086..11db5d38 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java @@ -154,8 +154,8 @@ public class Camera1Engine extends CameraEngine implements // Set parameters that might have been set before the camera was opened. LOG.i("onStartEngine:", "Applying default parameters."); Camera.Parameters params = mCamera.getParameters(); - mCameraOptions = new CameraOptions(params, getAngles() - .flip(Reference.SENSOR, Reference.VIEW)); + mCameraOptions = new CameraOptions(params, mCameraId, + getAngles().flip(Reference.SENSOR, Reference.VIEW)); applyAllParameters(params); mCamera.setParameters(params); mCamera.setDisplayOrientation(getAngles().offset(Reference.SENSOR, Reference.VIEW, diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/DeviceEncoders.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/DeviceEncoders.java index 54707e3f..33c3244a 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/DeviceEncoders.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/DeviceEncoders.java @@ -221,22 +221,28 @@ public class DeviceEncoders { int width = size.getWidth(); int height = size.getHeight(); double aspect = (double) width / height; + LOG.i("getSupportedVideoSize - started. width:", width, "height:", height); // If width is too large, scale down, but keep aspect ratio. if (mVideoCapabilities.getSupportedWidths().getUpper() < width) { width = mVideoCapabilities.getSupportedWidths().getUpper(); height = (int) Math.round(width / aspect); + LOG.i("getSupportedVideoSize - exceeds maxWidth! width:", width, + "height:", height); } // If height is too large, scale down, but keep aspect ratio. if (mVideoCapabilities.getSupportedHeights().getUpper() < height) { height = mVideoCapabilities.getSupportedHeights().getUpper(); width = (int) Math.round(aspect * height); + LOG.i("getSupportedVideoSize - exceeds maxHeight! width:", width, + "height:", height); } // Adjust the alignment. while (width % mVideoCapabilities.getWidthAlignment() != 0) width--; while (height % mVideoCapabilities.getHeightAlignment() != 0) height--; + LOG.i("getSupportedVideoSize - aligned. width:", width, "height:", height); // It's still possible that we're BELOW the lower. if (!mVideoCapabilities.getSupportedWidths().contains(width)) { @@ -256,9 +262,7 @@ public class DeviceEncoders { " Might be an aspect ratio issue." + " Desired size:" + new Size(width, height)); } - Size adjusted = new Size(width, height); - LOG.i("getSupportedVideoSize -", "inputSize:", size, "adjustedSize:", adjusted); - return adjusted; + return new Size(width, height); } /** diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/CamcorderProfiles.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/CamcorderProfiles.java index cc70194c..93ec596e 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/CamcorderProfiles.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/CamcorderProfiles.java @@ -4,6 +4,7 @@ import android.annotation.SuppressLint; import android.media.CamcorderProfile; import android.os.Build; +import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.size.Size; import java.util.ArrayList; @@ -20,6 +21,9 @@ import androidx.annotation.NonNull; */ public class CamcorderProfiles { + private static final String TAG = CamcorderProfiles.class.getSimpleName(); + private static final CameraLogger LOG = CameraLogger.create(TAG); + @SuppressLint("UseSparseArrays") private static Map sizeToProfileMap = new HashMap<>(); @@ -53,6 +57,7 @@ public class CamcorderProfiles { int camera1Id = Integer.parseInt(cameraId); return get(camera1Id, targetSize); } catch (NumberFormatException e) { + LOG.w("NumberFormatException for Camera2 id:", cameraId); return CamcorderProfile.get(CamcorderProfile.QUALITY_LOW); } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/Full1VideoRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/Full1VideoRecorder.java index 5d69f102..7716b9f4 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/Full1VideoRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/Full1VideoRecorder.java @@ -1,6 +1,7 @@ package com.otaliastudios.cameraview.video; import android.hardware.Camera; +import android.media.CamcorderProfile; import android.media.MediaRecorder; import android.util.Log; @@ -34,15 +35,18 @@ public class Full1VideoRecorder extends FullVideoRecorder { } @Override - protected boolean onPrepareMediaRecorder(@NonNull VideoResult.Stub stub, - @NonNull MediaRecorder mediaRecorder) { + protected void applyVideoSource(@NonNull VideoResult.Stub stub, + @NonNull MediaRecorder mediaRecorder) { mediaRecorder.setCamera(mCamera); mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); + } + @NonNull + @Override + protected CamcorderProfile getCamcorderProfile(@NonNull VideoResult.Stub stub) { // 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); + return CamcorderProfiles.get(mCameraId, size); } @Override diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/Full2VideoRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/Full2VideoRecorder.java index d8e78f1f..395b6dfb 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/Full2VideoRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/Full2VideoRecorder.java @@ -4,6 +4,7 @@ import android.annotation.SuppressLint; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.CaptureResult; import android.hardware.camera2.TotalCaptureResult; +import android.media.CamcorderProfile; import android.media.MediaRecorder; import android.os.Build; import android.view.Surface; @@ -69,19 +70,22 @@ public class Full2VideoRecorder extends FullVideoRecorder { action.start(mHolder); } - @SuppressLint("NewApi") @Override - protected boolean onPrepareMediaRecorder(@NonNull VideoResult.Stub stub, - @NonNull MediaRecorder mediaRecorder) { + protected void applyVideoSource(@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); + } + + @NonNull + @Override + protected CamcorderProfile getCamcorderProfile(@NonNull VideoResult.Stub stub) { // This was an option: get the surface from outside this class, using // MediaCodec.createPersistentInputSurface(). But it doesn't really help since the // Camera2 engine refuses a surface that has not been configured, so even with that trick // we would have to attach the surface to this recorder before creating the CameraSession. // mediaRecorder.setInputSurface(mInputSurface); - return super.onPrepareMediaRecorder(stub, mediaRecorder); + Size size = stub.rotation % 180 != 0 ? stub.size.flip() : stub.size; + return CamcorderProfiles.get(mCameraId, size); } /** 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 c7c1310b..bed7a1d0 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java @@ -8,6 +8,7 @@ import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.controls.Audio; import com.otaliastudios.cameraview.controls.VideoCodec; import com.otaliastudios.cameraview.internal.DeviceEncoders; +import com.otaliastudios.cameraview.internal.utils.CamcorderProfiles; import com.otaliastudios.cameraview.size.Size; import androidx.annotation.NonNull; @@ -17,10 +18,9 @@ import androidx.annotation.Nullable; * 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} + * {@link #prepareMediaRecorder(VideoResult.Stub)}. This will call two abstract methods: + * - {@link #getCamcorderProfile(VideoResult.Stub)} + * - {@link #applyVideoSource(VideoResult.Stub, MediaRecorder)} * * 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 @@ -32,7 +32,7 @@ public abstract class FullVideoRecorder extends VideoRecorder { private static final CameraLogger LOG = CameraLogger.create(TAG); @SuppressWarnings("WeakerAccess") protected MediaRecorder mMediaRecorder; - @SuppressWarnings("WeakerAccess") protected CamcorderProfile mProfile; + private CamcorderProfile mProfile; private boolean mMediaRecorderPrepared; @@ -40,99 +40,140 @@ public abstract class FullVideoRecorder extends VideoRecorder { super(listener); } - @SuppressWarnings({"WeakerAccess", "UnusedReturnValue"}) - protected boolean prepareMediaRecorder(@NonNull VideoResult.Stub stub) { + /** + * Subclasses should return an appropriate CamcorderProfile. + * This could be taken from the {@link CamcorderProfiles} utility class based on the + * stub declared size, for instance. + * + * @param stub the stub + * @return the profile + */ + @NonNull + protected abstract CamcorderProfile getCamcorderProfile(@NonNull VideoResult.Stub stub); + + /** + * Subclasses should apply a video source to the given recorder. + * + * @param stub the stub + * @param mediaRecorder the recorder + */ + protected abstract void applyVideoSource(@NonNull VideoResult.Stub stub, + @NonNull MediaRecorder mediaRecorder); + + @SuppressWarnings("WeakerAccess") + protected final boolean prepareMediaRecorder(@NonNull VideoResult.Stub stub) { if (mMediaRecorderPrepared) return true; - return onPrepareMediaRecorder(stub, new MediaRecorder()); + // We kind of trust the stub size at this point. It's coming from CameraOptions sizes + // and it's clipped to be less than CamcorderProfile's highest available profile. + // However, we still can't trust the developer parameters (e.g. bit rates), and even + // without them, the camera declared sizes can cause crashes in MediaRecorder (#467, #602). + // A possible solution was to prepare without checking DeviceEncoders first, and should it + // fail, prepare again checking them. However, when parameters are wrong, MediaRecorder + // fails on start() instead of prepare() (start failed -19), so this wouldn't be effective. + return prepareMediaRecorder(stub, true); } - protected boolean onPrepareMediaRecorder(@NonNull VideoResult.Stub stub, - @NonNull MediaRecorder mediaRecorder) { - mMediaRecorder = mediaRecorder; + @SuppressWarnings("SameParameterValue") + private boolean prepareMediaRecorder(@NonNull VideoResult.Stub stub, + boolean applyEncodersConstraints) { + // 1. Create reference and ask for the CamcorderProfile + mMediaRecorder = new MediaRecorder(); + mProfile = getCamcorderProfile(stub); + + // 2. Set the video and audio sources. + applyVideoSource(stub, mMediaRecorder); boolean hasAudio = stub.audio == Audio.ON || stub.audio == Audio.MONO || stub.audio == Audio.STEREO; if (hasAudio) { mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT); } - mMediaRecorder.setOutputFormat(mProfile.fileFormat); - // Get the audio mime type - // https://android.googlesource.com/platform/frameworks/av/+/master/media/libmediaplayerservice/StagefrightRecorder.cpp#1096 - // https://github.com/MrAlex94/Waterfox-Old/blob/master/media/libstagefright/frameworks/av/media/libstagefright/MediaDefs.cpp - String audioType; - switch (mProfile.audioCodec) { - case MediaRecorder.AudioEncoder.AMR_NB: audioType = "audio/3gpp"; break; - case MediaRecorder.AudioEncoder.AMR_WB: audioType = "audio/amr-wb"; break; - case MediaRecorder.AudioEncoder.AAC: - case MediaRecorder.AudioEncoder.HE_AAC: - case MediaRecorder.AudioEncoder.AAC_ELD: audioType = "audio/mp4a-latm"; break; - case MediaRecorder.AudioEncoder.VORBIS: audioType = "audio/vorbis"; break; - case MediaRecorder.AudioEncoder.DEFAULT: - default: audioType = "audio/3gpp"; - } - - // Get the video mime type - // https://android.googlesource.com/platform/frameworks/av/+/master/media/libmediaplayerservice/StagefrightRecorder.cpp#1650 - // https://github.com/MrAlex94/Waterfox-Old/blob/master/media/libstagefright/frameworks/av/media/libstagefright/MediaDefs.cpp - String videoType; + // 3. Set the output format. Before, change the profile data if the user + // has specified a specific codec. if (stub.videoCodec == VideoCodec.H_264) { mProfile.videoCodec = MediaRecorder.VideoEncoder.H264; - } - if (stub.videoCodec == VideoCodec.H_263) { + mProfile.fileFormat = MediaRecorder.OutputFormat.MPEG_4; + } else if (stub.videoCodec == VideoCodec.H_263) { mProfile.videoCodec = MediaRecorder.VideoEncoder.H263; + mProfile.fileFormat = MediaRecorder.OutputFormat.MPEG_4; // should work } - switch (mProfile.videoCodec) { - case MediaRecorder.VideoEncoder.H263: videoType = "video/3gpp"; break; - case MediaRecorder.VideoEncoder.H264: videoType = "video/avc"; break; - case MediaRecorder.VideoEncoder.MPEG_4_SP: videoType = "video/mp4v-es"; break; - case MediaRecorder.VideoEncoder.VP8: videoType = "video/x-vnd.on2.vp8"; break; - case MediaRecorder.VideoEncoder.HEVC: videoType = "video/hevc"; break; - case MediaRecorder.VideoEncoder.DEFAULT: - default: videoType = "video/avc"; - } + mMediaRecorder.setOutputFormat(mProfile.fileFormat); - // Merge stub and profile - stub.videoFrameRate = stub.videoFrameRate > 0 ? stub.videoFrameRate - : mProfile.videoFrameRate; - stub.videoBitRate = stub.videoBitRate > 0 ? stub.videoBitRate : mProfile.videoBitRate; - if (hasAudio) { - stub.audioBitRate = stub.audioBitRate > 0 ? stub.audioBitRate : mProfile.audioBitRate; - } + // 4. Update the VideoResult stub with information from the profile, if the + // stub values are absent or incomplete + if (stub.videoFrameRate <= 0) stub.videoFrameRate = mProfile.videoFrameRate; + if (stub.videoBitRate <= 0) stub.videoBitRate = mProfile.videoBitRate; + if (stub.audioBitRate <= 0 && hasAudio) stub.audioBitRate = mProfile.audioBitRate; - // Check DeviceEncoders support - boolean flip = stub.rotation % 180 != 0; - if (flip) stub.size = stub.size.flip(); - Size newVideoSize = null; - int newVideoBitRate = 0; - int newAudioBitRate = 0; - int newVideoFrameRate = 0; - int videoEncoderOffset = 0; - int audioEncoderOffset = 0; - boolean encodersFound = false; - while (!encodersFound) { - DeviceEncoders encoders = new DeviceEncoders(DeviceEncoders.MODE_RESPECT_ORDER, - videoType, audioType, videoEncoderOffset, audioEncoderOffset); - try { - newVideoSize = encoders.getSupportedVideoSize(stub.size); - newVideoBitRate = encoders.getSupportedVideoBitRate(stub.videoBitRate); - newAudioBitRate = encoders.getSupportedAudioBitRate(stub.audioBitRate); - newVideoFrameRate = encoders.getSupportedVideoFrameRate(newVideoSize, - stub.videoFrameRate); - encodersFound = true; - } catch (DeviceEncoders.VideoException videoException) { - videoEncoderOffset++; - } catch (DeviceEncoders.AudioException audioException) { - audioEncoderOffset++; + // 5. Update the VideoResult stub with DeviceEncoders constraints + if (applyEncodersConstraints) { + // A. Get the audio mime type + // https://android.googlesource.com/platform/frameworks/av/+/master/media/libmediaplayerservice/StagefrightRecorder.cpp#1096 + // https://github.com/MrAlex94/Waterfox-Old/blob/master/media/libstagefright/frameworks/av/media/libstagefright/MediaDefs.cpp + String audioType; + switch (mProfile.audioCodec) { + case MediaRecorder.AudioEncoder.AMR_NB: audioType = "audio/3gpp"; break; + case MediaRecorder.AudioEncoder.AMR_WB: audioType = "audio/amr-wb"; break; + case MediaRecorder.AudioEncoder.AAC: + case MediaRecorder.AudioEncoder.HE_AAC: + case MediaRecorder.AudioEncoder.AAC_ELD: audioType = "audio/mp4a-latm"; break; + case MediaRecorder.AudioEncoder.VORBIS: audioType = "audio/vorbis"; break; + case MediaRecorder.AudioEncoder.DEFAULT: + default: audioType = "audio/3gpp"; + } + // B. Get the video mime type + // https://android.googlesource.com/platform/frameworks/av/+/master/media/libmediaplayerservice/StagefrightRecorder.cpp#1650 + // https://github.com/MrAlex94/Waterfox-Old/blob/master/media/libstagefright/frameworks/av/media/libstagefright/MediaDefs.cpp + String videoType; + switch (mProfile.videoCodec) { + case MediaRecorder.VideoEncoder.H263: videoType = "video/3gpp"; break; + case MediaRecorder.VideoEncoder.H264: videoType = "video/avc"; break; + case MediaRecorder.VideoEncoder.MPEG_4_SP: videoType = "video/mp4v-es"; break; + case MediaRecorder.VideoEncoder.VP8: videoType = "video/x-vnd.on2.vp8"; break; + case MediaRecorder.VideoEncoder.HEVC: videoType = "video/hevc"; break; + case MediaRecorder.VideoEncoder.DEFAULT: + default: videoType = "video/avc"; } + // C. Check DeviceEncoders support + boolean flip = stub.rotation % 180 != 0; + if (flip) stub.size = stub.size.flip(); + Size newVideoSize = null; + int newVideoBitRate = 0; + int newAudioBitRate = 0; + int newVideoFrameRate = 0; + int videoEncoderOffset = 0; + int audioEncoderOffset = 0; + boolean encodersFound = false; + while (!encodersFound) { + LOG.i("prepareMediaRecorder:", "Checking DeviceEncoders...", + "videoOffset:", videoEncoderOffset, + "audioOffset:", audioEncoderOffset); + DeviceEncoders encoders = new DeviceEncoders(DeviceEncoders.MODE_RESPECT_ORDER, + videoType, audioType, videoEncoderOffset, audioEncoderOffset); + try { + newVideoSize = encoders.getSupportedVideoSize(stub.size); + newVideoBitRate = encoders.getSupportedVideoBitRate(stub.videoBitRate); + newAudioBitRate = encoders.getSupportedAudioBitRate(stub.audioBitRate); + newVideoFrameRate = encoders.getSupportedVideoFrameRate(newVideoSize, + stub.videoFrameRate); + encodersFound = true; + } catch (DeviceEncoders.VideoException videoException) { + videoEncoderOffset++; + } catch (DeviceEncoders.AudioException audioException) { + audioEncoderOffset++; + } + } + // D. Apply results + stub.size = newVideoSize; + stub.videoBitRate = newVideoBitRate; + stub.audioBitRate = newAudioBitRate; + stub.videoFrameRate = newVideoFrameRate; + if (flip) stub.size = stub.size.flip(); } - stub.size = newVideoSize; - stub.videoBitRate = newVideoBitRate; - stub.audioBitRate = newAudioBitRate; - stub.videoFrameRate = newVideoFrameRate; - if (flip) stub.size = stub.size.flip(); - // Set video params + // 6A. Configure MediaRecorder from stub and from profile (video) + boolean flip = stub.rotation % 180 != 0; mMediaRecorder.setVideoSize( flip ? stub.size.getHeight() : stub.size.getWidth(), flip ? stub.size.getWidth() : stub.size.getHeight()); @@ -140,7 +181,7 @@ public abstract class FullVideoRecorder extends VideoRecorder { mMediaRecorder.setVideoEncoder(mProfile.videoCodec); mMediaRecorder.setVideoEncodingBitRate(stub.videoBitRate); - // Set audio params + // 6B. Configure MediaRecorder from stub and from profile (audio) if (hasAudio) { if (stub.audio == Audio.ON) { mMediaRecorder.setAudioChannels(mProfile.audioChannels); @@ -154,7 +195,7 @@ public abstract class FullVideoRecorder extends VideoRecorder { mMediaRecorder.setAudioEncodingBitRate(stub.audioBitRate); } - // Set other params + // 7. Set other params if (stub.location != null) { mMediaRecorder.setLocation( (float) stub.location.getLatitude(), @@ -180,7 +221,7 @@ public abstract class FullVideoRecorder extends VideoRecorder { } }); - // Prepare the Recorder + // 8. Prepare the Recorder try { mMediaRecorder.prepare(); mMediaRecorderPrepared = true; @@ -220,13 +261,15 @@ public abstract class FullVideoRecorder extends VideoRecorder { try { mMediaRecorder.stop(); } catch (Exception e) { - LOG.w("stop:", "Error while closing media recorder.", e); // This can happen if stopVideo() is called right after takeVideo() // (in which case we don't care). Or when prepare()/start() have failed for // some reason and we are not allowed to call stop. // Make sure we don't override the error if one exists already. mResult = null; - if (mError == null) mError = e; + if (mError == null) { + LOG.w("stop:", "Error while closing media recorder.", e); + mError = e; + } } mMediaRecorder.release(); }