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