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