Frame processors (#82)
* Create the Frame class * Implement callback dispatcher * Dispatch actual preview frames * Add docs * Readme nits * Don't leak processors * Rename clear() to release() * Add preview format and Size * Readme nits againpull/83/head
parent
399e0e4d76
commit
e40f93acfb
@ -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; |
||||||
|
} |
||||||
|
} |
@ -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); |
||||||
|
} |
@ -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()); |
||||||
|
} |
||||||
|
|
||||||
|
} |
Loading…
Reference in new issue