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