diff --git a/README.md b/README.md
index 4b3a36ed..1583a407 100644
--- a/README.md
+++ b/README.md
@@ -39,11 +39,12 @@ See below for a [list of what was done](#roadmap) and [licensing info](#contribu
- Take high-resolution pictures with `capturePicture`
- Take quick snapshots as a freeze frame of the preview with `captureSnapshot` (similar to Snapchat and Instagram)
- Control HDR, flash, zoom, white balance, exposure correction and more
+- **Frame processing** support
- **Metadata** support for pictures and videos
- Automatically detected orientation tags
- Plug in location tags with `setLocation()` API
- `CameraUtils` to help with Bitmaps and orientations
-- Lightweight, no dependencies, just support `ExifInterface`
+- **Lightweight**, no dependencies, just support `ExifInterface`
- Works down to API level 15
# Docs
@@ -57,12 +58,12 @@ See below for a [list of what was done](#roadmap) and [licensing info](#contribu
- [Center Inside](#center-inside)
- [Center Crop](#center-crop)
- [Camera Controls](#camera-controls)
+- [Frame Processing](#frame-processing)
- [Other APIs](#other-apis)
- [Permissions Behavior](#permissions-behavior)
-- [Manifest file](#manifest-file)
- [Logging](#logging)
-- [Roadmap](#roadmap)
- [Device-specific issues](#device-specific-issues)
+- [Roadmap](#roadmap)
## Usage
@@ -437,6 +438,43 @@ cameraView.setPlaySounds(true);
cameraView.setPlaySounds(false);
```
+## Frame Processing
+
+We support frame processors that will receive data from the camera preview stream:
+
+```java
+cameraView.addFrameProcessor(new FrameProcessor() {
+ @Override
+ @WorkerThread
+ public void process(Frame frame) {
+ byte[] data = frame.getData();
+ int rotation = frame.getRotation();
+ long time = frame.getTime();
+ Size size = frame.getSize();
+ int format = frame.getFormat();
+ // Process...
+ }
+}
+```
+
+For your convenience, the `FrameProcessor` method is run in a background thread so you can do your job
+in a synchronous fashion. Once the process method returns, internally we will re-use the `Frame` instance and
+apply new data to it. So:
+
+- you can do your job synchronously in the `process()` method
+- if you must hold the `Frame` instance longer, use `frame = frame.freeze()` to get a frozen instance
+ that will not be affected
+
+|Frame API|Type|Description|
+|---------|----|-----------|
+|`frame.getData()`|`byte[]`|The current preview frame, in its original orientation.|
+|`frame.getTime()`|`long`|The preview timestamp, in `System.currentTimeMillis()` reference.|
+|`frame.getRotation()`|`int`|The rotation that should be applied to the byte array in order to see what the user sees.|
+|`frame.getSize()`|`Size`|The frame size, before any rotation is applied, to access data.|
+|`frame.getFormat()`|`int`|The frame `ImageFormat`. This will always be `ImageFormat.NV21` for now.|
+|`frame.freeze()`|`Frame`|Clones this frame and makes it immutable. Can be expensive because requires copying the byte array.|
+|`frame.release()`|`-`|Disposes the content of this frame. Should be used on frozen frames to release memory.|
+
## Other APIs
Other APIs not mentioned above are provided, and are well documented and commented in code.
@@ -461,7 +499,7 @@ Other APIs not mentioned above are provided, and are well documented and comment
|`getSnapshotSize()`|Returns `getPreviewSize()`, since a snapshot is a preview frame.|
|`getPictureSize()`|Returns the size of the output picture. The aspect ratio is consistent with `getPreviewSize()`.|
-Take also a look at public methods in `CameraUtils`, `CameraOptions`, `ExtraProperties`, `CameraLogger`.
+Take also a look at public methods in `CameraUtils`, `CameraOptions`, `ExtraProperties`.
## Permissions behavior
@@ -470,13 +508,13 @@ Take also a look at public methods in `CameraUtils`, `CameraOptions`, `ExtraProp
- `android.permission.CAMERA` : required for capturing pictures and videos
- `android.permission.RECORD_AUDIO` : required for capturing videos with `Audio.ON` (the default)
-You can handle permissions yourself and then call `CameraView.start()` once they are acquired. If they are not, `CameraView` will request permissions to the user based on whether they are needed. In that case, you can restart the camera if you have a successful response from `onRequestPermissionResults()`.
+### Declaration
-## Manifest file
+The library manifest file declares the `android.permission.CAMERA` permission, but not the audio one.
+This means that:
-The library manifest file is not strict and only asks for camera permissions. This means that:
-
-- If you wish to record videos with `Audio.ON` (the default), you should also add `android.permission.RECORD_AUDIO` to required permissions
+- If you wish to record videos with `Audio.ON` (the default), you should also add
+ `android.permission.RECORD_AUDIO` to required permissions
```xml
@@ -490,7 +528,18 @@ The library manifest file is not strict and only asks for camera permissions. Th
android:required="true"/>
```
-If you don't request this feature, you can use `CameraUtils.hasCameras()` to detect if current device has cameras, and then start the camera view.
+If you don't request this feature, you can use `CameraUtils.hasCameras()` to detect if current
+device has cameras, and then start the camera view.
+
+### Handling
+
+On Marshmallow+, the user must explicitly approve our permissions. You can
+
+- handle permissions yourself and then call `cameraView.start()` once they are acquired
+- or call `cameraView.start()` anyway: `CameraView` will present a permission request to the user based on
+ whether they are needed or not with the current configuration.
+
+In the second case, you should restart the camera if you have a successful response from `onRequestPermissionResults()`.
## Logging
@@ -512,9 +561,21 @@ CameraLogger.registerLogger(new Logger() {
Make sure you enable the logger using `CameraLogger.setLogLevel(@LogLevel int)`. The default will only
log error events.
+## Device-specific issues
+
+There are a couple of known issues if you are working with certain devices. The emulator is one of
+the most tricky in this sense.
+
+- Devices, or activities, with hardware acceleration turned off: this can be the case with emulators.
+ In this case we will use SurfaceView as our surface provider. That is intrinsically flawed and can't
+ deal with all we want to do here (runtime layout changes, scaling, etc.). So, nothing to do in this case.
+- Devices with no support for MediaRecorder: the emulator does not support it, officially. This means
+ that video/audio recording is flawed. Again, not our fault.
+
## Roadmap
-This is what was done since the library was forked. I have kept the original structure, but practically all the code was changed.
+This is what was done since the library was forked. I have kept the original structure, but practically
+all the code was changed.
- *a huge number of serious bugs fixed*
- *decent orientation support for both pictures and videos*
@@ -542,28 +603,18 @@ This is what was done since the library was forked. I have kept the original str
- *Tests!*
- *`CameraLogger` APIs for logging and bug reports*
- *Better threading, start() in worker thread and callbacks in UI*
+- *Frame processor support*
+- *inject external loggers*
These are still things that need to be done, off the top of my head:
- [ ] `Camera2` integration
-- [ ] check onPause / onStop / onSaveInstanceState consistency
- [ ] add a `setPreferredAspectRatio` API to choose the capture size. Preview size will adapt, and then, if let free, the CameraView will adapt as well
- [ ] animate grid lines similar to stock camera app
- [ ] add onRequestPermissionResults for easy permission callback
- [ ] better error handling, maybe with a onError(e) method in the public listener, or have each public method return a boolean
- [ ] decent code coverage
-## Device-specific issues
-
-There are a couple of known issues if you are working with certain devices. The emulator is one of
-the most tricky in this sense.
-
-- Devices, or activities, with hardware acceleration turned off: this can be the case with emulators.
- In this case we will use SurfaceView as our surface provider. That is intrinsically flawed and can't
- deal with all we want to do here (runtime layout changes, scaling, etc.). So, nothing to do in this case.
-- Devices with no support for MediaRecorder: the emulator does not support it, officially. This means
- that video/audio recording is flawed. Again, not our fault.
-
# Contributing and licenses
The original project which served as a starting point for this library,
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraCallbacksTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraCallbacksTest.java
index f735fbe9..b6f4e871 100644
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraCallbacksTest.java
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraCallbacksTest.java
@@ -38,6 +38,7 @@ public class CameraCallbacksTest extends BaseTest {
private CameraView camera;
private CameraListener listener;
+ private FrameProcessor processor;
private MockCameraController mockController;
private MockCameraPreview mockPreview;
private Task task;
@@ -50,6 +51,7 @@ public class CameraCallbacksTest extends BaseTest {
public void run() {
Context context = context();
listener = mock(CameraListener.class);
+ processor = mock(FrameProcessor.class);
camera = new CameraView(context) {
@Override
protected CameraController instantiateCameraController(CameraCallbacks callbacks) {
@@ -70,6 +72,7 @@ public class CameraCallbacksTest extends BaseTest {
};
camera.instantiatePreview();
camera.addCameraListener(listener);
+ camera.addFrameProcessor(processor);
task = new Task<>();
task.listen();
}
@@ -294,4 +297,13 @@ public class CameraCallbacksTest extends BaseTest {
Bitmap bitmap = BitmapFactory.decodeByteArray(result, 0, result.length);
return new int[]{ bitmap.getWidth(), bitmap.getHeight() };
}
+
+ @Test
+ public void testProcessFrame() {
+ completeTask().when(processor).process(any(Frame.class));
+ camera.mCameraCallbacks.dispatchFrame(new byte[]{0, 1, 2, 3}, 1000, 90, new Size(1, 1), 0);
+
+ assertNotNull(task.await(200));
+ verify(processor, times(1)).process(any(Frame.class));
+ }
}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
index ed987fa6..3f274413 100644
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
@@ -3,6 +3,7 @@ package com.otaliastudios.cameraview;
import android.content.Context;
import android.location.Location;
+import android.support.annotation.NonNull;
import android.support.test.filters.MediumTest;
import android.support.test.runner.AndroidJUnit4;
import android.view.MotionEvent;
@@ -551,5 +552,49 @@ public class CameraViewTest extends BaseTest {
//endregion
+ //region Lists of listeners and processors
+
+ @Test
+ public void testCameraListenerList() {
+ assertTrue(cameraView.mListeners.isEmpty());
+
+ CameraListener listener = new CameraListener() {};
+ cameraView.addCameraListener(listener);
+ assertEquals(cameraView.mListeners.size(), 1);
+
+ cameraView.removeCameraListener(listener);
+ assertEquals(cameraView.mListeners.size(), 0);
+
+ cameraView.addCameraListener(listener);
+ cameraView.addCameraListener(listener);
+ assertEquals(cameraView.mListeners.size(), 2);
+
+ cameraView.clearCameraListeners();
+ assertTrue(cameraView.mListeners.isEmpty());
+ }
+
+ @Test
+ public void testFrameProcessorsList() {
+ assertTrue(cameraView.mFrameProcessors.isEmpty());
+
+ FrameProcessor processor = new FrameProcessor() {
+ public void process(@NonNull Frame frame) {}
+ };
+ cameraView.addFrameProcessor(processor);
+ assertEquals(cameraView.mFrameProcessors.size(), 1);
+
+ cameraView.removeFrameProcessor(processor);
+ assertEquals(cameraView.mFrameProcessors.size(), 0);
+
+ cameraView.addFrameProcessor(processor);
+ cameraView.addFrameProcessor(processor);
+ assertEquals(cameraView.mFrameProcessors.size(), 2);
+
+ cameraView.clearFrameProcessors();
+ assertTrue(cameraView.mFrameProcessors.isEmpty());
+ }
+
+ //endregion
+
// TODO: test permissions
}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java
index 7f798576..7a855e4d 100644
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java
@@ -504,4 +504,24 @@ public class IntegrationTest extends BaseTest {
assertTrue(b.getHeight() == size.getHeight() || b.getHeight() == size.getWidth());
}
+ //endregion
+
+ //region Frame Processing
+
+ @Test
+ public void testFrameProcessing() throws Exception {
+ FrameProcessor processor = mock(FrameProcessor.class);
+ camera.addFrameProcessor(processor);
+
+ camera.start();
+ waitForOpen(true);
+
+ // Expect 30 frames
+ CountDownLatch latch = new CountDownLatch(30);
+ doCountDown(latch).when(processor).process(any(Frame.class));
+ boolean did = latch.await(4, TimeUnit.SECONDS);
+ assertTrue(did);
+ }
+
+ //endregion
}
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java
index 0537cf9f..a55805e5 100644
--- a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java
+++ b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java
@@ -21,7 +21,7 @@ import java.util.List;
@SuppressWarnings("deprecation")
-class Camera1 extends CameraController {
+class Camera1 extends CameraController implements Camera.PreviewCallback {
private static final String TAG = Camera1.class.getSimpleName();
private static final CameraLogger LOG = CameraLogger.create(TAG);
@@ -143,10 +143,14 @@ class Camera1 extends CameraController {
);
synchronized (mLock) {
Camera.Parameters params = mCamera.getParameters();
+ mPreviewFormat = params.getPreviewFormat();
params.setPreviewSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); // <- not allowed during preview
params.setPictureSize(mCaptureSize.getWidth(), mCaptureSize.getHeight()); // <- allowed
mCamera.setParameters(params);
}
+
+ mCamera.setPreviewCallback(this); // Frame processing
+
LOG.i("setup:", "Starting preview with startPreview().");
mCamera.startPreview();
LOG.i("setup:", "Started preview with startPreview().");
@@ -475,7 +479,6 @@ class Camera1 extends CameraController {
// Got to rotate the preview frame, since byte[] data here does not include
// EXIF tags automatically set by camera. So either we add EXIF, or we rotate.
// Adding EXIF to a byte array, unfortunately, is hard.
- Camera.Parameters params = mCamera.getParameters();
final int sensorToDevice = computeExifRotation();
final int sensorToDisplay = computeSensorToDisplayOffset();
final boolean exifFlip = computeExifFlip();
@@ -484,7 +487,7 @@ class Camera1 extends CameraController {
final int preHeight = mPreviewSize.getHeight();
final int postWidth = flip ? preHeight : preWidth;
final int postHeight = flip ? preWidth : preHeight;
- final int format = params.getPreviewFormat();
+ final int format = mPreviewFormat;
WorkerHandler.run(new Runnable() {
@Override
public void run() {
@@ -496,11 +499,21 @@ class Camera1 extends CameraController {
mIsCapturingImage = false;
}
});
+ mCamera.setPreviewCallback(Camera1.this);
}
});
return true;
}
+ @Override
+ public void onPreviewFrame(byte[] data, Camera camera) {
+ mCameraCallbacks.dispatchFrame(data,
+ System.currentTimeMillis(),
+ computeExifRotation(),
+ mPreviewSize,
+ mPreviewFormat);
+ }
+
@Override
boolean shouldFlipSizes() {
int offset = computeSensorToDisplayOffset();
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java
index be221965..cad2eeba 100644
--- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java
+++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java
@@ -32,6 +32,7 @@ abstract class CameraController implements CameraPreview.SurfaceCallback {
protected Size mCaptureSize;
protected Size mPreviewSize;
+ protected int mPreviewFormat;
protected ExtraProperties mExtraProperties;
protected CameraOptions mOptions;
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
index 82772168..8172e6d9 100644
--- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
+++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
@@ -61,8 +61,9 @@ public class CameraView extends FrameLayout {
private CameraPreview mCameraPreview;
private OrientationHelper mOrientationHelper;
private CameraController mCameraController;
- private ArrayList mListeners = new ArrayList<>(2);
private MediaActionSound mSound;
+ /* for tests */ ArrayList mListeners = new ArrayList<>(2);
+ /* for tests */ ArrayList mFrameProcessors = new ArrayList<>(1);
// Views
GridLinesLayout mGridLinesLayout;
@@ -74,6 +75,7 @@ public class CameraView extends FrameLayout {
// Threading
private Handler mUiHandler;
private WorkerHandler mWorkerHandler;
+ private WorkerHandler mFrameProcessorsHandler;
public CameraView(@NonNull Context context) {
super(context, null);
@@ -119,6 +121,7 @@ public class CameraView extends FrameLayout {
mCameraController = instantiateCameraController(mCameraCallbacks);
mUiHandler = new Handler(Looper.getMainLooper());
mWorkerHandler = WorkerHandler.get("CameraViewWorker");
+ mFrameProcessorsHandler = WorkerHandler.get("FrameProcessorsWorker");
// Views
mGridLinesLayout = new GridLinesLayout(context);
@@ -577,9 +580,14 @@ public class CameraView extends FrameLayout {
mCameraController.stop();
}
+
+ /**
+ * Destroys this instance, releasing immediately
+ * the camera resource.
+ */
public void destroy() {
- // TODO: this is not strictly needed
- clearCameraListeners(); // Release
+ clearCameraListeners();
+ clearFrameProcessors();
mCameraController.stopImmediately();
}
@@ -1092,6 +1100,40 @@ public class CameraView extends FrameLayout {
}
+ /**
+ * Adds a {@link FrameProcessor} instance to be notified of
+ * new frames in the preview stream.
+ *
+ * @param processor a frame processor.
+ */
+ public void addFrameProcessor(FrameProcessor processor) {
+ if (processor != null) {
+ mFrameProcessors.add(processor);
+ }
+ }
+
+
+ /**
+ * Remove a {@link FrameProcessor} that was previously registered.
+ *
+ * @param processor a frame processor
+ */
+ public void removeFrameProcessor(FrameProcessor processor) {
+ if (processor != null) {
+ mFrameProcessors.remove(processor);
+ }
+ }
+
+
+ /**
+ * Clears the list of {@link FrameProcessor} that have been registered
+ * to preview frames.
+ */
+ public void clearFrameProcessors() {
+ mFrameProcessors.clear();
+ }
+
+
/**
* Asks the camera to capture an image of the current scene.
* This will trigger {@link CameraListener#onPictureTaken(byte[])} if a listener
@@ -1310,6 +1352,7 @@ public class CameraView extends FrameLayout {
void dispatchOnFocusEnd(@Nullable Gesture trigger, boolean success, PointF where);
void dispatchOnZoomChanged(final float newValue, final PointF[] fingers);
void dispatchOnExposureCorrectionChanged(float newValue, float[] bounds, PointF[] fingers);
+ void dispatchFrame(byte[] frame, long time, int rotation, Size size, int previewFormat);
}
private class Callbacks implements CameraCallbacks {
@@ -1321,6 +1364,9 @@ public class CameraView extends FrameLayout {
private Integer mDisplayOffset;
private Integer mDeviceOrientation;
+ // Frame processing
+ private Frame mFrame;
+
Callbacks() {}
@Override
@@ -1557,71 +1603,29 @@ public class CameraView extends FrameLayout {
}
});
}
- }
-
- //endregion
-
- //region Deprecated
-
- /**
- * This does nothing.
- * @deprecated
- * @param focus no-op
- */
- @Deprecated
- public void setFocus(int focus) {
- }
-
-
- /**
- * This does nothing.
- * @return no-op
- * @deprecated
- */
- @Deprecated
- public int getFocus() {
- return 0;
- }
-
-
- /**
- * This does nothing.
- * @deprecated
- * @param method no-op
- */
- @Deprecated
- public void setCaptureMethod(int method) {}
-
-
- /**
- * This does nothing.
- * @deprecated
- * @param permissions no-op
- */
- @Deprecated
- public void setPermissionPolicy(int permissions) {}
+ @Override
+ public void dispatchFrame(final byte[] frame, final long time, final int rotation,
+ final Size size, final int previewFormat) {
+ mLogger.i("dispatchFrame", time, rotation, "processors:", mFrameProcessors.size());
+ if (mFrameProcessors.isEmpty()) return;
+ if (mFrame == null) mFrame = new Frame();
+ mFrameProcessorsHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mFrame.set(frame, time, rotation, size, previewFormat);
+ for (FrameProcessor processor : mFrameProcessors) {
+ processor.process(mFrame);
+ }
+ }
+ });
- /**
- * Sets the zoom mode for the current session.
- *
- * @param zoom no-op
- * @deprecated use {@link #mapGesture(Gesture, GestureAction)} to map zoom control to gestures
- */
- @Deprecated
- public void setZoomMode(int zoom) {
+ }
}
+ //endregion
- /**
- * Gets the current zoom mode.
- * @return no-op
- * @deprecated use {@link #mapGesture(Gesture, GestureAction)} to map zoom control to gestures
- */
- @Deprecated
- public int getZoomMode() {
- return 0;
- }
+ //region Deprecated
//endregion
}
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/Frame.java b/cameraview/src/main/java/com/otaliastudios/cameraview/Frame.java
new file mode 100644
index 00000000..f7bc677c
--- /dev/null
+++ b/cameraview/src/main/java/com/otaliastudios/cameraview/Frame.java
@@ -0,0 +1,106 @@
+package com.otaliastudios.cameraview;
+
+/**
+ * A preview frame to be processed by {@link FrameProcessor}s.
+ */
+public class Frame {
+
+ private byte[] mData = null;
+ private long mTime = -1;
+ private int mRotation = 0;
+ private Size mSize = null;
+ private int mFormat = -1;
+
+ Frame() {}
+
+ void set(byte[] data, long time, int rotation, Size size, int format) {
+ this.mData = data;
+ this.mTime = time;
+ this.mRotation = rotation;
+ this.mSize = size;
+ this.mFormat = format;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ // We want a super fast implementation here, do not compare arrays.
+ return obj instanceof Frame && ((Frame) obj).mTime == mTime;
+ }
+
+ /**
+ * Clones the frame, returning a frozen content that will not be overwritten.
+ * This can be kept or safely passed to other threads.
+ * Using freeze without clearing with {@link #release()} can result in memory leaks.
+ *
+ * @return a frozen Frame
+ */
+ public Frame freeze() {
+ byte[] data = new byte[mData.length];
+ System.arraycopy(mData, 0, data, 0, mData.length);
+ Frame other = new Frame();
+ other.set(data, mTime, mRotation, mSize, mFormat);
+ return other;
+ }
+
+ /**
+ * Disposes the contents of this frame. Can be useful for frozen frames
+ * that are not useful anymore.
+ */
+ public void release() {
+ mData = null;
+ mRotation = 0;
+ mTime = -1;
+ mSize = null;
+ mFormat = -1;
+ }
+
+ /**
+ * Returns the frame data.
+ * @return the frame data
+ */
+ public byte[] getData() {
+ return mData;
+ }
+
+ /**
+ * Returns the milliseconds epoch for this frame,
+ * in the {@link System#currentTimeMillis()} reference.
+ *
+ * @return time data
+ */
+ public long getTime() {
+ return mTime;
+ }
+
+ /**
+ * Returns the clock-wise rotation that should be applied on the data
+ * array, such that the resulting frame matches what the user is seeing
+ * on screen.
+ *
+ * @return clock-wise rotation
+ */
+ public int getRotation() {
+ return mRotation;
+ }
+
+ /**
+ * Returns the frame size.
+ *
+ * @return frame size
+ */
+ public Size getSize() {
+ return mSize;
+ }
+
+ /**
+ * Returns the data format, in one of the
+ * {@link android.graphics.ImageFormat} constants.
+ * This will always be {@link android.graphics.ImageFormat#NV21} for now.
+ *
+ * @return the data format
+ * @see android.graphics.ImageFormat
+ */
+ public int getFormat() {
+ return mFormat;
+ }
+}
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/FrameProcessor.java b/cameraview/src/main/java/com/otaliastudios/cameraview/FrameProcessor.java
new file mode 100644
index 00000000..1c3245ef
--- /dev/null
+++ b/cameraview/src/main/java/com/otaliastudios/cameraview/FrameProcessor.java
@@ -0,0 +1,24 @@
+package com.otaliastudios.cameraview;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.WorkerThread;
+
+/**
+ * A FrameProcessor will process {@link Frame}s coming from the camera preview.
+ * It must be passed to {@link CameraView#addFrameProcessor(FrameProcessor)}.
+ */
+public interface FrameProcessor {
+
+ /**
+ * Processes the given frame. The frame will hold the correct values only for the
+ * duration of this method. When it returns, the frame contents will be replaced.
+ *
+ * To keep working with the Frame in an async manner, please use {@link Frame#freeze()},
+ * which will return an immutable Frame. In that case you can pass / hold the frame for
+ * as long as you want, and then release its contents using {@link Frame#release()}.
+ *
+ * @param frame the new frame
+ */
+ @WorkerThread
+ void process(@NonNull Frame frame);
+}
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/Size.java b/cameraview/src/main/java/com/otaliastudios/cameraview/Size.java
index b6d33548..a3e2e217 100644
--- a/cameraview/src/main/java/com/otaliastudios/cameraview/Size.java
+++ b/cameraview/src/main/java/com/otaliastudios/cameraview/Size.java
@@ -7,7 +7,7 @@ public class Size implements Comparable {
private final int mWidth;
private final int mHeight;
- public Size(int width, int height) {
+ Size(int width, int height) {
mWidth = width;
mHeight = height;
}
diff --git a/cameraview/src/test/java/com/otaliastudios/cameraview/FrameTest.java b/cameraview/src/test/java/com/otaliastudios/cameraview/FrameTest.java
new file mode 100644
index 00000000..ce3755da
--- /dev/null
+++ b/cameraview/src/test/java/com/otaliastudios/cameraview/FrameTest.java
@@ -0,0 +1,78 @@
+package com.otaliastudios.cameraview;
+
+
+import android.graphics.ImageFormat;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+public class FrameTest {
+
+ @Test
+ public void testDefaults() {
+ Frame frame = new Frame();
+ assertEquals(frame.getTime(), -1);
+ assertEquals(frame.getFormat(), -1);
+ assertEquals(frame.getRotation(), 0);
+ assertNull(frame.getData());
+ assertNull(frame.getSize());
+ }
+
+ @Test
+ public void testEquals() {
+ Frame f1 = new Frame();
+ long time = 1000;
+ f1.set(null, time, 90, null, ImageFormat.NV21);
+ Frame f2 = new Frame();
+ f2.set(new byte[2], time, 0, new Size(10, 10), ImageFormat.NV21);
+ assertEquals(f1, f2);
+
+ f2.set(new byte[2], time + 1, 0, new Size(10, 10), ImageFormat.NV21);
+ assertNotEquals(f1, f2);
+ }
+
+ @Test
+ public void testRelease() {
+ Frame frame = new Frame();
+ frame.set(new byte[2], 1000, 90, new Size(10, 10), ImageFormat.NV21);
+ frame.release();
+
+ assertEquals(frame.getTime(), -1);
+ assertEquals(frame.getFormat(), -1);
+ assertEquals(frame.getRotation(), 0);
+ assertNull(frame.getData());
+ assertNull(frame.getSize());
+ }
+
+ @Test
+ public void testFreeze() {
+ Frame frame = new Frame();
+ byte[] data = new byte[]{0, 1, 5, 0, 7, 3, 4, 5};
+ long time = 1000;
+ int rotation = 90;
+ Size size = new Size(10, 10);
+ int format = ImageFormat.NV21;
+ frame.set(data, time, rotation, size, format);
+
+ Frame frozen = frame.freeze();
+ assertArrayEquals(data, frozen.getData());
+ assertEquals(time, frozen.getTime());
+ assertEquals(rotation, frozen.getRotation());
+ assertEquals(size, frozen.getSize());
+
+ // Mutate the first, ensure that frozen is not affected
+ frame.set(new byte[]{3, 2, 1}, 50, 180, new Size(1, 1), ImageFormat.JPEG);
+ assertArrayEquals(data, frozen.getData());
+ assertEquals(time, frozen.getTime());
+ assertEquals(rotation, frozen.getRotation());
+ assertEquals(size, frozen.getSize());
+ assertEquals(format, frozen.getFormat());
+ }
+
+}