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 @Test
public void testDesiredSize() { public void testDesiredSize() {
preview.setInputStreamSize(160, 90, false); preview.setStreamSize(160, 90, false);
assertEquals(160, preview.getInputStreamSize().getWidth()); assertEquals(160, preview.getStreamSize().getWidth());
assertEquals(90, preview.getInputStreamSize().getHeight()); assertEquals(90, preview.getStreamSize().getHeight());
} }
@Test @Test
public void testSurfaceAvailable() { public void testSurfaceAvailable() {
ensureAvailable(); ensureAvailable();
verify(callback, times(1)).onSurfaceAvailable(); verify(callback, times(1)).onSurfaceAvailable();
assertEquals(surfaceSize.getWidth(), preview.getOutputSurfaceSize().getWidth()); assertEquals(surfaceSize.getWidth(), preview.getSurfaceSize().getWidth());
assertEquals(surfaceSize.getHeight(), preview.getOutputSurfaceSize().getHeight()); assertEquals(surfaceSize.getHeight(), preview.getSurfaceSize().getHeight());
} }
@Test @Test
@ -121,8 +121,8 @@ public abstract class CameraPreviewTest extends BaseTest {
ensureDestroyed(); ensureDestroyed();
// This might be called twice in Texture because it overrides ensureDestroyed method // This might be called twice in Texture because it overrides ensureDestroyed method
verify(callback, atLeastOnce()).onSurfaceDestroyed(); verify(callback, atLeastOnce()).onSurfaceDestroyed();
assertEquals(0, preview.getOutputSurfaceSize().getWidth()); assertEquals(0, preview.getSurfaceSize().getWidth());
assertEquals(0, preview.getOutputSurfaceSize().getHeight()); assertEquals(0, preview.getSurfaceSize().getHeight());
} }
@Test @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. // Since desired is 'desired', let's fake a new view size that is consistent with it.
// Ensure crop is not happening anymore. // Ensure crop is not happening anymore.
preview.mCropTask.listen(); preview.mCropTask.listen();
preview.dispatchOnOutputSurfaceSizeChanged((int) (50f * desired), 50); // Wait... preview.dispatchOnSurfaceSizeChanged((int) (50f * desired), 50); // Wait...
preview.mCropTask.await(); preview.mCropTask.await();
assertEquals(desired, getViewAspectRatioWithScale(), 0.01f); assertEquals(desired, getViewAspectRatioWithScale(), 0.01f);
assertFalse(preview.isCropping()); assertFalse(preview.isCropping());
@ -154,19 +154,19 @@ public abstract class CameraPreviewTest extends BaseTest {
private void setDesiredAspectRatio(float desiredAspectRatio) { private void setDesiredAspectRatio(float desiredAspectRatio) {
preview.mCropTask.listen(); preview.mCropTask.listen();
preview.setInputStreamSize((int) (10f * desiredAspectRatio), 10, false); // Wait... preview.setStreamSize((int) (10f * desiredAspectRatio), 10, false); // Wait...
preview.mCropTask.await(); preview.mCropTask.await();
assertEquals(desiredAspectRatio, getViewAspectRatioWithScale(), 0.01f); assertEquals(desiredAspectRatio, getViewAspectRatioWithScale(), 0.01f);
} }
private float getViewAspectRatio() { private float getViewAspectRatio() {
Size size = preview.getOutputSurfaceSize(); Size size = preview.getSurfaceSize();
return AspectRatio.of(size.getWidth(), size.getHeight()).toFloat(); return AspectRatio.of(size.getWidth(), size.getHeight()).toFloat();
} }
private float getViewAspectRatioWithScale() { private float getViewAspectRatioWithScale() {
Size size = preview.getOutputSurfaceSize(); Size size = preview.getSurfaceSize();
int newWidth = (int) (((float) size.getWidth()) * getCropScaleX()); int newWidth = (int) (((float) size.getWidth()) * getCropScaleX());
int newHeight = (int) (((float) size.getHeight()) * getCropScaleY()); int newHeight = (int) (((float) size.getHeight()) * getCropScaleY());
return AspectRatio.of(newWidth, newHeight).toFloat(); return AspectRatio.of(newWidth, newHeight).toFloat();

@ -288,14 +288,14 @@ public class CameraViewTest extends BaseTest {
//region testMeasure //region testMeasure
private void mockPreviewSize() { private void mockPreviewStreamSize() {
Size size = new Size(900, 1600); Size size = new Size(900, 1600);
mockController.setMockPreviewSize(size); mockController.setMockPreviewStreamSize(size);
} }
@Test @Test
public void testMeasure_early() { public void testMeasure_early() {
mockController.setMockPreviewSize(null); mockController.setMockPreviewStreamSize(null);
cameraView.measure( cameraView.measure(
makeMeasureSpec(500, EXACTLY), makeMeasureSpec(500, EXACTLY),
makeMeasureSpec(500, EXACTLY)); makeMeasureSpec(500, EXACTLY));
@ -305,7 +305,7 @@ public class CameraViewTest extends BaseTest {
@Test @Test
public void testMeasure_matchParentBoth() { public void testMeasure_matchParentBoth() {
mockPreviewSize(); mockPreviewStreamSize();
// Respect parent/layout constraints on both dimensions. // Respect parent/layout constraints on both dimensions.
cameraView.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); cameraView.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
@ -331,7 +331,7 @@ public class CameraViewTest extends BaseTest {
@Test @Test
public void testMeasure_wrapContentBoth() { public void testMeasure_wrapContentBoth() {
mockPreviewSize(); mockPreviewStreamSize();
// Respect parent constraints, but fit aspect ratio. // Respect parent constraints, but fit aspect ratio.
// Fit into a 160x160 parent so we espect final width to be 90. // Fit into a 160x160 parent so we espect final width to be 90.
@ -345,7 +345,7 @@ public class CameraViewTest extends BaseTest {
@Test @Test
public void testMeasure_wrapContentSingle() { public void testMeasure_wrapContentSingle() {
mockPreviewSize(); mockPreviewStreamSize();
// Respect MATCH_PARENT on height, change width to fit the aspect ratio. // Respect MATCH_PARENT on height, change width to fit the aspect ratio.
cameraView.setLayoutParams(new ViewGroup.LayoutParams(WRAP_CONTENT, MATCH_PARENT)); cameraView.setLayoutParams(new ViewGroup.LayoutParams(WRAP_CONTENT, MATCH_PARENT));
@ -366,7 +366,7 @@ public class CameraViewTest extends BaseTest {
@Test @Test
public void testMeasure_scrollableContainer() { public void testMeasure_scrollableContainer() {
mockPreviewSize(); mockPreviewStreamSize();
// Assume a vertical scroll view. It will pass UNSPECIFIED as height. // 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. // 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 @Test
public void testPreviewSizeSelector() { public void testPreviewStreamSizeSelector() {
SizeSelector source = SizeSelectors.minHeight(50); SizeSelector source = SizeSelectors.minHeight(50);
cameraView.setPreviewSize(source); cameraView.setPreviewStreamSize(source);
SizeSelector result = mockController.getPreviewSizeSelector(); SizeSelector result = mockController.getPreviewStreamSizeSelector();
assertNotNull(result); assertNotNull(result);
assertEquals(result, source); assertEquals(result, source);
} }
@ -661,5 +661,17 @@ public class CameraViewTest extends BaseTest {
//endregion //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 // TODO: test permissions
} }

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

@ -23,8 +23,8 @@ public class MockCameraController extends CameraController {
mCameraOptions = options; mCameraOptions = options;
} }
void setMockPreviewSize(Size size) { void setMockPreviewStreamSize(Size size) {
mPreviewSize = size; mPreviewStreamSize = size;
} }
void mockStarted(boolean started) { void mockStarted(boolean started) {

@ -23,7 +23,7 @@ public class TextureCameraPreviewTest extends CameraPreviewTest {
if (isHardwareAccelerated()) { if (isHardwareAccelerated()) {
super.ensureAvailable(); super.ensureAvailable();
} else { } else {
preview.dispatchOnOutputSurfaceAvailable( preview.dispatchOnSurfaceAvailable(
surfaceSize.getWidth(), surfaceSize.getWidth(),
surfaceSize.getHeight()); surfaceSize.getHeight());
} }
@ -34,7 +34,7 @@ public class TextureCameraPreviewTest extends CameraPreviewTest {
super.ensureDestroyed(); super.ensureDestroyed();
if (!isHardwareAccelerated()) { if (!isHardwareAccelerated()) {
// Ensure it is called. // Ensure it is called.
preview.dispatchOnOutputSurfaceDestroyed(); preview.dispatchOnSurfaceDestroyed();
} }
} }

@ -72,7 +72,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
*/ */
@Override @Override
public void onSurfaceAvailable() { public void onSurfaceAvailable() {
LOG.i("onSurfaceAvailable:", "Size is", mPreview.getOutputSurfaceSize()); LOG.i("onSurfaceAvailable:", "Size is", getPreviewSurfaceSize(REF_VIEW));
schedule(null, false, new Runnable() { schedule(null, false, new Runnable() {
@Override @Override
public void run() { public void run() {
@ -90,19 +90,19 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
*/ */
@Override @Override
public void onSurfaceChanged() { public void onSurfaceChanged() {
LOG.i("onSurfaceChanged, size is", mPreview.getOutputSurfaceSize()); LOG.i("onSurfaceChanged, size is", getPreviewSurfaceSize(REF_VIEW));
schedule(null, true, new Runnable() { schedule(null, true, new Runnable() {
@Override @Override
public void run() { public void run() {
if (!mIsBound) return; if (!mIsBound) return;
// Compute a new camera preview size. // Compute a new camera preview size.
Size newSize = computePreviewSize(sizesFromList(mCamera.getParameters().getSupportedPreviewSizes())); Size newSize = computePreviewStreamSize(sizesFromList(mCamera.getParameters().getSupportedPreviewSizes()));
if (newSize.equals(mPreviewSize)) return; if (newSize.equals(mPreviewStreamSize)) return;
// Apply. // Apply.
LOG.i("onSurfaceChanged:", "Computed a new preview size. Going on."); LOG.i("onSurfaceChanged:", "Computed a new preview size. Going on.");
mPreviewSize = newSize; mPreviewStreamSize = newSize;
stopPreview(); stopPreview();
startPreview("onSurfaceChanged:"); startPreview("onSurfaceChanged:");
} }
@ -135,10 +135,12 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
LOG.i("bindToSurface:", "Started"); LOG.i("bindToSurface:", "Started");
Object output = mPreview.getOutput(); Object output = mPreview.getOutput();
try { try {
if (mPreview.getOutputClass() == SurfaceHolder.class) { if (output instanceof SurfaceHolder) {
mCamera.setPreviewDisplay((SurfaceHolder) output); mCamera.setPreviewDisplay((SurfaceHolder) output);
} else { } else if (output instanceof SurfaceTexture) {
mCamera.setPreviewTexture((SurfaceTexture) output); mCamera.setPreviewTexture((SurfaceTexture) output);
} else {
throw new RuntimeException("Unknown CameraPreview output class.");
} }
} catch (IOException e) { } catch (IOException e) {
LOG.e("bindToSurface:", "Failed to bind.", e); LOG.e("bindToSurface:", "Failed to bind.", e);
@ -146,20 +148,22 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
} }
mCaptureSize = computeCaptureSize(); mCaptureSize = computeCaptureSize();
mPreviewSize = computePreviewSize(sizesFromList(mCamera.getParameters().getSupportedPreviewSizes())); mPreviewStreamSize = computePreviewStreamSize(sizesFromList(mCamera.getParameters().getSupportedPreviewSizes()));
mIsBound = true; mIsBound = true;
} }
@WorkerThread @WorkerThread
private void unbindFromSurface() { private void unbindFromSurface() {
mIsBound = false; mIsBound = false;
mPreviewSize = null; mPreviewStreamSize = null;
mCaptureSize = null; mCaptureSize = null;
try { try {
if (mPreview.getOutputClass() == SurfaceHolder.class) { if (mPreview.getOutputClass() == SurfaceHolder.class) {
mCamera.setPreviewDisplay(null); mCamera.setPreviewDisplay(null);
} else { } else if (mPreview.getOutputClass() == SurfaceTexture.class) {
mCamera.setPreviewTexture(null); mCamera.setPreviewTexture(null);
} else {
throw new RuntimeException("Unknown CameraPreview output class.");
} }
} catch (IOException e) { } catch (IOException e) {
LOG.e("unbindFromSurface", "Could not release surface", 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. // To be called when the preview size is setup or changed.
private void startPreview(String log) { private void startPreview(String log) {
LOG.i(log, "Dispatching onCameraPreviewSizeChanged."); LOG.i(log, "Dispatching onCameraPreviewStreamSizeChanged.");
mCameraCallbacks.onCameraPreviewSizeChanged(); mCameraCallbacks.onCameraPreviewStreamSizeChanged();
Size previewSize = getPreviewSize(REF_VIEW); Size previewSize = getPreviewStreamSize(REF_VIEW);
boolean wasFlipped = flip(REF_SENSOR, 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(); Camera.Parameters params = mCamera.getParameters();
mPreviewFormat = params.getPreviewFormat(); 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) { if (mMode == Mode.PICTURE) {
params.setPictureSize(mCaptureSize.getWidth(), mCaptureSize.getHeight()); // <- allowed params.setPictureSize(mCaptureSize.getWidth(), mCaptureSize.getHeight()); // <- allowed
} else { } else {
@ -196,7 +200,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
mCamera.setPreviewCallbackWithBuffer(null); // Release anything left mCamera.setPreviewCallbackWithBuffer(null); // Release anything left
mCamera.setPreviewCallbackWithBuffer(this); // Add ourselves mCamera.setPreviewCallbackWithBuffer(this); // Add ourselves
mFrameManager.allocate(ImageFormat.getBitsPerPixel(mPreviewFormat), mPreviewSize); mFrameManager.allocate(ImageFormat.getBitsPerPixel(mPreviewFormat), mPreviewStreamSize);
LOG.i(log, "Starting preview with startPreview()."); LOG.i(log, "Starting preview with startPreview().");
try { try {
@ -289,7 +293,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
} }
mCameraOptions = null; mCameraOptions = null;
mCamera = null; mCamera = null;
mPreviewSize = null; mPreviewStreamSize = null;
mCaptureSize = null; mCaptureSize = null;
mIsBound = false; mIsBound = false;
LOG.w("onStop:", "Clean up.", "Returning."); 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 @Override
void takePictureSnapshot(@NonNull final AspectRatio viewAspectRatio) { void takePictureSnapshot(@NonNull final AspectRatio viewAspectRatio) {
LOG.v("takePictureSnapshot: scheduling"); LOG.v("takePictureSnapshot: scheduling");
@ -588,7 +595,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
result.location = mLocation; result.location = mLocation;
result.isSnapshot = true; result.isSnapshot = true;
result.facing = mFacing; 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. 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; AspectRatio outputRatio = flip(REF_OUTPUT, REF_VIEW) ? viewAspectRatio.inverse() : viewAspectRatio;
// LOG.e("ROTBUG_pic", "aspectRatio (REF_VIEW):", 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, Frame frame = mFrameManager.getFrame(data,
System.currentTimeMillis(), System.currentTimeMillis(),
offset(REF_SENSOR, REF_OUTPUT), offset(REF_SENSOR, REF_OUTPUT),
mPreviewSize, mPreviewStreamSize,
mPreviewFormat); mPreviewFormat);
mCameraCallbacks.dispatchFrame(frame); 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") @SuppressLint("NewApi")
@Override @Override
void takeVideoSnapshot(@NonNull final File file, @NonNull final AspectRatio viewAspectRatio) { 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 // 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. // 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; AspectRatio outputRatio = flip(REF_OUTPUT, REF_VIEW) ? viewAspectRatio.inverse() : viewAspectRatio;
Rect outputCrop = CropHelper.computeCrop(outputSize, outputRatio); Rect outputCrop = CropHelper.computeCrop(outputSize, outputRatio);
outputSize = new Size(outputCrop.width(), outputCrop.height()); outputSize = new Size(outputCrop.width(), outputCrop.height());

@ -8,6 +8,7 @@ import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread; import androidx.annotation.WorkerThread;
import java.io.File; import java.io.File;
@ -49,10 +50,15 @@ abstract class CameraController implements
protected float mExposureCorrectionValue; protected float mExposureCorrectionValue;
protected boolean mPlaySounds; protected boolean mPlaySounds;
@Nullable private SizeSelector mPreviewSizeSelector; @Nullable private SizeSelector mPreviewStreamSizeSelector;
private SizeSelector mPictureSizeSelector; private SizeSelector mPictureSizeSelector;
private SizeSelector mVideoSizeSelector; 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 int mCameraId;
protected CameraOptions mCameraOptions; protected CameraOptions mCameraOptions;
protected Mapper mMapper; protected Mapper mMapper;
@ -64,7 +70,7 @@ abstract class CameraController implements
protected int mVideoBitRate; protected int mVideoBitRate;
protected int mAudioBitRate; protected int mAudioBitRate;
protected Size mCaptureSize; protected Size mCaptureSize;
protected Size mPreviewSize; protected Size mPreviewStreamSize;
protected int mPreviewFormat; protected int mPreviewFormat;
protected int mSensorOffset; protected int mSensorOffset;
@ -279,8 +285,8 @@ abstract class CameraController implements
mDeviceOrientation = deviceOrientation; mDeviceOrientation = deviceOrientation;
} }
final void setPreviewSizeSelector(@Nullable SizeSelector selector) { final void setPreviewStreamSizeSelector(@Nullable SizeSelector selector) {
mPreviewSizeSelector = selector; mPreviewStreamSizeSelector = selector;
} }
final void setPictureSizeSelector(@NonNull SizeSelector selector) { final void setPictureSizeSelector(@NonNull SizeSelector selector) {
@ -311,6 +317,14 @@ abstract class CameraController implements
mAudioBitRate = audioBitRate; mAudioBitRate = audioBitRate;
} }
final void setSnapshotMaxWidth(int maxWidth) {
mSnapshotMaxWidth = maxWidth;
}
final void setSnapshotMaxHeight(int maxHeight) {
mSnapshotMaxHeight = maxHeight;
}
//endregion //endregion
//region Abstract setters and APIs //region Abstract setters and APIs
@ -421,8 +435,8 @@ abstract class CameraController implements
} }
@Nullable @Nullable
/* for tests */ final SizeSelector getPreviewSizeSelector() { /* for tests */ final SizeSelector getPreviewStreamSizeSelector() {
return mPreviewSizeSelector; return mPreviewStreamSizeSelector;
} }
@NonNull @NonNull
@ -493,19 +507,71 @@ abstract class CameraController implements
return offset(reference1, reference2) % 180 != 0; return offset(reference1, reference2) % 180 != 0;
} }
@Nullable
final Size getPictureSize(@SuppressWarnings("SameParameterValue") int reference) { final Size getPictureSize(@SuppressWarnings("SameParameterValue") int reference) {
if (mCaptureSize == null || mMode == Mode.VIDEO) return null; if (mCaptureSize == null || mMode == Mode.VIDEO) return null;
return flip(REF_SENSOR, reference) ? mCaptureSize.flip() : mCaptureSize; return flip(REF_SENSOR, reference) ? mCaptureSize.flip() : mCaptureSize;
} }
@Nullable
final Size getVideoSize(@SuppressWarnings("SameParameterValue") int reference) { final Size getVideoSize(@SuppressWarnings("SameParameterValue") int reference) {
if (mCaptureSize == null || mMode == Mode.PICTURE) return null; if (mCaptureSize == null || mMode == Mode.PICTURE) return null;
return flip(REF_SENSOR, reference) ? mCaptureSize.flip() : mCaptureSize; return flip(REF_SENSOR, reference) ? mCaptureSize.flip() : mCaptureSize;
} }
final Size getPreviewSize(int reference) { @Nullable
if (mPreviewSize == null) return null; final Size getPreviewStreamSize(int reference) {
return flip(REF_SENSOR, reference) ? mPreviewSize.flip() : mPreviewSize; 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 @NonNull
@SuppressWarnings("WeakerAccess") @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, // 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. // we must convert all of them to REF_VIEW, then flip back when returning.
boolean flip = flip(REF_SENSOR, REF_VIEW); boolean flip = flip(REF_SENSOR, REF_VIEW);
@ -559,12 +625,12 @@ abstract class CameraController implements
sizes.add(flip ? size.flip() : size); 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. // 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()); AspectRatio targetRatio = AspectRatio.of(mCaptureSize.getWidth(), mCaptureSize.getHeight());
if (flip) targetRatio = targetRatio.inverse(); 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 SizeSelector matchRatio = SizeSelectors.and( // Match this aspect ratio and sort by biggest
SizeSelectors.aspectRatio(targetRatio, 0), SizeSelectors.aspectRatio(targetRatio, 0),
SizeSelectors.biggest()); SizeSelectors.biggest());
@ -582,14 +648,14 @@ abstract class CameraController implements
// Apply the external selector with this as a fallback, // Apply the external selector with this as a fallback,
// and return a size in REF_SENSOR reference. // and return a size in REF_SENSOR reference.
SizeSelector selector; SizeSelector selector;
if (mPreviewSizeSelector != null) { if (mPreviewStreamSizeSelector != null) {
selector = SizeSelectors.or(mPreviewSizeSelector, matchAll); selector = SizeSelectors.or(mPreviewStreamSizeSelector, matchAll);
} else { } else {
selector = matchAll; selector = matchAll;
} }
Size result = selector.select(sizes).get(0); Size result = selector.select(sizes).get(0);
if (flip) result = result.flip(); if (flip) result = result.flip();
LOG.i("computePreviewSize:", "result:", result, "flip:", flip); LOG.i("computePreviewStreamSize:", "result:", result, "flip:", flip);
return result; return result;
} }

@ -311,7 +311,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
*/ */
@Override @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Size previewSize = mCameraController.getPreviewSize(CameraController.REF_VIEW); Size previewSize = mCameraController.getPreviewStreamSize(CameraController.REF_VIEW);
if (previewSize == null) { if (previewSize == null) {
LOG.w("onMeasure:", "surface is not ready. Calling default behavior."); LOG.w("onMeasure:", "surface is not ready. Calling default behavior.");
super.onMeasure(widthMeasureSpec, heightMeasureSpec); 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)} * upscaling. If all you want is set an aspect ratio, use {@link #setPictureSize(SizeSelector)}
* and {@link #setVideoSize(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. * is recomputed accordingly.
* *
* See the {@link SizeSelectors} class for handy utilities for creating selectors. * See the {@link SizeSelectors} class for handy utilities for creating selectors.
* *
* @param selector a size selector * @param selector a size selector
*/ */
public void setPreviewSize(@NonNull SizeSelector selector) { public void setPreviewStreamSize(@NonNull SizeSelector selector) {
mCameraController.setPreviewSizeSelector(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 * 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. // 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. // 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()); AspectRatio viewRatio = AspectRatio.of(getWidth(), getHeight());
Rect crop = CropHelper.computeCrop(preview, viewRatio); Rect crop = CropHelper.computeCrop(preview, viewRatio);
Size cropSize = new Size(crop.width(), crop.height()); Size cropSize = new Size(crop.width(), crop.height());
@ -1597,7 +1618,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
interface CameraCallbacks extends OrientationHelper.Callback { interface CameraCallbacks extends OrientationHelper.Callback {
void dispatchOnCameraOpened(CameraOptions options); void dispatchOnCameraOpened(CameraOptions options);
void dispatchOnCameraClosed(); void dispatchOnCameraClosed();
void onCameraPreviewSizeChanged(); void onCameraPreviewStreamSizeChanged();
void onShutter(boolean shouldPlaySound); void onShutter(boolean shouldPlaySound);
void dispatchOnVideoTaken(VideoResult result); void dispatchOnVideoTaken(VideoResult result);
void dispatchOnPictureTaken(PictureResult result); void dispatchOnPictureTaken(PictureResult result);
@ -1642,8 +1663,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
} }
@Override @Override
public void onCameraPreviewSizeChanged() { public void onCameraPreviewStreamSizeChanged() {
mLogger.i("onCameraPreviewSizeChanged"); mLogger.i("onCameraPreviewStreamSizeChanged");
// Camera preview size has changed. // Camera preview size has changed.
// Request a layout pass for onMeasure() to do its stuff. // Request a layout pass for onMeasure() to do its stuff.
// Potentially this will change CameraView size, which changes Surface size, // Potentially this will change CameraView size, which changes Surface size,

@ -37,7 +37,7 @@ class SnapshotPictureRecorder extends PictureRecorder {
mCamera = camera; mCamera = camera;
mOutputRatio = outputRatio; mOutputRatio = outputRatio;
mFormat = mController.mPreviewFormat; mFormat = mController.mPreviewFormat;
mSensorPreviewSize = mController.mPreviewSize; mSensorPreviewSize = mController.mPreviewStreamSize;
} }
@Override @Override
@ -189,7 +189,7 @@ class SnapshotPictureRecorder extends PictureRecorder {
// It seems that the buffers are already cleared here, so we need to allocate again. // It seems that the buffers are already cleared here, so we need to allocate again.
camera.setPreviewCallbackWithBuffer(null); // Release anything left camera.setPreviewCallbackWithBuffer(null); // Release anything left
camera.setPreviewCallbackWithBuffer(mController); // Add ourselves 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="cameraVideoBitRate" format="integer|reference" />
<attr name="cameraAudioBitRate" 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"> <attr name="cameraGestureTap" format="enum">
<enum name="none" value="0" /> <enum name="none" value="0" />
<enum name="focus" value="1" /> <enum name="focus" value="1" />

@ -6,6 +6,13 @@ import androidx.annotation.Nullable;
import android.view.View; import android.view.View;
import android.view.ViewGroup; 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> { abstract class CameraPreview<T extends View, Output> {
protected final static CameraLogger LOG = CameraLogger.create(CameraPreview.class.getSimpleName()); 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. // As far as I can see, these are the actual preview dimensions, as set in CameraParameters.
// This is called by the CameraImpl. // This is called by the CameraImpl.
// These must be alredy rotated, if needed, to be consistent with surface/view sizes. // These must be alredy rotated, if needed, to be consistent with surface/view sizes.
void setInputStreamSize(int width, int height, boolean wasFlipped) { void setStreamSize(int width, int height, boolean wasFlipped) {
LOG.i("setInputStreamSize:", "desiredW=", width, "desiredH=", height); LOG.i("setStreamSize:", "desiredW=", width, "desiredH=", height);
mInputStreamWidth = width; mInputStreamWidth = width;
mInputStreamHeight = height; mInputStreamHeight = height;
mInputFlipped = wasFlipped; mInputFlipped = wasFlipped;
@ -71,12 +78,12 @@ abstract class CameraPreview<T extends View, Output> {
} }
@NonNull @NonNull
final Size getInputStreamSize() { final Size getStreamSize() {
return new Size(mInputStreamWidth, mInputStreamHeight); return new Size(mInputStreamWidth, mInputStreamHeight);
} }
@NonNull @NonNull
final Size getOutputSurfaceSize() { final Size getSurfaceSize() {
return new Size(mOutputSurfaceWidth, mOutputSurfaceHeight); return new Size(mOutputSurfaceWidth, mOutputSurfaceHeight);
} }
@ -90,8 +97,8 @@ abstract class CameraPreview<T extends View, Output> {
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
protected final void dispatchOnOutputSurfaceAvailable(int width, int height) { protected final void dispatchOnSurfaceAvailable(int width, int height) {
LOG.i("dispatchOnOutputSurfaceAvailable:", "w=", width, "h=", height); LOG.i("dispatchOnSurfaceAvailable:", "w=", width, "h=", height);
mOutputSurfaceWidth = width; mOutputSurfaceWidth = width;
mOutputSurfaceHeight = height; mOutputSurfaceHeight = height;
if (mOutputSurfaceWidth > 0 && mOutputSurfaceHeight > 0) { 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. // As far as I can see, these are the view/surface dimensions.
// This is called by subclasses. // This is called by subclasses.
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
protected final void dispatchOnOutputSurfaceSizeChanged(int width, int height) { protected final void dispatchOnSurfaceSizeChanged(int width, int height) {
LOG.i("dispatchOnOutputSurfaceSizeChanged:", "w=", width, "h=", height); LOG.i("dispatchOnSurfaceSizeChanged:", "w=", width, "h=", height);
if (width != mOutputSurfaceWidth || height != mOutputSurfaceHeight) { if (width != mOutputSurfaceWidth || height != mOutputSurfaceHeight) {
mOutputSurfaceWidth = width; mOutputSurfaceWidth = width;
mOutputSurfaceHeight = height; mOutputSurfaceHeight = height;
@ -117,7 +124,7 @@ abstract class CameraPreview<T extends View, Output> {
} }
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
protected final void dispatchOnOutputSurfaceDestroyed() { protected final void dispatchOnSurfaceDestroyed() {
mOutputSurfaceWidth = 0; mOutputSurfaceWidth = 0;
mOutputSurfaceHeight = 0; mOutputSurfaceHeight = 0;
mSurfaceCallback.onSurfaceDestroyed(); mSurfaceCallback.onSurfaceDestroyed();

@ -85,7 +85,7 @@ class GlCameraPreview extends CameraPreview<GLSurfaceView, SurfaceTexture> imple
@Override @Override
public void surfaceDestroyed(SurfaceHolder holder) { public void surfaceDestroyed(SurfaceHolder holder) {
dispatchOnOutputSurfaceDestroyed(); dispatchOnSurfaceDestroyed();
mDispatched = false; mDispatched = false;
} }
}); });
@ -159,7 +159,7 @@ class GlCameraPreview extends CameraPreview<GLSurfaceView, SurfaceTexture> imple
@Override @Override
public void onSurfaceChanged(GL10 gl, final int width, final int height) { public void onSurfaceChanged(GL10 gl, final int width, final int height) {
if (!mDispatched) { if (!mDispatched) {
dispatchOnOutputSurfaceAvailable(width, height); dispatchOnSurfaceAvailable(width, height);
mDispatched = true; mDispatched = true;
} else if (mOutputSurfaceWidth == width && mOutputSurfaceHeight == height) { } else if (mOutputSurfaceWidth == width && mOutputSurfaceHeight == height) {
// I was experimenting and this was happening. // 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 // 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 // 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. // force recreate the EGLContext by calling onPause and onResume in the UI thread.
dispatchOnOutputSurfaceDestroyed(); dispatchOnSurfaceDestroyed();
getView().post(new Runnable() { getView().post(new Runnable() {
@Override @Override
public void run() { public void run() {
getView().onPause(); getView().onPause();
getView().onResume(); 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) { public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
LOG.i("callback:", "surfaceChanged", "w:", width, "h:", height, "dispatched:", mDispatched); LOG.i("callback:", "surfaceChanged", "w:", width, "h:", height, "dispatched:", mDispatched);
if (!mDispatched) { if (!mDispatched) {
dispatchOnOutputSurfaceAvailable(width, height); dispatchOnSurfaceAvailable(width, height);
mDispatched = true; mDispatched = true;
} else { } else {
dispatchOnOutputSurfaceSizeChanged(width, height); dispatchOnSurfaceSizeChanged(width, height);
} }
} }
@Override @Override
public void surfaceDestroyed(SurfaceHolder holder) { public void surfaceDestroyed(SurfaceHolder holder) {
LOG.i("callback:", "surfaceDestroyed"); LOG.i("callback:", "surfaceDestroyed");
dispatchOnOutputSurfaceDestroyed(); dispatchOnSurfaceDestroyed();
mDispatched = false; mDispatched = false;
} }
}); });

@ -28,17 +28,17 @@ class TextureCameraPreview extends CameraPreview<TextureView, SurfaceTexture> {
@Override @Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
dispatchOnOutputSurfaceAvailable(width, height); dispatchOnSurfaceAvailable(width, height);
} }
@Override @Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
dispatchOnOutputSurfaceSizeChanged(width, height); dispatchOnSurfaceSizeChanged(width, height);
} }
@Override @Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
dispatchOnOutputSurfaceDestroyed(); dispatchOnSurfaceDestroyed();
return true; return true;
} }
@ -70,8 +70,8 @@ class TextureCameraPreview extends CameraPreview<TextureView, SurfaceTexture> {
@TargetApi(15) @TargetApi(15)
@Override @Override
void setInputStreamSize(int width, int height, boolean wasFlipped) { void setStreamSize(int width, int height, boolean wasFlipped) {
super.setInputStreamSize(width, height, wasFlipped); super.setStreamSize(width, height, wasFlipped);
if (getView().getSurfaceTexture() != null) { if (getView().getSurfaceTexture() != null) {
getView().getSurfaceTexture().setDefaultBufferSize(width, height); getView().getSurfaceTexture().setDefaultBufferSize(width, height);
} }

@ -2,8 +2,11 @@ package com.otaliastudios.cameraview.demo;
import android.app.Activity; import android.app.Activity;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import android.util.Log;
import android.widget.ImageView; import android.widget.ImageView;
import com.otaliastudios.cameraview.AspectRatio; import com.otaliastudios.cameraview.AspectRatio;
@ -45,6 +48,18 @@ public class PicturePreviewActivity extends Activity {
imageView.setImageBitmap(bitmap); 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 @Override

@ -5,6 +5,8 @@ import android.media.MediaPlayer;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import android.util.Log;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.MediaController; import android.widget.MediaController;
@ -73,6 +75,11 @@ public class VideoPreviewActivity extends Activity {
lp.height = (int) (viewWidth * (videoHeight / videoWidth)); lp.height = (int) (viewWidth * (videoHeight / videoWidth));
videoView.setLayoutParams(lp); videoView.setLayoutParams(lp);
playVideo(); 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 disqus: 1
--- ---
If you are planning to use the snapshot APIs, the size of the media output is that of the preview, 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). accounting for any cropping made when [measuring the view](preview-size.html) and other constraints.
If you are planning to use the standard APIs for capturing, then what follows applies. Please read the [Snapshot Size](snapshot-size.html) document.
If you are planning to use the standard APIs, then what follows applies.
### Controlling Size ### 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`| |`takePicture()`|Pictures|Standard|`yes`|`no`|`no`|That of `setPictureSize`|
|`takeVideo(File)`|Videos|Standard|`no`|`yes`|`no`|That of `setVideoSize`| |`takeVideo(File)`|Videos|Standard|`no`|`yes`|`no`|That of `setVideoSize`|
|`takePictureSnapshot()`|Pictures|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 view| |`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: Please note that the video snaphot features requires:

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

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

@ -4,7 +4,7 @@ title: "More features"
subtitle: "Undocumented features & more" subtitle: "Undocumented features & more"
description: "Undocumented features & more" description: "Undocumented features & more"
category: docs category: docs
order: 11 order: 13
date: 2018-12-20 20:41:20 date: 2018-12-20 20:41:20
disqus: 1 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**. 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, **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.** 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 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: 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 - 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 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 ```java
cameraView.setPreviewSize(new SizeSelector() { cameraView.setPreviewStreamSize(new SizeSelector() {
@Override @Override
public List<Size> select(List<Size> source) { public List<Size> select(List<Size> source) {
// Receives a list of available sizes. // 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. 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. 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" subtitle: "Permissions and Manifest setup"
description: "Permissions and Manifest setup" description: "Permissions and Manifest setup"
category: docs category: docs
order: 8 order: 10
date: 2018-12-20 20:03:03 date: 2018-12-20 20:03:03
disqus: 1 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