pull/572/head
Mattia Iavarone 6 years ago
parent 6f4ef00754
commit 01d7109ee1
  1. 5
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegration2Test.java
  2. 26
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java
  3. 18
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/frame/FrameManagerTest.java
  4. 6
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/ScrollGestureFinderTest.java
  5. 28
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java
  6. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/frame/Frame.java
  7. 45
      cameraview/src/main/java/com/otaliastudios/cameraview/frame/FrameManager.java
  8. 4
      cameraview/src/test/java/com/otaliastudios/cameraview/frame/FrameTest.java

@ -27,4 +27,9 @@ public class CameraIntegration2Test extends CameraIntegrationTest {
protected Engine getEngine() { protected Engine getEngine() {
return Engine.CAMERA2; return Engine.CAMERA2;
} }
@Override
public void testFrameProcessing_afterVideo() throws Exception {
super.testFrameProcessing_afterVideo();
}
} }

@ -136,6 +136,7 @@ public abstract class CameraIntegrationTest extends BaseTest {
} }
} }
@SuppressWarnings("StatementWithEmptyBody")
private CameraOptions openSync(boolean expectSuccess) { private CameraOptions openSync(boolean expectSuccess) {
camera.open(); camera.open();
final Op<CameraOptions> open = new Op<>(true); final Op<CameraOptions> open = new Op<>(true);
@ -143,10 +144,11 @@ public abstract class CameraIntegrationTest extends BaseTest {
CameraOptions result = open.await(DELAY); CameraOptions result = open.await(DELAY);
if (expectSuccess) { if (expectSuccess) {
assertNotNull("Can open", result); assertNotNull("Can open", result);
// Extra wait for the bind state. // Extra wait for the bind and preview state, so we run tests in a fully operational
// TODO fix this and other while {} in this class in a more elegant way. // state. If we didn't do so, we could have null values, for example, in getPictureSize
//noinspection StatementWithEmptyBody // or in getSnapshotSize.
while (controller.getBindState() != CameraEngine.STATE_STARTED) {} while (controller.getBindState() != CameraEngine.STATE_STARTED) {}
while (controller.getPreviewState() != CameraEngine.STATE_STARTED) {}
} else { } else {
assertNull("Should not open", result); assertNull("Should not open", result);
} }
@ -194,6 +196,9 @@ public abstract class CameraIntegrationTest extends BaseTest {
video.listen(); video.listen();
result = video.await(DELAY); result = video.await(DELAY);
} }
// Sleep another 1000, because camera.isTakingVideo() might return false even
// if the result still has to be dispatched. Rare but could happen.
try { Thread.sleep(1000); } catch (InterruptedException ignore) {}
} }
// Now we should be OK. // Now we should be OK.
@ -684,14 +689,11 @@ public abstract class CameraIntegrationTest extends BaseTest {
assertEquals(1, latch.getCount()); assertEquals(1, latch.getCount());
} }
@SuppressWarnings("StatementWithEmptyBody")
@Test @Test
public void testCapturePicture_size() { public void testCapturePicture_size() {
openSync(true); openSync(true);
// PictureSize can still be null after opened.
// TODO be more elegant
while (camera.getPictureSize() == null) {}
Size size = camera.getPictureSize(); Size size = camera.getPictureSize();
assertNotNull(size);
camera.takePicture(); camera.takePicture();
PictureResult result = waitForPictureResult(true); PictureResult result = waitForPictureResult(true);
assertNotNull(result); assertNotNull(result);
@ -734,14 +736,11 @@ public abstract class CameraIntegrationTest extends BaseTest {
assertEquals(1, latch.getCount()); assertEquals(1, latch.getCount());
} }
@SuppressWarnings("StatementWithEmptyBody")
@Test @Test
public void testCaptureSnapshot_size() { public void testCaptureSnapshot_size() {
openSync(true); openSync(true);
// SnapshotSize can still be null after opened.
// TODO be more elegant
while (camera.getSnapshotSize() == null) {}
Size size = camera.getSnapshotSize(); Size size = camera.getSnapshotSize();
assertNotNull(size);
camera.takePictureSnapshot(); camera.takePictureSnapshot();
PictureResult result = waitForPictureResult(true); PictureResult result = waitForPictureResult(true);
@ -764,8 +763,8 @@ public abstract class CameraIntegrationTest extends BaseTest {
// Expect 30 frames // Expect 30 frames
CountDownLatch latch = new CountDownLatch(30); CountDownLatch latch = new CountDownLatch(30);
doCountDown(latch).when(mock).process(any(Frame.class)); doCountDown(latch).when(mock).process(any(Frame.class));
boolean did = latch.await(60, TimeUnit.SECONDS); boolean did = latch.await(15, TimeUnit.SECONDS);
assertTrue(did); assertTrue("Latch count should be 0: " + latch.getCount(), did);
} }
@Test @Test
@ -811,6 +810,7 @@ public abstract class CameraIntegrationTest extends BaseTest {
openSync(true); openSync(true);
takeVideoSync(true,4000); takeVideoSync(true,4000);
waitForVideoResult(true); waitForVideoResult(true);
assert30Frames(processor); assert30Frames(processor);
} }

@ -3,14 +3,18 @@ package com.otaliastudios.cameraview.frame;
import android.graphics.ImageFormat; import android.graphics.ImageFormat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.Matchers.any; import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
@ -18,7 +22,9 @@ import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
public class FrameManagerTest { @RunWith(AndroidJUnit4.class)
@SmallTest
public class FrameManagerTest extends BaseTest {
private FrameManager.BufferCallback callback; private FrameManager.BufferCallback callback;
@ -68,11 +74,11 @@ public class FrameManagerTest {
// Since frame1 is already taken and poolSize = 1, a new Frame is created. // Since frame1 is already taken and poolSize = 1, a new Frame is created.
Frame frame2 = manager.getFrame(new byte[length], 0, 0); Frame frame2 = manager.getFrame(new byte[length], 0, 0);
// Release the first frame so it goes back into the pool. // Release the first frame so it goes back into the pool.
manager.onFrameReleased(frame1); manager.onFrameReleased(frame1, frame1.getData());
reset(callback); reset(callback);
// Release the second. The pool is already full, so onBufferAvailable should not be called // Release the second. The pool is already full, so onBufferAvailable should not be called
// since this Frame instance will NOT be reused. // since this Frame instance will NOT be reused.
manager.onFrameReleased(frame2); manager.onFrameReleased(frame2, frame2.getData());
verify(callback, never()).onBufferAvailable(frame2.getData()); verify(callback, never()).onBufferAvailable(frame2.getData());
} }
@ -87,7 +93,7 @@ public class FrameManagerTest {
// Release the frame and ensure that onBufferAvailable is called. // Release the frame and ensure that onBufferAvailable is called.
reset(callback); reset(callback);
manager.onFrameReleased(frame); manager.onFrameReleased(frame, frame.getData());
verify(callback, times(1)).onBufferAvailable(picture); verify(callback, times(1)).onBufferAvailable(picture);
} }
@ -105,7 +111,7 @@ public class FrameManagerTest {
// Now release the old frame and ensure that onBufferAvailable is NOT called, // Now release the old frame and ensure that onBufferAvailable is NOT called,
// because the released data has wrong length. // because the released data has wrong length.
manager.onFrameReleased(frame); manager.onFrameReleased(frame, frame.getData());
reset(callback); reset(callback);
verify(callback, never()).onBufferAvailable(picture); verify(callback, never()).onBufferAvailable(picture);
} }

@ -21,6 +21,8 @@ import static org.junit.Assert.assertTrue;
@SmallTest @SmallTest
public class ScrollGestureFinderTest extends GestureFinderTest<ScrollGestureFinder> { public class ScrollGestureFinderTest extends GestureFinderTest<ScrollGestureFinder> {
private final static long WAIT = 2000; // 500 was too short
@Override @Override
protected ScrollGestureFinder createFinder(@NonNull GestureFinder.Controller controller) { protected ScrollGestureFinder createFinder(@NonNull GestureFinder.Controller controller) {
return new ScrollGestureFinder(controller); return new ScrollGestureFinder(controller);
@ -42,7 +44,7 @@ public class ScrollGestureFinderTest extends GestureFinderTest<ScrollGestureFind
touchOp.listen(); touchOp.listen();
touchOp.start(); touchOp.start();
onLayout().perform(swipeUp()); onLayout().perform(swipeUp());
Gesture found = touchOp.await(500); Gesture found = touchOp.await(WAIT);
assertNull(found); assertNull(found);
} }
@ -50,7 +52,7 @@ public class ScrollGestureFinderTest extends GestureFinderTest<ScrollGestureFind
touchOp.listen(); touchOp.listen();
touchOp.start(); touchOp.start();
onLayout().perform(scroll); onLayout().perform(scroll);
Gesture found = touchOp.await(500); Gesture found = touchOp.await(WAIT);
assertEquals(found, expected); assertEquals(found, expected);
// How will this move our parameter? // How will this move our parameter?

@ -727,12 +727,30 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
@Override @Override
protected void onStopVideo() { protected void onStopVideo() {
// When video ends, we have to restart the repeating request for TEMPLATE_PREVIEW,
// this time without the video recorder surface. We do this before stopping the
// recorder. If we stop first, the camera will try to fill an "abandoned" Surface
// and, on some devices with a poor internal implementation, this crashes. See #549
boolean isFullVideo = mVideoRecorder instanceof Full2VideoRecorder; boolean isFullVideo = mVideoRecorder instanceof Full2VideoRecorder;
if (isFullVideo) { if (isFullVideo) {
// Workaround for #549: when video ends we must stop the recorder and remove the recorder
// surface from camera outputs. On some devices, order matters. If we stop the recorder
// and AFTER send camera frames to it, the camera will try to fill the recorder "abandoned"
// Surface and on some devices with a poor internal implementation (LEGACY?) this crashes.
try {
mSession.stopRepeating();
mSession.abortCaptures();
} catch (CameraAccessException e) {
throw createCameraException(e);
}
}
super.onStopVideo();
}
@Override
public void onVideoResult(@Nullable VideoResult.Stub result, @Nullable Exception exception) {
boolean isFullVideo = mVideoRecorder instanceof Full2VideoRecorder;
super.onVideoResult(result, exception);
if (isFullVideo) {
// When video ends, we must restart the repeating request for TEMPLATE_PREVIEW,
// this time without the video recorder surface.
try { try {
createRepeatingRequestBuilder(CameraDevice.TEMPLATE_PREVIEW); createRepeatingRequestBuilder(CameraDevice.TEMPLATE_PREVIEW);
addRepeatingRequestBuilderSurfaces(); addRepeatingRequestBuilderSurfaces();
@ -741,7 +759,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
throw createCameraException(e); throw createCameraException(e);
} }
} }
super.onStopVideo();
} }
//endregion //endregion

@ -44,7 +44,7 @@ public class Frame {
if (!hasContent()) { if (!hasContent()) {
LOG.e("Frame is dead! time:", mTime, "lastTime:", mLastTime); LOG.e("Frame is dead! time:", mTime, "lastTime:", mLastTime);
throw new RuntimeException("You should not access a released frame. " + throw new RuntimeException("You should not access a released frame. " +
"If this frame was passed to a FrameProcessor, you can only use its contents synchronously," + "If this frame was passed to a FrameProcessor, you can only use its contents synchronously, " +
"for the duration of the process() method."); "for the duration of the process() method.");
} }
} }
@ -80,12 +80,16 @@ public class Frame {
public void release() { public void release() {
if (!hasContent()) return; if (!hasContent()) return;
LOG.v("Frame with time", mTime, "is being released."); LOG.v("Frame with time", mTime, "is being released.");
mManager.onFrameReleased(this); byte[] data = mData;
mData = null; mData = null;
mRotation = 0; mRotation = 0;
mTime = -1; mTime = -1;
mSize = null; mSize = null;
mFormat = -1; mFormat = -1;
// After the manager is notified, this frame instance can be taken by
// someone else, possibly from another thread. So this should be the
// last call in this method. If we null data after, we can have issues.
mManager.onFrameReleased(this, data);
} }
/** /**

@ -202,27 +202,6 @@ public class FrameManager {
return frame; return frame;
} }
/**
* Releases all frames controlled by this manager and
* clears the pool.
* In BUFFER_MODE_ENQUEUE, releases also all the buffers.
*/
public void release() {
if (!isSetUp()) {
LOG.w("release called twice. Ignoring.");
return;
}
LOG.i("release: Clearing the frame and buffer queue.");
mFrameQueue.clear();
if (mBufferMode == BUFFER_MODE_ENQUEUE) {
mBufferQueue.clear();
}
mBufferSize = -1;
mFrameSize = null;
mFrameFormat = -1;
}
/** /**
* Called by child frames when they are released. * Called by child frames when they are released.
* This might be called from old Frames that belong to an old 'setUp' * This might be called from old Frames that belong to an old 'setUp'
@ -231,12 +210,11 @@ public class FrameManager {
* *
* @param frame the released frame * @param frame the released frame
*/ */
void onFrameReleased(@NonNull Frame frame) { void onFrameReleased(@NonNull Frame frame, @NonNull byte[] buffer) {
if (!isSetUp()) return; if (!isSetUp()) return;
// If frame queue is full, let's drop everything. // If frame queue is full, let's drop everything.
// If frame queue accepts this frame, let's recycle the buffer as well. // If frame queue accepts this frame, let's recycle the buffer as well.
if (mFrameQueue.offer(frame)) { if (mFrameQueue.offer(frame)) {
byte[] buffer = frame.getData();
int currSize = buffer.length; int currSize = buffer.length;
int reqSize = mBufferSize; int reqSize = mBufferSize;
if (currSize == reqSize) { if (currSize == reqSize) {
@ -248,4 +226,25 @@ public class FrameManager {
} }
} }
} }
/**
* Releases all frames controlled by this manager and
* clears the pool.
* In BUFFER_MODE_ENQUEUE, releases also all the buffers.
*/
public void release() {
if (!isSetUp()) {
LOG.w("release called twice. Ignoring.");
return;
}
LOG.i("release: Clearing the frame and buffer queue.");
mFrameQueue.clear();
if (mBufferMode == BUFFER_MODE_ENQUEUE) {
mBufferQueue.clear();
}
mBufferSize = -1;
mFrameSize = null;
mFrameFormat = -1;
}
} }

@ -15,6 +15,8 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@ -52,7 +54,7 @@ public class FrameTest {
final Frame frame = new Frame(manager); final Frame frame = new Frame(manager);
frame.setContent(new byte[2], 1000, 90, new Size(10, 10), ImageFormat.NV21); frame.setContent(new byte[2], 1000, 90, new Size(10, 10), ImageFormat.NV21);
frame.release(); frame.release();
verify(manager, times(1)).onFrameReleased(frame); verify(manager, times(1)).onFrameReleased(eq(frame), any(byte[].class));
assertThrows(new Runnable() { public void run() { frame.getTime(); }}); assertThrows(new Runnable() { public void run() { frame.getTime(); }});
assertThrows(new Runnable() { public void run() { frame.getFormat(); }}); assertThrows(new Runnable() { public void run() { frame.getFormat(); }});

Loading…
Cancel
Save