diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraPreviewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraPreviewTest.java index 05f5e462..85c2374d 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraPreviewTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraPreviewTest.java @@ -102,17 +102,17 @@ public abstract class CameraPreviewTest extends BaseTest { @Test public void testDesiredSize() { - preview.setInputStreamSize(160, 90, false); - assertEquals(160, preview.getInputStreamSize().getWidth()); - assertEquals(90, preview.getInputStreamSize().getHeight()); + preview.setStreamSize(160, 90, false); + assertEquals(160, preview.getStreamSize().getWidth()); + assertEquals(90, preview.getStreamSize().getHeight()); } @Test public void testSurfaceAvailable() { ensureAvailable(); verify(callback, times(1)).onSurfaceAvailable(); - assertEquals(surfaceSize.getWidth(), preview.getOutputSurfaceSize().getWidth()); - assertEquals(surfaceSize.getHeight(), preview.getOutputSurfaceSize().getHeight()); + assertEquals(surfaceSize.getWidth(), preview.getSurfaceSize().getWidth()); + assertEquals(surfaceSize.getHeight(), preview.getSurfaceSize().getHeight()); } @Test @@ -121,8 +121,8 @@ public abstract class CameraPreviewTest extends BaseTest { ensureDestroyed(); // This might be called twice in Texture because it overrides ensureDestroyed method verify(callback, atLeastOnce()).onSurfaceDestroyed(); - assertEquals(0, preview.getOutputSurfaceSize().getWidth()); - assertEquals(0, preview.getOutputSurfaceSize().getHeight()); + assertEquals(0, preview.getSurfaceSize().getWidth()); + assertEquals(0, preview.getSurfaceSize().getHeight()); } @Test @@ -146,7 +146,7 @@ public abstract class CameraPreviewTest extends BaseTest { // Since desired is 'desired', let's fake a new view size that is consistent with it. // Ensure crop is not happening anymore. preview.mCropTask.listen(); - preview.dispatchOnOutputSurfaceSizeChanged((int) (50f * desired), 50); // Wait... + preview.dispatchOnSurfaceSizeChanged((int) (50f * desired), 50); // Wait... preview.mCropTask.await(); assertEquals(desired, getViewAspectRatioWithScale(), 0.01f); assertFalse(preview.isCropping()); @@ -154,19 +154,19 @@ public abstract class CameraPreviewTest extends BaseTest { private void setDesiredAspectRatio(float desiredAspectRatio) { preview.mCropTask.listen(); - preview.setInputStreamSize((int) (10f * desiredAspectRatio), 10, false); // Wait... + preview.setStreamSize((int) (10f * desiredAspectRatio), 10, false); // Wait... preview.mCropTask.await(); assertEquals(desiredAspectRatio, getViewAspectRatioWithScale(), 0.01f); } private float getViewAspectRatio() { - Size size = preview.getOutputSurfaceSize(); + Size size = preview.getSurfaceSize(); return AspectRatio.of(size.getWidth(), size.getHeight()).toFloat(); } private float getViewAspectRatioWithScale() { - Size size = preview.getOutputSurfaceSize(); + Size size = preview.getSurfaceSize(); int newWidth = (int) (((float) size.getWidth()) * getCropScaleX()); int newHeight = (int) (((float) size.getHeight()) * getCropScaleY()); return AspectRatio.of(newWidth, newHeight).toFloat(); diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java index aed9b4af..04a42fad 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java @@ -288,14 +288,14 @@ public class CameraViewTest extends BaseTest { //region testMeasure - private void mockPreviewSize() { + private void mockPreviewStreamSize() { Size size = new Size(900, 1600); - mockController.setMockPreviewSize(size); + mockController.setMockPreviewStreamSize(size); } @Test public void testMeasure_early() { - mockController.setMockPreviewSize(null); + mockController.setMockPreviewStreamSize(null); cameraView.measure( makeMeasureSpec(500, EXACTLY), makeMeasureSpec(500, EXACTLY)); @@ -305,7 +305,7 @@ public class CameraViewTest extends BaseTest { @Test public void testMeasure_matchParentBoth() { - mockPreviewSize(); + mockPreviewStreamSize(); // Respect parent/layout constraints on both dimensions. cameraView.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); @@ -331,7 +331,7 @@ public class CameraViewTest extends BaseTest { @Test public void testMeasure_wrapContentBoth() { - mockPreviewSize(); + mockPreviewStreamSize(); // Respect parent constraints, but fit aspect ratio. // Fit into a 160x160 parent so we espect final width to be 90. @@ -345,7 +345,7 @@ public class CameraViewTest extends BaseTest { @Test public void testMeasure_wrapContentSingle() { - mockPreviewSize(); + mockPreviewStreamSize(); // Respect MATCH_PARENT on height, change width to fit the aspect ratio. cameraView.setLayoutParams(new ViewGroup.LayoutParams(WRAP_CONTENT, MATCH_PARENT)); @@ -366,7 +366,7 @@ public class CameraViewTest extends BaseTest { @Test public void testMeasure_scrollableContainer() { - mockPreviewSize(); + mockPreviewStreamSize(); // Assume a vertical scroll view. It will pass UNSPECIFIED as height. // We respect MATCH_PARENT on width (160), and enlarge height to match the aspect ratio. @@ -559,10 +559,10 @@ public class CameraViewTest extends BaseTest { } @Test - public void testPreviewSizeSelector() { + public void testPreviewStreamSizeSelector() { SizeSelector source = SizeSelectors.minHeight(50); - cameraView.setPreviewSize(source); - SizeSelector result = mockController.getPreviewSizeSelector(); + cameraView.setPreviewStreamSize(source); + SizeSelector result = mockController.getPreviewStreamSizeSelector(); assertNotNull(result); assertEquals(result, source); } @@ -661,5 +661,17 @@ public class CameraViewTest extends BaseTest { //endregion + //region Snapshots + + @Test + public void testSetSnapshotMaxSize() { + cameraView.setSnapshotMaxWidth(500); + cameraView.setSnapshotMaxHeight(1000); + assertEquals(mockController.mSnapshotMaxWidth, 500); + assertEquals(mockController.mSnapshotMaxHeight, 1000); + } + + //endregion + // TODO: test permissions } diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java index 98760646..d7d47eb4 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java @@ -89,7 +89,7 @@ public class IntegrationTest extends BaseTest { } @After - public void tearDown() throws Exception { + public void tearDown() { camera.stopVideo(); camera.destroy(); WorkerHandler.destroy(); diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java index 7be7053d..1026b215 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java @@ -23,8 +23,8 @@ public class MockCameraController extends CameraController { mCameraOptions = options; } - void setMockPreviewSize(Size size) { - mPreviewSize = size; + void setMockPreviewStreamSize(Size size) { + mPreviewStreamSize = size; } void mockStarted(boolean started) { diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/TextureCameraPreviewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/TextureCameraPreviewTest.java index 0f604fc7..4e5e3251 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/TextureCameraPreviewTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/TextureCameraPreviewTest.java @@ -23,7 +23,7 @@ public class TextureCameraPreviewTest extends CameraPreviewTest { if (isHardwareAccelerated()) { super.ensureAvailable(); } else { - preview.dispatchOnOutputSurfaceAvailable( + preview.dispatchOnSurfaceAvailable( surfaceSize.getWidth(), surfaceSize.getHeight()); } @@ -34,7 +34,7 @@ public class TextureCameraPreviewTest extends CameraPreviewTest { super.ensureDestroyed(); if (!isHardwareAccelerated()) { // Ensure it is called. - preview.dispatchOnOutputSurfaceDestroyed(); + preview.dispatchOnSurfaceDestroyed(); } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java index 7c6f9b52..758bb895 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java @@ -72,7 +72,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera */ @Override public void onSurfaceAvailable() { - LOG.i("onSurfaceAvailable:", "Size is", mPreview.getOutputSurfaceSize()); + LOG.i("onSurfaceAvailable:", "Size is", getPreviewSurfaceSize(REF_VIEW)); schedule(null, false, new Runnable() { @Override public void run() { @@ -90,19 +90,19 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera */ @Override public void onSurfaceChanged() { - LOG.i("onSurfaceChanged, size is", mPreview.getOutputSurfaceSize()); + LOG.i("onSurfaceChanged, size is", getPreviewSurfaceSize(REF_VIEW)); schedule(null, true, new Runnable() { @Override public void run() { if (!mIsBound) return; // Compute a new camera preview size. - Size newSize = computePreviewSize(sizesFromList(mCamera.getParameters().getSupportedPreviewSizes())); - if (newSize.equals(mPreviewSize)) return; + Size newSize = computePreviewStreamSize(sizesFromList(mCamera.getParameters().getSupportedPreviewSizes())); + if (newSize.equals(mPreviewStreamSize)) return; // Apply. LOG.i("onSurfaceChanged:", "Computed a new preview size. Going on."); - mPreviewSize = newSize; + mPreviewStreamSize = newSize; stopPreview(); startPreview("onSurfaceChanged:"); } @@ -135,10 +135,12 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera LOG.i("bindToSurface:", "Started"); Object output = mPreview.getOutput(); try { - if (mPreview.getOutputClass() == SurfaceHolder.class) { + if (output instanceof SurfaceHolder) { mCamera.setPreviewDisplay((SurfaceHolder) output); - } else { + } else if (output instanceof SurfaceTexture) { mCamera.setPreviewTexture((SurfaceTexture) output); + } else { + throw new RuntimeException("Unknown CameraPreview output class."); } } catch (IOException e) { LOG.e("bindToSurface:", "Failed to bind.", e); @@ -146,20 +148,22 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera } mCaptureSize = computeCaptureSize(); - mPreviewSize = computePreviewSize(sizesFromList(mCamera.getParameters().getSupportedPreviewSizes())); + mPreviewStreamSize = computePreviewStreamSize(sizesFromList(mCamera.getParameters().getSupportedPreviewSizes())); mIsBound = true; } @WorkerThread private void unbindFromSurface() { mIsBound = false; - mPreviewSize = null; + mPreviewStreamSize = null; mCaptureSize = null; try { if (mPreview.getOutputClass() == SurfaceHolder.class) { mCamera.setPreviewDisplay(null); - } else { + } else if (mPreview.getOutputClass() == SurfaceTexture.class) { mCamera.setPreviewTexture(null); + } else { + throw new RuntimeException("Unknown CameraPreview output class."); } } catch (IOException e) { LOG.e("unbindFromSurface", "Could not release surface", e); @@ -172,16 +176,16 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera // To be called when the preview size is setup or changed. private void startPreview(String log) { - LOG.i(log, "Dispatching onCameraPreviewSizeChanged."); - mCameraCallbacks.onCameraPreviewSizeChanged(); + LOG.i(log, "Dispatching onCameraPreviewStreamSizeChanged."); + mCameraCallbacks.onCameraPreviewStreamSizeChanged(); - Size previewSize = getPreviewSize(REF_VIEW); + Size previewSize = getPreviewStreamSize(REF_VIEW); boolean wasFlipped = flip(REF_SENSOR, REF_VIEW); - mPreview.setInputStreamSize(previewSize.getWidth(), previewSize.getHeight(), wasFlipped); + mPreview.setStreamSize(previewSize.getWidth(), previewSize.getHeight(), wasFlipped); Camera.Parameters params = mCamera.getParameters(); mPreviewFormat = params.getPreviewFormat(); - params.setPreviewSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); // <- not allowed during preview + params.setPreviewSize(mPreviewStreamSize.getWidth(), mPreviewStreamSize.getHeight()); // <- not allowed during preview if (mMode == Mode.PICTURE) { params.setPictureSize(mCaptureSize.getWidth(), mCaptureSize.getHeight()); // <- allowed } else { @@ -196,7 +200,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera mCamera.setPreviewCallbackWithBuffer(null); // Release anything left mCamera.setPreviewCallbackWithBuffer(this); // Add ourselves - mFrameManager.allocate(ImageFormat.getBitsPerPixel(mPreviewFormat), mPreviewSize); + mFrameManager.allocate(ImageFormat.getBitsPerPixel(mPreviewFormat), mPreviewStreamSize); LOG.i(log, "Starting preview with startPreview()."); try { @@ -289,7 +293,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera } mCameraOptions = null; mCamera = null; - mPreviewSize = null; + mPreviewStreamSize = null; mCaptureSize = null; mIsBound = false; LOG.w("onStop:", "Clean up.", "Returning."); @@ -574,7 +578,10 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera }); } - + /** + * Just a note about the snapshot size - it is the PreviewStreamSize, cropped with the view ratio. + * @param viewAspectRatio the view aspect ratio + */ @Override void takePictureSnapshot(@NonNull final AspectRatio viewAspectRatio) { LOG.v("takePictureSnapshot: scheduling"); @@ -588,7 +595,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera result.location = mLocation; result.isSnapshot = true; result.facing = mFacing; - result.size = getPreviewSize(REF_OUTPUT); // Not the real size: it will be cropped to match the view ratio + result.size = getUncroppedSnapshotSize(REF_OUTPUT); // Not the real size: it will be cropped to match the view ratio result.rotation = offset(REF_SENSOR, REF_OUTPUT); // Actually it will be rotated and set to 0. AspectRatio outputRatio = flip(REF_OUTPUT, REF_VIEW) ? viewAspectRatio.inverse() : viewAspectRatio; // LOG.e("ROTBUG_pic", "aspectRatio (REF_VIEW):", viewAspectRatio); @@ -611,7 +618,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera Frame frame = mFrameManager.getFrame(data, System.currentTimeMillis(), offset(REF_SENSOR, REF_OUTPUT), - mPreviewSize, + mPreviewStreamSize, mPreviewFormat); mCameraCallbacks.dispatchFrame(frame); } @@ -693,6 +700,10 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera }); } + /** + * @param file the output file + * @param viewAspectRatio the view aspect ratio + */ @SuppressLint("NewApi") @Override void takeVideoSnapshot(@NonNull final File file, @NonNull final AspectRatio viewAspectRatio) { @@ -755,7 +766,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera // Based on this we will use VO for everything. See if we get issues about distortion // and maybe we can improve. The reason why this happen is beyond my understanding. - Size outputSize = getPreviewSize(REF_OUTPUT); + Size outputSize = getUncroppedSnapshotSize(REF_OUTPUT); AspectRatio outputRatio = flip(REF_OUTPUT, REF_VIEW) ? viewAspectRatio.inverse() : viewAspectRatio; Rect outputCrop = CropHelper.computeCrop(outputSize, outputRatio); outputSize = new Size(outputCrop.width(), outputCrop.height()); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java index cfedbb9c..4b28bb31 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java @@ -8,6 +8,7 @@ import android.os.Handler; import android.os.Looper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.annotation.WorkerThread; import java.io.File; @@ -49,10 +50,15 @@ abstract class CameraController implements protected float mExposureCorrectionValue; protected boolean mPlaySounds; - @Nullable private SizeSelector mPreviewSizeSelector; + @Nullable private SizeSelector mPreviewStreamSizeSelector; private SizeSelector mPictureSizeSelector; private SizeSelector mVideoSizeSelector; + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + int mSnapshotMaxWidth = Integer.MAX_VALUE; // in REF_VIEW for consistency with SizeSelectors + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + int mSnapshotMaxHeight = Integer.MAX_VALUE; // in REF_VIEW for consistency with SizeSelectors + protected int mCameraId; protected CameraOptions mCameraOptions; protected Mapper mMapper; @@ -64,7 +70,7 @@ abstract class CameraController implements protected int mVideoBitRate; protected int mAudioBitRate; protected Size mCaptureSize; - protected Size mPreviewSize; + protected Size mPreviewStreamSize; protected int mPreviewFormat; protected int mSensorOffset; @@ -279,8 +285,8 @@ abstract class CameraController implements mDeviceOrientation = deviceOrientation; } - final void setPreviewSizeSelector(@Nullable SizeSelector selector) { - mPreviewSizeSelector = selector; + final void setPreviewStreamSizeSelector(@Nullable SizeSelector selector) { + mPreviewStreamSizeSelector = selector; } final void setPictureSizeSelector(@NonNull SizeSelector selector) { @@ -311,6 +317,14 @@ abstract class CameraController implements mAudioBitRate = audioBitRate; } + final void setSnapshotMaxWidth(int maxWidth) { + mSnapshotMaxWidth = maxWidth; + } + + final void setSnapshotMaxHeight(int maxHeight) { + mSnapshotMaxHeight = maxHeight; + } + //endregion //region Abstract setters and APIs @@ -421,8 +435,8 @@ abstract class CameraController implements } @Nullable - /* for tests */ final SizeSelector getPreviewSizeSelector() { - return mPreviewSizeSelector; + /* for tests */ final SizeSelector getPreviewStreamSizeSelector() { + return mPreviewStreamSizeSelector; } @NonNull @@ -493,19 +507,71 @@ abstract class CameraController implements return offset(reference1, reference2) % 180 != 0; } + @Nullable final Size getPictureSize(@SuppressWarnings("SameParameterValue") int reference) { if (mCaptureSize == null || mMode == Mode.VIDEO) return null; return flip(REF_SENSOR, reference) ? mCaptureSize.flip() : mCaptureSize; } + @Nullable final Size getVideoSize(@SuppressWarnings("SameParameterValue") int reference) { if (mCaptureSize == null || mMode == Mode.PICTURE) return null; return flip(REF_SENSOR, reference) ? mCaptureSize.flip() : mCaptureSize; } - final Size getPreviewSize(int reference) { - if (mPreviewSize == null) return null; - return flip(REF_SENSOR, reference) ? mPreviewSize.flip() : mPreviewSize; + @Nullable + final Size getPreviewStreamSize(int reference) { + if (mPreviewStreamSize == null) return null; + return flip(REF_SENSOR, reference) ? mPreviewStreamSize.flip() : mPreviewStreamSize; + } + + @SuppressWarnings("SameParameterValue") + @Nullable + final Size getPreviewSurfaceSize(int reference) { + if (mPreview == null) return null; + return flip(REF_VIEW, reference) ? mPreview.getSurfaceSize().flip() : mPreview.getSurfaceSize(); + } + + /** + * Returns the snapshot size, but not cropped with the view dimensions, which + * is what we will do before creating the snapshot. However, cropping is done at various + * levels so we don't want to perform the op here. + * + * The base snapshot size is based on PreviewStreamSize (later cropped with view ratio). Why? + * One might be tempted to say that it is the SurfaceSize (which already matches the view ratio). + * + * The camera sensor will capture preview frames with PreviewStreamSize and that's it. Then they + * are hardware-scaled by the preview surface, but this does not affect the snapshot, as the + * snapshot recorder simply creates another surface. + * + * Done tests to ensure that this is true, by using + * 1. small SurfaceSize and biggest() PreviewStreamSize: output is not low quality + * 2. big SurfaceSize and smallest() PreviewStreamSize: output is low quality + * In both cases the result.size here was set to the biggest of the two. + * + * I could not find the same evidence for videos, but I would say that the same things should + * apply, despite the capturing mechanism being different. + */ + @Nullable + final Size getUncroppedSnapshotSize(int reference) { + Size baseSize = getPreviewStreamSize(reference); + if (baseSize == null) return null; + boolean flip = flip(reference, REF_VIEW); + int maxWidth = flip ? mSnapshotMaxHeight : mSnapshotMaxWidth; + int maxHeight = flip ? mSnapshotMaxWidth : mSnapshotMaxHeight; + float baseRatio = AspectRatio.of(baseSize).toFloat(); + float maxValuesRatio = AspectRatio.of(maxWidth, maxHeight).toFloat(); + if (maxValuesRatio >= baseRatio) { + // Height is the real constraint. + int outHeight = Math.min(baseSize.getHeight(), maxHeight); + int outWidth = (int) Math.floor((float) outHeight * baseRatio); + return new Size(outWidth, outHeight); + } else { + // Width is the real constraint. + int outWidth = Math.min(baseSize.getWidth(), maxWidth); + int outHeight = (int) Math.floor((float) outWidth / baseRatio); + return new Size(outWidth, outHeight); + } } @@ -550,7 +616,7 @@ abstract class CameraController implements @NonNull @SuppressWarnings("WeakerAccess") - protected final Size computePreviewSize(@NonNull List previewSizes) { + protected final Size computePreviewStreamSize(@NonNull List previewSizes) { // These sizes come in REF_SENSOR. Since there is an external selector involved, // we must convert all of them to REF_VIEW, then flip back when returning. boolean flip = flip(REF_SENSOR, REF_VIEW); @@ -559,12 +625,12 @@ abstract class CameraController implements sizes.add(flip ? size.flip() : size); } - // Create our own default selector, which will be used if the external mPreviewSizeSelector + // Create our own default selector, which will be used if the external mPreviewStreamSizeSelector // is null, or if it fails in finding a size. - Size targetMinSize = mPreview.getOutputSurfaceSize(); + Size targetMinSize = getPreviewSurfaceSize(REF_VIEW); AspectRatio targetRatio = AspectRatio.of(mCaptureSize.getWidth(), mCaptureSize.getHeight()); if (flip) targetRatio = targetRatio.inverse(); - LOG.i("size:", "computePreviewSize:", "targetRatio:", targetRatio, "targetMinSize:", targetMinSize); + LOG.i("size:", "computePreviewStreamSize:", "targetRatio:", targetRatio, "targetMinSize:", targetMinSize); SizeSelector matchRatio = SizeSelectors.and( // Match this aspect ratio and sort by biggest SizeSelectors.aspectRatio(targetRatio, 0), SizeSelectors.biggest()); @@ -582,14 +648,14 @@ abstract class CameraController implements // Apply the external selector with this as a fallback, // and return a size in REF_SENSOR reference. SizeSelector selector; - if (mPreviewSizeSelector != null) { - selector = SizeSelectors.or(mPreviewSizeSelector, matchAll); + if (mPreviewStreamSizeSelector != null) { + selector = SizeSelectors.or(mPreviewStreamSizeSelector, matchAll); } else { selector = matchAll; } Size result = selector.select(sizes).get(0); if (flip) result = result.flip(); - LOG.i("computePreviewSize:", "result:", result, "flip:", flip); + LOG.i("computePreviewStreamSize:", "result:", result, "flip:", flip); return result; } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java index 7f2f6213..deafbe50 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java @@ -311,7 +311,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - Size previewSize = mCameraController.getPreviewSize(CameraController.REF_VIEW); + Size previewSize = mCameraController.getPreviewStreamSize(CameraController.REF_VIEW); if (previewSize == null) { LOG.w("onMeasure:", "surface is not ready. Calling default behavior."); super.onMeasure(widthMeasureSpec, heightMeasureSpec); @@ -1073,15 +1073,15 @@ public class CameraView extends FrameLayout implements LifecycleObserver { * upscaling. If all you want is set an aspect ratio, use {@link #setPictureSize(SizeSelector)} * and {@link #setVideoSize(SizeSelector)}. * - * When size changes, the {@link CameraView} is remeasured so any WRAP_CONTENT dimension + * When stream size changes, the {@link CameraView} is remeasured so any WRAP_CONTENT dimension * is recomputed accordingly. * * See the {@link SizeSelectors} class for handy utilities for creating selectors. * * @param selector a size selector */ - public void setPreviewSize(@NonNull SizeSelector selector) { - mCameraController.setPreviewSizeSelector(selector); + public void setPreviewStreamSize(@NonNull SizeSelector selector) { + mCameraController.setPreviewStreamSizeSelector(selector); } @@ -1388,6 +1388,27 @@ public class CameraView extends FrameLayout implements LifecycleObserver { }); } + /** + * Sets the max width for snapshots taken with {@link #takePictureSnapshot()} or + * {@link #takeVideoSnapshot(File)}. If the snapshot width exceeds this value, the snapshot + * will be scaled down to match this constraint. + * + * @param maxWidth max width for snapshots + */ + public void setSnapshotMaxWidth(int maxWidth) { + mCameraController.setSnapshotMaxWidth(maxWidth); + } + + /** + * Sets the max height for snapshots taken with {@link #takePictureSnapshot()} or + * {@link #takeVideoSnapshot(File)}. If the snapshot height exceeds this value, the snapshot + * will be scaled down to match this constraint. + * + * @param maxHeight max height for snapshots + */ + public void setSnapshotMaxHeight(int maxHeight) { + mCameraController.setSnapshotMaxHeight(maxHeight); + } /** * Returns the size used for snapshots, or null if it hasn't been computed @@ -1402,7 +1423,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { // Get the preview size and crop according to the current view size. // It's better to do calculations in the REF_VIEW reference, and then flip if needed. - Size preview = mCameraController.getPreviewSize(CameraController.REF_VIEW); + Size preview = mCameraController.getUncroppedSnapshotSize(CameraController.REF_VIEW); AspectRatio viewRatio = AspectRatio.of(getWidth(), getHeight()); Rect crop = CropHelper.computeCrop(preview, viewRatio); Size cropSize = new Size(crop.width(), crop.height()); @@ -1597,7 +1618,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { interface CameraCallbacks extends OrientationHelper.Callback { void dispatchOnCameraOpened(CameraOptions options); void dispatchOnCameraClosed(); - void onCameraPreviewSizeChanged(); + void onCameraPreviewStreamSizeChanged(); void onShutter(boolean shouldPlaySound); void dispatchOnVideoTaken(VideoResult result); void dispatchOnPictureTaken(PictureResult result); @@ -1642,8 +1663,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver { } @Override - public void onCameraPreviewSizeChanged() { - mLogger.i("onCameraPreviewSizeChanged"); + public void onCameraPreviewStreamSizeChanged() { + mLogger.i("onCameraPreviewStreamSizeChanged"); // Camera preview size has changed. // Request a layout pass for onMeasure() to do its stuff. // Potentially this will change CameraView size, which changes Surface size, diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/SnapshotPictureRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/SnapshotPictureRecorder.java index 11fb97dc..444d5a70 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/SnapshotPictureRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/SnapshotPictureRecorder.java @@ -37,7 +37,7 @@ class SnapshotPictureRecorder extends PictureRecorder { mCamera = camera; mOutputRatio = outputRatio; mFormat = mController.mPreviewFormat; - mSensorPreviewSize = mController.mPreviewSize; + mSensorPreviewSize = mController.mPreviewStreamSize; } @Override @@ -189,7 +189,7 @@ class SnapshotPictureRecorder extends PictureRecorder { // It seems that the buffers are already cleared here, so we need to allocate again. camera.setPreviewCallbackWithBuffer(null); // Release anything left camera.setPreviewCallbackWithBuffer(mController); // Add ourselves - mController.mFrameManager.allocate(ImageFormat.getBitsPerPixel(mFormat), mController.mPreviewSize); + mController.mFrameManager.allocate(ImageFormat.getBitsPerPixel(mFormat), mController.mPreviewStreamSize); } }); } diff --git a/cameraview/src/main/res/values/attrs.xml b/cameraview/src/main/res/values/attrs.xml index f25628fe..5ea86273 100644 --- a/cameraview/src/main/res/values/attrs.xml +++ b/cameraview/src/main/res/values/attrs.xml @@ -25,6 +25,9 @@ + + + diff --git a/cameraview/src/main/views/com/otaliastudios/cameraview/CameraPreview.java b/cameraview/src/main/views/com/otaliastudios/cameraview/CameraPreview.java index 43bb0ec2..9469ef61 100644 --- a/cameraview/src/main/views/com/otaliastudios/cameraview/CameraPreview.java +++ b/cameraview/src/main/views/com/otaliastudios/cameraview/CameraPreview.java @@ -6,6 +6,13 @@ import androidx.annotation.Nullable; import android.view.View; import android.view.ViewGroup; +/** + * A CameraPreview takes in input stream from the {@link CameraController}, and streams it + * into an output surface that belongs to the view hierarchy. + * + * @param the type of view which hosts the content surface + * @param the type of output, either {@link android.view.SurfaceHolder} or {@link android.graphics.SurfaceTexture} + */ abstract class CameraPreview { protected final static CameraLogger LOG = CameraLogger.create(CameraPreview.class.getSimpleName()); @@ -60,8 +67,8 @@ abstract class CameraPreview { // As far as I can see, these are the actual preview dimensions, as set in CameraParameters. // This is called by the CameraImpl. // These must be alredy rotated, if needed, to be consistent with surface/view sizes. - void setInputStreamSize(int width, int height, boolean wasFlipped) { - LOG.i("setInputStreamSize:", "desiredW=", width, "desiredH=", height); + void setStreamSize(int width, int height, boolean wasFlipped) { + LOG.i("setStreamSize:", "desiredW=", width, "desiredH=", height); mInputStreamWidth = width; mInputStreamHeight = height; mInputFlipped = wasFlipped; @@ -71,12 +78,12 @@ abstract class CameraPreview { } @NonNull - final Size getInputStreamSize() { + final Size getStreamSize() { return new Size(mInputStreamWidth, mInputStreamHeight); } @NonNull - final Size getOutputSurfaceSize() { + final Size getSurfaceSize() { return new Size(mOutputSurfaceWidth, mOutputSurfaceHeight); } @@ -90,8 +97,8 @@ abstract class CameraPreview { @SuppressWarnings("WeakerAccess") - protected final void dispatchOnOutputSurfaceAvailable(int width, int height) { - LOG.i("dispatchOnOutputSurfaceAvailable:", "w=", width, "h=", height); + protected final void dispatchOnSurfaceAvailable(int width, int height) { + LOG.i("dispatchOnSurfaceAvailable:", "w=", width, "h=", height); mOutputSurfaceWidth = width; mOutputSurfaceHeight = height; if (mOutputSurfaceWidth > 0 && mOutputSurfaceHeight > 0) { @@ -104,8 +111,8 @@ abstract class CameraPreview { // As far as I can see, these are the view/surface dimensions. // This is called by subclasses. @SuppressWarnings("WeakerAccess") - protected final void dispatchOnOutputSurfaceSizeChanged(int width, int height) { - LOG.i("dispatchOnOutputSurfaceSizeChanged:", "w=", width, "h=", height); + protected final void dispatchOnSurfaceSizeChanged(int width, int height) { + LOG.i("dispatchOnSurfaceSizeChanged:", "w=", width, "h=", height); if (width != mOutputSurfaceWidth || height != mOutputSurfaceHeight) { mOutputSurfaceWidth = width; mOutputSurfaceHeight = height; @@ -117,7 +124,7 @@ abstract class CameraPreview { } @SuppressWarnings("WeakerAccess") - protected final void dispatchOnOutputSurfaceDestroyed() { + protected final void dispatchOnSurfaceDestroyed() { mOutputSurfaceWidth = 0; mOutputSurfaceHeight = 0; mSurfaceCallback.onSurfaceDestroyed(); diff --git a/cameraview/src/main/views/com/otaliastudios/cameraview/GlCameraPreview.java b/cameraview/src/main/views/com/otaliastudios/cameraview/GlCameraPreview.java index 44ece2a9..d29463f3 100644 --- a/cameraview/src/main/views/com/otaliastudios/cameraview/GlCameraPreview.java +++ b/cameraview/src/main/views/com/otaliastudios/cameraview/GlCameraPreview.java @@ -85,7 +85,7 @@ class GlCameraPreview extends CameraPreview imple @Override public void surfaceDestroyed(SurfaceHolder holder) { - dispatchOnOutputSurfaceDestroyed(); + dispatchOnSurfaceDestroyed(); mDispatched = false; } }); @@ -159,7 +159,7 @@ class GlCameraPreview extends CameraPreview imple @Override public void onSurfaceChanged(GL10 gl, final int width, final int height) { if (!mDispatched) { - dispatchOnOutputSurfaceAvailable(width, height); + dispatchOnSurfaceAvailable(width, height); mDispatched = true; } else if (mOutputSurfaceWidth == width && mOutputSurfaceHeight == height) { // I was experimenting and this was happening. @@ -168,13 +168,13 @@ class GlCameraPreview extends CameraPreview imple // With other CameraPreview implementation we could just dispatch the 'size changed' event // to the controller and everything would go straight. In case of GL, apparently we have to // force recreate the EGLContext by calling onPause and onResume in the UI thread. - dispatchOnOutputSurfaceDestroyed(); + dispatchOnSurfaceDestroyed(); getView().post(new Runnable() { @Override public void run() { getView().onPause(); getView().onResume(); - dispatchOnOutputSurfaceAvailable(width, height); + dispatchOnSurfaceAvailable(width, height); } }); } diff --git a/cameraview/src/main/views/com/otaliastudios/cameraview/SurfaceCameraPreview.java b/cameraview/src/main/views/com/otaliastudios/cameraview/SurfaceCameraPreview.java index 7defa65b..ac63767d 100644 --- a/cameraview/src/main/views/com/otaliastudios/cameraview/SurfaceCameraPreview.java +++ b/cameraview/src/main/views/com/otaliastudios/cameraview/SurfaceCameraPreview.java @@ -44,17 +44,17 @@ class SurfaceCameraPreview extends CameraPreview { public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { LOG.i("callback:", "surfaceChanged", "w:", width, "h:", height, "dispatched:", mDispatched); if (!mDispatched) { - dispatchOnOutputSurfaceAvailable(width, height); + dispatchOnSurfaceAvailable(width, height); mDispatched = true; } else { - dispatchOnOutputSurfaceSizeChanged(width, height); + dispatchOnSurfaceSizeChanged(width, height); } } @Override public void surfaceDestroyed(SurfaceHolder holder) { LOG.i("callback:", "surfaceDestroyed"); - dispatchOnOutputSurfaceDestroyed(); + dispatchOnSurfaceDestroyed(); mDispatched = false; } }); diff --git a/cameraview/src/main/views/com/otaliastudios/cameraview/TextureCameraPreview.java b/cameraview/src/main/views/com/otaliastudios/cameraview/TextureCameraPreview.java index c5664013..05b641fc 100644 --- a/cameraview/src/main/views/com/otaliastudios/cameraview/TextureCameraPreview.java +++ b/cameraview/src/main/views/com/otaliastudios/cameraview/TextureCameraPreview.java @@ -28,17 +28,17 @@ class TextureCameraPreview extends CameraPreview { @Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { - dispatchOnOutputSurfaceAvailable(width, height); + dispatchOnSurfaceAvailable(width, height); } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { - dispatchOnOutputSurfaceSizeChanged(width, height); + dispatchOnSurfaceSizeChanged(width, height); } @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { - dispatchOnOutputSurfaceDestroyed(); + dispatchOnSurfaceDestroyed(); return true; } @@ -70,8 +70,8 @@ class TextureCameraPreview extends CameraPreview { @TargetApi(15) @Override - void setInputStreamSize(int width, int height, boolean wasFlipped) { - super.setInputStreamSize(width, height, wasFlipped); + void setStreamSize(int width, int height, boolean wasFlipped) { + super.setStreamSize(width, height, wasFlipped); if (getView().getSurfaceTexture() != null) { getView().getSurfaceTexture().setDefaultBufferSize(width, height); } diff --git a/demo/src/main/java/com/otaliastudios/cameraview/demo/PicturePreviewActivity.java b/demo/src/main/java/com/otaliastudios/cameraview/demo/PicturePreviewActivity.java index 2805c027..97fba873 100644 --- a/demo/src/main/java/com/otaliastudios/cameraview/demo/PicturePreviewActivity.java +++ b/demo/src/main/java/com/otaliastudios/cameraview/demo/PicturePreviewActivity.java @@ -2,8 +2,11 @@ package com.otaliastudios.cameraview.demo; import android.app.Activity; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.os.Bundle; import androidx.annotation.Nullable; + +import android.util.Log; import android.widget.ImageView; import com.otaliastudios.cameraview.AspectRatio; @@ -45,6 +48,18 @@ public class PicturePreviewActivity extends Activity { imageView.setImageBitmap(bitmap); } }); + + if (result.isSnapshot()) { + // Log the real size for debugging reason. + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeByteArray(result.getData(), 0, result.getData().length, options); + if (result.getRotation() % 180 != 0) { + Log.e("PicturePreview", "The picture full size is " + result.getSize().getHeight() + "x" + result.getSize().getWidth()); + } else { + Log.e("PicturePreview", "The picture full size is " + result.getSize().getWidth() + "x" + result.getSize().getHeight()); + } + } } @Override diff --git a/demo/src/main/java/com/otaliastudios/cameraview/demo/VideoPreviewActivity.java b/demo/src/main/java/com/otaliastudios/cameraview/demo/VideoPreviewActivity.java index cee2d78f..d7921671 100644 --- a/demo/src/main/java/com/otaliastudios/cameraview/demo/VideoPreviewActivity.java +++ b/demo/src/main/java/com/otaliastudios/cameraview/demo/VideoPreviewActivity.java @@ -5,6 +5,8 @@ import android.media.MediaPlayer; import android.net.Uri; import android.os.Bundle; import androidx.annotation.Nullable; + +import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.MediaController; @@ -73,6 +75,11 @@ public class VideoPreviewActivity extends Activity { lp.height = (int) (viewWidth * (videoHeight / videoWidth)); videoView.setLayoutParams(lp); playVideo(); + + if (result.isSnapshot()) { + // Log the real size for debugging reason. + Log.e("VideoPreview", "The video full size is " + videoWidth + "x" + videoHeight); + } } }); } diff --git a/docs/_posts/2018-12-20-capture-size.md b/docs/_posts/2018-12-20-capture-size.md index 5e34b529..b4a76b34 100644 --- a/docs/_posts/2018-12-20-capture-size.md +++ b/docs/_posts/2018-12-20-capture-size.md @@ -9,9 +9,11 @@ date: 2018-12-20 22:07:22 disqus: 1 --- -If you are planning to use the snapshot APIs, the size of the media output is that of the preview, -accounting for any cropping made when [measuring the view](preview-size.html). -If you are planning to use the standard APIs for capturing, then what follows applies. +If you are planning to use the snapshot APIs, the size of the media output is that of the preview stream, +accounting for any cropping made when [measuring the view](preview-size.html) and other constraints. +Please read the [Snapshot Size](snapshot-size.html) document. + +If you are planning to use the standard APIs, then what follows applies. ### Controlling Size diff --git a/docs/_posts/2018-12-20-capturing-media.md b/docs/_posts/2018-12-20-capturing-media.md index 1538f7ae..2138b383 100644 --- a/docs/_posts/2018-12-20-capturing-media.md +++ b/docs/_posts/2018-12-20-capturing-media.md @@ -46,8 +46,8 @@ resulting snapshots are square as well, no matter what the sensor available size |------|-----|-------|--------------------------|------------------------|---------|-----------| |`takePicture()`|Pictures|Standard|`yes`|`no`|`no`|That of `setPictureSize`| |`takeVideo(File)`|Videos|Standard|`no`|`yes`|`no`|That of `setVideoSize`| -|`takePictureSnapshot()`|Pictures|Snapshot|`yes`|`yes`|`yes`|That of the view| -|`takeVideoSnapshot(File)`|Videos|Snapshot|`yes`|`yes`|`yes`|That of the view| +|`takePictureSnapshot()`|Pictures|Snapshot|`yes`|`yes`|`yes`|That of the preview stream, [or less](snapshot-size.html)| +|`takeVideoSnapshot(File)`|Videos|Snapshot|`yes`|`yes`|`yes`|That of the preview stream, [or less](snapshot-size.html)| Please note that the video snaphot features requires: diff --git a/docs/_posts/2018-12-20-debugging.md b/docs/_posts/2018-12-20-debugging.md index dc7e1ab4..2e2d302d 100644 --- a/docs/_posts/2018-12-20-debugging.md +++ b/docs/_posts/2018-12-20-debugging.md @@ -2,7 +2,7 @@ layout: page title: "Debugging" category: docs -order: 10 +order: 12 date: 2018-12-20 20:02:38 disqus: 1 --- diff --git a/docs/_posts/2018-12-20-error-handling.md b/docs/_posts/2018-12-20-error-handling.md index 72c98cb1..1a24041e 100644 --- a/docs/_posts/2018-12-20-error-handling.md +++ b/docs/_posts/2018-12-20-error-handling.md @@ -2,7 +2,7 @@ layout: page title: "Error Handling" category: docs -order: 9 +order: 11 date: 2018-12-20 20:02:31 disqus: 1 --- diff --git a/docs/_posts/2018-12-20-more-features.md b/docs/_posts/2018-12-20-more-features.md index 984dd940..a971f84a 100644 --- a/docs/_posts/2018-12-20-more-features.md +++ b/docs/_posts/2018-12-20-more-features.md @@ -4,7 +4,7 @@ title: "More features" subtitle: "Undocumented features & more" description: "Undocumented features & more" category: docs -order: 11 +order: 13 date: 2018-12-20 20:41:20 disqus: 1 --- diff --git a/docs/_posts/2018-12-20-preview-size.md b/docs/_posts/2018-12-20-preview-size.md index 4573f591..5914b5c6 100644 --- a/docs/_posts/2018-12-20-preview-size.md +++ b/docs/_posts/2018-12-20-preview-size.md @@ -55,13 +55,13 @@ This means that part of the preview might be hidden, and the output might contai that were not visible during the capture, **unless it is taken as a snapshot, since snapshots account for cropping**. -## Advanced feature: Preview Size Selection +## Advanced feature: Preview Stream Size Selection **Only do this if you know what you are doing. This is typically not needed - prefer picture/video size selectors, -as they will drive the preview size selection and, eventually, the view size. If what you want is just +as they will drive the preview stream size selection and, eventually, the view size. If what you want is just choose an aspect ratio, do so with [Capture Size](capture-size.html) selection.** -As said, `WRAP_CONTENT` adapts the view boundaries to the preview size. The preview size must be determined +As said, `WRAP_CONTENT` adapts the view boundaries to the preview stream size. The preview stream size must be determined based on the sizes that the device sensor & hardware actually support. This operation is done automatically by the engine. The default selector will do the following: @@ -70,10 +70,10 @@ by the engine. The default selector will do the following: - Try to match both, or just one, or fallback to the biggest available size There are not so many reason why you would replace this, other than control the frame processor size -or, indirectly, the snapshot size. You can, however, hook into the process using `setPreviewSize(SizeSelector)`: +or, indirectly, the snapshot size. You can, however, hook into the process using `setPreviewStreamSize(SizeSelector)`: ```java -cameraView.setPreviewSize(new SizeSelector() { +cameraView.setPreviewStreamSize(new SizeSelector() { @Override public List select(List source) { // Receives a list of available sizes. @@ -82,7 +82,7 @@ cameraView.setPreviewSize(new SizeSelector() { }); ``` -After the preview size is determined, if it has changed since list time, the `CameraView` will receive +After the preview stream size is determined, if it has changed since list time, the `CameraView` will receive another call to `onMeasure` so the `WRAP_CONTENT` magic can take place. To understand how SizeSelectors work and the available utilities, please read the [Capture Size](capture-size.html) document. diff --git a/docs/_posts/2018-12-20-runtime-permissions.md b/docs/_posts/2018-12-20-runtime-permissions.md index 0cbcd03c..5027a65f 100644 --- a/docs/_posts/2018-12-20-runtime-permissions.md +++ b/docs/_posts/2018-12-20-runtime-permissions.md @@ -4,7 +4,7 @@ title: "Runtime Permissions" subtitle: "Permissions and Manifest setup" description: "Permissions and Manifest setup" category: docs -order: 8 +order: 10 date: 2018-12-20 20:03:03 disqus: 1 --- diff --git a/docs/_posts/2019-02-24-snapshot-size.md b/docs/_posts/2019-02-24-snapshot-size.md new file mode 100644 index 00000000..8703a232 --- /dev/null +++ b/docs/_posts/2019-02-24-snapshot-size.md @@ -0,0 +1,58 @@ +--- +layout: page +title: "Snapshot Size" +subtitle: "Sizing the snapshots output" +description: "Sizing the snapshots output" +category: docs +order: 9 +date: 2019-02-24 17:36:39 +disqus: 1 +--- + +Snapshots are captured from the preview stream instead of using a separate capture channel. +They are extremely fast, small in size, and give you a low-quality output that can be easily +uploaded or processed. + +The snapshot size is based on the size of the preview stream, which is described in the [Preview Size](preview-size.html) document. +Although the preview stream size is customizable, note that this is considered an advanced feature, +as the best preview stream size selector already does a good job for the vast majority of use cases. + +When taking snapshots, the preview stream size is then changed to match some constraints. + +### Matching the preview ratio + +Snapshots will automatically be cropped to match the preview aspect ratio. This means that if your +preview is square, you can finally take a square picture or video, regardless of the available sensor sizes. + +Take a look at the [Preview Size](preview-size.html) document to learn about preview sizing. + +### Other constraints + +You can refine the size further by applying `maxWidth` and a `maxHeight` constraints: + +```java +cameraView.setSnapshotMaxWidth(500); +cameraView.setSnapshotMaxHeight(500); +``` + +These values apply to both picture and video snapshots. If the snapshot dimensions exceed these values +(both default `Integer.MAX_VALUE`), the snapshot will be scaled down to match the constraints. + +This is very useful as it decouples the snapshot size logic from the preview. By using small constraints, +you can have a pleasant, good looking preview stream, while still capturing fast, low-res snapshots +with no issues. + +### XML Attributes + +```xml + +``` + +### Related APIs + +|Method|Description| +|------|-----------| +|`setSnapshotMaxWidth(int)`|Sets the max width for snapshots. If out of bounds, the output will be scaled down.| +|`setSnapshotMaxHeight(int)`|Sets the max height for snapshots. If out of bounds, the output will be scaled down.| \ No newline at end of file