Frame Processing maxWidth, maxHeight and format (#704)
* Create CameraEngine and CameraBaseEngine * Promote filters to stable - no experimental flag * Fix setSnapshotMaxWidth / Height bugs * Add setFrameProcessingMaxWidth and setFrameProcessingMaxHeight * Add setFrameProcessingMaxWidth and setFrameProcessingMaxHeight (docs) * Prepare Frame for Images, abstract FrameManager, create ByteBufferFrameManager * Fix tests * Fix unit tests * Send Images for Camera2 * Tests * Add CameraView.setFrameProcessingFormat(int), tests, docs * Add CameraOptions.getSupportedFrameProcessingFormats(), tests * Add CameraEngine support, integration tests * Fix demo app, add getFrameProcessingPoolSize * Fix tests * Fix testspull/708/head
parent
4a6b9be905
commit
e1721bb77d
@ -0,0 +1,105 @@ |
|||||||
|
package com.otaliastudios.cameraview.frame; |
||||||
|
|
||||||
|
|
||||||
|
import android.graphics.ImageFormat; |
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4; |
||||||
|
import androidx.test.filters.SmallTest; |
||||||
|
|
||||||
|
import com.otaliastudios.cameraview.BaseTest; |
||||||
|
import com.otaliastudios.cameraview.size.Size; |
||||||
|
|
||||||
|
import org.junit.After; |
||||||
|
import org.junit.Before; |
||||||
|
import org.junit.Test; |
||||||
|
import org.junit.runner.RunWith; |
||||||
|
|
||||||
|
import static org.mockito.Matchers.any; |
||||||
|
import static org.mockito.Mockito.mock; |
||||||
|
import static org.mockito.Mockito.never; |
||||||
|
import static org.mockito.Mockito.reset; |
||||||
|
import static org.mockito.Mockito.times; |
||||||
|
import static org.mockito.Mockito.verify; |
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4.class) |
||||||
|
@SmallTest |
||||||
|
public class ByteBufferFrameManagerTest extends BaseTest { |
||||||
|
|
||||||
|
private ByteBufferFrameManager.BufferCallback callback; |
||||||
|
|
||||||
|
@Before |
||||||
|
public void setUp() { |
||||||
|
callback = mock(ByteBufferFrameManager.BufferCallback.class); |
||||||
|
} |
||||||
|
|
||||||
|
@After |
||||||
|
public void tearDown() { |
||||||
|
callback = null; |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testAllocate() { |
||||||
|
ByteBufferFrameManager manager = new ByteBufferFrameManager(1, callback); |
||||||
|
manager.setUp(ImageFormat.NV21, new Size(50, 50)); |
||||||
|
verify(callback, times(1)).onBufferAvailable(any(byte[].class)); |
||||||
|
reset(callback); |
||||||
|
|
||||||
|
manager = new ByteBufferFrameManager(5, callback); |
||||||
|
manager.setUp(ImageFormat.NV21, new Size(50, 50)); |
||||||
|
verify(callback, times(5)).onBufferAvailable(any(byte[].class)); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testOnFrameReleased_alreadyFull() { |
||||||
|
ByteBufferFrameManager manager = new ByteBufferFrameManager(1, callback); |
||||||
|
manager.setUp(ImageFormat.NV21, new Size(50, 50)); |
||||||
|
int length = manager.getFrameBytes(); |
||||||
|
|
||||||
|
Frame frame1 = manager.getFrame(new byte[length], 0, 0); |
||||||
|
// Since frame1 is already taken and poolSize = 1, a new Frame is created.
|
||||||
|
Frame frame2 = manager.getFrame(new byte[length], 0, 0); |
||||||
|
// Release the first frame so it goes back into the pool.
|
||||||
|
manager.onFrameReleased(frame1, (byte[]) frame1.getData()); |
||||||
|
reset(callback); |
||||||
|
// Release the second. The pool is already full, so onBufferAvailable should not be called
|
||||||
|
// since this Frame instance will NOT be reused.
|
||||||
|
manager.onFrameReleased(frame2, (byte[]) frame2.getData()); |
||||||
|
verify(callback, never()).onBufferAvailable((byte[]) frame2.getData()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testOnFrameReleased_sameLength() { |
||||||
|
ByteBufferFrameManager manager = new ByteBufferFrameManager(1, callback); |
||||||
|
manager.setUp(ImageFormat.NV21, new Size(50, 50)); |
||||||
|
int length = manager.getFrameBytes(); |
||||||
|
|
||||||
|
// A camera preview frame comes. Request a frame.
|
||||||
|
byte[] picture = new byte[length]; |
||||||
|
Frame frame = manager.getFrame(picture, 0, 0); |
||||||
|
|
||||||
|
// Release the frame and ensure that onBufferAvailable is called.
|
||||||
|
reset(callback); |
||||||
|
manager.onFrameReleased(frame, (byte[]) frame.getData()); |
||||||
|
verify(callback, times(1)).onBufferAvailable(picture); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testOnFrameReleased_differentLength() { |
||||||
|
ByteBufferFrameManager manager = new ByteBufferFrameManager(1, callback); |
||||||
|
manager.setUp(ImageFormat.NV21, new Size(50, 50)); |
||||||
|
int length = manager.getFrameBytes(); |
||||||
|
|
||||||
|
// A camera preview frame comes. Request a frame.
|
||||||
|
byte[] picture = new byte[length]; |
||||||
|
Frame frame = manager.getFrame(picture, 0, 0); |
||||||
|
|
||||||
|
// Don't release the frame. Change the allocation size.
|
||||||
|
manager.setUp(ImageFormat.NV16, new Size(15, 15)); |
||||||
|
|
||||||
|
// Now release the old frame and ensure that onBufferAvailable is NOT called,
|
||||||
|
// because the released data has wrong length.
|
||||||
|
manager.onFrameReleased(frame, (byte[]) frame.getData()); |
||||||
|
reset(callback); |
||||||
|
verify(callback, never()).onBufferAvailable(picture); |
||||||
|
} |
||||||
|
} |
@ -1,100 +0,0 @@ |
|||||||
package com.otaliastudios.cameraview.internal.utils; |
|
||||||
|
|
||||||
|
|
||||||
import android.graphics.Bitmap; |
|
||||||
import android.graphics.BitmapFactory; |
|
||||||
import android.graphics.Canvas; |
|
||||||
import android.graphics.Color; |
|
||||||
import android.graphics.ImageFormat; |
|
||||||
import android.graphics.Paint; |
|
||||||
import android.graphics.PorterDuff; |
|
||||||
import android.graphics.Rect; |
|
||||||
import android.graphics.YuvImage; |
|
||||||
import android.media.Image; |
|
||||||
import android.media.ImageReader; |
|
||||||
import android.os.Handler; |
|
||||||
import android.os.Looper; |
|
||||||
import android.view.Surface; |
|
||||||
|
|
||||||
import androidx.annotation.NonNull; |
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4; |
|
||||||
import androidx.test.filters.SmallTest; |
|
||||||
|
|
||||||
import com.otaliastudios.cameraview.BaseTest; |
|
||||||
import com.otaliastudios.cameraview.tools.Op; |
|
||||||
import com.otaliastudios.cameraview.tools.SdkExclude; |
|
||||||
|
|
||||||
import org.junit.Test; |
|
||||||
import org.junit.runner.RunWith; |
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream; |
|
||||||
|
|
||||||
import static org.junit.Assert.assertNotNull; |
|
||||||
|
|
||||||
/** |
|
||||||
* Starting from API 29, surface.lockCanvas() sets the surface format to RGBA_8888: |
|
||||||
* https://github.com/aosp-mirror/platform_frameworks_base/blob/android10-release/core/jni/android_view_Surface.cpp#L215-L217 .
|
|
||||||
* For this reason, acquireLatestImage crashes because we requested a different format. |
|
||||||
*/ |
|
||||||
@SdkExclude(minSdkVersion = 29) |
|
||||||
@RunWith(AndroidJUnit4.class) |
|
||||||
@SmallTest |
|
||||||
public class ImageHelperTest extends BaseTest { |
|
||||||
|
|
||||||
@NonNull |
|
||||||
private Image getImage() { |
|
||||||
ImageReader reader = ImageReader.newInstance(100, 100, ImageFormat.YUV_420_888, 1); |
|
||||||
Surface readerSurface = reader.getSurface(); |
|
||||||
final Op<Image> imageOp = new Op<>(); |
|
||||||
reader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() { |
|
||||||
@Override |
|
||||||
public void onImageAvailable(ImageReader reader) { |
|
||||||
Image image = reader.acquireLatestImage(); |
|
||||||
if (image != null) imageOp.controller().end(image); |
|
||||||
} |
|
||||||
}, new Handler(Looper.getMainLooper())); |
|
||||||
|
|
||||||
// Write on reader surface.
|
|
||||||
Canvas readerCanvas = readerSurface.lockCanvas(null); |
|
||||||
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); |
|
||||||
paint.setColor(Color.RED); |
|
||||||
readerCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.MULTIPLY); |
|
||||||
readerCanvas.drawCircle(50, 50, 50, paint); |
|
||||||
readerSurface.unlockCanvasAndPost(readerCanvas); |
|
||||||
|
|
||||||
// Wait
|
|
||||||
Image image = imageOp.await(5000); |
|
||||||
assertNotNull(image); |
|
||||||
return image; |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void testImage() { |
|
||||||
Image image = getImage(); |
|
||||||
int width = image.getWidth(); |
|
||||||
int height = image.getHeight(); |
|
||||||
int bitsPerPixel = ImageFormat.getBitsPerPixel(ImageFormat.NV21); |
|
||||||
int sizeBits = width * height * bitsPerPixel; |
|
||||||
int sizeBytes = (int) Math.ceil(sizeBits / 8.0d); |
|
||||||
byte[] bytes = new byte[sizeBytes]; |
|
||||||
ImageHelper.convertToNV21(image, bytes); |
|
||||||
image.close(); |
|
||||||
|
|
||||||
// Read the image
|
|
||||||
YuvImage yuvImage = new YuvImage(bytes, ImageFormat.NV21, width, height, null); |
|
||||||
ByteArrayOutputStream jpegStream = new ByteArrayOutputStream(); |
|
||||||
yuvImage.compressToJpeg(new Rect(0, 0, width, height), 100, jpegStream); |
|
||||||
byte[] jpegByteArray = jpegStream.toByteArray(); |
|
||||||
Bitmap bitmap = BitmapFactory.decodeByteArray(jpegByteArray, 0, jpegByteArray.length); |
|
||||||
assertNotNull(bitmap); |
|
||||||
|
|
||||||
// Wanted to do assertions on the color here but it doesn't work. There must be an issue
|
|
||||||
// with how we are drawing the image in this test, since in real camera, the algorithm works well.
|
|
||||||
// So for now let's just test that nothing crashes during this process.
|
|
||||||
// int color = bitmap.getPixel(bitmap.getWidth() - 1, bitmap.getHeight() - 1);
|
|
||||||
// assertEquals(Color.red(color), 255, 5);
|
|
||||||
// assertEquals(Color.green(color), 0, 5);
|
|
||||||
// assertEquals(Color.blue(color), 0, 5);
|
|
||||||
// assertEquals(Color.alpha(color), 0, 5);
|
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,927 @@ |
|||||||
|
package com.otaliastudios.cameraview.engine; |
||||||
|
|
||||||
|
import android.location.Location; |
||||||
|
|
||||||
|
import androidx.annotation.CallSuper; |
||||||
|
import androidx.annotation.NonNull; |
||||||
|
import androidx.annotation.Nullable; |
||||||
|
import androidx.annotation.VisibleForTesting; |
||||||
|
|
||||||
|
import com.google.android.gms.tasks.Task; |
||||||
|
import com.google.android.gms.tasks.Tasks; |
||||||
|
import com.otaliastudios.cameraview.CameraException; |
||||||
|
import com.otaliastudios.cameraview.CameraOptions; |
||||||
|
import com.otaliastudios.cameraview.PictureResult; |
||||||
|
import com.otaliastudios.cameraview.VideoResult; |
||||||
|
import com.otaliastudios.cameraview.controls.Audio; |
||||||
|
import com.otaliastudios.cameraview.controls.Facing; |
||||||
|
import com.otaliastudios.cameraview.controls.Flash; |
||||||
|
import com.otaliastudios.cameraview.controls.Hdr; |
||||||
|
import com.otaliastudios.cameraview.controls.Mode; |
||||||
|
import com.otaliastudios.cameraview.controls.PictureFormat; |
||||||
|
import com.otaliastudios.cameraview.controls.VideoCodec; |
||||||
|
import com.otaliastudios.cameraview.controls.WhiteBalance; |
||||||
|
import com.otaliastudios.cameraview.engine.offset.Angles; |
||||||
|
import com.otaliastudios.cameraview.engine.offset.Reference; |
||||||
|
import com.otaliastudios.cameraview.engine.orchestrator.CameraState; |
||||||
|
import com.otaliastudios.cameraview.frame.FrameManager; |
||||||
|
import com.otaliastudios.cameraview.overlay.Overlay; |
||||||
|
import com.otaliastudios.cameraview.picture.PictureRecorder; |
||||||
|
import com.otaliastudios.cameraview.preview.CameraPreview; |
||||||
|
import com.otaliastudios.cameraview.size.AspectRatio; |
||||||
|
import com.otaliastudios.cameraview.size.Size; |
||||||
|
import com.otaliastudios.cameraview.size.SizeSelector; |
||||||
|
import com.otaliastudios.cameraview.size.SizeSelectors; |
||||||
|
import com.otaliastudios.cameraview.video.VideoRecorder; |
||||||
|
|
||||||
|
import java.io.File; |
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.Collection; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Abstract implementation of {@link CameraEngine} that helps in common tasks. |
||||||
|
*/ |
||||||
|
public abstract class CameraBaseEngine extends CameraEngine { |
||||||
|
|
||||||
|
@SuppressWarnings("WeakerAccess") protected CameraPreview mPreview; |
||||||
|
@SuppressWarnings("WeakerAccess") protected CameraOptions mCameraOptions; |
||||||
|
@SuppressWarnings("WeakerAccess") protected PictureRecorder mPictureRecorder; |
||||||
|
@SuppressWarnings("WeakerAccess") protected VideoRecorder mVideoRecorder; |
||||||
|
@SuppressWarnings("WeakerAccess") protected Size mCaptureSize; |
||||||
|
@SuppressWarnings("WeakerAccess") protected Size mPreviewStreamSize; |
||||||
|
@SuppressWarnings("WeakerAccess") protected Size mFrameProcessingSize; |
||||||
|
@SuppressWarnings("WeakerAccess") protected int mFrameProcessingFormat; |
||||||
|
@SuppressWarnings("WeakerAccess") protected boolean mHasFrameProcessors; |
||||||
|
@SuppressWarnings("WeakerAccess") protected Flash mFlash; |
||||||
|
@SuppressWarnings("WeakerAccess") protected WhiteBalance mWhiteBalance; |
||||||
|
@SuppressWarnings("WeakerAccess") protected VideoCodec mVideoCodec; |
||||||
|
@SuppressWarnings("WeakerAccess") protected Hdr mHdr; |
||||||
|
@SuppressWarnings("WeakerAccess") protected PictureFormat mPictureFormat; |
||||||
|
@SuppressWarnings("WeakerAccess") protected Location mLocation; |
||||||
|
@SuppressWarnings("WeakerAccess") protected float mZoomValue; |
||||||
|
@SuppressWarnings("WeakerAccess") protected float mExposureCorrectionValue; |
||||||
|
@SuppressWarnings("WeakerAccess") protected boolean mPlaySounds; |
||||||
|
@SuppressWarnings("WeakerAccess") protected boolean mPictureMetering; |
||||||
|
@SuppressWarnings("WeakerAccess") protected boolean mPictureSnapshotMetering; |
||||||
|
@SuppressWarnings("WeakerAccess") protected float mPreviewFrameRate; |
||||||
|
|
||||||
|
private final FrameManager mFrameManager; |
||||||
|
private final Angles mAngles; |
||||||
|
@Nullable private SizeSelector mPreviewStreamSizeSelector; |
||||||
|
private SizeSelector mPictureSizeSelector; |
||||||
|
private SizeSelector mVideoSizeSelector; |
||||||
|
private Facing mFacing; |
||||||
|
private Mode mMode; |
||||||
|
private Audio mAudio; |
||||||
|
private long mVideoMaxSize; |
||||||
|
private int mVideoMaxDuration; |
||||||
|
private int mVideoBitRate; |
||||||
|
private int mAudioBitRate; |
||||||
|
private long mAutoFocusResetDelayMillis; |
||||||
|
private int mSnapshotMaxWidth; // in REF_VIEW like SizeSelectors
|
||||||
|
private int mSnapshotMaxHeight; // in REF_VIEW like SizeSelectors
|
||||||
|
private int mFrameProcessingMaxWidth; // in REF_VIEW like SizeSelectors
|
||||||
|
private int mFrameProcessingMaxHeight; // in REF_VIEW like SizeSelectors
|
||||||
|
private Overlay mOverlay; |
||||||
|
|
||||||
|
// Ops used for testing.
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mZoomTask |
||||||
|
= Tasks.forResult(null); |
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mExposureCorrectionTask |
||||||
|
= Tasks.forResult(null); |
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mFlashTask |
||||||
|
= Tasks.forResult(null); |
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mWhiteBalanceTask |
||||||
|
= Tasks.forResult(null); |
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mHdrTask |
||||||
|
= Tasks.forResult(null); |
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mLocationTask |
||||||
|
= Tasks.forResult(null); |
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mPlaySoundsTask |
||||||
|
= Tasks.forResult(null); |
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mPreviewFrameRateTask |
||||||
|
= Tasks.forResult(null); |
||||||
|
|
||||||
|
@SuppressWarnings("WeakerAccess") |
||||||
|
protected CameraBaseEngine(@NonNull Callback callback) { |
||||||
|
super(callback); |
||||||
|
mFrameManager = instantiateFrameManager(); |
||||||
|
mAngles = new Angles(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Called at construction time to get a frame manager that can later be |
||||||
|
* accessed through {@link #getFrameManager()}. |
||||||
|
* @return a frame manager |
||||||
|
*/ |
||||||
|
@NonNull |
||||||
|
protected abstract FrameManager instantiateFrameManager(); |
||||||
|
|
||||||
|
@NonNull |
||||||
|
@Override |
||||||
|
public final Angles getAngles() { |
||||||
|
return mAngles; |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
@Override |
||||||
|
public FrameManager getFrameManager() { |
||||||
|
return mFrameManager; |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public final CameraOptions getCameraOptions() { |
||||||
|
return mCameraOptions; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final void setPreview(@NonNull CameraPreview cameraPreview) { |
||||||
|
if (mPreview != null) mPreview.setSurfaceCallback(null); |
||||||
|
mPreview = cameraPreview; |
||||||
|
mPreview.setSurfaceCallback(this); |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
@Override |
||||||
|
public final CameraPreview getPreview() { |
||||||
|
return mPreview; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final void setOverlay(@Nullable Overlay overlay) { |
||||||
|
mOverlay = overlay; |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public final Overlay getOverlay() { |
||||||
|
return mOverlay; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final void setPreviewStreamSizeSelector(@Nullable SizeSelector selector) { |
||||||
|
mPreviewStreamSizeSelector = selector; |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public final SizeSelector getPreviewStreamSizeSelector() { |
||||||
|
return mPreviewStreamSizeSelector; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final void setPictureSizeSelector(@NonNull SizeSelector selector) { |
||||||
|
mPictureSizeSelector = selector; |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
@Override |
||||||
|
public final SizeSelector getPictureSizeSelector() { |
||||||
|
return mPictureSizeSelector; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final void setVideoSizeSelector(@NonNull SizeSelector selector) { |
||||||
|
mVideoSizeSelector = selector; |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
@Override |
||||||
|
public final SizeSelector getVideoSizeSelector() { |
||||||
|
return mVideoSizeSelector; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final void setVideoMaxSize(long videoMaxSizeBytes) { |
||||||
|
mVideoMaxSize = videoMaxSizeBytes; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final long getVideoMaxSize() { |
||||||
|
return mVideoMaxSize; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final void setVideoMaxDuration(int videoMaxDurationMillis) { |
||||||
|
mVideoMaxDuration = videoMaxDurationMillis; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final int getVideoMaxDuration() { |
||||||
|
return mVideoMaxDuration; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final void setVideoCodec(@NonNull VideoCodec codec) { |
||||||
|
mVideoCodec = codec; |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
@Override |
||||||
|
public final VideoCodec getVideoCodec() { |
||||||
|
return mVideoCodec; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final void setVideoBitRate(int videoBitRate) { |
||||||
|
mVideoBitRate = videoBitRate; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final int getVideoBitRate() { |
||||||
|
return mVideoBitRate; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final void setAudioBitRate(int audioBitRate) { |
||||||
|
mAudioBitRate = audioBitRate; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final int getAudioBitRate() { |
||||||
|
return mAudioBitRate; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final void setSnapshotMaxWidth(int maxWidth) { |
||||||
|
mSnapshotMaxWidth = maxWidth; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final int getSnapshotMaxWidth() { |
||||||
|
return mSnapshotMaxWidth; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final void setSnapshotMaxHeight(int maxHeight) { |
||||||
|
mSnapshotMaxHeight = maxHeight; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final int getSnapshotMaxHeight() { |
||||||
|
return mSnapshotMaxHeight; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final void setFrameProcessingMaxWidth(int maxWidth) { |
||||||
|
mFrameProcessingMaxWidth = maxWidth; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final int getFrameProcessingMaxWidth() { |
||||||
|
return mFrameProcessingMaxWidth; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final void setFrameProcessingMaxHeight(int maxHeight) { |
||||||
|
mFrameProcessingMaxHeight = maxHeight; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final int getFrameProcessingMaxHeight() { |
||||||
|
return mFrameProcessingMaxHeight; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final int getFrameProcessingFormat() { |
||||||
|
return mFrameProcessingFormat; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final void setAutoFocusResetDelay(long delayMillis) { |
||||||
|
mAutoFocusResetDelayMillis = delayMillis; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final long getAutoFocusResetDelay() { |
||||||
|
return mAutoFocusResetDelayMillis; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Helper function for subclasses. |
||||||
|
* @return true if AF should be reset |
||||||
|
*/ |
||||||
|
@SuppressWarnings("WeakerAccess") |
||||||
|
protected final boolean shouldResetAutoFocus() { |
||||||
|
return mAutoFocusResetDelayMillis > 0 && mAutoFocusResetDelayMillis != Long.MAX_VALUE; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Sets a new facing value. This will restart the engine session (if there's any) |
||||||
|
* so that we can open the new facing camera. |
||||||
|
* @param facing facing |
||||||
|
*/ |
||||||
|
@Override |
||||||
|
public final void setFacing(final @NonNull Facing facing) { |
||||||
|
final Facing old = mFacing; |
||||||
|
if (facing != old) { |
||||||
|
mFacing = facing; |
||||||
|
getOrchestrator().scheduleStateful("facing", CameraState.ENGINE, |
||||||
|
new Runnable() { |
||||||
|
@Override |
||||||
|
public void run() { |
||||||
|
if (collectCameraInfo(facing)) { |
||||||
|
restart(); |
||||||
|
} else { |
||||||
|
mFacing = old; |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
@Override |
||||||
|
public final Facing getFacing() { |
||||||
|
return mFacing; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Sets a new audio value that will be used for video recordings. |
||||||
|
* @param audio desired audio |
||||||
|
*/ |
||||||
|
@Override |
||||||
|
public final void setAudio(@NonNull Audio audio) { |
||||||
|
if (mAudio != audio) { |
||||||
|
if (isTakingVideo()) { |
||||||
|
LOG.w("Audio setting was changed while recording. " + |
||||||
|
"Changes will take place starting from next video"); |
||||||
|
} |
||||||
|
mAudio = audio; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
@Override |
||||||
|
public final Audio getAudio() { |
||||||
|
return mAudio; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Sets the desired mode (either picture or video). |
||||||
|
* @param mode desired mode. |
||||||
|
*/ |
||||||
|
@Override |
||||||
|
public final void setMode(@NonNull Mode mode) { |
||||||
|
if (mode != mMode) { |
||||||
|
mMode = mode; |
||||||
|
getOrchestrator().scheduleStateful("mode", CameraState.ENGINE, |
||||||
|
new Runnable() { |
||||||
|
@Override |
||||||
|
public void run() { |
||||||
|
restart(); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
@Override |
||||||
|
public final Mode getMode() { |
||||||
|
return mMode; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final float getZoomValue() { |
||||||
|
return mZoomValue; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final float getExposureCorrectionValue() { |
||||||
|
return mExposureCorrectionValue; |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
@Override |
||||||
|
public final Flash getFlash() { |
||||||
|
return mFlash; |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
@Override |
||||||
|
public final WhiteBalance getWhiteBalance() { |
||||||
|
return mWhiteBalance; |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
@Override |
||||||
|
public final Hdr getHdr() { |
||||||
|
return mHdr; |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public final Location getLocation() { |
||||||
|
return mLocation; |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
@Override |
||||||
|
public final PictureFormat getPictureFormat() { |
||||||
|
return mPictureFormat; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final float getPreviewFrameRate() { |
||||||
|
return mPreviewFrameRate; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final boolean hasFrameProcessors() { |
||||||
|
return mHasFrameProcessors; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final void setPictureMetering(boolean enable) { |
||||||
|
mPictureMetering = enable; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final boolean getPictureMetering() { |
||||||
|
return mPictureMetering; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final void setPictureSnapshotMetering(boolean enable) { |
||||||
|
mPictureSnapshotMetering = enable; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final boolean getPictureSnapshotMetering() { |
||||||
|
return mPictureSnapshotMetering; |
||||||
|
} |
||||||
|
|
||||||
|
//region Picture and video control
|
||||||
|
|
||||||
|
@Override |
||||||
|
public final boolean isTakingPicture() { |
||||||
|
return mPictureRecorder != null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public /* final */ void takePicture(final @NonNull PictureResult.Stub stub) { |
||||||
|
// Save boolean before scheduling! See how Camera2Engine calls this with a temp value.
|
||||||
|
final boolean metering = mPictureMetering; |
||||||
|
getOrchestrator().scheduleStateful("take picture", CameraState.BIND, |
||||||
|
new Runnable() { |
||||||
|
@Override |
||||||
|
public void run() { |
||||||
|
LOG.i("takePicture:", "running. isTakingPicture:", isTakingPicture()); |
||||||
|
if (isTakingPicture()) return; |
||||||
|
if (mMode == Mode.VIDEO) { |
||||||
|
throw new IllegalStateException("Can't take hq pictures while in VIDEO mode"); |
||||||
|
} |
||||||
|
stub.isSnapshot = false; |
||||||
|
stub.location = mLocation; |
||||||
|
stub.facing = mFacing; |
||||||
|
stub.format = mPictureFormat; |
||||||
|
onTakePicture(stub, metering); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* The snapshot size is the {@link #getPreviewStreamSize(Reference)}, but cropped based on the |
||||||
|
* view/surface aspect ratio. |
||||||
|
* @param stub a picture stub |
||||||
|
*/ |
||||||
|
@Override |
||||||
|
public /* final */ void takePictureSnapshot(final @NonNull PictureResult.Stub stub) { |
||||||
|
// Save boolean before scheduling! See how Camera2Engine calls this with a temp value.
|
||||||
|
final boolean metering = mPictureSnapshotMetering; |
||||||
|
getOrchestrator().scheduleStateful("take picture snapshot", CameraState.BIND, |
||||||
|
new Runnable() { |
||||||
|
@Override |
||||||
|
public void run() { |
||||||
|
LOG.i("takePictureSnapshot:", "running. isTakingPicture:", isTakingPicture()); |
||||||
|
if (isTakingPicture()) return; |
||||||
|
stub.location = mLocation; |
||||||
|
stub.isSnapshot = true; |
||||||
|
stub.facing = mFacing; |
||||||
|
stub.format = PictureFormat.JPEG; |
||||||
|
// Leave the other parameters to subclasses.
|
||||||
|
//noinspection ConstantConditions
|
||||||
|
AspectRatio ratio = AspectRatio.of(getPreviewSurfaceSize(Reference.OUTPUT)); |
||||||
|
onTakePictureSnapshot(stub, ratio, metering); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onPictureShutter(boolean didPlaySound) { |
||||||
|
getCallback().onShutter(!didPlaySound); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onPictureResult(@Nullable PictureResult.Stub result, @Nullable Exception error) { |
||||||
|
mPictureRecorder = null; |
||||||
|
if (result != null) { |
||||||
|
getCallback().dispatchOnPictureTaken(result); |
||||||
|
} else { |
||||||
|
LOG.e("onPictureResult", "result is null: something went wrong.", error); |
||||||
|
getCallback().dispatchError(new CameraException(error, |
||||||
|
CameraException.REASON_PICTURE_FAILED)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final boolean isTakingVideo() { |
||||||
|
return mVideoRecorder != null && mVideoRecorder.isRecording(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final void takeVideo(final @NonNull VideoResult.Stub stub, final @NonNull File file) { |
||||||
|
getOrchestrator().scheduleStateful("take video", CameraState.BIND, new Runnable() { |
||||||
|
@Override |
||||||
|
public void run() { |
||||||
|
LOG.i("takeVideo:", "running. isTakingVideo:", isTakingVideo()); |
||||||
|
if (isTakingVideo()) return; |
||||||
|
if (mMode == Mode.PICTURE) { |
||||||
|
throw new IllegalStateException("Can't record video while in PICTURE mode"); |
||||||
|
} |
||||||
|
stub.file = file; |
||||||
|
stub.isSnapshot = false; |
||||||
|
stub.videoCodec = mVideoCodec; |
||||||
|
stub.location = mLocation; |
||||||
|
stub.facing = mFacing; |
||||||
|
stub.audio = mAudio; |
||||||
|
stub.maxSize = mVideoMaxSize; |
||||||
|
stub.maxDuration = mVideoMaxDuration; |
||||||
|
stub.videoBitRate = mVideoBitRate; |
||||||
|
stub.audioBitRate = mAudioBitRate; |
||||||
|
onTakeVideo(stub); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param stub a video stub |
||||||
|
* @param file the output file |
||||||
|
*/ |
||||||
|
@Override |
||||||
|
public final void takeVideoSnapshot(@NonNull final VideoResult.Stub stub, |
||||||
|
@NonNull final File file) { |
||||||
|
getOrchestrator().scheduleStateful("take video snapshot", CameraState.BIND, |
||||||
|
new Runnable() { |
||||||
|
@Override |
||||||
|
public void run() { |
||||||
|
LOG.i("takeVideoSnapshot:", "running. isTakingVideo:", isTakingVideo()); |
||||||
|
stub.file = file; |
||||||
|
stub.isSnapshot = true; |
||||||
|
stub.videoCodec = mVideoCodec; |
||||||
|
stub.location = mLocation; |
||||||
|
stub.facing = mFacing; |
||||||
|
stub.videoBitRate = mVideoBitRate; |
||||||
|
stub.audioBitRate = mAudioBitRate; |
||||||
|
stub.audio = mAudio; |
||||||
|
stub.maxSize = mVideoMaxSize; |
||||||
|
stub.maxDuration = mVideoMaxDuration; |
||||||
|
//noinspection ConstantConditions
|
||||||
|
AspectRatio ratio = AspectRatio.of(getPreviewSurfaceSize(Reference.OUTPUT)); |
||||||
|
onTakeVideoSnapshot(stub, ratio); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final void stopVideo() { |
||||||
|
getOrchestrator().schedule("stop video", true, new Runnable() { |
||||||
|
@Override |
||||||
|
public void run() { |
||||||
|
LOG.i("stopVideo", "running. isTakingVideo?", isTakingVideo()); |
||||||
|
onStopVideo(); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
@EngineThread |
||||||
|
@SuppressWarnings("WeakerAccess") |
||||||
|
protected void onStopVideo() { |
||||||
|
if (mVideoRecorder != null) { |
||||||
|
mVideoRecorder.stop(false); |
||||||
|
// Do not null this, so we respond correctly to isTakingVideo(),
|
||||||
|
// which checks for recorder presence and recorder.isRecording().
|
||||||
|
// It will be nulled in onVideoResult.
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@CallSuper |
||||||
|
@Override |
||||||
|
public void onVideoResult(@Nullable VideoResult.Stub result, @Nullable Exception exception) { |
||||||
|
mVideoRecorder = null; |
||||||
|
if (result != null) { |
||||||
|
getCallback().dispatchOnVideoTaken(result); |
||||||
|
} else { |
||||||
|
LOG.e("onVideoResult", "result is null: something went wrong.", exception); |
||||||
|
getCallback().dispatchError(new CameraException(exception, |
||||||
|
CameraException.REASON_VIDEO_FAILED)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onVideoRecordingStart() { |
||||||
|
getCallback().dispatchOnVideoRecordingStart(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onVideoRecordingEnd() { |
||||||
|
getCallback().dispatchOnVideoRecordingEnd(); |
||||||
|
} |
||||||
|
|
||||||
|
@EngineThread |
||||||
|
protected abstract void onTakePicture(@NonNull PictureResult.Stub stub, boolean doMetering); |
||||||
|
|
||||||
|
@EngineThread |
||||||
|
protected abstract void onTakePictureSnapshot(@NonNull PictureResult.Stub stub, |
||||||
|
@NonNull AspectRatio outputRatio, |
||||||
|
boolean doMetering); |
||||||
|
|
||||||
|
@EngineThread |
||||||
|
protected abstract void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub, |
||||||
|
@NonNull AspectRatio outputRatio); |
||||||
|
|
||||||
|
@EngineThread |
||||||
|
protected abstract void onTakeVideo(@NonNull VideoResult.Stub stub); |
||||||
|
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region Size / Surface
|
||||||
|
|
||||||
|
@Override |
||||||
|
public final void onSurfaceChanged() { |
||||||
|
LOG.i("onSurfaceChanged:", "Size is", getPreviewSurfaceSize(Reference.VIEW)); |
||||||
|
getOrchestrator().scheduleStateful("surface changed", CameraState.BIND, |
||||||
|
new Runnable() { |
||||||
|
@Override |
||||||
|
public void run() { |
||||||
|
// Compute a new camera preview size and apply.
|
||||||
|
Size newSize = computePreviewStreamSize(); |
||||||
|
if (newSize.equals(mPreviewStreamSize)) { |
||||||
|
LOG.i("onSurfaceChanged:", |
||||||
|
"The computed preview size is identical. No op."); |
||||||
|
} else { |
||||||
|
LOG.i("onSurfaceChanged:", |
||||||
|
"Computed a new preview size. Calling onPreviewStreamSizeChanged()."); |
||||||
|
mPreviewStreamSize = newSize; |
||||||
|
onPreviewStreamSizeChanged(); |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* The preview stream size has changed. At this point, some engine might want to |
||||||
|
* simply call {@link #restartPreview()}, others to {@link #restartBind()}. |
||||||
|
* |
||||||
|
* It basically depends on the step at which the preview stream size is actually used. |
||||||
|
*/ |
||||||
|
@EngineThread |
||||||
|
protected abstract void onPreviewStreamSizeChanged(); |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public final Size getPictureSize(@SuppressWarnings("SameParameterValue") @NonNull Reference reference) { |
||||||
|
Size size = mCaptureSize; |
||||||
|
if (size == null || mMode == Mode.VIDEO) return null; |
||||||
|
return getAngles().flip(Reference.SENSOR, reference) ? size.flip() : size; |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public final Size getVideoSize(@SuppressWarnings("SameParameterValue") @NonNull Reference reference) { |
||||||
|
Size size = mCaptureSize; |
||||||
|
if (size == null || mMode == Mode.PICTURE) return null; |
||||||
|
return getAngles().flip(Reference.SENSOR, reference) ? size.flip() : size; |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public final Size getPreviewStreamSize(@NonNull Reference reference) { |
||||||
|
Size size = mPreviewStreamSize; |
||||||
|
if (size == null) return null; |
||||||
|
return getAngles().flip(Reference.SENSOR, reference) ? size.flip() : size; |
||||||
|
} |
||||||
|
|
||||||
|
@SuppressWarnings("SameParameterValue") |
||||||
|
@Nullable |
||||||
|
private Size getPreviewSurfaceSize(@NonNull Reference reference) { |
||||||
|
CameraPreview preview = mPreview; |
||||||
|
if (preview == null) return null; |
||||||
|
return getAngles().flip(Reference.VIEW, reference) ? preview.getSurfaceSize().flip() |
||||||
|
: preview.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's 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. |
||||||
|
* |
||||||
|
* @param reference the reference system |
||||||
|
* @return the uncropped snapshot size |
||||||
|
*/ |
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public final Size getUncroppedSnapshotSize(@NonNull Reference reference) { |
||||||
|
Size baseSize = getPreviewStreamSize(reference); |
||||||
|
if (baseSize == null) return null; |
||||||
|
boolean flip = getAngles().flip(reference, Reference.VIEW); |
||||||
|
int maxWidth = flip ? mSnapshotMaxHeight : mSnapshotMaxWidth; |
||||||
|
int maxHeight = flip ? mSnapshotMaxWidth : mSnapshotMaxHeight; |
||||||
|
if (maxWidth <= 0) maxWidth = Integer.MAX_VALUE; |
||||||
|
if (maxHeight <= 0) maxHeight = Integer.MAX_VALUE; |
||||||
|
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); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* This is called either on cameraView.start(), or when the underlying surface changes. |
||||||
|
* It is possible that in the first call the preview surface has not already computed its |
||||||
|
* dimensions. |
||||||
|
* But when it does, the {@link CameraPreview.SurfaceCallback} should be called, |
||||||
|
* and this should be refreshed. |
||||||
|
* |
||||||
|
* @return the capture size |
||||||
|
*/ |
||||||
|
@NonNull |
||||||
|
@SuppressWarnings("WeakerAccess") |
||||||
|
protected final Size computeCaptureSize() { |
||||||
|
return computeCaptureSize(mMode); |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
@SuppressWarnings("WeakerAccess") |
||||||
|
protected final Size computeCaptureSize(@NonNull Mode mode) { |
||||||
|
// We want to pass stuff into the REF_VIEW reference, not the sensor one.
|
||||||
|
// This is already managed by CameraOptions, so we just flip again at the end.
|
||||||
|
boolean flip = getAngles().flip(Reference.SENSOR, Reference.VIEW); |
||||||
|
SizeSelector selector; |
||||||
|
Collection<Size> sizes; |
||||||
|
if (mode == Mode.PICTURE) { |
||||||
|
selector = mPictureSizeSelector; |
||||||
|
sizes = mCameraOptions.getSupportedPictureSizes(); |
||||||
|
} else { |
||||||
|
selector = mVideoSizeSelector; |
||||||
|
sizes = mCameraOptions.getSupportedVideoSizes(); |
||||||
|
} |
||||||
|
selector = SizeSelectors.or(selector, SizeSelectors.biggest()); |
||||||
|
List<Size> list = new ArrayList<>(sizes); |
||||||
|
Size result = selector.select(list).get(0); |
||||||
|
if (!list.contains(result)) { |
||||||
|
throw new RuntimeException("SizeSelectors must not return Sizes other than " + |
||||||
|
"those in the input list."); |
||||||
|
} |
||||||
|
LOG.i("computeCaptureSize:", "result:", result, "flip:", flip, "mode:", mode); |
||||||
|
if (flip) result = result.flip(); // Go back to REF_SENSOR
|
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* This is called anytime {@link #computePreviewStreamSize()} is called. |
||||||
|
* This means that it should be called during the binding process, when |
||||||
|
* we can be sure that the camera is available (engineState == STARTED). |
||||||
|
* @return a list of available sizes for preview |
||||||
|
*/ |
||||||
|
@EngineThread |
||||||
|
@NonNull |
||||||
|
protected abstract List<Size> getPreviewStreamAvailableSizes(); |
||||||
|
|
||||||
|
@EngineThread |
||||||
|
@NonNull |
||||||
|
@SuppressWarnings("WeakerAccess") |
||||||
|
protected final Size computePreviewStreamSize() { |
||||||
|
@NonNull List<Size> previewSizes = getPreviewStreamAvailableSizes(); |
||||||
|
// 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 = getAngles().flip(Reference.SENSOR, Reference.VIEW); |
||||||
|
List<Size> sizes = new ArrayList<>(previewSizes.size()); |
||||||
|
for (Size size : previewSizes) { |
||||||
|
sizes.add(flip ? size.flip() : size); |
||||||
|
} |
||||||
|
|
||||||
|
// 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 = getPreviewSurfaceSize(Reference.VIEW); |
||||||
|
if (targetMinSize == null) { |
||||||
|
throw new IllegalStateException("targetMinSize should not be null here."); |
||||||
|
} |
||||||
|
AspectRatio targetRatio = AspectRatio.of(mCaptureSize.getWidth(), mCaptureSize.getHeight()); |
||||||
|
if (flip) targetRatio = targetRatio.flip(); |
||||||
|
LOG.i("computePreviewStreamSize:", |
||||||
|
"targetRatio:", targetRatio, |
||||||
|
"targetMinSize:", targetMinSize); |
||||||
|
SizeSelector matchRatio = SizeSelectors.and( // Match this aspect ratio and sort by biggest
|
||||||
|
SizeSelectors.aspectRatio(targetRatio, 0), |
||||||
|
SizeSelectors.biggest()); |
||||||
|
SizeSelector matchSize = SizeSelectors.and( // Bigger than this size, and sort by smallest
|
||||||
|
SizeSelectors.minHeight(targetMinSize.getHeight()), |
||||||
|
SizeSelectors.minWidth(targetMinSize.getWidth()), |
||||||
|
SizeSelectors.smallest()); |
||||||
|
SizeSelector matchAll = SizeSelectors.or( |
||||||
|
SizeSelectors.and(matchRatio, matchSize), // Try to respect both constraints.
|
||||||
|
matchSize, // If couldn't match aspect ratio, at least respect the size
|
||||||
|
matchRatio, // If couldn't respect size, at least match aspect ratio
|
||||||
|
SizeSelectors.biggest() // If couldn't match any, take the biggest.
|
||||||
|
); |
||||||
|
|
||||||
|
// Apply the external selector with this as a fallback,
|
||||||
|
// and return a size in REF_SENSOR reference.
|
||||||
|
SizeSelector selector; |
||||||
|
if (mPreviewStreamSizeSelector != null) { |
||||||
|
selector = SizeSelectors.or(mPreviewStreamSizeSelector, matchAll); |
||||||
|
} else { |
||||||
|
selector = matchAll; |
||||||
|
} |
||||||
|
Size result = selector.select(sizes).get(0); |
||||||
|
if (!sizes.contains(result)) { |
||||||
|
throw new RuntimeException("SizeSelectors must not return Sizes other than " + |
||||||
|
"those in the input list."); |
||||||
|
} |
||||||
|
if (flip) result = result.flip(); |
||||||
|
LOG.i("computePreviewStreamSize:", "result:", result, "flip:", flip); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* This is called anytime {@link #computeFrameProcessingSize()} is called. |
||||||
|
* Implementors can return null if frame processor size is not selectable |
||||||
|
* @return a list of available sizes for frame processing |
||||||
|
*/ |
||||||
|
@EngineThread |
||||||
|
@NonNull |
||||||
|
protected abstract List<Size> getFrameProcessingAvailableSizes(); |
||||||
|
|
||||||
|
@EngineThread |
||||||
|
@NonNull |
||||||
|
@SuppressWarnings("WeakerAccess") |
||||||
|
protected final Size computeFrameProcessingSize() { |
||||||
|
@NonNull List<Size> frameSizes = getFrameProcessingAvailableSizes(); |
||||||
|
// 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 = getAngles().flip(Reference.SENSOR, Reference.VIEW); |
||||||
|
List<Size> sizes = new ArrayList<>(frameSizes.size()); |
||||||
|
for (Size size : frameSizes) { |
||||||
|
sizes.add(flip ? size.flip() : size); |
||||||
|
} |
||||||
|
AspectRatio targetRatio = AspectRatio.of( |
||||||
|
mPreviewStreamSize.getWidth(), |
||||||
|
mPreviewStreamSize.getHeight()); |
||||||
|
if (flip) targetRatio = targetRatio.flip(); |
||||||
|
int maxWidth = mFrameProcessingMaxWidth; |
||||||
|
int maxHeight = mFrameProcessingMaxHeight; |
||||||
|
if (maxWidth <= 0 || maxWidth == Integer.MAX_VALUE) maxWidth = 640; |
||||||
|
if (maxHeight <= 0 || maxHeight == Integer.MAX_VALUE) maxHeight = 640; |
||||||
|
Size targetMaxSize = new Size(maxWidth, maxHeight); |
||||||
|
LOG.i("computeFrameProcessingSize:", |
||||||
|
"targetRatio:", targetRatio, |
||||||
|
"targetMaxSize:", targetMaxSize); |
||||||
|
SizeSelector matchRatio = SizeSelectors.aspectRatio(targetRatio, 0); |
||||||
|
SizeSelector matchSize = SizeSelectors.and( |
||||||
|
SizeSelectors.maxHeight(targetMaxSize.getHeight()), |
||||||
|
SizeSelectors.maxWidth(targetMaxSize.getWidth()), |
||||||
|
SizeSelectors.biggest()); |
||||||
|
SizeSelector matchAll = SizeSelectors.or( |
||||||
|
SizeSelectors.and(matchRatio, matchSize), // Try to respect both constraints.
|
||||||
|
matchSize, // If couldn't match aspect ratio, at least respect the size
|
||||||
|
SizeSelectors.smallest() // If couldn't match any, take the smallest.
|
||||||
|
); |
||||||
|
Size result = matchAll.select(sizes).get(0); |
||||||
|
if (!sizes.contains(result)) { |
||||||
|
throw new RuntimeException("SizeSelectors must not return Sizes other than " + |
||||||
|
"those in the input list."); |
||||||
|
} |
||||||
|
if (flip) result = result.flip(); |
||||||
|
LOG.i("computeFrameProcessingSize:", "result:", result, "flip:", flip); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
//endregion
|
||||||
|
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,162 @@ |
|||||||
|
package com.otaliastudios.cameraview.frame; |
||||||
|
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull; |
||||||
|
import androidx.annotation.Nullable; |
||||||
|
|
||||||
|
import com.otaliastudios.cameraview.size.Size; |
||||||
|
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue; |
||||||
|
|
||||||
|
/** |
||||||
|
* This class manages the allocation of byte buffers and {@link Frame} objects. |
||||||
|
* We are interested in recycling both of them, especially byte[] buffers which can create a lot |
||||||
|
* of overhead. |
||||||
|
* |
||||||
|
* The pool size applies to both the {@link Frame} pool and the byte[] pool - it makes sense to use |
||||||
|
* the same number since they are consumed at the same time. |
||||||
|
* |
||||||
|
* We can work in two modes, depending on whether a |
||||||
|
* {@link BufferCallback} is passed to the constructor. The modes changes the buffer behavior. |
||||||
|
* |
||||||
|
* 1. {@link #BUFFER_MODE_DISPATCH}: in this mode, as soon as we have a buffer, it is dispatched to |
||||||
|
* the {@link BufferCallback}. The callback should then fill the buffer, and finally call |
||||||
|
* {@link FrameManager#getFrame(Object, long, int)} to receive a frame. |
||||||
|
* This is used for Camera1. |
||||||
|
* |
||||||
|
* 2. {@link #BUFFER_MODE_ENQUEUE}: in this mode, the manager internally keeps a queue of byte |
||||||
|
* buffers, instead of handing them to the callback. The users can ask for buffers through |
||||||
|
* {@link #getBuffer()}. |
||||||
|
* This buffer can be filled with data and used to get a frame |
||||||
|
* {@link FrameManager#getFrame(Object, long, int)}, or, in case it was not filled, returned to |
||||||
|
* the queue using {@link #onBufferUnused(byte[])}. |
||||||
|
* This is used for Camera2. |
||||||
|
*/ |
||||||
|
public class ByteBufferFrameManager extends FrameManager<byte[]> { |
||||||
|
|
||||||
|
/** |
||||||
|
* Receives callbacks on buffer availability |
||||||
|
* (when a Frame is released, we reuse its buffer). |
||||||
|
*/ |
||||||
|
public interface BufferCallback { |
||||||
|
void onBufferAvailable(@NonNull byte[] buffer); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* In this mode, we have a {@link #mBufferCallback} and dispatch |
||||||
|
* new buffers to the callback. |
||||||
|
*/ |
||||||
|
private final static int BUFFER_MODE_DISPATCH = 0; |
||||||
|
|
||||||
|
/** |
||||||
|
* In this mode, we have a {@link #mBufferQueue} where we store |
||||||
|
* buffers and only dispatch when requested. |
||||||
|
*/ |
||||||
|
private final static int BUFFER_MODE_ENQUEUE = 1; |
||||||
|
|
||||||
|
private LinkedBlockingQueue<byte[]> mBufferQueue; |
||||||
|
private BufferCallback mBufferCallback; |
||||||
|
private final int mBufferMode; |
||||||
|
|
||||||
|
/** |
||||||
|
* Construct a new frame manager. |
||||||
|
* The construction must be followed by an {@link #setUp(int, Size)} call |
||||||
|
* as soon as the parameters are known. |
||||||
|
* |
||||||
|
* @param poolSize the size of the backing pool. |
||||||
|
* @param callback a callback |
||||||
|
*/ |
||||||
|
public ByteBufferFrameManager(int poolSize, @Nullable BufferCallback callback) { |
||||||
|
super(poolSize, byte[].class); |
||||||
|
if (callback != null) { |
||||||
|
mBufferCallback = callback; |
||||||
|
mBufferMode = BUFFER_MODE_DISPATCH; |
||||||
|
} else { |
||||||
|
mBufferQueue = new LinkedBlockingQueue<>(poolSize); |
||||||
|
mBufferMode = BUFFER_MODE_ENQUEUE; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public void setUp(int format, @NonNull Size size) { |
||||||
|
super.setUp(format, size); |
||||||
|
int bytes = getFrameBytes(); |
||||||
|
for (int i = 0; i < getPoolSize(); i++) { |
||||||
|
if (mBufferMode == BUFFER_MODE_DISPATCH) { |
||||||
|
mBufferCallback.onBufferAvailable(new byte[bytes]); |
||||||
|
} else { |
||||||
|
mBufferQueue.offer(new byte[bytes]); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns a new byte buffer than can be filled. |
||||||
|
* This can only be called in {@link #BUFFER_MODE_ENQUEUE} mode! Where the frame |
||||||
|
* manager also holds a queue of the byte buffers. |
||||||
|
* |
||||||
|
* If not null, the buffer returned by this method can be filled and used to get |
||||||
|
* a new frame through {@link FrameManager#getFrame(Object, long, int)}. |
||||||
|
* |
||||||
|
* @return a buffer, or null |
||||||
|
*/ |
||||||
|
@Nullable |
||||||
|
public byte[] getBuffer() { |
||||||
|
if (mBufferMode != BUFFER_MODE_ENQUEUE) { |
||||||
|
throw new IllegalStateException("Can't call getBuffer() " + |
||||||
|
"when not in BUFFER_MODE_ENQUEUE."); |
||||||
|
} |
||||||
|
return mBufferQueue.poll(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Can be called if the buffer obtained by {@link #getBuffer()} |
||||||
|
* was not used to construct a frame, so it can be put back into the queue. |
||||||
|
* @param buffer a buffer |
||||||
|
*/ |
||||||
|
public void onBufferUnused(@NonNull byte[] buffer) { |
||||||
|
if (mBufferMode != BUFFER_MODE_ENQUEUE) { |
||||||
|
throw new IllegalStateException("Can't call onBufferUnused() " + |
||||||
|
"when not in BUFFER_MODE_ENQUEUE."); |
||||||
|
} |
||||||
|
|
||||||
|
if (isSetUp()) { |
||||||
|
mBufferQueue.offer(buffer); |
||||||
|
} else { |
||||||
|
LOG.w("onBufferUnused: buffer was returned but we're not set up anymore."); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void onFrameDataReleased(@NonNull byte[] data, boolean recycled) { |
||||||
|
if (recycled && data.length == getFrameBytes()) { |
||||||
|
if (mBufferMode == BUFFER_MODE_DISPATCH) { |
||||||
|
mBufferCallback.onBufferAvailable(data); |
||||||
|
} else { |
||||||
|
mBufferQueue.offer(data); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
@Override |
||||||
|
protected byte[] onCloneFrameData(@NonNull byte[] data) { |
||||||
|
byte[] clone = new byte[data.length]; |
||||||
|
System.arraycopy(data, 0, clone, 0, data.length); |
||||||
|
return clone; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Releases all frames controlled by this manager and |
||||||
|
* clears the pool. |
||||||
|
* In BUFFER_MODE_ENQUEUE, releases also all the buffers. |
||||||
|
*/ |
||||||
|
@Override |
||||||
|
public void release() { |
||||||
|
super.release(); |
||||||
|
if (mBufferMode == BUFFER_MODE_ENQUEUE) { |
||||||
|
mBufferQueue.clear(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
package com.otaliastudios.cameraview.frame; |
||||||
|
|
||||||
|
import android.media.Image; |
||||||
|
import android.os.Build; |
||||||
|
|
||||||
|
import androidx.annotation.NonNull; |
||||||
|
import androidx.annotation.RequiresApi; |
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.KITKAT) |
||||||
|
public class ImageFrameManager extends FrameManager<Image> { |
||||||
|
|
||||||
|
public ImageFrameManager(int poolSize) { |
||||||
|
super(poolSize, Image.class); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void onFrameDataReleased(@NonNull Image data, boolean recycled) { |
||||||
|
try { |
||||||
|
data.close(); |
||||||
|
} catch (Exception ignore) {} |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
@Override |
||||||
|
protected Image onCloneFrameData(@NonNull Image data) { |
||||||
|
throw new RuntimeException("Cannot freeze() an Image Frame. " + |
||||||
|
"Please consider using the frame synchronously in your process() method, " + |
||||||
|
"which also gives better performance."); |
||||||
|
} |
||||||
|
} |
@ -1,100 +0,0 @@ |
|||||||
package com.otaliastudios.cameraview.internal.utils; |
|
||||||
|
|
||||||
import android.graphics.ImageFormat; |
|
||||||
import android.media.Image; |
|
||||||
|
|
||||||
import java.nio.ByteBuffer; |
|
||||||
|
|
||||||
import androidx.annotation.NonNull; |
|
||||||
import androidx.annotation.RequiresApi; |
|
||||||
|
|
||||||
/** |
|
||||||
* Conversions for {@link android.media.Image}s into byte arrays. |
|
||||||
*/ |
|
||||||
@RequiresApi(19) |
|
||||||
public class ImageHelper { |
|
||||||
|
|
||||||
/** |
|
||||||
* From https://stackoverflow.com/a/52740776/4288782 .
|
|
||||||
* The result array should have a size that is at least 3/2 * w * h. |
|
||||||
* This is correctly computed by {@link com.otaliastudios.cameraview.frame.FrameManager}. |
|
||||||
* |
|
||||||
* @param image input image |
|
||||||
* @param result output array |
|
||||||
*/ |
|
||||||
public static void convertToNV21(@NonNull Image image, @NonNull byte[] result) { |
|
||||||
if (image.getFormat() != ImageFormat.YUV_420_888) { |
|
||||||
throw new IllegalStateException("CAn only convert from YUV_420_888."); |
|
||||||
} |
|
||||||
int width = image.getWidth(); |
|
||||||
int height = image.getHeight(); |
|
||||||
int ySize = width * height; |
|
||||||
int uvSize = width * height / 4; |
|
||||||
|
|
||||||
ByteBuffer yBuffer = image.getPlanes()[0].getBuffer(); // Y
|
|
||||||
ByteBuffer uBuffer = image.getPlanes()[1].getBuffer(); // U
|
|
||||||
ByteBuffer vBuffer = image.getPlanes()[2].getBuffer(); // V
|
|
||||||
|
|
||||||
int rowStride = image.getPlanes()[0].getRowStride(); |
|
||||||
|
|
||||||
if (image.getPlanes()[0].getPixelStride() != 1) { |
|
||||||
throw new AssertionError("Something wrong in convertToNV21"); |
|
||||||
} |
|
||||||
|
|
||||||
int pos = 0; |
|
||||||
|
|
||||||
if (rowStride == width) { // likely
|
|
||||||
yBuffer.get(result, 0, ySize); |
|
||||||
pos += ySize; |
|
||||||
} |
|
||||||
else { |
|
||||||
int yBufferPos = width - rowStride; // not an actual position
|
|
||||||
for (; pos<ySize; pos+=width) { |
|
||||||
yBufferPos += rowStride - width; |
|
||||||
yBuffer.position(yBufferPos); |
|
||||||
yBuffer.get(result, pos, width); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
rowStride = image.getPlanes()[2].getRowStride(); |
|
||||||
int pixelStride = image.getPlanes()[2].getPixelStride(); |
|
||||||
|
|
||||||
if (rowStride != image.getPlanes()[1].getRowStride()) { |
|
||||||
throw new AssertionError("Something wrong in convertToNV21"); |
|
||||||
} |
|
||||||
if (pixelStride != image.getPlanes()[1].getPixelStride()) { |
|
||||||
throw new AssertionError("Something wrong in convertToNV21"); |
|
||||||
} |
|
||||||
|
|
||||||
if (pixelStride == 2 && rowStride == width && uBuffer.get(0) == vBuffer.get(1)) { |
|
||||||
// maybe V an U planes overlap as per NV21, which means vBuffer[1]
|
|
||||||
// is alias of uBuffer[0]
|
|
||||||
byte savePixel = vBuffer.get(1); |
|
||||||
vBuffer.put(1, (byte)0); |
|
||||||
if (uBuffer.get(0) == 0) { |
|
||||||
vBuffer.put(1, (byte)255); |
|
||||||
//noinspection ConstantConditions
|
|
||||||
if (uBuffer.get(0) == 255) { |
|
||||||
vBuffer.put(1, savePixel); |
|
||||||
vBuffer.get(result, ySize, uvSize); |
|
||||||
return; // shortcut
|
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// unfortunately, the check failed. We must save U and V pixel by pixel
|
|
||||||
vBuffer.put(1, savePixel); |
|
||||||
} |
|
||||||
|
|
||||||
// other optimizations could check if (pixelStride == 1) or (pixelStride == 2),
|
|
||||||
// but performance gain would be less significant
|
|
||||||
|
|
||||||
for (int row=0; row<height/2; row++) { |
|
||||||
for (int col=0; col<width/2; col++) { |
|
||||||
int vuPos = col*pixelStride + row*rowStride; |
|
||||||
result[pos++] = vBuffer.get(vuPos); |
|
||||||
result[pos++] = uBuffer.get(vuPos); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
Loading…
Reference in new issue