New snapshot max size APIs (#393)

* New getSurfaceSize internal method

* Rename setPreviewSize and all internals to setPreviewStreamSize

* Rename getSurfaceSize to getPreviewSurfaceSize

* New snapshotMaxWidth and snapshotMaxHeight APIs

* Add docs

* Improve rescaling logic

* Add tests
pull/402/head
Mattia Iavarone 6 years ago committed by GitHub
parent d462b83048
commit 5e5af877e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 22
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraPreviewTest.java
  2. 32
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
  3. 2
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java
  4. 4
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java
  5. 4
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/TextureCameraPreviewTest.java
  6. 53
      cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java
  7. 98
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java
  8. 37
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  9. 4
      cameraview/src/main/java/com/otaliastudios/cameraview/SnapshotPictureRecorder.java
  10. 3
      cameraview/src/main/res/values/attrs.xml
  11. 25
      cameraview/src/main/views/com/otaliastudios/cameraview/CameraPreview.java
  12. 8
      cameraview/src/main/views/com/otaliastudios/cameraview/GlCameraPreview.java
  13. 6
      cameraview/src/main/views/com/otaliastudios/cameraview/SurfaceCameraPreview.java
  14. 10
      cameraview/src/main/views/com/otaliastudios/cameraview/TextureCameraPreview.java
  15. 15
      demo/src/main/java/com/otaliastudios/cameraview/demo/PicturePreviewActivity.java
  16. 7
      demo/src/main/java/com/otaliastudios/cameraview/demo/VideoPreviewActivity.java
  17. 8
      docs/_posts/2018-12-20-capture-size.md
  18. 4
      docs/_posts/2018-12-20-capturing-media.md
  19. 2
      docs/_posts/2018-12-20-debugging.md
  20. 2
      docs/_posts/2018-12-20-error-handling.md
  21. 2
      docs/_posts/2018-12-20-more-features.md
  22. 12
      docs/_posts/2018-12-20-preview-size.md
  23. 2
      docs/_posts/2018-12-20-runtime-permissions.md
  24. 58
      docs/_posts/2019-02-24-snapshot-size.md

@ -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();

@ -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
}

@ -89,7 +89,7 @@ public class IntegrationTest extends BaseTest {
}
@After
public void tearDown() throws Exception {
public void tearDown() {
camera.stopVideo();
camera.destroy();
WorkerHandler.destroy();

@ -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) {

@ -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();
}
}

@ -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());

@ -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<Size> previewSizes) {
protected final Size computePreviewStreamSize(@NonNull List<Size> 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;
}

@ -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,

@ -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);
}
});
}

@ -25,6 +25,9 @@
<attr name="cameraVideoBitRate" format="integer|reference" />
<attr name="cameraAudioBitRate" format="integer|reference" />
<attr name="cameraSnapshotMaxWidth" format="integer|reference" />
<attr name="cameraSnapshotMaxHeight" format="integer|reference" />
<attr name="cameraGestureTap" format="enum">
<enum name="none" value="0" />
<enum name="focus" value="1" />

@ -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 <T> the type of view which hosts the content surface
* @param <Output> the type of output, either {@link android.view.SurfaceHolder} or {@link android.graphics.SurfaceTexture}
*/
abstract class CameraPreview<T extends View, Output> {
protected final static CameraLogger LOG = CameraLogger.create(CameraPreview.class.getSimpleName());
@ -60,8 +67,8 @@ abstract class CameraPreview<T extends View, Output> {
// 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<T extends View, Output> {
}
@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<T extends View, Output> {
@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<T extends View, Output> {
// 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<T extends View, Output> {
}
@SuppressWarnings("WeakerAccess")
protected final void dispatchOnOutputSurfaceDestroyed() {
protected final void dispatchOnSurfaceDestroyed() {
mOutputSurfaceWidth = 0;
mOutputSurfaceHeight = 0;
mSurfaceCallback.onSurfaceDestroyed();

@ -85,7 +85,7 @@ class GlCameraPreview extends CameraPreview<GLSurfaceView, SurfaceTexture> imple
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
dispatchOnOutputSurfaceDestroyed();
dispatchOnSurfaceDestroyed();
mDispatched = false;
}
});
@ -159,7 +159,7 @@ class GlCameraPreview extends CameraPreview<GLSurfaceView, SurfaceTexture> 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<GLSurfaceView, SurfaceTexture> 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);
}
});
}

@ -44,17 +44,17 @@ class SurfaceCameraPreview extends CameraPreview<SurfaceView, SurfaceHolder> {
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;
}
});

@ -28,17 +28,17 @@ class TextureCameraPreview extends CameraPreview<TextureView, SurfaceTexture> {
@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<TextureView, SurfaceTexture> {
@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);
}

@ -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

@ -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);
}
}
});
}

@ -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

@ -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:

@ -2,7 +2,7 @@
layout: page
title: "Debugging"
category: docs
order: 10
order: 12
date: 2018-12-20 20:02:38
disqus: 1
---

@ -2,7 +2,7 @@
layout: page
title: "Error Handling"
category: docs
order: 9
order: 11
date: 2018-12-20 20:02:31
disqus: 1
---

@ -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
---

@ -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<Size> select(List<Size> 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.

@ -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
---

@ -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
<com.otaliastudios.cameraview.CameraView
app:cameraSnapshotMaxWidth="500"
app:cameraSnapshotMaxHeight="500"/>
```
### 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.|
Loading…
Cancel
Save