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