Recycle buffers and Frame instances (#94)
parent
04cf364b49
commit
a31dd1d57c
@ -0,0 +1,93 @@ |
|||||||
|
package com.otaliastudios.cameraview; |
||||||
|
|
||||||
|
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue; |
||||||
|
|
||||||
|
/** |
||||||
|
* This class manages the allocation of buffers and Frame objects. |
||||||
|
* We are interested in both recycling byte[] buffers so they are not allocated for each |
||||||
|
* preview frame, and in recycling Frame instances (so we don't instantiate a lot). |
||||||
|
* |
||||||
|
* For this, we keep a mPoolSize integer that defines the size of instances to keep. |
||||||
|
* Whether this does make sense, it depends on how slow the frame processors are. |
||||||
|
* If they are very slow, it is possible that some frames will be skipped. |
||||||
|
* |
||||||
|
* - byte[] buffer pool: |
||||||
|
* this is not kept here, because Camera1 internals already have one that we can't control, but |
||||||
|
* it should be OK. The only thing we do is allocate mPoolSize buffers when requested. |
||||||
|
* - Frame pool: |
||||||
|
* We keep a list of mPoolSize recycled instances, to be reused when a new buffer is available. |
||||||
|
*/ |
||||||
|
class FrameManager { |
||||||
|
|
||||||
|
interface BufferCallback { |
||||||
|
void onBufferAvailable(byte[] buffer); |
||||||
|
} |
||||||
|
|
||||||
|
private int mPoolSize; |
||||||
|
private int mBufferSize; |
||||||
|
private BufferCallback mCallback; |
||||||
|
private LinkedBlockingQueue<Frame> mQueue; |
||||||
|
|
||||||
|
FrameManager(int poolSize, BufferCallback callback) { |
||||||
|
mPoolSize = poolSize; |
||||||
|
mCallback = callback; |
||||||
|
mQueue = new LinkedBlockingQueue<>(mPoolSize); |
||||||
|
mBufferSize = -1; |
||||||
|
} |
||||||
|
|
||||||
|
void release() { |
||||||
|
for (Frame frame : mQueue) { |
||||||
|
frame.releaseManager(); |
||||||
|
frame.release(); |
||||||
|
} |
||||||
|
mQueue.clear(); |
||||||
|
mBufferSize = -1; |
||||||
|
} |
||||||
|
|
||||||
|
void onFrameReleased(Frame frame) { |
||||||
|
byte[] buffer = frame.getData(); |
||||||
|
boolean willRecycle = mQueue.offer(frame); |
||||||
|
if (!willRecycle) { |
||||||
|
frame.releaseManager(); |
||||||
|
} |
||||||
|
if (buffer != null && mCallback != null) { |
||||||
|
int currSize = buffer.length; |
||||||
|
int reqSize = mBufferSize; |
||||||
|
if (currSize == reqSize) { |
||||||
|
mCallback.onBufferAvailable(buffer); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns a new Frame for the given data. This must be called |
||||||
|
* - after {@link #allocate(int, Size)}, which sets the buffer size |
||||||
|
* - after the byte buffer given by allocate() has been filled. |
||||||
|
* If this is called X times in a row without releasing frames, it will allocate |
||||||
|
* X frames and that's bad. Callers must wait for the preview buffer to be available. |
||||||
|
* |
||||||
|
* In Camera1, this is always respected thanks to its internals. |
||||||
|
* |
||||||
|
* @return a new frame |
||||||
|
*/ |
||||||
|
Frame getFrame(byte[] data, long time, int rotation, Size previewSize, int previewFormat) { |
||||||
|
Frame frame = mQueue.poll(); |
||||||
|
if (frame == null) frame = new Frame(this); |
||||||
|
frame.set(data, time, rotation, previewSize, previewFormat); |
||||||
|
return frame; |
||||||
|
} |
||||||
|
|
||||||
|
int allocate(int bitsPerPixel, Size previewSize) { |
||||||
|
mBufferSize = getBufferSize(bitsPerPixel, previewSize); |
||||||
|
for (int i = 0; i < mPoolSize; i++) { |
||||||
|
mCallback.onBufferAvailable(new byte[mBufferSize]); |
||||||
|
} |
||||||
|
return mBufferSize; |
||||||
|
} |
||||||
|
|
||||||
|
private int getBufferSize(int bitsPerPixel, Size previewSize) { |
||||||
|
long sizeInBits = previewSize.getHeight() * previewSize.getWidth() * bitsPerPixel; |
||||||
|
return (int) Math.ceil(sizeInBits / 8.0d) + 1; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,124 @@ |
|||||||
|
package com.otaliastudios.cameraview; |
||||||
|
|
||||||
|
|
||||||
|
import android.graphics.ImageFormat; |
||||||
|
|
||||||
|
import org.junit.After; |
||||||
|
import org.junit.Before; |
||||||
|
import org.junit.Test; |
||||||
|
import org.mockito.invocation.InvocationOnMock; |
||||||
|
import org.mockito.stubbing.Answer; |
||||||
|
|
||||||
|
import static org.junit.Assert.assertArrayEquals; |
||||||
|
import static org.junit.Assert.assertEquals; |
||||||
|
import static org.junit.Assert.assertNotEquals; |
||||||
|
import static org.junit.Assert.assertNull; |
||||||
|
import static org.junit.Assert.assertTrue; |
||||||
|
import static org.mockito.Matchers.any; |
||||||
|
import static org.mockito.Mockito.doAnswer; |
||||||
|
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; |
||||||
|
import static org.mockito.Mockito.when; |
||||||
|
|
||||||
|
public class FrameManagerTest { |
||||||
|
|
||||||
|
private FrameManager.BufferCallback callback; |
||||||
|
|
||||||
|
@Before |
||||||
|
public void setUp() { |
||||||
|
callback = mock(FrameManager.BufferCallback.class); |
||||||
|
} |
||||||
|
|
||||||
|
@After |
||||||
|
public void tearDown() { |
||||||
|
callback = null; |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testAllocate() { |
||||||
|
FrameManager manager = new FrameManager(1, callback); |
||||||
|
manager.allocate(4, new Size(50, 50)); |
||||||
|
verify(callback, times(1)).onBufferAvailable(any(byte[].class)); |
||||||
|
reset(callback); |
||||||
|
|
||||||
|
manager = new FrameManager(5, callback); |
||||||
|
manager.allocate(4, new Size(50, 50)); |
||||||
|
verify(callback, times(5)).onBufferAvailable(any(byte[].class)); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testFrameRecycling() { |
||||||
|
// A 1-pool manager will always recycle the same frame.
|
||||||
|
FrameManager manager = new FrameManager(1, callback); |
||||||
|
manager.allocate(4, new Size(50, 50)); |
||||||
|
|
||||||
|
Frame first = manager.getFrame(null, 0, 0, null, 0); |
||||||
|
first.release(); |
||||||
|
|
||||||
|
Frame second = manager.getFrame(null, 0, 0, null, 0); |
||||||
|
second.release(); |
||||||
|
|
||||||
|
assertEquals(first, second); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testOnFrameReleased_nullBuffer() { |
||||||
|
FrameManager manager = new FrameManager(1, callback); |
||||||
|
manager.allocate(4, new Size(50, 50)); |
||||||
|
reset(callback); |
||||||
|
|
||||||
|
Frame frame = manager.getFrame(null, 0, 0, null, 0); |
||||||
|
manager.onFrameReleased(frame); |
||||||
|
verify(callback, never()).onBufferAvailable(frame.getData()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testOnFrameReleased_sameLength() { |
||||||
|
FrameManager manager = new FrameManager(1, callback); |
||||||
|
int length = manager.allocate(4, new Size(50, 50)); |
||||||
|
|
||||||
|
// A camera preview frame comes. Request a frame.
|
||||||
|
byte[] picture = new byte[length]; |
||||||
|
Frame frame = manager.getFrame(picture, 0, 0, null, 0); |
||||||
|
|
||||||
|
// Release the frame and ensure that onBufferAvailable is called.
|
||||||
|
reset(callback); |
||||||
|
manager.onFrameReleased(frame); |
||||||
|
verify(callback, times(1)).onBufferAvailable(picture); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testOnFrameReleased_differentLength() { |
||||||
|
FrameManager manager = new FrameManager(1, callback); |
||||||
|
int length = manager.allocate(4, new Size(50, 50)); |
||||||
|
|
||||||
|
// A camera preview frame comes. Request a frame.
|
||||||
|
byte[] picture = new byte[length]; |
||||||
|
Frame frame = manager.getFrame(picture, 0, 0, null, 0); |
||||||
|
|
||||||
|
// Don't release the frame. Change the allocation size.
|
||||||
|
manager.allocate(2, 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); |
||||||
|
reset(callback); |
||||||
|
verify(callback, never()).onBufferAvailable(picture); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testRelease() { |
||||||
|
FrameManager manager = new FrameManager(1, callback); |
||||||
|
int length = manager.allocate(4, new Size(50, 50)); |
||||||
|
Frame first = manager.getFrame(new byte[length], 0, 0, null, 0); |
||||||
|
first.release(); // Store this frame in the queue.
|
||||||
|
|
||||||
|
// Release the whole manager and ensure it clears the frame.
|
||||||
|
manager.release(); |
||||||
|
assertNull(first.getData()); |
||||||
|
assertNull(first.mManager); |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue