You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
CameraView/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java

738 lines
25 KiB

package com.otaliastudios.cameraview.engine;
import android.graphics.Bitmap;
import android.graphics.PointF;
import android.hardware.Camera;
import android.os.Build;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.CameraException;
import com.otaliastudios.cameraview.CameraListener;
import com.otaliastudios.cameraview.CameraOptions;
import com.otaliastudios.cameraview.CameraUtils;
import com.otaliastudios.cameraview.CameraView;
import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.TestActivity;
import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.Engine;
import com.otaliastudios.cameraview.controls.Flash;
import com.otaliastudios.cameraview.controls.Hdr;
import com.otaliastudios.cameraview.controls.Mode;
import com.otaliastudios.cameraview.controls.WhiteBalance;
import com.otaliastudios.cameraview.frame.Frame;
import com.otaliastudios.cameraview.frame.FrameProcessor;
import com.otaliastudios.cameraview.internal.utils.Op;
import com.otaliastudios.cameraview.internal.utils.WorkerHandler;
import com.otaliastudios.cameraview.size.Size;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.rule.ActivityTestRule;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentMatcher;
import java.io.File;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
public abstract class CameraIntegrationTest extends BaseTest {
private final static long DELAY = 9000;
@Rule
public ActivityTestRule<TestActivity> rule = new ActivityTestRule<>(TestActivity.class);
private CameraView camera;
private CameraEngine controller;
private CameraListener listener;
private Op<Throwable> uiExceptionOp;
@BeforeClass
public static void grant() {
grantPermissions();
}
@NonNull
protected abstract Engine getEngine();
@Before
public void setUp() {
WorkerHandler.destroy();
ui(new Runnable() {
@Override
public void run() {
camera = new CameraView(rule.getActivity()) {
@NonNull
@Override
protected CameraEngine instantiateCameraEngine(@NonNull Engine engine, @NonNull CameraEngine.Callback callback) {
controller = super.instantiateCameraEngine(getEngine(), callback);
return controller;
}
};
listener = mock(CameraListener.class);
camera.setExperimental(true);
camera.setEngine(getEngine());
camera.addCameraListener(listener);
rule.getActivity().inflate(camera);
// Ensure that controller exceptions are thrown on this thread (not on the UI thread).
// TODO this makes debugging for wrong tests very hard, as we don't get the exception
// unless waitForUiException() is called.
uiExceptionOp = new Op<>(true);
WorkerHandler crashThread = WorkerHandler.get("CrashThread");
crashThread.getThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
uiExceptionOp.end(e);
}
});
controller.mCrashHandler = crashThread.getHandler();
}
});
}
@After
public void tearDown() {
camera.stopVideo();
camera.destroy();
WorkerHandler.destroy();
}
private void waitForUiException() throws Throwable {
Throwable throwable = uiExceptionOp.await(DELAY);
if (throwable != null) {
throw throwable;
}
}
private CameraOptions openSync(boolean expectSuccess) {
camera.open();
final Op<CameraOptions> open = new Op<>(true);
doEndTask(open, 0).when(listener).onCameraOpened(any(CameraOptions.class));
CameraOptions result = open.await(DELAY);
if (expectSuccess) {
assertNotNull("Can open", result);
// Extra wait for the bind state.
while (controller.getBindState() != CameraEngine.STATE_STARTED) {}
} else {
assertNull("Should not open", result);
}
return result;
}
private void closeSync(boolean expectSuccess) {
camera.close();
final Op<Boolean> close = new Op<>(true);
doEndTask(close, true).when(listener).onCameraClosed();
Boolean result = close.await(DELAY);
if (expectSuccess) {
assertNotNull("Can close", result);
} else {
assertNull("Should not close", result);
}
}
@SuppressWarnings("UnusedReturnValue")
@Nullable
private VideoResult waitForVideoResult(boolean expectSuccess) {
final Op<VideoResult> video = new Op<>(true);
doEndTask(video, 0).when(listener).onVideoTaken(any(VideoResult.class));
doEndTask(video, null).when(listener).onCameraError(argThat(new ArgumentMatcher<CameraException>() {
@Override
public boolean matches(CameraException argument) {
return argument.getReason() == CameraException.REASON_VIDEO_FAILED;
}
}));
VideoResult result = video.await(DELAY);
if (expectSuccess) {
assertNotNull("Should end video", result);
} else {
assertNull("Should not end video", result);
}
return result;
}
@Nullable
private PictureResult waitForPictureResult(boolean expectSuccess) {
final Op<PictureResult> pic = new Op<>(true);
doEndTask(pic, 0).when(listener).onPictureTaken(any(PictureResult.class));
doEndTask(pic, null).when(listener).onCameraError(argThat(new ArgumentMatcher<CameraException>() {
@Override
public boolean matches(CameraException argument) {
return argument.getReason() == CameraException.REASON_PICTURE_FAILED;
}
}));
PictureResult result = pic.await(DELAY);
if (expectSuccess) {
assertNotNull("Can take picture", result);
} else {
assertNull("Should not take picture", result);
}
return result;
}
private void takeVideoSync(boolean expectSuccess) {
takeVideoSync(expectSuccess,0);
}
private void takeVideoSync(boolean expectSuccess, int duration) {
final Op<Boolean> op = new Op<>(true);
doEndTask(op, true).when(listener).onVideoRecordingStart();
doEndTask(op, false).when(listener).onCameraError(argThat(new ArgumentMatcher<CameraException>() {
@Override
public boolean matches(CameraException argument) {
return argument.getReason() == CameraException.REASON_VIDEO_FAILED;
}
}));
File file = new File(context().getFilesDir(), "video.mp4");
if (duration > 0) {
camera.takeVideo(file, duration);
} else {
camera.takeVideo(file);
}
Boolean result = op.await(DELAY);
if (expectSuccess) {
assertNotNull("should start video recording or get CameraError", result);
assertTrue("should start video recording successfully", result);
} else {
assertTrue("should not start video recording", result == null || !result);
}
}
private void takeVideoSnapshotSync(boolean expectSuccess) {
takeVideoSnapshotSync(expectSuccess,0);
}
private void takeVideoSnapshotSync(boolean expectSuccess, int duration) {
final Op<Boolean> op = new Op<>(true);
doEndTask(op, true).when(listener).onVideoRecordingStart();
doEndTask(op, false).when(listener).onCameraError(argThat(new ArgumentMatcher<CameraException>() {
@Override
public boolean matches(CameraException argument) {
return argument.getReason() == CameraException.REASON_VIDEO_FAILED;
}
}));
File file = new File(context().getFilesDir(), "video.mp4");
if (duration > 0) {
camera.takeVideoSnapshot(file, duration);
} else {
camera.takeVideoSnapshot(file);
}
Boolean result = op.await(DELAY);
if (expectSuccess) {
assertNotNull("should start video recording or get CameraError", result);
assertTrue("should start video recording successfully", result);
} else {
assertTrue("should not start video recording", result == null || !result);
}
}
//region test open/close
@Test
public void testOpenClose() {
// Starting and stopping are hard to get since they happen on another thread.
assertEquals(controller.getEngineState(), CameraEngine.STATE_STOPPED);
openSync(true);
assertEquals(controller.getEngineState(), CameraEngine.STATE_STARTED);
closeSync(true);
assertEquals(controller.getEngineState(), CameraEngine.STATE_STOPPED);
}
@Test
public void testOpenTwice() {
openSync(true);
openSync(false);
}
@Test
public void testCloseTwice() {
closeSync(false);
}
@Test
// This works great on the device but crashes often on the emulator.
// There must be something wrong with the emulated camera...
// Like stopPreview() and release() are not really sync calls?
public void testConcurrentCalls() throws Exception {
final CountDownLatch latch = new CountDownLatch(4);
doCountDown(latch).when(listener).onCameraOpened(any(CameraOptions.class));
doCountDown(latch).when(listener).onCameraClosed();
camera.open();
camera.close();
camera.open();
camera.close();
boolean did = latch.await(10, TimeUnit.SECONDS);
assertTrue("Handles concurrent calls to start & stop, " + latch.getCount(), did);
}
@Test
public void testStartInitializesOptions() {
assertNull(camera.getCameraOptions());
openSync(true);
assertNotNull(camera.getCameraOptions());
}
//endregion
//region test Facing/SessionType
// Test things that should reset the camera.
@Test
public void testSetFacing() throws Exception {
CameraOptions o = openSync(true);
int size = o.getSupportedFacing().size();
if (size > 1) {
// set facing should call stop and start again.
final CountDownLatch latch = new CountDownLatch(2);
doCountDown(latch).when(listener).onCameraOpened(any(CameraOptions.class));
doCountDown(latch).when(listener).onCameraClosed();
camera.toggleFacing();
boolean did = latch.await(2, TimeUnit.SECONDS);
assertTrue("Handles setFacing while active", did);
}
}
@Test
public void testSetMode() throws Exception {
camera.setMode(Mode.PICTURE);
openSync(true);
// set session type should call stop and start again.
final CountDownLatch latch = new CountDownLatch(2);
doCountDown(latch).when(listener).onCameraOpened(any(CameraOptions.class));
doCountDown(latch).when(listener).onCameraClosed();
camera.setMode(Mode.VIDEO);
boolean did = latch.await(2, TimeUnit.SECONDS);
assertTrue("Handles setMode while active", did);
assertEquals(camera.getMode(), Mode.VIDEO);
}
//endregion
//region test Set Parameters
// When camera is open, parameters will be set only if supported.
@Test
public void testSetZoom() {
CameraOptions options = openSync(true);
controller.mZoomOp.listen();
float oldValue = camera.getZoom();
float newValue = 0.65f;
camera.setZoom(newValue);
controller.mZoomOp.await(500);
if (options.isZoomSupported()) {
assertEquals(newValue, camera.getZoom(), 0f);
} else {
assertEquals(oldValue, camera.getZoom(), 0f);
}
}
@Test
public void testSetExposureCorrection() {
CameraOptions options = openSync(true);
controller.mExposureCorrectionOp.listen();
float oldValue = camera.getExposureCorrection();
float newValue = options.getExposureCorrectionMaxValue();
camera.setExposureCorrection(newValue);
controller.mExposureCorrectionOp.await(300);
if (options.isExposureCorrectionSupported()) {
assertEquals(newValue, camera.getExposureCorrection(), 0f);
} else {
assertEquals(oldValue, camera.getExposureCorrection(), 0f);
}
}
@Test
public void testSetFlash() {
CameraOptions options = openSync(true);
Flash[] values = Flash.values();
Flash oldValue = camera.getFlash();
for (Flash value : values) {
controller.mFlashOp.listen();
camera.setFlash(value);
controller.mFlashOp.await(300);
if (options.supports(value)) {
assertEquals(camera.getFlash(), value);
oldValue = value;
} else {
assertEquals(camera.getFlash(), oldValue);
}
}
}
@Test
public void testSetWhiteBalance() {
CameraOptions options = openSync(true);
WhiteBalance[] values = WhiteBalance.values();
WhiteBalance oldValue = camera.getWhiteBalance();
for (WhiteBalance value : values) {
controller.mWhiteBalanceOp.listen();
camera.setWhiteBalance(value);
controller.mWhiteBalanceOp.await(300);
if (options.supports(value)) {
assertEquals(camera.getWhiteBalance(), value);
oldValue = value;
} else {
assertEquals(camera.getWhiteBalance(), oldValue);
}
}
}
@Test
public void testSetHdr() {
CameraOptions options = openSync(true);
Hdr[] values = Hdr.values();
Hdr oldValue = camera.getHdr();
for (Hdr value : values) {
controller.mHdrOp.listen();
camera.setHdr(value);
controller.mHdrOp.await(300);
if (options.supports(value)) {
assertEquals(camera.getHdr(), value);
oldValue = value;
} else {
assertEquals(camera.getHdr(), oldValue);
}
}
}
@Test
public void testSetAudio() {
// TODO: when permissions are managed, check that Audio.ON triggers the audio permission
openSync(true);
Audio[] values = Audio.values();
for (Audio value : values) {
camera.setAudio(value);
assertEquals(camera.getAudio(), value);
}
}
@Test
public void testSetLocation() {
openSync(true);
controller.mLocationOp.listen();
camera.setLocation(10d, 2d);
controller.mLocationOp.await(300);
assertNotNull(camera.getLocation());
assertEquals(camera.getLocation().getLatitude(), 10d, 0d);
assertEquals(camera.getLocation().getLongitude(), 2d, 0d);
// This also ensures there are no crashes when attaching it to camera parameters.
}
@Test
public void testSetPlaySounds() {
controller.mPlaySoundsOp.listen();
boolean oldValue = camera.getPlaySounds();
boolean newValue = !oldValue;
camera.setPlaySounds(newValue);
controller.mPlaySoundsOp.await(300);
if (controller instanceof Camera1Engine) {
Camera1Engine camera1Engine = (Camera1Engine) controller;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
Camera.CameraInfo info = new Camera.CameraInfo();
Camera.getCameraInfo(camera1Engine.mCameraId, info);
if (info.canDisableShutterSound) {
assertEquals(newValue, camera.getPlaySounds());
}
} else {
assertEquals(oldValue, camera.getPlaySounds());
}
} else {
// TODO do when Camera2 is completed
}
}
//endregion
//region test takeVideo
@Test(expected = RuntimeException.class)
public void testStartVideo_whileInPictureMode() throws Throwable {
// Fails on Travis. Some emulators can't deal with MediaRecorder
// Error while starting MediaRecorder. java.lang.RuntimeException: start failed.
// as documented. This works locally though.
camera.setMode(Mode.PICTURE);
openSync(true);
takeVideoSync(false);
waitForUiException();
}
@Test
public void testStartEndVideo() {
// Fails on Travis. Some emulators can't deal with MediaRecorder,
// Error while starting MediaRecorder. java.lang.RuntimeException: start failed.
// as documented. This works locally though.
camera.setMode(Mode.VIDEO);
openSync(true);
takeVideoSync(true, 4000);
waitForVideoResult(true);
}
@Test
public void testEndVideo_withoutStarting() {
camera.setMode(Mode.VIDEO);
openSync(true);
camera.stopVideo();
waitForVideoResult(false);
}
@Test
public void testEndVideo_withMaxSize() {
camera.setMode(Mode.VIDEO);
camera.setVideoMaxSize(3000*1000); // Less is risky
openSync(true);
takeVideoSync(true);
waitForVideoResult(true);
}
@Test
public void testEndVideo_withMaxDuration() {
camera.setMode(Mode.VIDEO);
camera.setVideoMaxDuration(4000);
openSync(true);
takeVideoSync(true);
waitForVideoResult(true);
}
//endregion
//region startAutoFocus
@Test
public void testStartAutoFocus() {
CameraOptions o = openSync(true);
final Op<PointF> focus = new Op<>(true);
doEndTask(focus, 0).when(listener).onAutoFocusStart(any(PointF.class));
camera.startAutoFocus(1, 1);
PointF point = focus.await(300);
if (o.isAutoFocusSupported()) {
assertNotNull(point);
assertEquals(point, new PointF(1, 1));
} else {
assertNull(point);
}
}
@Test
public void testStopAutoFocus() {
CameraOptions o = openSync(true);
final Op<PointF> focus = new Op<>(true);
doEndTask(focus, 1).when(listener).onAutoFocusEnd(anyBoolean(), any(PointF.class));
camera.startAutoFocus(1, 1);
// Stop is not guaranteed to be called, we use a delay. So wait at least the delay time.
PointF point = focus.await(1000 + Camera1Engine.AUTOFOCUS_END_DELAY_MILLIS);
if (o.isAutoFocusSupported()) {
assertNotNull(point);
assertEquals(point, new PointF(1, 1));
} else {
assertNull(point);
}
}
//endregion
//region capture
@Test
public void testCapturePicture_beforeStarted() {
camera.takePicture();
waitForPictureResult(false);
}
@Test
public void testCapturePicture_concurrentCalls() throws Exception {
// Second take should fail.
openSync(true);
CountDownLatch latch = new CountDownLatch(2);
doCountDown(latch).when(listener).onPictureTaken(any(PictureResult.class));
camera.takePicture();
camera.takePicture();
boolean did = latch.await(4, TimeUnit.SECONDS);
assertFalse(did);
assertEquals(latch.getCount(), 1);
}
@Test
public void testCapturePicture_size() throws Exception {
openSync(true);
// PictureSize can still be null after opened.
while (camera.getPictureSize() == null) {}
Size size = camera.getPictureSize();
camera.takePicture();
PictureResult result = waitForPictureResult(true);
Bitmap bitmap = CameraUtils.decodeBitmap(result.getData(), Integer.MAX_VALUE, Integer.MAX_VALUE);
assertEquals(result.getSize(), size);
assertEquals(bitmap.getWidth(), size.getWidth());
assertEquals(bitmap.getHeight(), size.getHeight());
assertNotNull(result.getData());
assertNull(result.getLocation());
assertFalse(result.isSnapshot());
}
@Test(expected = RuntimeException.class)
public void testCapturePicture_whileInVideoMode() throws Throwable {
camera.setMode(Mode.VIDEO);
openSync(true);
camera.takePicture();
waitForUiException();
}
@Test
public void testCaptureSnapshot_beforeStarted() {
camera.takePictureSnapshot();
waitForPictureResult(false);
}
@Test
public void testCaptureSnapshot_concurrentCalls() throws Exception {
// Second take should fail.
openSync(true);
CountDownLatch latch = new CountDownLatch(2);
doCountDown(latch).when(listener).onPictureTaken(any(PictureResult.class));
camera.takePictureSnapshot();
camera.takePictureSnapshot();
boolean did = latch.await(6, TimeUnit.SECONDS);
assertFalse(did);
assertEquals(1, latch.getCount());
}
@Test
public void testCaptureSnapshot_size() throws Exception {
openSync(true);
// SnapshotSize can still be null after opened.
while (camera.getSnapshotSize() == null) {}
Size size = camera.getSnapshotSize();
camera.takePictureSnapshot();
PictureResult result = waitForPictureResult(true);
Bitmap bitmap = CameraUtils.decodeBitmap(result.getData(), Integer.MAX_VALUE, Integer.MAX_VALUE);
assertEquals(result.getSize(), size);
assertEquals(bitmap.getWidth(), size.getWidth());
assertEquals(bitmap.getHeight(), size.getHeight());
assertNotNull(result.getData());
assertNull(result.getLocation());
assertTrue(result.isSnapshot());
}
//endregion
//region Frame Processing
private void assert30Frames(FrameProcessor mock) throws Exception {
// Expect 30 frames
CountDownLatch latch = new CountDownLatch(30);
doCountDown(latch).when(mock).process(any(Frame.class));
boolean did = latch.await(60, TimeUnit.SECONDS);
assertTrue(did);
}
@Test
public void testFrameProcessing_simple() throws Exception {
FrameProcessor processor = mock(FrameProcessor.class);
camera.addFrameProcessor(processor);
openSync(true);
assert30Frames(processor);
}
@Test
public void testFrameProcessing_afterSnapshot() throws Exception {
FrameProcessor processor = mock(FrameProcessor.class);
camera.addFrameProcessor(processor);
openSync(true);
// In Camera1, snapshots will clear the preview callback
// Ensure we restore correctly
camera.takePictureSnapshot();
waitForPictureResult(true);
assert30Frames(processor);
}
@Test
public void testFrameProcessing_afterRestart() throws Exception {
FrameProcessor processor = mock(FrameProcessor.class);
camera.addFrameProcessor(processor);
openSync(true);
closeSync(true);
openSync(true);
assert30Frames(processor);
}
@Test
public void testFrameProcessing_afterVideo() throws Exception {
FrameProcessor processor = mock(FrameProcessor.class);
camera.addFrameProcessor(processor);
camera.setMode(Mode.VIDEO);
openSync(true);
takeVideoSync(true,4000);
waitForVideoResult(true);
assert30Frames(processor);
}
@Test
public void testFrameProcessing_freezeRelease() throws Exception {
// Ensure that freeze/release cycles do not cause OOMs.
// There was a bug doing this and it might resurface for any improper
// disposal of the frames.
FrameProcessor source = new FreezeReleaseFrameProcessor();
FrameProcessor processor = spy(source);
camera.addFrameProcessor(processor);
openSync(true);
assert30Frames(processor);
}
public class FreezeReleaseFrameProcessor implements FrameProcessor {
@Override
public void process(@NonNull Frame frame) {
frame.freeze().release();
}
}
//endregion
}