takeVideo improvements (#617)

* takeVideo improvements

* Try to fix tests

* Reorder operations in FullVideoRecorder
pull/632/head
Mattia Iavarone 5 years ago committed by GitHub
parent 5b92e81b83
commit 824abbc55a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 66
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java
  2. 28
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java
  3. 4
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java
  4. 10
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/DeviceEncoders.java
  5. 5
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/CamcorderProfiles.java
  6. 12
      cameraview/src/main/java/com/otaliastudios/cameraview/video/Full1VideoRecorder.java
  7. 16
      cameraview/src/main/java/com/otaliastudios/cameraview/video/Full2VideoRecorder.java
  8. 211
      cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java

@ -41,7 +41,7 @@ public class CameraOptions1Test extends BaseTest {
@Test @Test
public void testEmpty() { 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.getSupportedPictureAspectRatios().isEmpty());
assertTrue(o.getSupportedPictureSizes().isEmpty()); assertTrue(o.getSupportedPictureSizes().isEmpty());
assertTrue(o.getSupportedWhiteBalance().isEmpty()); assertTrue(o.getSupportedWhiteBalance().isEmpty());
@ -71,7 +71,7 @@ public class CameraOptions1Test extends BaseTest {
); );
Camera.Parameters params = mock(Camera.Parameters.class); Camera.Parameters params = mock(Camera.Parameters.class);
when(params.getSupportedPictureSizes()).thenReturn(sizes); when(params.getSupportedPictureSizes()).thenReturn(sizes);
CameraOptions o = new CameraOptions(params, false); CameraOptions o = new CameraOptions(params, 0, false);
Collection<Size> supportedSizes = o.getSupportedPictureSizes(); Collection<Size> supportedSizes = o.getSupportedPictureSizes();
assertEquals(supportedSizes.size(), sizes.size()); assertEquals(supportedSizes.size(), sizes.size());
for (Camera.Size size : sizes) { for (Camera.Size size : sizes) {
@ -90,7 +90,7 @@ public class CameraOptions1Test extends BaseTest {
); );
Camera.Parameters params = mock(Camera.Parameters.class); Camera.Parameters params = mock(Camera.Parameters.class);
when(params.getSupportedPictureSizes()).thenReturn(sizes); when(params.getSupportedPictureSizes()).thenReturn(sizes);
CameraOptions o = new CameraOptions(params, true); CameraOptions o = new CameraOptions(params, 0, true);
Collection<Size> supportedSizes = o.getSupportedPictureSizes(); Collection<Size> supportedSizes = o.getSupportedPictureSizes();
assertEquals(supportedSizes.size(), sizes.size()); assertEquals(supportedSizes.size(), sizes.size());
for (Camera.Size size : sizes) { for (Camera.Size size : sizes) {
@ -115,7 +115,7 @@ public class CameraOptions1Test extends BaseTest {
Camera.Parameters params = mock(Camera.Parameters.class); Camera.Parameters params = mock(Camera.Parameters.class);
when(params.getSupportedPictureSizes()).thenReturn(sizes); when(params.getSupportedPictureSizes()).thenReturn(sizes);
CameraOptions o = new CameraOptions(params, false); CameraOptions o = new CameraOptions(params, 0, false);
Collection<AspectRatio> supportedRatios = o.getSupportedPictureAspectRatios(); Collection<AspectRatio> supportedRatios = o.getSupportedPictureAspectRatios();
assertEquals(supportedRatios.size(), expected.size()); assertEquals(supportedRatios.size(), expected.size());
for (AspectRatio ratio : expected) { for (AspectRatio ratio : expected) {
@ -126,15 +126,17 @@ public class CameraOptions1Test extends BaseTest {
@Test @Test
public void testVideoSizes() { 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<Camera.Size> sizes = Arrays.asList( List<Camera.Size> sizes = Arrays.asList(
mockCameraSize(100, 200), mockCameraSize(10, 20),
mockCameraSize(50, 50), mockCameraSize(5, 5),
mockCameraSize(1600, 900), mockCameraSize(16, 9),
mockCameraSize(1000, 2000) mockCameraSize(20, 40)
); );
Camera.Parameters params = mock(Camera.Parameters.class); Camera.Parameters params = mock(Camera.Parameters.class);
when(params.getSupportedVideoSizes()).thenReturn(sizes); when(params.getSupportedVideoSizes()).thenReturn(sizes);
CameraOptions o = new CameraOptions(params, false); CameraOptions o = new CameraOptions(params, 0, false);
Collection<Size> supportedSizes = o.getSupportedVideoSizes(); Collection<Size> supportedSizes = o.getSupportedVideoSizes();
assertEquals(supportedSizes.size(), sizes.size()); assertEquals(supportedSizes.size(), sizes.size());
for (Camera.Size size : sizes) { for (Camera.Size size : sizes) {
@ -147,15 +149,15 @@ public class CameraOptions1Test extends BaseTest {
public void testVideoSizesNull() { public void testVideoSizesNull() {
// When videoSizes is null, we take the preview sizes. // When videoSizes is null, we take the preview sizes.
List<Camera.Size> sizes = Arrays.asList( List<Camera.Size> sizes = Arrays.asList(
mockCameraSize(100, 200), mockCameraSize(10, 20),
mockCameraSize(50, 50), mockCameraSize(5, 5),
mockCameraSize(1600, 900), mockCameraSize(16, 9),
mockCameraSize(1000, 2000) mockCameraSize(20, 40)
); );
Camera.Parameters params = mock(Camera.Parameters.class); Camera.Parameters params = mock(Camera.Parameters.class);
when(params.getSupportedVideoSizes()).thenReturn(null); when(params.getSupportedVideoSizes()).thenReturn(null);
when(params.getSupportedPreviewSizes()).thenReturn(sizes); when(params.getSupportedPreviewSizes()).thenReturn(sizes);
CameraOptions o = new CameraOptions(params, false); CameraOptions o = new CameraOptions(params, 0, false);
Collection<Size> supportedSizes = o.getSupportedVideoSizes(); Collection<Size> supportedSizes = o.getSupportedVideoSizes();
assertEquals(supportedSizes.size(), sizes.size()); assertEquals(supportedSizes.size(), sizes.size());
for (Camera.Size size : sizes) { for (Camera.Size size : sizes) {
@ -167,14 +169,14 @@ public class CameraOptions1Test extends BaseTest {
@Test @Test
public void testVideoSizesFlip() { public void testVideoSizesFlip() {
List<Camera.Size> sizes = Arrays.asList( List<Camera.Size> sizes = Arrays.asList(
mockCameraSize(100, 200), mockCameraSize(10, 20),
mockCameraSize(50, 50), mockCameraSize(5, 5),
mockCameraSize(1600, 900), mockCameraSize(16, 9),
mockCameraSize(1000, 2000) mockCameraSize(20, 40)
); );
Camera.Parameters params = mock(Camera.Parameters.class); Camera.Parameters params = mock(Camera.Parameters.class);
when(params.getSupportedVideoSizes()).thenReturn(sizes); when(params.getSupportedVideoSizes()).thenReturn(sizes);
CameraOptions o = new CameraOptions(params, true); CameraOptions o = new CameraOptions(params, 0, true);
Collection<Size> supportedSizes = o.getSupportedVideoSizes(); Collection<Size> supportedSizes = o.getSupportedVideoSizes();
assertEquals(supportedSizes.size(), sizes.size()); assertEquals(supportedSizes.size(), sizes.size());
for (Camera.Size size : sizes) { for (Camera.Size size : sizes) {
@ -186,10 +188,10 @@ public class CameraOptions1Test extends BaseTest {
@Test @Test
public void testVideoAspectRatio() { public void testVideoAspectRatio() {
List<Camera.Size> sizes = Arrays.asList( List<Camera.Size> sizes = Arrays.asList(
mockCameraSize(100, 200), mockCameraSize(10, 20),
mockCameraSize(50, 50), mockCameraSize(5, 5),
mockCameraSize(1600, 900), mockCameraSize(16, 9),
mockCameraSize(1000, 2000) mockCameraSize(20, 40)
); );
Set<AspectRatio> expected = new HashSet<>(); Set<AspectRatio> expected = new HashSet<>();
@ -199,7 +201,7 @@ public class CameraOptions1Test extends BaseTest {
Camera.Parameters params = mock(Camera.Parameters.class); Camera.Parameters params = mock(Camera.Parameters.class);
when(params.getSupportedVideoSizes()).thenReturn(sizes); when(params.getSupportedVideoSizes()).thenReturn(sizes);
CameraOptions o = new CameraOptions(params, false); CameraOptions o = new CameraOptions(params, 0, false);
Collection<AspectRatio> supportedRatios = o.getSupportedVideoAspectRatios(); Collection<AspectRatio> supportedRatios = o.getSupportedVideoAspectRatios();
assertEquals(supportedRatios.size(), expected.size()); assertEquals(supportedRatios.size(), expected.size());
for (AspectRatio ratio : expected) { for (AspectRatio ratio : expected) {
@ -215,7 +217,7 @@ public class CameraOptions1Test extends BaseTest {
when(params.getMaxExposureCompensation()).thenReturn(0); when(params.getMaxExposureCompensation()).thenReturn(0);
when(params.getMinExposureCompensation()).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)); assertFalse(o.supports(GestureAction.AUTO_FOCUS));
assertTrue(o.supports(GestureAction.TAKE_PICTURE)); assertTrue(o.supports(GestureAction.TAKE_PICTURE));
assertTrue(o.supports(GestureAction.NONE)); assertTrue(o.supports(GestureAction.NONE));
@ -229,7 +231,7 @@ public class CameraOptions1Test extends BaseTest {
public void testAlwaysSupportedControls() { public void testAlwaysSupportedControls() {
// Grid, VideoQuality, SessionType and Audio are always supported. // Grid, VideoQuality, SessionType and Audio are always supported.
Camera.Parameters params = mock(Camera.Parameters.class); Camera.Parameters params = mock(Camera.Parameters.class);
CameraOptions o = new CameraOptions(params, false); CameraOptions o = new CameraOptions(params, 0, false);
Collection<Grid> grids = o.getSupportedControls(Grid.class); Collection<Grid> grids = o.getSupportedControls(Grid.class);
Collection<VideoCodec> video = o.getSupportedControls(VideoCodec.class); Collection<VideoCodec> video = o.getSupportedControls(VideoCodec.class);
@ -250,7 +252,7 @@ public class CameraOptions1Test extends BaseTest {
supported.add(cameraInfo.facing); 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(); Camera1Mapper m = Camera1Mapper.get();
Collection<Facing> s = o.getSupportedControls(Facing.class); Collection<Facing> s = o.getSupportedControls(Facing.class);
assertEquals(s.size(), supported.size()); assertEquals(s.size(), supported.size());
@ -269,7 +271,7 @@ public class CameraOptions1Test extends BaseTest {
Camera.Parameters.WHITE_BALANCE_SHADE // Not supported Camera.Parameters.WHITE_BALANCE_SHADE // Not supported
)); ));
CameraOptions o = new CameraOptions(params, false); CameraOptions o = new CameraOptions(params, 0, false);
Collection<WhiteBalance> w = o.getSupportedControls(WhiteBalance.class); Collection<WhiteBalance> w = o.getSupportedControls(WhiteBalance.class);
assertEquals(w.size(), 2); assertEquals(w.size(), 2);
assertTrue(w.contains(WhiteBalance.AUTO)); assertTrue(w.contains(WhiteBalance.AUTO));
@ -288,7 +290,7 @@ public class CameraOptions1Test extends BaseTest {
Camera.Parameters.FLASH_MODE_RED_EYE // Not supported Camera.Parameters.FLASH_MODE_RED_EYE // Not supported
)); ));
CameraOptions o = new CameraOptions(params, false); CameraOptions o = new CameraOptions(params, 0, false);
Collection<Flash> f = o.getSupportedControls(Flash.class); Collection<Flash> f = o.getSupportedControls(Flash.class);
assertEquals(f.size(), 3); assertEquals(f.size(), 3);
assertTrue(f.contains(Flash.OFF)); assertTrue(f.contains(Flash.OFF));
@ -309,7 +311,7 @@ public class CameraOptions1Test extends BaseTest {
Camera.Parameters.SCENE_MODE_FIREWORKS // Not supported Camera.Parameters.SCENE_MODE_FIREWORKS // Not supported
)); ));
CameraOptions o = new CameraOptions(params, false); CameraOptions o = new CameraOptions(params, 0, false);
Collection<Hdr> h = o.getSupportedControls(Hdr.class); Collection<Hdr> h = o.getSupportedControls(Hdr.class);
assertEquals(h.size(), 2); assertEquals(h.size(), 2);
assertTrue(h.contains(Hdr.OFF)); assertTrue(h.contains(Hdr.OFF));
@ -326,7 +328,7 @@ public class CameraOptions1Test extends BaseTest {
when(params.isZoomSupported()).thenReturn(true); when(params.isZoomSupported()).thenReturn(true);
//noinspection ArraysAsListWithZeroOrOneArgument //noinspection ArraysAsListWithZeroOrOneArgument
when(params.getSupportedFocusModes()).thenReturn(Arrays.asList(Camera.Parameters.FOCUS_MODE_AUTO)); 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.isZoomSupported());
assertTrue(o.isAutoFocusSupported()); assertTrue(o.isAutoFocusSupported());
} }
@ -337,7 +339,7 @@ public class CameraOptions1Test extends BaseTest {
when(params.getMaxExposureCompensation()).thenReturn(10); when(params.getMaxExposureCompensation()).thenReturn(10);
when(params.getMinExposureCompensation()).thenReturn(-10); when(params.getMinExposureCompensation()).thenReturn(-10);
when(params.getExposureCompensationStep()).thenReturn(0.5f); when(params.getExposureCompensationStep()).thenReturn(0.5f);
CameraOptions o = new CameraOptions(params, false); CameraOptions o = new CameraOptions(params, 0, false);
assertTrue(o.isExposureCorrectionSupported()); assertTrue(o.isExposureCorrectionSupported());
assertEquals(o.getExposureCorrectionMinValue(), -10f * 0.5f, 0f); assertEquals(o.getExposureCorrectionMinValue(), -10f * 0.5f, 0f);
assertEquals(o.getExposureCorrectionMaxValue(), 10f * 0.5f, 0f); assertEquals(o.getExposureCorrectionMaxValue(), 10f * 0.5f, 0f);

@ -62,7 +62,7 @@ public class CameraOptions {
private boolean autoFocusSupported; private boolean autoFocusSupported;
public CameraOptions(@NonNull Camera.Parameters params, boolean flipSizes) { public CameraOptions(@NonNull Camera.Parameters params, int cameraId, boolean flipSizes) {
List<String> strings; List<String> strings;
Camera1Mapper mapper = Camera1Mapper.get(); Camera1Mapper mapper = Camera1Mapper.get();
@ -127,23 +127,33 @@ public class CameraOptions {
} }
// Video Sizes // 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<Camera.Size> vsizes = params.getSupportedVideoSizes(); List<Camera.Size> vsizes = params.getSupportedVideoSizes();
if (vsizes != null) { if (vsizes != null) {
for (Camera.Size size : vsizes) { for (Camera.Size size : vsizes) {
int width = flipSizes ? size.height : size.width; if (size.width <= videoMaxSize.getWidth()
int height = flipSizes ? size.width : size.height; && size.height <= videoMaxSize.getHeight()) {
supportedVideoSizes.add(new Size(width, height)); int width = flipSizes ? size.height : size.width;
supportedVideoAspectRatio.add(AspectRatio.of(width, height)); int height = flipSizes ? size.width : size.height;
supportedVideoSizes.add(new Size(width, height));
supportedVideoAspectRatio.add(AspectRatio.of(width, height));
}
} }
} else { } else {
// StackOverflow threads seems to agree that if getSupportedVideoSizes is null, // StackOverflow threads seems to agree that if getSupportedVideoSizes is null,
// previews can be used. // previews can be used.
List<Camera.Size> fallback = params.getSupportedPreviewSizes(); List<Camera.Size> fallback = params.getSupportedPreviewSizes();
for (Camera.Size size : fallback) { for (Camera.Size size : fallback) {
int width = flipSizes ? size.height : size.width; if (size.width <= videoMaxSize.getWidth()
int height = flipSizes ? size.width : size.height; && size.height <= videoMaxSize.getHeight()) {
supportedVideoSizes.add(new Size(width, height)); int width = flipSizes ? size.height : size.width;
supportedVideoAspectRatio.add(AspectRatio.of(width, height)); int height = flipSizes ? size.width : size.height;
supportedVideoSizes.add(new Size(width, height));
supportedVideoAspectRatio.add(AspectRatio.of(width, height));
}
} }
} }
} }

@ -154,8 +154,8 @@ public class Camera1Engine extends CameraEngine implements
// Set parameters that might have been set before the camera was opened. // Set parameters that might have been set before the camera was opened.
LOG.i("onStartEngine:", "Applying default parameters."); LOG.i("onStartEngine:", "Applying default parameters.");
Camera.Parameters params = mCamera.getParameters(); Camera.Parameters params = mCamera.getParameters();
mCameraOptions = new CameraOptions(params, getAngles() mCameraOptions = new CameraOptions(params, mCameraId,
.flip(Reference.SENSOR, Reference.VIEW)); getAngles().flip(Reference.SENSOR, Reference.VIEW));
applyAllParameters(params); applyAllParameters(params);
mCamera.setParameters(params); mCamera.setParameters(params);
mCamera.setDisplayOrientation(getAngles().offset(Reference.SENSOR, Reference.VIEW, mCamera.setDisplayOrientation(getAngles().offset(Reference.SENSOR, Reference.VIEW,

@ -221,22 +221,28 @@ public class DeviceEncoders {
int width = size.getWidth(); int width = size.getWidth();
int height = size.getHeight(); int height = size.getHeight();
double aspect = (double) width / height; 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 width is too large, scale down, but keep aspect ratio.
if (mVideoCapabilities.getSupportedWidths().getUpper() < width) { if (mVideoCapabilities.getSupportedWidths().getUpper() < width) {
width = mVideoCapabilities.getSupportedWidths().getUpper(); width = mVideoCapabilities.getSupportedWidths().getUpper();
height = (int) Math.round(width / aspect); 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 height is too large, scale down, but keep aspect ratio.
if (mVideoCapabilities.getSupportedHeights().getUpper() < height) { if (mVideoCapabilities.getSupportedHeights().getUpper() < height) {
height = mVideoCapabilities.getSupportedHeights().getUpper(); height = mVideoCapabilities.getSupportedHeights().getUpper();
width = (int) Math.round(aspect * height); width = (int) Math.round(aspect * height);
LOG.i("getSupportedVideoSize - exceeds maxHeight! width:", width,
"height:", height);
} }
// Adjust the alignment. // Adjust the alignment.
while (width % mVideoCapabilities.getWidthAlignment() != 0) width--; while (width % mVideoCapabilities.getWidthAlignment() != 0) width--;
while (height % mVideoCapabilities.getHeightAlignment() != 0) height--; while (height % mVideoCapabilities.getHeightAlignment() != 0) height--;
LOG.i("getSupportedVideoSize - aligned. width:", width, "height:", height);
// It's still possible that we're BELOW the lower. // It's still possible that we're BELOW the lower.
if (!mVideoCapabilities.getSupportedWidths().contains(width)) { if (!mVideoCapabilities.getSupportedWidths().contains(width)) {
@ -256,9 +262,7 @@ public class DeviceEncoders {
" Might be an aspect ratio issue." + " Might be an aspect ratio issue." +
" Desired size:" + new Size(width, height)); " Desired size:" + new Size(width, height));
} }
Size adjusted = new Size(width, height); return new Size(width, height);
LOG.i("getSupportedVideoSize -", "inputSize:", size, "adjustedSize:", adjusted);
return adjusted;
} }
/** /**

@ -4,6 +4,7 @@ import android.annotation.SuppressLint;
import android.media.CamcorderProfile; import android.media.CamcorderProfile;
import android.os.Build; import android.os.Build;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
import java.util.ArrayList; import java.util.ArrayList;
@ -20,6 +21,9 @@ import androidx.annotation.NonNull;
*/ */
public class CamcorderProfiles { public class CamcorderProfiles {
private static final String TAG = CamcorderProfiles.class.getSimpleName();
private static final CameraLogger LOG = CameraLogger.create(TAG);
@SuppressLint("UseSparseArrays") @SuppressLint("UseSparseArrays")
private static Map<Size, Integer> sizeToProfileMap = new HashMap<>(); private static Map<Size, Integer> sizeToProfileMap = new HashMap<>();
@ -53,6 +57,7 @@ public class CamcorderProfiles {
int camera1Id = Integer.parseInt(cameraId); int camera1Id = Integer.parseInt(cameraId);
return get(camera1Id, targetSize); return get(camera1Id, targetSize);
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
LOG.w("NumberFormatException for Camera2 id:", cameraId);
return CamcorderProfile.get(CamcorderProfile.QUALITY_LOW); return CamcorderProfile.get(CamcorderProfile.QUALITY_LOW);
} }
} }

@ -1,6 +1,7 @@
package com.otaliastudios.cameraview.video; package com.otaliastudios.cameraview.video;
import android.hardware.Camera; import android.hardware.Camera;
import android.media.CamcorderProfile;
import android.media.MediaRecorder; import android.media.MediaRecorder;
import android.util.Log; import android.util.Log;
@ -34,15 +35,18 @@ public class Full1VideoRecorder extends FullVideoRecorder {
} }
@Override @Override
protected boolean onPrepareMediaRecorder(@NonNull VideoResult.Stub stub, protected void applyVideoSource(@NonNull VideoResult.Stub stub,
@NonNull MediaRecorder mediaRecorder) { @NonNull MediaRecorder mediaRecorder) {
mediaRecorder.setCamera(mCamera); mediaRecorder.setCamera(mCamera);
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
}
@NonNull
@Override
protected CamcorderProfile getCamcorderProfile(@NonNull VideoResult.Stub stub) {
// Get a profile of quality compatible with the chosen size. // Get a profile of quality compatible with the chosen size.
Size size = stub.rotation % 180 != 0 ? stub.size.flip() : stub.size; Size size = stub.rotation % 180 != 0 ? stub.size.flip() : stub.size;
mProfile = CamcorderProfiles.get(mCameraId, size); return CamcorderProfiles.get(mCameraId, size);
return super.onPrepareMediaRecorder(stub, mediaRecorder);
} }
@Override @Override

@ -4,6 +4,7 @@ import android.annotation.SuppressLint;
import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult; import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.TotalCaptureResult; import android.hardware.camera2.TotalCaptureResult;
import android.media.CamcorderProfile;
import android.media.MediaRecorder; import android.media.MediaRecorder;
import android.os.Build; import android.os.Build;
import android.view.Surface; import android.view.Surface;
@ -69,19 +70,22 @@ public class Full2VideoRecorder extends FullVideoRecorder {
action.start(mHolder); action.start(mHolder);
} }
@SuppressLint("NewApi")
@Override @Override
protected boolean onPrepareMediaRecorder(@NonNull VideoResult.Stub stub, protected void applyVideoSource(@NonNull VideoResult.Stub stub,
@NonNull MediaRecorder mediaRecorder) { @NonNull MediaRecorder mediaRecorder) {
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); 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 // This was an option: get the surface from outside this class, using
// MediaCodec.createPersistentInputSurface(). But it doesn't really help since the // 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 // 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. // we would have to attach the surface to this recorder before creating the CameraSession.
// mediaRecorder.setInputSurface(mInputSurface); // mediaRecorder.setInputSurface(mInputSurface);
return super.onPrepareMediaRecorder(stub, mediaRecorder); Size size = stub.rotation % 180 != 0 ? stub.size.flip() : stub.size;
return CamcorderProfiles.get(mCameraId, size);
} }
/** /**

@ -8,6 +8,7 @@ import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.controls.Audio; import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.VideoCodec; import com.otaliastudios.cameraview.controls.VideoCodec;
import com.otaliastudios.cameraview.internal.DeviceEncoders; import com.otaliastudios.cameraview.internal.DeviceEncoders;
import com.otaliastudios.cameraview.internal.utils.CamcorderProfiles;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -17,10 +18,9 @@ import androidx.annotation.Nullable;
* A {@link VideoRecorder} that uses {@link android.media.MediaRecorder} APIs. * A {@link VideoRecorder} that uses {@link android.media.MediaRecorder} APIs.
* *
* When started, the media recorder will be prepared in * When started, the media recorder will be prepared in
* {@link #onPrepareMediaRecorder(VideoResult.Stub, MediaRecorder)}. * {@link #prepareMediaRecorder(VideoResult.Stub)}. This will call two abstract methods:
* Subclasses should override this method and, before calling super(), do two things: * - {@link #getCamcorderProfile(VideoResult.Stub)}
* - set the media recorder VideoSource * - {@link #applyVideoSource(VideoResult.Stub, MediaRecorder)}
* - define {@link #mProfile}
* *
* Subclasses can also call {@link #prepareMediaRecorder(VideoResult.Stub)} before start happens, * 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 * 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); private static final CameraLogger LOG = CameraLogger.create(TAG);
@SuppressWarnings("WeakerAccess") protected MediaRecorder mMediaRecorder; @SuppressWarnings("WeakerAccess") protected MediaRecorder mMediaRecorder;
@SuppressWarnings("WeakerAccess") protected CamcorderProfile mProfile; private CamcorderProfile mProfile;
private boolean mMediaRecorderPrepared; private boolean mMediaRecorderPrepared;
@ -40,99 +40,140 @@ public abstract class FullVideoRecorder extends VideoRecorder {
super(listener); 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; 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, @SuppressWarnings("SameParameterValue")
@NonNull MediaRecorder mediaRecorder) { private boolean prepareMediaRecorder(@NonNull VideoResult.Stub stub,
mMediaRecorder = mediaRecorder; 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 boolean hasAudio = stub.audio == Audio.ON
|| stub.audio == Audio.MONO || stub.audio == Audio.MONO
|| stub.audio == Audio.STEREO; || stub.audio == Audio.STEREO;
if (hasAudio) { if (hasAudio) {
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT); mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT);
} }
mMediaRecorder.setOutputFormat(mProfile.fileFormat);
// Get the audio mime type // 3. Set the output format. Before, change the profile data if the user
// https://android.googlesource.com/platform/frameworks/av/+/master/media/libmediaplayerservice/StagefrightRecorder.cpp#1096 // has specified a specific codec.
// 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;
if (stub.videoCodec == VideoCodec.H_264) { if (stub.videoCodec == VideoCodec.H_264) {
mProfile.videoCodec = MediaRecorder.VideoEncoder.H264; mProfile.videoCodec = MediaRecorder.VideoEncoder.H264;
} mProfile.fileFormat = MediaRecorder.OutputFormat.MPEG_4;
if (stub.videoCodec == VideoCodec.H_263) { } else if (stub.videoCodec == VideoCodec.H_263) {
mProfile.videoCodec = MediaRecorder.VideoEncoder.H263; mProfile.videoCodec = MediaRecorder.VideoEncoder.H263;
mProfile.fileFormat = MediaRecorder.OutputFormat.MPEG_4; // should work
} }
switch (mProfile.videoCodec) { mMediaRecorder.setOutputFormat(mProfile.fileFormat);
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";
}
// Merge stub and profile // 4. Update the VideoResult stub with information from the profile, if the
stub.videoFrameRate = stub.videoFrameRate > 0 ? stub.videoFrameRate // stub values are absent or incomplete
: mProfile.videoFrameRate; if (stub.videoFrameRate <= 0) stub.videoFrameRate = mProfile.videoFrameRate;
stub.videoBitRate = stub.videoBitRate > 0 ? stub.videoBitRate : mProfile.videoBitRate; if (stub.videoBitRate <= 0) stub.videoBitRate = mProfile.videoBitRate;
if (hasAudio) { if (stub.audioBitRate <= 0 && hasAudio) stub.audioBitRate = mProfile.audioBitRate;
stub.audioBitRate = stub.audioBitRate > 0 ? stub.audioBitRate : mProfile.audioBitRate;
}
// Check DeviceEncoders support // 5. Update the VideoResult stub with DeviceEncoders constraints
boolean flip = stub.rotation % 180 != 0; if (applyEncodersConstraints) {
if (flip) stub.size = stub.size.flip(); // A. Get the audio mime type
Size newVideoSize = null; // https://android.googlesource.com/platform/frameworks/av/+/master/media/libmediaplayerservice/StagefrightRecorder.cpp#1096
int newVideoBitRate = 0; // https://github.com/MrAlex94/Waterfox-Old/blob/master/media/libstagefright/frameworks/av/media/libstagefright/MediaDefs.cpp
int newAudioBitRate = 0; String audioType;
int newVideoFrameRate = 0; switch (mProfile.audioCodec) {
int videoEncoderOffset = 0; case MediaRecorder.AudioEncoder.AMR_NB: audioType = "audio/3gpp"; break;
int audioEncoderOffset = 0; case MediaRecorder.AudioEncoder.AMR_WB: audioType = "audio/amr-wb"; break;
boolean encodersFound = false; case MediaRecorder.AudioEncoder.AAC:
while (!encodersFound) { case MediaRecorder.AudioEncoder.HE_AAC:
DeviceEncoders encoders = new DeviceEncoders(DeviceEncoders.MODE_RESPECT_ORDER, case MediaRecorder.AudioEncoder.AAC_ELD: audioType = "audio/mp4a-latm"; break;
videoType, audioType, videoEncoderOffset, audioEncoderOffset); case MediaRecorder.AudioEncoder.VORBIS: audioType = "audio/vorbis"; break;
try { case MediaRecorder.AudioEncoder.DEFAULT:
newVideoSize = encoders.getSupportedVideoSize(stub.size); default: audioType = "audio/3gpp";
newVideoBitRate = encoders.getSupportedVideoBitRate(stub.videoBitRate); }
newAudioBitRate = encoders.getSupportedAudioBitRate(stub.audioBitRate); // B. Get the video mime type
newVideoFrameRate = encoders.getSupportedVideoFrameRate(newVideoSize, // https://android.googlesource.com/platform/frameworks/av/+/master/media/libmediaplayerservice/StagefrightRecorder.cpp#1650
stub.videoFrameRate); // https://github.com/MrAlex94/Waterfox-Old/blob/master/media/libstagefright/frameworks/av/media/libstagefright/MediaDefs.cpp
encodersFound = true; String videoType;
} catch (DeviceEncoders.VideoException videoException) { switch (mProfile.videoCodec) {
videoEncoderOffset++; case MediaRecorder.VideoEncoder.H263: videoType = "video/3gpp"; break;
} catch (DeviceEncoders.AudioException audioException) { case MediaRecorder.VideoEncoder.H264: videoType = "video/avc"; break;
audioEncoderOffset++; 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( mMediaRecorder.setVideoSize(
flip ? stub.size.getHeight() : stub.size.getWidth(), flip ? stub.size.getHeight() : stub.size.getWidth(),
flip ? stub.size.getWidth() : stub.size.getHeight()); flip ? stub.size.getWidth() : stub.size.getHeight());
@ -140,7 +181,7 @@ public abstract class FullVideoRecorder extends VideoRecorder {
mMediaRecorder.setVideoEncoder(mProfile.videoCodec); mMediaRecorder.setVideoEncoder(mProfile.videoCodec);
mMediaRecorder.setVideoEncodingBitRate(stub.videoBitRate); mMediaRecorder.setVideoEncodingBitRate(stub.videoBitRate);
// Set audio params // 6B. Configure MediaRecorder from stub and from profile (audio)
if (hasAudio) { if (hasAudio) {
if (stub.audio == Audio.ON) { if (stub.audio == Audio.ON) {
mMediaRecorder.setAudioChannels(mProfile.audioChannels); mMediaRecorder.setAudioChannels(mProfile.audioChannels);
@ -154,7 +195,7 @@ public abstract class FullVideoRecorder extends VideoRecorder {
mMediaRecorder.setAudioEncodingBitRate(stub.audioBitRate); mMediaRecorder.setAudioEncodingBitRate(stub.audioBitRate);
} }
// Set other params // 7. Set other params
if (stub.location != null) { if (stub.location != null) {
mMediaRecorder.setLocation( mMediaRecorder.setLocation(
(float) stub.location.getLatitude(), (float) stub.location.getLatitude(),
@ -180,7 +221,7 @@ public abstract class FullVideoRecorder extends VideoRecorder {
} }
}); });
// Prepare the Recorder // 8. Prepare the Recorder
try { try {
mMediaRecorder.prepare(); mMediaRecorder.prepare();
mMediaRecorderPrepared = true; mMediaRecorderPrepared = true;
@ -220,13 +261,15 @@ public abstract class FullVideoRecorder extends VideoRecorder {
try { try {
mMediaRecorder.stop(); mMediaRecorder.stop();
} catch (Exception e) { } catch (Exception e) {
LOG.w("stop:", "Error while closing media recorder.", e);
// This can happen if stopVideo() is called right after takeVideo() // This can happen if stopVideo() is called right after takeVideo()
// (in which case we don't care). Or when prepare()/start() have failed for // (in which case we don't care). Or when prepare()/start() have failed for
// some reason and we are not allowed to call stop. // some reason and we are not allowed to call stop.
// Make sure we don't override the error if one exists already. // Make sure we don't override the error if one exists already.
mResult = null; mResult = null;
if (mError == null) mError = e; if (mError == null) {
LOG.w("stop:", "Error while closing media recorder.", e);
mError = e;
}
} }
mMediaRecorder.release(); mMediaRecorder.release();
} }

Loading…
Cancel
Save