Improve coverage (#507)

* Do not use ViewCompat

* Remove TODOs, improve logging

* SizeSelectorParser and CameraUtils tests

* DefaultAutoFOcusMarkerTest

* MarkerParserTest and improve MarkerLayoutTest

* Add MediaEncoderEngine comments

* Add ExifHelper test

* ImageHelper test

* More internal/utils tests
pull/513/head
Mattia Iavarone 5 years ago committed by GitHub
parent 6962744d4f
commit 1318b7d10b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      cameraview/build.gradle
  2. 96
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraUtilsTest.java
  3. 55
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/utils/CamcorderProfilesTest.java
  4. 102
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/utils/ImageHelperTest.java
  5. 52
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/utils/RotationHelperTest.java
  6. 208
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/utils/WorkerHandlerTest.java
  7. 104
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/markers/DefaultAutoFocusMarkerTest.java
  8. 46
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/markers/MarkerLayoutTest.java
  9. 79
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/markers/MarkerParserTest.java
  10. 211
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/size/SizeSelectorParserTest.java
  11. 7
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  12. 13
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/ImageHelper.java
  13. 5
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/WorkerHandler.java
  14. 5
      cameraview/src/main/java/com/otaliastudios/cameraview/markers/DefaultAutoFocusMarker.java
  15. 5
      cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioMediaEncoder.java
  16. 10
      cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoder.java
  17. 11
      cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoderEngine.java
  18. 41
      cameraview/src/test/java/com/otaliastudios/cameraview/internal/utils/ExifHelperTest.java
  19. 38
      demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java
  20. 1
      demo/src/main/res/layout/activity_camera.xml

@ -223,6 +223,7 @@ task mergedCoverageReport(type: JacocoReport) {
classFilter.add('**/com/otaliastudios/cameraview/engine/Camera2Engine**.*') classFilter.add('**/com/otaliastudios/cameraview/engine/Camera2Engine**.*')
classFilter.add('**/com/otaliastudios/cameraview/picture/**.*') classFilter.add('**/com/otaliastudios/cameraview/picture/**.*')
classFilter.add('**/com/otaliastudios/cameraview/video/**.*') classFilter.add('**/com/otaliastudios/cameraview/video/**.*')
// TODO these below could be testable ALSO outside of the integration tests
classFilter.add('**/com/otaliastudios/cameraview/video/encoding/**.*') classFilter.add('**/com/otaliastudios/cameraview/video/encoding/**.*')
} }
classDirectories = fileTree(dir: classDir, excludes: classFilter); classDirectories = fileTree(dir: classDir, excludes: classFilter);

@ -8,6 +8,9 @@ import android.graphics.Color;
import com.otaliastudios.cameraview.internal.utils.Op; import com.otaliastudios.cameraview.internal.utils.Op;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.annotation.UiThreadTest;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest; import androidx.test.filters.SmallTest;
@ -15,6 +18,11 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.Charset;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
import static org.junit.Assert.*; import static org.junit.Assert.*;
@ -28,26 +36,66 @@ public class CameraUtilsTest extends BaseTest {
Context context = mock(Context.class); Context context = mock(Context.class);
PackageManager pm = mock(PackageManager.class); PackageManager pm = mock(PackageManager.class);
when(context.getPackageManager()).thenReturn(pm); when(context.getPackageManager()).thenReturn(pm);
when(pm.hasSystemFeature(anyString())).thenReturn(true); when(pm.hasSystemFeature(PackageManager.FEATURE_CAMERA)).thenReturn(true);
when(pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT)).thenReturn(true);
assertTrue(CameraUtils.hasCameras(context)); assertTrue(CameraUtils.hasCameras(context));
when(pm.hasSystemFeature(PackageManager.FEATURE_CAMERA)).thenReturn(false);
when(pm.hasSystemFeature(anyString())).thenReturn(false); when(pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT)).thenReturn(true);
assertTrue(CameraUtils.hasCameras(context));
when(pm.hasSystemFeature(PackageManager.FEATURE_CAMERA)).thenReturn(false);
when(pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT)).thenReturn(false);
assertFalse(CameraUtils.hasCameras(context)); assertFalse(CameraUtils.hasCameras(context));
} }
// Encodes bitmap and decodes again using our utility. @NonNull
private Op<Bitmap> encodeDecodeTask(Bitmap source) { private Op<String> writeAndReadString(@NonNull String data) {
return encodeDecodeTask(source, 0, 0); final File file = new File(getContext().getFilesDir(), "string.txt");
final byte[] bytes = data.getBytes(Charset.forName("UTF-8"));
final Op<String> result = new Op<>(true);
final FileCallback callback = new FileCallback() {
@Override
public void onFileReady(@Nullable File file) {
if (file == null) {
result.end(null);
} else {
// Read back the file.
try {
FileInputStream stream = new FileInputStream(file);
byte[] bytes = new byte[stream.available()];
stream.read(bytes);
result.end(new String(bytes, Charset.forName("UTF-8")));
} catch (IOException e) {
result.end(null);
}
}
}
};
uiSync(new Runnable() {
@Override
public void run() {
CameraUtils.writeToFile(bytes, file, callback);
}
});
return result;
} }
@Test
public void testWriteToFile() {
Op<String> op = writeAndReadString("testString");
String result = op.await(2000);
assertEquals("testString", result);
}
// Encodes bitmap and decodes again using our utility. // Encodes bitmap and decodes again using our utility.
private Op<Bitmap> encodeDecodeTask(Bitmap source, final int maxWidth, final int maxHeight) { private Op<Bitmap> encodeDecodeTask(@NonNull Bitmap source, final int maxWidth, final int maxHeight, boolean async) {
final ByteArrayOutputStream os = new ByteArrayOutputStream(); final ByteArrayOutputStream os = new ByteArrayOutputStream();
// Using lossy JPG we can't have strict comparison of values after compression. // Using lossy JPG we can't have strict comparison of values after compression.
source.compress(Bitmap.CompressFormat.PNG, 100, os); source.compress(Bitmap.CompressFormat.PNG, 100, os);
final byte[] data = os.toByteArray(); final byte[] data = os.toByteArray();
final Op<Bitmap> decode = new Op<>(true); final Op<Bitmap> decode = new Op<>(true);
if (async) {
final BitmapCallback callback = new BitmapCallback() { final BitmapCallback callback = new BitmapCallback() {
@Override @Override
public void onBitmapReady(Bitmap bitmap) { public void onBitmapReady(Bitmap bitmap) {
@ -66,6 +114,15 @@ public class CameraUtilsTest extends BaseTest {
} }
} }
}); });
} else {
Bitmap result;
if (maxWidth > 0 && maxHeight > 0) {
result = CameraUtils.decodeBitmap(data, maxWidth, maxHeight);
} else {
result = CameraUtils.decodeBitmap(data);
}
decode.end(result);
}
return decode; return decode;
} }
@ -75,7 +132,24 @@ public class CameraUtilsTest extends BaseTest {
Bitmap source = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); Bitmap source = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
source.setPixel(0, 0, color); source.setPixel(0, 0, color);
Op<Bitmap> decode = encodeDecodeTask(source); Op<Bitmap> decode = encodeDecodeTask(source, 0, 0, true);
Bitmap other = decode.await(800);
assertNotNull(other);
assertEquals(100, w);
assertEquals(200, h);
assertEquals(color, other.getPixel(0, 0));
assertEquals(0, other.getPixel(0, h-1));
assertEquals(0, other.getPixel(w-1, 0));
assertEquals(0, other.getPixel(w-1, h-1));
}
@Test
public void testDecodeBitmapSync() {
int w = 100, h = 200, color = Color.WHITE;
Bitmap source = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
source.setPixel(0, 0, color);
Op<Bitmap> decode = encodeDecodeTask(source, 0, 0, false);
Bitmap other = decode.await(800); Bitmap other = decode.await(800);
assertNotNull(other); assertNotNull(other);
assertEquals(100, w); assertEquals(100, w);
@ -94,19 +168,19 @@ public class CameraUtilsTest extends BaseTest {
Op<Bitmap> op; Op<Bitmap> op;
Bitmap other; Bitmap other;
op = encodeDecodeTask(source, 100, 100); op = encodeDecodeTask(source, 100, 100, true);
other = op.await(800); other = op.await(800);
assertNotNull(other); assertNotNull(other);
assertTrue(other.getWidth() <= 100); assertTrue(other.getWidth() <= 100);
assertTrue(other.getHeight() <= 100); assertTrue(other.getHeight() <= 100);
op = encodeDecodeTask(source, Integer.MAX_VALUE, Integer.MAX_VALUE); op = encodeDecodeTask(source, Integer.MAX_VALUE, Integer.MAX_VALUE, true);
other = op.await(800); other = op.await(800);
assertNotNull(other); assertNotNull(other);
assertEquals(other.getWidth(), width); assertEquals(other.getWidth(), width);
assertEquals(other.getHeight(), height); assertEquals(other.getHeight(), height);
op = encodeDecodeTask(source, 6000, 6000); op = encodeDecodeTask(source, 6000, 6000, true);
other = op.await(800); other = op.await(800);
assertNotNull(other); assertNotNull(other);
assertEquals(other.getWidth(), width); assertEquals(other.getWidth(), width);

@ -0,0 +1,55 @@
package com.otaliastudios.cameraview.internal.utils;
import android.media.CamcorderProfile;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.CameraUtils;
import com.otaliastudios.cameraview.size.Size;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class CamcorderProfilesTest extends BaseTest {
private String getCameraId() {
if (CameraUtils.hasCameras(getContext())) {
return "0";
}
return null;
}
@Test
public void testInvalidCameraReturnsLowest() {
CamcorderProfile invalid = CamcorderProfiles.get("invalid", new Size(100, 100));
CamcorderProfile lowest = CamcorderProfile.get(CamcorderProfile.QUALITY_LOW);
assertEquals(lowest.videoFrameWidth, invalid.videoFrameWidth);
assertEquals(lowest.videoFrameHeight, invalid.videoFrameHeight);
}
@Test
public void testGet() {
String cameraId = getCameraId();
if (cameraId == null) return;
int cameraIdInt = Integer.parseInt(cameraId);
// Not much we can test. Let's just ask for lowest and highest.
CamcorderProfile low = CamcorderProfiles.get(cameraId, new Size(1, 1));
CamcorderProfile high = CamcorderProfiles.get(cameraId, new Size(Integer.MAX_VALUE, Integer.MAX_VALUE));
// Compare with lowest
CamcorderProfile lowest = CamcorderProfile.get(cameraIdInt, CamcorderProfile.QUALITY_LOW);
CamcorderProfile highest = CamcorderProfile.get(cameraIdInt, CamcorderProfile.QUALITY_HIGH);
assertEquals(lowest.videoFrameWidth, low.videoFrameWidth);
assertEquals(lowest.videoFrameHeight, low.videoFrameHeight);
assertEquals(highest.videoFrameWidth, high.videoFrameWidth);
assertEquals(highest.videoFrameHeight, high.videoFrameHeight);
}
}

@ -0,0 +1,102 @@
package com.otaliastudios.cameraview.internal.utils;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ImageFormat;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.YuvImage;
import android.media.Image;
import android.media.ImageReader;
import android.opengl.GLES20;
import android.os.Handler;
import android.os.Looper;
import android.view.Surface;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.internal.egl.EglCore;
import com.otaliastudios.cameraview.internal.egl.EglViewport;
import com.otaliastudios.cameraview.internal.egl.EglWindowSurface;
import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ImageHelperTest extends BaseTest {
@NonNull
private Image getImage() {
ImageReader reader = ImageReader.newInstance(100, 100, ImageFormat.YUV_420_888, 1);
Surface readerSurface = reader.getSurface();
final Op<Image> imageOp = new Op<>(true);
reader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
Image image = reader.acquireLatestImage();
if (image != null) imageOp.end(image);
}
}, new Handler(Looper.getMainLooper()));
// Write on reader surface.
Canvas readerCanvas = readerSurface.lockCanvas(null);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.RED);
readerCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.MULTIPLY);
readerCanvas.drawCircle(50, 50, 50, paint);
readerSurface.unlockCanvasAndPost(readerCanvas);
// Wait
Image image = imageOp.await(5000);
assertNotNull(image);
return image;
}
@Test
public void testImage() {
Image image = getImage();
int width = image.getWidth();
int height = image.getHeight();
int bitsPerPixel = ImageFormat.getBitsPerPixel(ImageFormat.NV21);
int sizeBits = width * height * bitsPerPixel;
int sizeBytes = (int) Math.ceil(sizeBits / 8.0d);
byte[] bytes = new byte[sizeBytes];
ImageHelper.convertToNV21(image, bytes);
image.close();
// Read the image
YuvImage yuvImage = new YuvImage(bytes, ImageFormat.NV21, width, height, null);
ByteArrayOutputStream jpegStream = new ByteArrayOutputStream();
yuvImage.compressToJpeg(new Rect(0, 0, width, height), 100, jpegStream);
byte[] jpegByteArray = jpegStream.toByteArray();
Bitmap bitmap = BitmapFactory.decodeByteArray(jpegByteArray, 0, jpegByteArray.length);
assertNotNull(bitmap);
// Wanted to do assertions on the color here but it doesn't work. There must be an issue
// with how we are drawing the image in this test, since in real camera, the algorithm works well.
// So for now let's just test that nothing crashes during this process.
// int color = bitmap.getPixel(bitmap.getWidth() - 1, bitmap.getHeight() - 1);
// assertEquals(Color.red(color), 255, 5);
// assertEquals(Color.green(color), 0, 5);
// assertEquals(Color.blue(color), 0, 5);
// assertEquals(Color.alpha(color), 0, 5);
}
}

@ -0,0 +1,52 @@
package com.otaliastudios.cameraview.internal.utils;
import android.graphics.ImageFormat;
import android.graphics.YuvImage;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.size.Size;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class RotationHelperTest extends BaseTest {
@Test(expected = IllegalArgumentException.class)
public void testInvalidRotation1() {
RotationHelper.rotate(new byte[10], new Size(1, 1), -1);
}
@Test(expected = IllegalArgumentException.class)
public void testInvalidRotation2() {
RotationHelper.rotate(new byte[10], new Size(1, 1), -90);
}
@Test(expected = IllegalArgumentException.class)
public void testInvalidRotation3() {
RotationHelper.rotate(new byte[10], new Size(1, 1), 360);
}
@Test
public void testRotate() {
// Just test that nothing happens.
Size inputSize = new Size(160, 90);
int inputSizeBits = inputSize.getWidth() * inputSize.getHeight() * ImageFormat.getBitsPerPixel(ImageFormat.NV21);
int inputSizeBytes = (int) Math.ceil(inputSizeBits / 8.0d);
byte[] input = new byte[inputSizeBytes];
byte[] output = RotationHelper.rotate(input, inputSize, 90);
assertEquals(input.length, output.length);
Size outputSize = inputSize.flip();
YuvImage image = new YuvImage(output, ImageFormat.NV21, outputSize.getWidth(), outputSize.getHeight(), null);
assertNotNull(image);
}
}

@ -1,41 +1,229 @@
package com.otaliastudios.cameraview.internal.utils; package com.otaliastudios.cameraview.internal.utils;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
import com.otaliastudios.cameraview.BaseTest; import com.otaliastudios.cameraview.BaseTest;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest; import androidx.test.filters.SmallTest;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.junit.Assert.assertNotNull;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@SmallTest @SmallTest
public class WorkerHandlerTest extends BaseTest { public class WorkerHandlerTest extends BaseTest {
@Test @Test
public void testCache() { public void testGetFromCache() {
WorkerHandler w1 = WorkerHandler.get("handler1"); WorkerHandler first = WorkerHandler.get("first");
WorkerHandler w1a = WorkerHandler.get("handler1"); WorkerHandler second = WorkerHandler.get("first");
WorkerHandler w2 = WorkerHandler.get("handler2"); assertSame(first, second);
assertSame(w1, w1a);
assertNotSame(w1, w2);
} }
@Test @Test
public void testStaticRun() { public void testGetAnother() {
final Op<Boolean> op = new Op<>(true); WorkerHandler first = WorkerHandler.get("first");
Runnable action = new Runnable() { WorkerHandler second = WorkerHandler.get("second");
assertNotSame(first, second);
}
@NonNull
private Runnable getRunnableForOp(final @NonNull Op<Boolean> op) {
return new Runnable() {
@Override @Override
public void run() { public void run() {
op.end(true); op.end(true);
} }
}; };
WorkerHandler.execute(action); }
@NonNull
private Callable<Boolean> getCallableForOp(final @NonNull Op<Boolean> op) {
return new Callable<Boolean>() {
@Override
public Boolean call() {
op.end(true);
return true;
}
};
}
@NonNull
private Callable<Void> getThrowCallable() {
return new Callable<Void>() {
@Override
public Void call() {
throw new RuntimeException("Fake error");
}
};
}
private void waitOp(@NonNull Op<Boolean> op) {
Boolean result = op.await(500); Boolean result = op.await(500);
assertNotNull(result); assertNotNull(result);
assertTrue(result); assertTrue(result);
} }
@Test
public void testFallbackExecute() {
final Op<Boolean> op = new Op<>(true);
WorkerHandler.execute(getRunnableForOp(op));
waitOp(op);
}
@Test
public void testPostRunnable() {
WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>(true);
handler.post(getRunnableForOp(op));
waitOp(op);
}
@Test
public void testPostCallable() {
WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>(true);
handler.post(getCallableForOp(op));
waitOp(op);
}
@Test
public void testPostCallable_throws() {
WorkerHandler handler = WorkerHandler.get("handler");
Task<Void> task = handler.post(getThrowCallable());
try { Tasks.await(task); } catch (ExecutionException | InterruptedException ignore) {}
assertTrue(task.isComplete());
assertFalse(task.isSuccessful());
}
@Test
public void testRunRunnable_background() {
WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>(true);
handler.run(getRunnableForOp(op));
waitOp(op);
}
@Test
public void testRunRunnable_sameThread() {
final WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op1 = new Op<>(true);
final Op<Boolean> op2 = new Op<>(true);
handler.post(new Runnable() {
@Override
public void run() {
handler.run(getRunnableForOp(op2));
assertTrue(op2.await(0)); // Do not wait.
op1.end(true);
}
});
waitOp(op1);
}
@Test
public void testRunCallable_background() {
WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>(true);
handler.run(getCallableForOp(op));
waitOp(op);
}
@Test
public void testRunCallable_sameThread() {
final WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op1 = new Op<>(true);
final Op<Boolean> op2 = new Op<>(true);
handler.post(new Runnable() {
@Override
public void run() {
handler.run(getCallableForOp(op2));
assertTrue(op2.await(0)); // Do not wait.
op1.end(true);
}
});
waitOp(op1);
}
@Test
public void testRunCallable_sameThread_throws() {
final WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>(true);
handler.post(new Runnable() {
@Override
public void run() {
Task<Void> task = handler.run(getThrowCallable());
assertTrue(task.isComplete()); // Already complete
assertFalse(task.isSuccessful());
op.end(true);
}
});
waitOp(op);
}
@Test
public void testPostDelayed_tooEarly() {
final WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>(true);
handler.post(1000, getRunnableForOp(op));
assertNull(op.await(500));
}
@Test
public void testPostDelayed() {
final WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>(true);
handler.post(1000, getRunnableForOp(op));
assertNotNull(op.await(2000));
}
@Test
public void testRemove() {
final WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>(true);
Runnable runnable = getRunnableForOp(op);
handler.post(1000, runnable);
handler.remove(runnable);
assertNull(op.await(2000));
}
@Test
public void testGetters() {
final WorkerHandler handler = WorkerHandler.get("handler");
assertNotNull(handler.getExecutor());
assertNotNull(handler.getHandler());
assertNotNull(handler.getLooper());
assertNotNull(handler.getThread());
}
@Test
public void testExecutor() {
final WorkerHandler handler = WorkerHandler.get("handler");
Executor executor = handler.getExecutor();
final Op<Boolean> op = new Op<>(true);
executor.execute(getRunnableForOp(op));
waitOp(op);
}
@Test
public void testDestroy() {
final WorkerHandler handler = WorkerHandler.get("handler");
assertTrue(handler.getThread().isAlive());
WorkerHandler.destroy();
// Wait for the thread to die.
try { handler.getThread().join(500); } catch (InterruptedException ignore) {}
assertFalse(handler.getThread().isAlive());
WorkerHandler newHandler = WorkerHandler.get("handler");
assertNotSame(handler, newHandler);
assertTrue(newHandler.getThread().isAlive());
}
} }

@ -0,0 +1,104 @@
package com.otaliastudios.cameraview.markers;
import android.graphics.PointF;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.test.annotation.UiThreadTest;
import com.otaliastudios.cameraview.BaseTest;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mockito;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
public class DefaultAutoFocusMarkerTest extends BaseTest {
private DefaultAutoFocusMarker marker;
@Before
public void setUp() {
marker = new DefaultAutoFocusMarker();
}
@After
public void tearDown() {
marker = null;
}
@Test
public void testOnAttach() {
assertNull(marker.mContainer);
assertNull(marker.mFill);
ViewGroup container = new FrameLayout(getContext());
View result = marker.onAttach(getContext(), container);
assertNotNull(result);
assertNotNull(marker.mContainer);
assertNotNull(marker.mFill);
}
@UiThreadTest
@Test
public void testOnAutoFocusStart() {
View mockContainer = spy(new View(getContext()));
View mockFill = spy(new View(getContext()));
marker.mContainer = mockContainer;
marker.mFill = mockFill;
marker.onAutoFocusStart(AutoFocusTrigger.GESTURE, new PointF());
verify(mockContainer, atLeastOnce()).clearAnimation();
verify(mockFill, atLeastOnce()).clearAnimation();
verify(mockContainer, atLeastOnce()).animate();
verify(mockFill, atLeastOnce()).animate();
}
@UiThreadTest
@Test
public void testOnAutoFocusStart_fromMethod() {
View mockContainer = spy(new View(getContext()));
View mockFill = spy(new View(getContext()));
marker.mContainer = mockContainer;
marker.mFill = mockFill;
marker.onAutoFocusStart(AutoFocusTrigger.METHOD, new PointF());
verify(mockContainer, never()).clearAnimation();
verify(mockFill, never()).clearAnimation();
verify(mockContainer, never()).animate();
verify(mockFill, never()).animate();
}
@UiThreadTest
@Test
public void testOnAutoFocusEnd() {
View mockContainer = spy(new View(getContext()));
View mockFill = spy(new View(getContext()));
marker.mContainer = mockContainer;
marker.mFill = mockFill;
marker.onAutoFocusEnd(AutoFocusTrigger.GESTURE, true, new PointF());
verify(mockContainer, atLeastOnce()).animate();
verify(mockFill, atLeastOnce()).animate();
}
@UiThreadTest
@Test
public void testOnAutoFocusEnd_fromMethod() {
View mockContainer = spy(new View(getContext()));
View mockFill = spy(new View(getContext()));
marker.mContainer = mockContainer;
marker.mFill = mockFill;
marker.onAutoFocusEnd(AutoFocusTrigger.METHOD, true, new PointF());
verify(mockContainer, never()).animate();
verify(mockFill, never()).animate();
}
}

@ -3,6 +3,9 @@ package com.otaliastudios.cameraview.markers;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.Context; import android.content.Context;
import android.graphics.PointF;
import android.view.View;
import android.view.ViewGroup;
import com.otaliastudios.cameraview.BaseTest; import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.TestActivity; import com.otaliastudios.cameraview.TestActivity;
@ -13,9 +16,20 @@ import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.mockito.Mockito; import org.mockito.Mockito;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.annotation.UiThreadTest; import androidx.test.annotation.UiThreadTest;
import androidx.test.rule.ActivityTestRule; import androidx.test.rule.ActivityTestRule;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@TargetApi(17) @TargetApi(17)
public class MarkerLayoutTest extends BaseTest { public class MarkerLayoutTest extends BaseTest {
@ -32,9 +46,9 @@ public class MarkerLayoutTest extends BaseTest {
@Override @Override
public void run() { public void run() {
TestActivity a = rule.getActivity(); TestActivity a = rule.getActivity();
markerLayout = Mockito.spy(new MarkerLayout(a)); markerLayout = spy(new MarkerLayout(a));
a.inflate(markerLayout); a.inflate(markerLayout);
autoFocusMarker = Mockito.spy(new DefaultAutoFocusMarker()); autoFocusMarker = spy(new DefaultAutoFocusMarker());
} }
}); });
} }
@ -43,7 +57,7 @@ public class MarkerLayoutTest extends BaseTest {
@UiThreadTest @UiThreadTest
public void testOnMarker_callsOnAttach() { public void testOnMarker_callsOnAttach() {
markerLayout.onMarker(MarkerLayout.TYPE_AUTOFOCUS, autoFocusMarker); markerLayout.onMarker(MarkerLayout.TYPE_AUTOFOCUS, autoFocusMarker);
Mockito.verify(autoFocusMarker, Mockito.times(1)).onAttach( Mockito.verify(autoFocusMarker, times(1)).onAttach(
Mockito.any(Context.class), Mockito.any(Context.class),
Mockito.eq(markerLayout)); Mockito.eq(markerLayout));
} }
@ -66,8 +80,32 @@ public class MarkerLayoutTest extends BaseTest {
markerLayout.onMarker(MarkerLayout.TYPE_AUTOFOCUS, null); markerLayout.onMarker(MarkerLayout.TYPE_AUTOFOCUS, null);
Assert.assertEquals(markerLayout.getChildCount(), 0); Assert.assertEquals(markerLayout.getChildCount(), 0);
Mockito.verify(autoFocusMarker, Mockito.times(2)).onAttach( Mockito.verify(autoFocusMarker, times(2)).onAttach(
Mockito.any(Context.class), Mockito.any(Context.class),
Mockito.eq(markerLayout)); Mockito.eq(markerLayout));
} }
@Test
@UiThreadTest
public void testOnEvent() {
final View mockView = spy(new View(getContext()));
// These fail, however it's not really needed.
// when(mockView.getWidth()).thenReturn(50);
// when(mockView.getHeight()).thenReturn(50);
AutoFocusMarker mockMarker = new AutoFocusMarker() {
public void onAutoFocusStart(@NonNull AutoFocusTrigger trigger, @NonNull PointF point) { }
public void onAutoFocusEnd(@NonNull AutoFocusTrigger trigger, boolean successful, @NonNull PointF point) { }
@Override
public View onAttach(@NonNull Context context, @NonNull ViewGroup container) {
return mockView;
}
};
markerLayout.onMarker(MarkerLayout.TYPE_AUTOFOCUS, mockMarker);
reset(mockView);
markerLayout.onEvent(MarkerLayout.TYPE_AUTOFOCUS, new PointF[]{new PointF(0, 0)});
verify(mockView, times(1)).clearAnimation();
verify(mockView, times(1)).setTranslationX(anyFloat());
verify(mockView, times(1)).setTranslationY(anyFloat());
}
} }

@ -0,0 +1,79 @@
package com.otaliastudios.cameraview.markers;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.PointF;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StyleableRes;
import androidx.arch.core.util.Function;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.R;
import com.otaliastudios.cameraview.size.Size;
import com.otaliastudios.cameraview.size.SizeSelectorParser;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.Arrays;
import java.util.List;
import static junit.framework.TestCase.assertNotNull;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class MarkerParserTest extends BaseTest {
@Test
public void testNullConstructor() {
TypedArray array = mock(TypedArray.class);
when(array.hasValue(R.styleable.CameraView_cameraAutoFocusMarker)).thenReturn(false);
when(array.getString(R.styleable.CameraView_cameraAutoFocusMarker)).thenReturn(null);
MarkerParser parser = new MarkerParser(array);
assertNull(parser.getAutoFocusMarker());
}
@Test
public void testConstructor() {
TypedArray array = mock(TypedArray.class);
when(array.hasValue(R.styleable.CameraView_cameraAutoFocusMarker)).thenReturn(true);
when(array.getString(R.styleable.CameraView_cameraAutoFocusMarker)).thenReturn(Marker.class.getName());
MarkerParser parser = new MarkerParser(array);
assertNotNull(parser.getAutoFocusMarker());
assertTrue(parser.getAutoFocusMarker() instanceof Marker);
}
public static class Marker implements AutoFocusMarker {
public Marker() { }
@Nullable
@Override
public View onAttach(@NonNull Context context, @NonNull ViewGroup container) {
return null;
}
@Override
public void onAutoFocusStart(@NonNull AutoFocusTrigger trigger, @NonNull PointF point) { }
@Override
public void onAutoFocusEnd(@NonNull AutoFocusTrigger trigger, boolean successful, @NonNull PointF point) { }
}
}

@ -0,0 +1,211 @@
package com.otaliastudios.cameraview.size;
import android.content.res.TypedArray;
import androidx.annotation.NonNull;
import androidx.annotation.StyleableRes;
import androidx.arch.core.util.Function;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.R;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class SizeSelectorParserTest extends BaseTest {
private MockTypedArray input;
private List<Size> sizes = Arrays.asList(
new Size(100, 200),
new Size(150, 300),
new Size(600, 900),
new Size(600, 600),
new Size(1600, 900),
new Size(30, 40),
new Size(40, 30),
new Size(2000, 4000)
);
@Before
public void setUp() {
input = new MockTypedArray();
}
@After
public void tearDown() {
input = null;
}
private void doAssert(@NonNull Function<List<Size>, Void> assertions) {
SizeSelectorParser parser = new SizeSelectorParser(input.array);
assertions.apply(parser.getPictureSizeSelector().select(sizes));
assertions.apply(parser.getVideoSizeSelector().select(sizes));
}
@Test
public void testWidth() {
input.setMinWidth(1500);
input.setMaxWidth(1700);
doAssert(new Function<List<Size>, Void>() {
@Override
public Void apply(List<Size> input) {
assertEquals(1, input.size());
assertEquals(new Size(1600, 900), input.get(0));
return null;
}
});
}
@Test
public void testHeight() {
input.setMinHeight(25);
input.setMaxHeight(35);
doAssert(new Function<List<Size>, Void>() {
@Override
public Void apply(List<Size> input) {
assertEquals(1, input.size());
assertEquals(new Size(40, 30), input.get(0));
return null;
}
});
}
@Test
public void testArea() {
input.setMinArea(30 * 30);
input.setMaxArea(40 * 40);
doAssert(new Function<List<Size>, Void>() {
@Override
public Void apply(List<Size> input) {
assertEquals(2, input.size());
assertTrue(input.contains(new Size(40, 30)));
assertTrue(input.contains(new Size(30, 40)));
return null;
}
});
}
@Test
public void testSmallest() {
input.setSmallest(true);
doAssert(new Function<List<Size>, Void>() {
@Override
public Void apply(List<Size> input) {
assertEquals(sizes.size(), input.size());
Size first = input.get(0);
assertEquals(30 * 40, first.getWidth() * first.getHeight());
return null;
}
});
}
@Test
public void testBiggest() {
input.setBiggest(true);
doAssert(new Function<List<Size>, Void>() {
@Override
public Void apply(List<Size> input) {
assertEquals(sizes.size(), input.size());
assertEquals(new Size(2000, 4000), input.get(0));
return null;
}
});
}
@Test
public void testAspectRatio() {
input.setAspectRatio("16:9");
doAssert(new Function<List<Size>, Void>() {
@Override
public Void apply(List<Size> input) {
assertEquals(1, input.size());
assertEquals(new Size(1600, 900), input.get(0));
return null;
}
});
}
@SuppressWarnings("SameParameterValue")
private class MockTypedArray {
private TypedArray array = mock(TypedArray.class);
private void setIntValue(@StyleableRes int index, int value) {
when(array.hasValue(index)).thenReturn(true);
when(array.getInteger(eq(index), anyInt())).thenReturn(value);
}
private void setBooleanValue(@StyleableRes int index, boolean value) {
when(array.hasValue(index)).thenReturn(true);
when(array.getBoolean(eq(index), anyBoolean())).thenReturn(value);
}
private void setStringValue(@StyleableRes int index, @NonNull String value) {
when(array.hasValue(index)).thenReturn(true);
when(array.getString(index)).thenReturn(value);
}
private void setMinWidth(int value) {
setIntValue(R.styleable.CameraView_cameraPictureSizeMinWidth, value);
setIntValue(R.styleable.CameraView_cameraVideoSizeMinWidth, value);
}
private void setMaxWidth(int value) {
setIntValue(R.styleable.CameraView_cameraPictureSizeMaxWidth, value);
setIntValue(R.styleable.CameraView_cameraVideoSizeMaxWidth, value);
}
private void setMinHeight(int value) {
setIntValue(R.styleable.CameraView_cameraPictureSizeMinHeight, value);
setIntValue(R.styleable.CameraView_cameraVideoSizeMinHeight, value);
}
private void setMaxHeight(int value) {
setIntValue(R.styleable.CameraView_cameraPictureSizeMaxHeight, value);
setIntValue(R.styleable.CameraView_cameraVideoSizeMaxHeight, value);
}
private void setMinArea(int value) {
setIntValue(R.styleable.CameraView_cameraPictureSizeMinArea, value);
setIntValue(R.styleable.CameraView_cameraVideoSizeMinArea, value);
}
private void setMaxArea(int value) {
setIntValue(R.styleable.CameraView_cameraPictureSizeMaxArea, value);
setIntValue(R.styleable.CameraView_cameraVideoSizeMaxArea, value);
}
private void setSmallest(boolean value) {
setBooleanValue(R.styleable.CameraView_cameraPictureSizeSmallest, value);
setBooleanValue(R.styleable.CameraView_cameraVideoSizeSmallest, value);
}
private void setBiggest(boolean value) {
setBooleanValue(R.styleable.CameraView_cameraPictureSizeBiggest, value);
setBooleanValue(R.styleable.CameraView_cameraVideoSizeBiggest, value);
}
private void setAspectRatio(@NonNull String value) {
setStringValue(R.styleable.CameraView_cameraPictureSizeAspectRatio, value);
setStringValue(R.styleable.CameraView_cameraVideoSizeAspectRatio, value);
}
}
}

@ -6,7 +6,6 @@ import android.annotation.TargetApi;
import android.app.Activity; import android.app.Activity;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import androidx.core.view.ViewCompat;
import androidx.lifecycle.Lifecycle; import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver; import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleOwner;
@ -830,14 +829,16 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
*/ */
public void setPreview(@NonNull Preview preview) { public void setPreview(@NonNull Preview preview) {
boolean isNew = preview != mPreview; boolean isNew = preview != mPreview;
if (!isNew) return; if (isNew) {
mPreview = preview; mPreview = preview;
if (!ViewCompat.isAttachedToWindow(this) && mCameraPreview != null) { boolean isAttachedToWindow = getWindowToken() != null;
if (!isAttachedToWindow && mCameraPreview != null) {
// Null the preview: will create another when re-attaching. // Null the preview: will create another when re-attaching.
mCameraPreview.onDestroy(); mCameraPreview.onDestroy();
mCameraPreview = null; mCameraPreview = null;
} }
} }
}
/** /**
* Returns the current preview control. * Returns the current preview control.

@ -1,12 +1,8 @@
package com.otaliastudios.cameraview.internal.utils; package com.otaliastudios.cameraview.internal.utils;
import android.graphics.ImageFormat; import android.graphics.ImageFormat;
import android.graphics.Rect;
import android.media.Image; import android.media.Image;
import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -18,7 +14,14 @@ import androidx.annotation.RequiresApi;
@RequiresApi(19) @RequiresApi(19)
public class ImageHelper { public class ImageHelper {
// https://stackoverflow.com/a/52740776/4288782 /**
* From https://stackoverflow.com/a/52740776/4288782 .
* The result array should have a size that is at least 3/2 * w * h.
* This is correctly computed by {@link com.otaliastudios.cameraview.frame.FrameManager}.
*
* @param image input image
* @param result output array
*/
public static void convertToNV21(@NonNull Image image, @NonNull byte[] result) { public static void convertToNV21(@NonNull Image image, @NonNull byte[] result) {
if (image.getFormat() != ImageFormat.YUV_420_888) { if (image.getFormat() != ImageFormat.YUV_420_888) {
throw new IllegalStateException("CAn only convert from YUV_420_888."); throw new IllegalStateException("CAn only convert from YUV_420_888.");

@ -165,6 +165,7 @@ public class WorkerHandler {
* Returns the android backing {@link Handler}. * Returns the android backing {@link Handler}.
* @return the handler * @return the handler
*/ */
@NonNull
public Handler getHandler() { public Handler getHandler() {
return mHandler; return mHandler;
} }
@ -207,7 +208,9 @@ public class WorkerHandler {
WorkerHandler handler = ref.get(); WorkerHandler handler = ref.get();
if (handler != null && handler.getThread().isAlive()) { if (handler != null && handler.getThread().isAlive()) {
handler.getThread().interrupt(); handler.getThread().interrupt();
// handler.getThread().quit(); handler.getThread().quit();
// after quit(), the thread will die at some point in the future. Might take some ms.
// try { handler.getThread().join(); } catch (InterruptedException ignore) {}
} }
ref.clear(); ref.clear();
} }

@ -12,6 +12,7 @@ import com.otaliastudios.cameraview.R;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
/** /**
* A default implementation of {@link AutoFocusMarker}. * A default implementation of {@link AutoFocusMarker}.
@ -20,8 +21,8 @@ import androidx.annotation.Nullable;
*/ */
public class DefaultAutoFocusMarker implements AutoFocusMarker { public class DefaultAutoFocusMarker implements AutoFocusMarker {
private View mContainer; @VisibleForTesting View mContainer;
private View mFill; @VisibleForTesting View mFill;
@Nullable @Nullable
@Override @Override

@ -28,7 +28,7 @@ public class AudioMediaEncoder extends MediaEncoder {
private static final String TAG = AudioMediaEncoder.class.getSimpleName(); private static final String TAG = AudioMediaEncoder.class.getSimpleName();
private static final CameraLogger LOG = CameraLogger.create(TAG); private static final CameraLogger LOG = CameraLogger.create(TAG);
private static final boolean PERFORMANCE_DEBUG = false; private static final boolean PERFORMANCE_DEBUG = true;
private static final boolean PERFORMANCE_FILL_GAPS = true; private static final boolean PERFORMANCE_FILL_GAPS = true;
private boolean mRequestStop = false; private boolean mRequestStop = false;
@ -67,7 +67,7 @@ public class AudioMediaEncoder extends MediaEncoder {
mConfig.channels); mConfig.channels);
audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
audioFormat.setInteger(MediaFormat.KEY_CHANNEL_MASK, mConfig.audioFormatChannels()); audioFormat.setInteger(MediaFormat.KEY_CHANNEL_MASK, mConfig.audioFormatChannels());
audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, mConfig.bitRate); // TODO multiply by channels? audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, mConfig.bitRate);
try { try {
mMediaCodec = MediaCodec.createEncoderByType(mConfig.mimeType); mMediaCodec = MediaCodec.createEncoderByType(mConfig.mimeType);
} catch (IOException e) { } catch (IOException e) {
@ -157,7 +157,6 @@ public class AudioMediaEncoder extends MediaEncoder {
@Override @Override
public void run() { public void run() {
mLastTimeUs = System.nanoTime() / 1000L;
mAudioRecord.startRecording(); mAudioRecord.startRecording();
while (!mRequestStop) { while (!mRequestStop) {
read(false); read(false);

@ -125,6 +125,8 @@ public abstract class MediaEncoder {
private long mStartTimeUs = Long.MIN_VALUE; // In unknown reference private long mStartTimeUs = Long.MIN_VALUE; // In unknown reference
private long mLastTimeUs = 0; private long mLastTimeUs = 0;
private long mDebugSetStateTimestamp = Long.MIN_VALUE;
/** /**
* Needs a readable name for the thread and for logging. * Needs a readable name for the thread and for logging.
* @param name a name * @param name a name
@ -135,6 +137,12 @@ public abstract class MediaEncoder {
} }
private void setState(int newState) { private void setState(int newState) {
if (mDebugSetStateTimestamp == Long.MIN_VALUE) {
mDebugSetStateTimestamp = System.currentTimeMillis();
}
long millis = System.currentTimeMillis() - mDebugSetStateTimestamp;
mDebugSetStateTimestamp = System.currentTimeMillis();
String newStateName = null; String newStateName = null;
switch (newState) { switch (newState) {
case STATE_NONE: newStateName = "NONE"; break; case STATE_NONE: newStateName = "NONE"; break;
@ -146,7 +154,7 @@ public abstract class MediaEncoder {
case STATE_STOPPING: newStateName = "STOPPING"; break; case STATE_STOPPING: newStateName = "STOPPING"; break;
case STATE_STOPPED: newStateName = "STOPPED"; break; case STATE_STOPPED: newStateName = "STOPPED"; break;
} }
LOG.w(mName, "setState:", newStateName); LOG.w(mName, "setState:", newStateName, "millisSinceLastState:", millis);
mState = newState; mState = newState;
} }

@ -309,6 +309,17 @@ public class MediaEncoderEngine {
/** /**
* Writes the given data to the muxer. Should be called after {@link #isStarted()} * Writes the given data to the muxer. Should be called after {@link #isStarted()}
* returns true. Note: this seems to be thread safe, no lock. * returns true. Note: this seems to be thread safe, no lock.
*
* TODO: Skip first frames from encoder A when encoder B reported a firstTimeMillis
* time that is significantly later. This can happen even if we wait for both to start,
* because {@link MediaEncoder#notifyFirstFrameMillis(long)} can be called while the
* muxer is still closed.
*
* The firstFrameMillis still has a value in computing the absolute times, but it is meant
* to be the time of the first frame read, not necessarily a frame that will be written.
*
* This controller should coordinate between firstFrameMillis and skip frames that have
* large differences.
*/ */
public void write(@NonNull OutputBufferPool pool, @NonNull OutputBuffer buffer) { public void write(@NonNull OutputBufferPool pool, @NonNull OutputBuffer buffer) {
if (!mMediaMuxerStarted) { if (!mMediaMuxerStarted) {

@ -0,0 +1,41 @@
package com.otaliastudios.cameraview.internal.utils;
import androidx.exifinterface.media.ExifInterface;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static junit.framework.Assert.assertNotNull;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class ExifHelperTest {
@Test
public void testValues() {
assertEquals(0, ExifHelper.readExifOrientation(ExifInterface.ORIENTATION_NORMAL));
assertEquals(0, ExifHelper.readExifOrientation(ExifInterface.ORIENTATION_FLIP_HORIZONTAL));
assertEquals(180, ExifHelper.readExifOrientation(ExifInterface.ORIENTATION_ROTATE_180));
assertEquals(180, ExifHelper.readExifOrientation(ExifInterface.ORIENTATION_FLIP_VERTICAL));
assertEquals(90, ExifHelper.readExifOrientation(ExifInterface.ORIENTATION_ROTATE_90));
assertEquals(90, ExifHelper.readExifOrientation(ExifInterface.ORIENTATION_TRANSPOSE));
assertEquals(270, ExifHelper.readExifOrientation(ExifInterface.ORIENTATION_ROTATE_270));
assertEquals(270, ExifHelper.readExifOrientation(ExifInterface.ORIENTATION_TRANSVERSE));
}
@Test
public void testUnknownValues() {
assertEquals(0, ExifHelper.readExifOrientation(-15));
assertEquals(0, ExifHelper.readExifOrientation(-1));
assertEquals(0, ExifHelper.readExifOrientation(195));
assertEquals(0, ExifHelper.readExifOrientation(Integer.MAX_VALUE));
}
}

@ -5,6 +5,11 @@ import android.animation.ValueAnimator;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ImageFormat;
import android.graphics.Rect;
import android.graphics.YuvImage;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.bottomsheet.BottomSheetBehavior;
@ -27,7 +32,10 @@ import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.controls.Mode; import com.otaliastudios.cameraview.controls.Mode;
import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.controls.Preview; import com.otaliastudios.cameraview.controls.Preview;
import com.otaliastudios.cameraview.frame.Frame;
import com.otaliastudios.cameraview.frame.FrameProcessor;
import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -36,6 +44,8 @@ import java.util.List;
public class CameraActivity extends AppCompatActivity implements View.OnClickListener, OptionView.Callback { public class CameraActivity extends AppCompatActivity implements View.OnClickListener, OptionView.Callback {
private final static CameraLogger LOG = CameraLogger.create("DemoApp"); private final static CameraLogger LOG = CameraLogger.create("DemoApp");
private final static boolean USE_FRAME_PROCESSOR = true;
private final static boolean DECODE_BITMAP = true;
private CameraView camera; private CameraView camera;
private ViewGroup controlPanel; private ViewGroup controlPanel;
@ -51,16 +61,33 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis
camera.setLifecycleOwner(this); camera.setLifecycleOwner(this);
camera.addCameraListener(new Listener()); camera.addCameraListener(new Listener());
/* camera.addFrameProcessor(new FrameProcessor() { if (USE_FRAME_PROCESSOR) {
camera.addFrameProcessor(new FrameProcessor() {
private long lastTime = System.currentTimeMillis(); private long lastTime = System.currentTimeMillis();
@Override @Override
public void process(@NonNull Frame frame) { public void process(@NonNull Frame frame) {
long newTime = frame.getTime(); long newTime = frame.getTime();
long delay = newTime - lastTime; long delay = newTime - lastTime;
lastTime = newTime; lastTime = newTime;
Log.e("Frames", "Delay: " + delay + "millis, FPS: " + 1000 / delay); LOG.e("Frame delayMillis:", delay, "FPS:", 1000 / delay);
if (DECODE_BITMAP) {
YuvImage yuvImage = new YuvImage(frame.getData(), ImageFormat.NV21,
frame.getSize().getWidth(),
frame.getSize().getHeight(),
null);
ByteArrayOutputStream jpegStream = new ByteArrayOutputStream();
yuvImage.compressToJpeg(new Rect(0, 0,
frame.getSize().getWidth(),
frame.getSize().getHeight()), 100, jpegStream);
byte[] jpegByteArray = jpegStream.toByteArray();
Bitmap bitmap = BitmapFactory.decodeByteArray(jpegByteArray, 0, jpegByteArray.length);
//noinspection ResultOfMethodCallIgnored
bitmap.toString();
}
}
});
} }
}); */
findViewById(R.id.edit).setOnClickListener(this); findViewById(R.id.edit).setOnClickListener(this);
findViewById(R.id.capturePicture).setOnClickListener(this); findViewById(R.id.capturePicture).setOnClickListener(this);
@ -174,20 +201,23 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis
// This can happen if picture was taken with a gesture. // This can happen if picture was taken with a gesture.
long callbackTime = System.currentTimeMillis(); long callbackTime = System.currentTimeMillis();
if (mCaptureTime == 0) mCaptureTime = callbackTime - 300; if (mCaptureTime == 0) mCaptureTime = callbackTime - 300;
LOG.w("onPictureTaken called! Launching activity. Delay:", callbackTime - mCaptureTime);
PicturePreviewActivity.setPictureResult(result); PicturePreviewActivity.setPictureResult(result);
Intent intent = new Intent(CameraActivity.this, PicturePreviewActivity.class); Intent intent = new Intent(CameraActivity.this, PicturePreviewActivity.class);
intent.putExtra("delay", callbackTime - mCaptureTime); intent.putExtra("delay", callbackTime - mCaptureTime);
LOG.w("Picture delay:", callbackTime - mCaptureTime);
startActivity(intent); startActivity(intent);
mCaptureTime = 0; mCaptureTime = 0;
LOG.w("onPictureTaken called! Launched activity.");
} }
@Override @Override
public void onVideoTaken(@NonNull VideoResult result) { public void onVideoTaken(@NonNull VideoResult result) {
super.onVideoTaken(result); super.onVideoTaken(result);
LOG.w("onVideoTaken called! Launching activity.");
VideoPreviewActivity.setVideoResult(result); VideoPreviewActivity.setVideoResult(result);
Intent intent = new Intent(CameraActivity.this, VideoPreviewActivity.class); Intent intent = new Intent(CameraActivity.this, VideoPreviewActivity.class);
startActivity(intent); startActivity(intent);
LOG.w("onVideoTaken called! Launched activity.");
} }
@Override @Override

@ -23,6 +23,7 @@
app:cameraGrid="off" app:cameraGrid="off"
app:cameraFlash="off" app:cameraFlash="off"
app:cameraAudio="on" app:cameraAudio="on"
app:cameraFacing="front"
app:cameraGestureTap="autoFocus" app:cameraGestureTap="autoFocus"
app:cameraGestureLongTap="none" app:cameraGestureLongTap="none"
app:cameraGesturePinch="zoom" app:cameraGesturePinch="zoom"

Loading…
Cancel
Save