diff --git a/README.md b/README.md
index 78f591cd..eea2942f 100644
--- a/README.md
+++ b/README.md
@@ -26,22 +26,22 @@ compile 'com.otaliastudios:cameraview:2.0.0-beta06'
```
- Fast & reliable
-- Gestures support
-- Camera1 or Camera2 powered engine
-- Frame processing support
-- OpenGL powered preview
-- Take high-quality content with `takePicture` and `takeVideo`
-- Take super-fast snapshots with `takePictureSnapshot` and `takeVideoSnapshot`
-- Smart sizing: create a `CameraView` of any size
-- Control HDR, flash, zoom, white balance, exposure, location, grid drawing & more
+- Gestures support [[docs]](https://natario1.github.io/CameraView/docs/gestures.html)
+- Camera1 or Camera2 powered engine [[docs]](https://natario1.github.io/CameraView/docs/previews.html)
+- Frame processing support [[docs]](https://natario1.github.io/CameraView/docs/frame-processing.html)
+- Watermarks & animated overlays [[docs]](https://natario1.github.io/CameraView/docs/watermarks-and-overlays.html)
+- OpenGL powered preview [[docs]](https://natario1.github.io/CameraView/docs/previews.html)
+- Take high-quality content with `takePicture` and `takeVideo` [[docs]](https://natario1.github.io/CameraView/docs/capturing-media.html)
+- Take super-fast snapshots with `takePictureSnapshot` and `takeVideoSnapshot` [[docs]](https://natario1.github.io/CameraView/docs/capturing-media.html)
+- Smart sizing: create a `CameraView` of any size [[docs]](https://natario1.github.io/CameraView/docs/preview-size.html)
+- Control HDR, flash, zoom, white balance, exposure, location, grid drawing & more [[docs]](https://natario1.github.io/CameraView/docs/controls.html)
- Lightweight
- Works down to API level 15
- Well tested
Read the [official website](https://natario1.github.io/CameraView) for setup instructions and documentation.
-
-- Coming from v1? Take a look at the [migration guide](https://natario1.github.io/CameraView/extra/v1-migration-guide.html)
-- Changelog is hosted [here](https://natario1.github.io/CameraView/about/changelog.html)
+You might also be interested in [changelog](https://natario1.github.io/CameraView/about/changelog.html)
+or in the [v1 migration guide](https://natario1.github.io/CameraView/extra/v1-migration-guide.html).
@@ -56,6 +56,70 @@ donation or become a sponsor, in which case your company logo will immediately s
Thank you for any contribution - it is a nice reward for what has been done until now, and a
motivation boost to push the library forward.
+```xml
+
+
+
+
+
+
+```
+
## Backers
Thanks to all backers! [Become a backer.](https://opencollective.com/cameraview#backer)
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseTest.java
index 03ab2945..f7b53309 100644
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseTest.java
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseTest.java
@@ -33,8 +33,6 @@ import static org.mockito.Mockito.mock;
public class BaseTest {
- public static CameraLogger LOG = CameraLogger.create("Test");
-
private static KeyguardManager.KeyguardLock keyguardLock;
private static PowerManager.WakeLock wakeLock;
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
index b66c20a3..2bdd952f 100644
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
@@ -8,7 +8,12 @@ import android.location.Location;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
+
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.LayoutInflater;
import android.view.MotionEvent;
+import android.view.View;
import android.view.ViewGroup;
import com.otaliastudios.cameraview.controls.Audio;
@@ -36,6 +41,7 @@ import com.otaliastudios.cameraview.internal.utils.Op;
import com.otaliastudios.cameraview.markers.AutoFocusMarker;
import com.otaliastudios.cameraview.markers.DefaultAutoFocusMarker;
import com.otaliastudios.cameraview.markers.MarkerLayout;
+import com.otaliastudios.cameraview.overlay.OverlayLayout;
import com.otaliastudios.cameraview.preview.MockCameraPreview;
import com.otaliastudios.cameraview.preview.CameraPreview;
import com.otaliastudios.cameraview.size.Size;
@@ -48,6 +54,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
+import org.w3c.dom.Attr;
import static org.junit.Assert.*;
@@ -777,5 +784,53 @@ public class CameraViewTest extends BaseTest {
//endregion
+ //region Overlays
+
+ @Test
+ public void testOverlays_generateLayoutParams() {
+ cameraView.mOverlayLayout = spy(cameraView.mOverlayLayout);
+ LayoutInflater inflater = LayoutInflater.from(context());
+ View overlay = inflater.inflate(com.otaliastudios.cameraview.test.R.layout.overlay, cameraView, false);
+ assertTrue(overlay.getLayoutParams() instanceof OverlayLayout.LayoutParams);
+ verify(cameraView.mOverlayLayout, times(1)).isOverlay(any(AttributeSet.class));
+ verify(cameraView.mOverlayLayout, times(1)).generateLayoutParams(any(AttributeSet.class));
+
+ }
+
+ @Test
+ public void testOverlays_dontGenerateLayoutParams() {
+ cameraView.mOverlayLayout = spy(cameraView.mOverlayLayout);
+ LayoutInflater inflater = LayoutInflater.from(context());
+ View overlay = inflater.inflate(com.otaliastudios.cameraview.test.R.layout.not_overlay, cameraView, false);
+ assertFalse(overlay.getLayoutParams() instanceof OverlayLayout.LayoutParams);
+ verify(cameraView.mOverlayLayout, times(1)).isOverlay(any(AttributeSet.class));
+ verify(cameraView.mOverlayLayout, never()).generateLayoutParams(any(AttributeSet.class));
+ }
+
+ @Test
+ public void testOverlays_addOverlayView() {
+ cameraView.mOverlayLayout = spy(cameraView.mOverlayLayout);
+ View overlay = new View(context());
+ OverlayLayout.LayoutParams params = new OverlayLayout.LayoutParams(10, 10);
+ int count = cameraView.getChildCount();
+ cameraView.addView(overlay, 0, params);
+ assertEquals(count, cameraView.getChildCount()); // Not added to CameraView
+ verify(cameraView.mOverlayLayout, times(1)).isOverlay(params);
+ verify(cameraView.mOverlayLayout, times(1)).addView(overlay, params);
+ }
+
+ @Test
+ public void testOverlays_dontAddOverlayView() {
+ cameraView.mOverlayLayout = spy(cameraView.mOverlayLayout);
+ View overlay = new View(context());
+ ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(10, 10);
+ int count = cameraView.getChildCount();
+ cameraView.addView(overlay, 0, params);
+ assertEquals(count + 1, cameraView.getChildCount());
+ verify(cameraView.mOverlayLayout, times(1)).isOverlay(params);
+ verify(cameraView.mOverlayLayout, never()).addView(overlay, params);
+ }
+
+ //endregion
// TODO: test permissions
}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java
index f3300011..3b9fd5a2 100644
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java
@@ -2,6 +2,7 @@ package com.otaliastudios.cameraview.engine;
import android.graphics.Bitmap;
+import android.graphics.Canvas;
import android.graphics.PointF;
import android.hardware.Camera;
import android.os.Build;
@@ -9,6 +10,7 @@ import android.os.Build;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.CameraException;
import com.otaliastudios.cameraview.CameraListener;
+import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.CameraOptions;
import com.otaliastudios.cameraview.CameraUtils;
import com.otaliastudios.cameraview.CameraView;
@@ -25,6 +27,7 @@ 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.overlay.Overlay;
import com.otaliastudios.cameraview.size.Size;
import androidx.annotation.NonNull;
@@ -48,14 +51,21 @@ 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.ArgumentMatchers.eq;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
public abstract class CameraIntegrationTest extends BaseTest {
- private final static long DELAY = 9000;
+ private final static CameraLogger LOG = CameraLogger.create(CameraIntegrationTest.class.getSimpleName());
+ private final static long DELAY = 8000;
+ private final static long VIDEO_DELAY = 16000;
@Rule
public ActivityTestRule rule = new ActivityTestRule<>(TestActivity.class);
@@ -75,6 +85,7 @@ public abstract class CameraIntegrationTest extends BaseTest {
@Before
public void setUp() {
+ LOG.e("Test started. Setting up camera.");
WorkerHandler.destroy();
ui(new Runnable() {
@@ -113,7 +124,7 @@ public abstract class CameraIntegrationTest extends BaseTest {
@After
public void tearDown() {
- camera.stopVideo();
+ LOG.e("Test ended. Tearing down camera.");
camera.destroy();
WorkerHandler.destroy();
}
@@ -133,6 +144,7 @@ public abstract class CameraIntegrationTest extends BaseTest {
if (expectSuccess) {
assertNotNull("Can open", result);
// Extra wait for the bind state.
+ // TODO fix this and other while {} in this class in a more elegant way.
while (controller.getBindState() != CameraEngine.STATE_STARTED) {}
} else {
assertNull("Should not open", result);
@@ -163,7 +175,7 @@ public abstract class CameraIntegrationTest extends BaseTest {
return argument.getReason() == CameraException.REASON_VIDEO_FAILED;
}
}));
- VideoResult result = video.await(DELAY);
+ VideoResult result = video.await(VIDEO_DELAY);
if (expectSuccess) {
assertNotNull("Should end video", result);
} else {
@@ -219,6 +231,7 @@ public abstract class CameraIntegrationTest extends BaseTest {
}
}
+ @SuppressWarnings("unused")
private void takeVideoSnapshotSync(boolean expectSuccess) {
takeVideoSnapshotSync(expectSuccess,0);
}
@@ -431,7 +444,6 @@ public abstract class CameraIntegrationTest extends BaseTest {
@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) {
@@ -472,7 +484,7 @@ public abstract class CameraIntegrationTest extends BaseTest {
assertEquals(oldValue, camera.getPlaySounds());
}
} else {
- // TODO do when Camera2 is completed
+ assertEquals(newValue, camera.getPlaySounds());
}
}
@@ -502,6 +514,14 @@ public abstract class CameraIntegrationTest extends BaseTest {
waitForVideoResult(true);
}
+ @Test
+ public void testStartEndVideoSnapshot() {
+ // TODO should check api level for snapshot?
+ openSync(true);
+ takeVideoSnapshotSync(true, 4000);
+ waitForVideoResult(true);
+ }
+
@Test
public void testEndVideo_withoutStarting() {
camera.setMode(Mode.VIDEO);
@@ -519,6 +539,15 @@ public abstract class CameraIntegrationTest extends BaseTest {
waitForVideoResult(true);
}
+ @Test
+ public void testEndVideoSnapshot_withMaxSize() {
+ // TODO
+ // camera.setVideoMaxSize(3000*1000);
+ // waitForOpen(true);
+ // waitForVideoStart();
+ // waitForVideoEnd(true);
+ }
+
@Test
public void testEndVideo_withMaxDuration() {
camera.setMode(Mode.VIDEO);
@@ -528,6 +557,15 @@ public abstract class CameraIntegrationTest extends BaseTest {
waitForVideoResult(true);
}
+ @Test
+ public void testEndVideoSnapshot_withMaxDuration() {
+ // TODO
+ // camera.setVideoMaxDuration(4000);
+ // waitForOpen(true);
+ // waitForVideoStart();
+ // waitForVideoEnd(true);
+ }
+
//endregion
//region startAutoFocus
@@ -589,18 +627,22 @@ public abstract class CameraIntegrationTest extends BaseTest {
camera.takePicture();
boolean did = latch.await(4, TimeUnit.SECONDS);
assertFalse(did);
- assertEquals(latch.getCount(), 1);
+ assertEquals(1, latch.getCount());
}
+ @SuppressWarnings("StatementWithEmptyBody")
@Test
public void testCapturePicture_size() throws Exception {
openSync(true);
// PictureSize can still be null after opened.
+ // TODO be more elegant
while (camera.getPictureSize() == null) {}
Size size = camera.getPictureSize();
camera.takePicture();
PictureResult result = waitForPictureResult(true);
+ assertNotNull(result);
Bitmap bitmap = CameraUtils.decodeBitmap(result.getData(), Integer.MAX_VALUE, Integer.MAX_VALUE);
+ assertNotNull(bitmap);
assertEquals(result.getSize(), size);
assertEquals(bitmap.getWidth(), size.getWidth());
assertEquals(bitmap.getHeight(), size.getHeight());
@@ -638,16 +680,20 @@ public abstract class CameraIntegrationTest extends BaseTest {
assertEquals(1, latch.getCount());
}
+ @SuppressWarnings("StatementWithEmptyBody")
@Test
public void testCaptureSnapshot_size() throws Exception {
openSync(true);
// SnapshotSize can still be null after opened.
+ // TODO be more elegant
while (camera.getSnapshotSize() == null) {}
Size size = camera.getSnapshotSize();
camera.takePictureSnapshot();
PictureResult result = waitForPictureResult(true);
+ assertNotNull(result);
Bitmap bitmap = CameraUtils.decodeBitmap(result.getData(), Integer.MAX_VALUE, Integer.MAX_VALUE);
+ assertNotNull(bitmap);
assertEquals(result.getSize(), size);
assertEquals(bitmap.getWidth(), size.getWidth());
assertEquals(bitmap.getHeight(), size.getHeight());
@@ -735,4 +781,32 @@ public abstract class CameraIntegrationTest extends BaseTest {
}
//endregion
+
+ //region Overlays
+
+ @Test
+ public void testOverlay_forPictureSnapshot() {
+ Overlay overlay = mock(Overlay.class);
+ when(overlay.drawsOn(any(Overlay.Target.class))).thenReturn(true);
+ controller.setOverlay(overlay);
+ openSync(true);
+ camera.takePictureSnapshot();
+ waitForPictureResult(true);
+ verify(overlay, atLeastOnce()).drawsOn(Overlay.Target.PICTURE_SNAPSHOT);
+ verify(overlay, times(1)).drawOn(eq(Overlay.Target.PICTURE_SNAPSHOT), any(Canvas.class));
+ }
+
+ @Test
+ public void testOverlay_forVideoSnapshot() {
+ Overlay overlay = mock(Overlay.class);
+ when(overlay.drawsOn(any(Overlay.Target.class))).thenReturn(true);
+ controller.setOverlay(overlay);
+ openSync(true);
+ takeVideoSnapshotSync(true, 4000);
+ waitForVideoResult(true);
+ verify(overlay, atLeastOnce()).drawsOn(Overlay.Target.VIDEO_SNAPSHOT);
+ verify(overlay, atLeastOnce()).drawOn(eq(Overlay.Target.VIDEO_SNAPSHOT), any(Canvas.class));
+ }
+
+ //endregion
}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/overlay/OverlayLayoutTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/overlay/OverlayLayoutTest.java
new file mode 100644
index 00000000..729385d3
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/overlay/OverlayLayoutTest.java
@@ -0,0 +1,197 @@
+package com.otaliastudios.cameraview.overlay;
+
+
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.util.Xml;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.test.annotation.UiThreadTest;
+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 org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatcher;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.w3c.dom.Attr;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.notNull;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+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;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class OverlayLayoutTest extends BaseTest {
+
+ private OverlayLayout overlayLayout;
+
+ @Before
+ public void setUp() {
+ overlayLayout = spy(new OverlayLayout(context()));
+ }
+
+ @After
+ public void tearDown() {
+ overlayLayout = null;
+ }
+
+ @Test
+ public void testIsOverlay_LayoutParams() {
+ ViewGroup.LayoutParams params;
+
+ params = new ViewGroup.LayoutParams(10, 10);
+ assertFalse(overlayLayout.isOverlay(params));
+
+ params = new OverlayLayout.LayoutParams(10, 10);
+ assertTrue(overlayLayout.isOverlay(params));
+ }
+
+ @Test
+ public void testIsOverlay_attributeSet() throws Exception {
+ int layout1 = com.otaliastudios.cameraview.test.R.layout.overlay;
+ int layout2 = com.otaliastudios.cameraview.test.R.layout.not_overlay;
+
+ AttributeSet set1 = getAttributeSet(layout1);
+ assertTrue(overlayLayout.isOverlay(set1));
+
+ AttributeSet set2 = getAttributeSet(layout2);
+ assertFalse(overlayLayout.isOverlay(set2));
+ }
+
+ @NonNull
+ private AttributeSet getAttributeSet(int layout) throws Exception {
+ // Get the attribute set in the correct state: use a parser and move to START_TAG
+ XmlResourceParser parser = context().getResources().getLayout(layout);
+ //noinspection StatementWithEmptyBody
+ while (parser.next() != XmlResourceParser.START_TAG) {}
+ return Xml.asAttributeSet(parser);
+ }
+
+ @Test
+ public void testLayoutParams_drawsOn() {
+ OverlayLayout.LayoutParams params = new OverlayLayout.LayoutParams(10, 10);
+
+ assertFalse(params.drawsOn(Overlay.Target.PREVIEW));
+ assertFalse(params.drawsOn(Overlay.Target.PICTURE_SNAPSHOT));
+ assertFalse(params.drawsOn(Overlay.Target.VIDEO_SNAPSHOT));
+
+ params.drawOnPreview = true;
+ assertTrue(params.drawsOn(Overlay.Target.PREVIEW));
+ params.drawOnPictureSnapshot = true;
+ assertTrue(params.drawsOn(Overlay.Target.PICTURE_SNAPSHOT));
+ params.drawOnVideoSnapshot = true;
+ assertTrue(params.drawsOn(Overlay.Target.VIDEO_SNAPSHOT));
+ }
+
+ @Test
+ public void testLayoutParams_toString() {
+ OverlayLayout.LayoutParams params = new OverlayLayout.LayoutParams(10, 10);
+ String string = params.toString();
+ assertTrue(string.contains("drawOnPreview"));
+ assertTrue(string.contains("drawOnPictureSnapshot"));
+ assertTrue(string.contains("drawOnVideoSnapshot"));
+ }
+
+ @Test
+ public void testDrawChild() {
+ Canvas canvas = new Canvas();
+ OverlayLayout.LayoutParams params = new OverlayLayout.LayoutParams(10, 10);
+ View child = new View(context());
+ child.setLayoutParams(params);
+ when(overlayLayout.doDrawChild(canvas, child, 0)).thenReturn(true);
+
+ overlayLayout.currentTarget = Overlay.Target.PREVIEW;
+ assertFalse(overlayLayout.drawChild(canvas, child, 0));
+ params.drawOnPreview = true;
+ assertTrue(overlayLayout.drawChild(canvas, child, 0));
+
+ overlayLayout.currentTarget = Overlay.Target.PICTURE_SNAPSHOT;
+ assertFalse(overlayLayout.drawChild(canvas, child, 0));
+ params.drawOnPictureSnapshot = true;
+ assertTrue(overlayLayout.drawChild(canvas, child, 0));
+
+ overlayLayout.currentTarget = Overlay.Target.VIDEO_SNAPSHOT;
+ assertFalse(overlayLayout.drawChild(canvas, child, 0));
+ params.drawOnVideoSnapshot = true;
+ assertTrue(overlayLayout.drawChild(canvas, child, 0));
+ }
+
+ @UiThreadTest
+ @Test
+ public void testDraw() {
+ Canvas canvas = new Canvas();
+ when(overlayLayout.drawsOn(Overlay.Target.PREVIEW)).thenReturn(false);
+ overlayLayout.draw(canvas);
+ verify(overlayLayout, never()).drawOn(Overlay.Target.PREVIEW, canvas);
+
+ when(overlayLayout.drawsOn(Overlay.Target.PREVIEW)).thenReturn(true);
+ overlayLayout.draw(canvas);
+ verify(overlayLayout, times(1)).drawOn(Overlay.Target.PREVIEW, canvas);
+ }
+
+ @UiThreadTest
+ @Test
+ public void testDrawOn() {
+ Canvas canvas = spy(new Canvas());
+ View child = new View(context());
+ OverlayLayout.LayoutParams params = new OverlayLayout.LayoutParams(10, 10);
+ params.drawOnPreview = true;
+ params.drawOnPictureSnapshot = true;
+ params.drawOnVideoSnapshot = true;
+ overlayLayout.addView(child, params);
+
+ overlayLayout.drawOn(Overlay.Target.PREVIEW, canvas);
+ verify(canvas, never()).scale(anyFloat(), anyFloat());
+ verify(overlayLayout, times(1)).doDrawChild(eq(canvas), eq(child), anyLong());
+ reset(canvas);
+ reset(overlayLayout);
+
+ overlayLayout.drawOn(Overlay.Target.PICTURE_SNAPSHOT, canvas);
+ verify(canvas, times(1)).scale(anyFloat(), anyFloat());
+ verify(overlayLayout, times(1)).doDrawChild(eq(canvas), eq(child), anyLong());
+ reset(canvas);
+ reset(overlayLayout);
+
+ overlayLayout.drawOn(Overlay.Target.VIDEO_SNAPSHOT, canvas);
+ verify(canvas, times(1)).scale(anyFloat(), anyFloat());
+ verify(overlayLayout, times(1)).doDrawChild(eq(canvas), eq(child), anyLong());
+ reset(canvas);
+ reset(overlayLayout);
+ }
+}
diff --git a/cameraview/src/androidTest/res/layout/not_overlay.xml b/cameraview/src/androidTest/res/layout/not_overlay.xml
new file mode 100644
index 00000000..158a0406
--- /dev/null
+++ b/cameraview/src/androidTest/res/layout/not_overlay.xml
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/cameraview/src/androidTest/res/layout/overlay.xml b/cameraview/src/androidTest/res/layout/overlay.xml
new file mode 100644
index 00000000..ff33576e
--- /dev/null
+++ b/cameraview/src/androidTest/res/layout/overlay.xml
@@ -0,0 +1,8 @@
+
+
\ No newline at end of file
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
index 2c6292fa..f8e0d6ff 100644
--- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
+++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
@@ -28,6 +28,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
+import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
@@ -62,6 +63,7 @@ import com.otaliastudios.cameraview.internal.utils.CropHelper;
import com.otaliastudios.cameraview.internal.utils.OrientationHelper;
import com.otaliastudios.cameraview.internal.utils.WorkerHandler;
import com.otaliastudios.cameraview.markers.MarkerParser;
+import com.otaliastudios.cameraview.overlay.OverlayLayout;
import com.otaliastudios.cameraview.preview.CameraPreview;
import com.otaliastudios.cameraview.preview.GlCameraPreview;
import com.otaliastudios.cameraview.preview.SurfaceCameraPreview;
@@ -125,12 +127,15 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
@VisibleForTesting ScrollGestureFinder mScrollGestureFinder;
// Views
- GridLinesLayout mGridLinesLayout;
- MarkerLayout mMarkerLayout;
+ @VisibleForTesting GridLinesLayout mGridLinesLayout;
+ @VisibleForTesting MarkerLayout mMarkerLayout;
private boolean mKeepScreenOn;
@SuppressWarnings({"FieldCanBeLocal", "unused"})
private boolean mExperimental;
+ // Overlays
+ @VisibleForTesting OverlayLayout mOverlayLayout;
+
// Threading
private Handler mUiHandler;
private WorkerHandler mFrameProcessorsHandler;
@@ -187,9 +192,11 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
// Views
mGridLinesLayout = new GridLinesLayout(context);
+ mOverlayLayout = new OverlayLayout(context);
mMarkerLayout = new MarkerLayout(context);
addView(mGridLinesLayout);
addView(mMarkerLayout);
+ addView(mOverlayLayout);
// Create the engine
doInstantiateEngine();
@@ -237,7 +244,10 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
* {@link #setEngine(Engine)} is called.
*/
private void doInstantiateEngine() {
+ LOG.w("doInstantiateEngine:", "instantiating. engine:", mEngine);
mCameraEngine = instantiateCameraEngine(mEngine, mCameraCallbacks);
+ LOG.w("doInstantiateEngine:", "instantiated. engine:", mCameraEngine.getClass().getSimpleName());
+ mCameraEngine.setOverlay(mOverlayLayout);
}
/**
@@ -247,7 +257,9 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
*/
@VisibleForTesting
void doInstantiatePreview() {
+ LOG.w("doInstantiateEngine:", "instantiating. preview:", mPreview);
mCameraPreview = instantiatePreview(mPreview, getContext(), this);
+ LOG.w("doInstantiateEngine:", "instantiated. preview:", mCameraPreview.getClass().getSimpleName());
mCameraEngine.setPreview(mCameraPreview);
}
@@ -279,7 +291,6 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
*/
@NonNull
protected CameraPreview instantiatePreview(@NonNull Preview preview, @NonNull Context context, @NonNull ViewGroup container) {
- LOG.w("preview:", "isHardwareAccelerated:", isHardwareAccelerated());
switch (preview) {
case SURFACE:
return new SurfaceCameraPreview(context, container);
@@ -300,6 +311,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (mCameraPreview == null) {
+
// isHardwareAccelerated will return the real value only after we are
// attached. That's why we instantiate the preview here.
doInstantiatePreview();
@@ -384,7 +396,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
// other than respect it. The preview will eventually be cropped at the sides (by PreviewImpl scaling)
// except the case in which these fixed dimensions manage to fit exactly the preview aspect ratio.
if (widthMode == EXACTLY && heightMode == EXACTLY) {
- LOG.w("onMeasure:", "both are MATCH_PARENT or fixed value. We adapt.",
+ LOG.i("onMeasure:", "both are MATCH_PARENT or fixed value. We adapt.",
"This means CROP_CENTER.", "(" + widthValue + "x" + heightValue + ")");
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
@@ -1218,6 +1230,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
mCameraEngine.setAutoFocusResetDelay(delayMillis);
}
+
/**
* Returns the current delay in milliseconds to reset the focus after an autofocus process.
* @return the current autofocus reset delay in milliseconds.
@@ -2078,4 +2091,26 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
}
//endregion
+
+ //region Overlays
+
+ @Override
+ public LayoutParams generateLayoutParams(AttributeSet attributeSet) {
+ if (mOverlayLayout.isOverlay(attributeSet)) {
+ return mOverlayLayout.generateLayoutParams(attributeSet);
+ }
+ return super.generateLayoutParams(attributeSet);
+ }
+
+ // We don't support removeView on overlays for now.
+ @Override
+ public void addView(View child, int index, ViewGroup.LayoutParams params) {
+ if (mOverlayLayout.isOverlay(params)) {
+ mOverlayLayout.addView(child, params);
+ } else {
+ super.addView(child, index, params);
+ }
+ }
+
+ //endregion
}
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java
index 0efad6b2..5ee4a480 100644
--- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java
+++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java
@@ -312,7 +312,7 @@ public class Camera1Engine extends CameraEngine implements
AspectRatio outputRatio = getAngles().flip(Reference.OUTPUT, Reference.VIEW) ? viewAspectRatio.flip() : viewAspectRatio;
if (mPreview instanceof GlCameraPreview && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
- mPictureRecorder = new SnapshotGlPictureRecorder(stub, this, (GlCameraPreview) mPreview, outputRatio);
+ mPictureRecorder = new SnapshotGlPictureRecorder(stub, this, (GlCameraPreview) mPreview, outputRatio, getOverlay());
} else {
mPictureRecorder = new Snapshot1PictureRecorder(stub, this, mCamera, outputRatio);
}
@@ -359,10 +359,20 @@ public class Camera1Engine extends CameraEngine implements
Rect outputCrop = CropHelper.computeCrop(outputSize, outputRatio);
outputSize = new Size(outputCrop.width(), outputCrop.height());
stub.size = outputSize;
+ // Vertical: 0 (270-0-0)
+ // Left (unlocked): 0 (270-90-270)
+ // Right (unlocked): 0 (270-270-90)
+ // Upside down (unlocked): 0 (270-180-180)
+ // Left (locked): 270 (270-0-270)
+ // Right (locked): 90 (270-0-90)
+ // Upside down (locked): 180 (270-0-180)
+ // The correct formula seems to be deviceOrientation+displayOffset,
+ // which means offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE).
stub.rotation = getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE);
+ LOG.i("onTakeVideoSnapshot", "rotation:", stub.rotation, "size:", stub.size);
// Start.
- mVideoRecorder = new SnapshotVideoRecorder(Camera1Engine.this, glPreview);
+ mVideoRecorder = new SnapshotVideoRecorder(Camera1Engine.this, glPreview, getOverlay(), stub.rotation);
mVideoRecorder.start(stub);
}
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java
index 964eb976..392529e1 100644
--- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java
+++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java
@@ -615,7 +615,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR); // Actually it will be rotated and set to 0.
AspectRatio outputRatio = getAngles().flip(Reference.OUTPUT, Reference.VIEW) ? viewAspectRatio.flip() : viewAspectRatio;
if (mPreview instanceof GlCameraPreview) {
- mPictureRecorder = new SnapshotGlPictureRecorder(stub, this, (GlCameraPreview) mPreview, outputRatio);
+ mPictureRecorder = new SnapshotGlPictureRecorder(stub, this, (GlCameraPreview) mPreview, outputRatio, getOverlay());
} else {
throw new RuntimeException("takePictureSnapshot with Camera2 is only supported with Preview.GL_SURFACE");
}
@@ -670,7 +670,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
private void doTakeVideo(@NonNull final VideoResult.Stub stub) {
if (!(mVideoRecorder instanceof Full2VideoRecorder)) {
- mVideoRecorder = new Full2VideoRecorder(this, mCameraId);
+ throw new IllegalStateException("doTakeVideo called, but video recorder is not a Full2VideoRecorder! " + mVideoRecorder);
}
Full2VideoRecorder recorder = (Full2VideoRecorder) mVideoRecorder;
try {
@@ -706,10 +706,22 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
Rect outputCrop = CropHelper.computeCrop(outputSize, outputRatio);
outputSize = new Size(outputCrop.width(), outputCrop.height());
stub.size = outputSize;
- stub.rotation = getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE);
+ // Vertical: 0 (270-0-0)
+ // Left (unlocked): 270 (270-90-270)
+ // Right (unlocked): 90 (270-270-90)
+ // Upside down (unlocked): 180 (270-180-180)
+ // Left (locked): 270 (270-0-270)
+ // Right (locked): 90 (270-0-90)
+ // Upside down (locked): 180 (270-0-180)
+ // Unlike Camera1, the correct formula seems to be deviceOrientation,
+ // which means offset(Reference.BASE, Reference.OUTPUT, Axis.ABSOLUTE).
+ stub.rotation = getAngles().offset(Reference.BASE, Reference.OUTPUT, Axis.ABSOLUTE);
+ LOG.i("onTakeVideoSnapshot", "rotation:", stub.rotation, "size:", stub.size);
// Start.
- mVideoRecorder = new SnapshotVideoRecorder(this, glPreview);
+ // The overlay rotation should alway be VIEW-OUTPUT, just liek Camera1Engine.
+ int overlayRotation = getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE);
+ mVideoRecorder = new SnapshotVideoRecorder(this, glPreview, getOverlay(), overlayRotation);
mVideoRecorder.start(stub);
}
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java
index 9de9388c..3a82de46 100644
--- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java
+++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java
@@ -18,6 +18,7 @@ import com.otaliastudios.cameraview.CameraException;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.CameraOptions;
import com.otaliastudios.cameraview.PictureResult;
+import com.otaliastudios.cameraview.overlay.Overlay;
import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.engine.offset.Angles;
import com.otaliastudios.cameraview.engine.offset.Reference;
@@ -184,6 +185,7 @@ public abstract class CameraEngine implements
private long mAutoFocusResetDelayMillis;
private int mSnapshotMaxWidth = Integer.MAX_VALUE; // in REF_VIEW for consistency with SizeSelectors
private int mSnapshotMaxHeight = Integer.MAX_VALUE; // in REF_VIEW for consistency with SizeSelectors
+ private Overlay overlay;
// Steps
private final Step.Callback mStepCallback = new Step.Callback() {
@@ -781,6 +783,15 @@ public abstract class CameraEngine implements
//region Final setters and getters
+ public final void setOverlay(@Nullable Overlay overlay) {
+ this.overlay = overlay;
+ }
+
+ @Nullable
+ public final Overlay getOverlay() {
+ return overlay;
+ }
+
@SuppressWarnings("WeakerAccess")
public final Angles getAngles() {
return mAngles;
@@ -1218,27 +1229,31 @@ public abstract class CameraEngine implements
@Nullable
public final Size getPictureSize(@SuppressWarnings("SameParameterValue") @NonNull Reference reference) {
- if (mCaptureSize == null || mMode == Mode.VIDEO) return null;
- return getAngles().flip(Reference.SENSOR, reference) ? mCaptureSize.flip() : mCaptureSize;
+ Size size = mCaptureSize;
+ if (size == null || mMode == Mode.VIDEO) return null;
+ return getAngles().flip(Reference.SENSOR, reference) ? size.flip() : size;
}
@Nullable
public final Size getVideoSize(@SuppressWarnings("SameParameterValue") @NonNull Reference reference) {
- if (mCaptureSize == null || mMode == Mode.PICTURE) return null;
- return getAngles().flip(Reference.SENSOR, reference) ? mCaptureSize.flip() : mCaptureSize;
+ Size size = mCaptureSize;
+ if (size == null || mMode == Mode.PICTURE) return null;
+ return getAngles().flip(Reference.SENSOR, reference) ? size.flip() : size;
}
@Nullable
public final Size getPreviewStreamSize(@NonNull Reference reference) {
- if (mPreviewStreamSize == null) return null;
- return getAngles().flip(Reference.SENSOR, reference) ? mPreviewStreamSize.flip() : mPreviewStreamSize;
+ Size size = mPreviewStreamSize;
+ if (size == null) return null;
+ return getAngles().flip(Reference.SENSOR, reference) ? size.flip() : size;
}
@SuppressWarnings("SameParameterValue")
@Nullable
private Size getPreviewSurfaceSize(@NonNull Reference reference) {
- if (mPreview == null) return null;
- return getAngles().flip(Reference.VIEW, reference) ? mPreview.getSurfaceSize().flip() : mPreview.getSurfaceSize();
+ CameraPreview preview = mPreview;
+ if (preview == null) return null;
+ return getAngles().flip(Reference.VIEW, reference) ? preview.getSurfaceSize().flip() : preview.getSurfaceSize();
}
/**
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/offset/Angles.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/offset/Angles.java
index 56c9199b..a8718418 100644
--- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/offset/Angles.java
+++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/offset/Angles.java
@@ -3,6 +3,7 @@ package com.otaliastudios.cameraview.engine.offset;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
+import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.controls.Facing;
/**
@@ -21,6 +22,9 @@ import com.otaliastudios.cameraview.controls.Facing;
*/
public class Angles {
+ private final static String TAG = Angles.class.getSimpleName();
+ private final static CameraLogger LOG = CameraLogger.create(TAG);
+
private Facing mSensorFacing;
@VisibleForTesting int mSensorOffset = 0;
@VisibleForTesting int mDisplayOffset = 0;
@@ -40,6 +44,7 @@ public class Angles {
if (mSensorFacing == Facing.FRONT) {
mSensorOffset = sanitizeOutput(360 - mSensorOffset);
}
+ print();
}
/**
@@ -49,6 +54,7 @@ public class Angles {
public void setDisplayOffset(int displayOffset) {
sanitizeInput(displayOffset);
mDisplayOffset = displayOffset;
+ print();
}
/**
@@ -58,6 +64,14 @@ public class Angles {
public void setDeviceOrientation(int deviceOrientation) {
sanitizeInput(deviceOrientation);
mDeviceOrientation = deviceOrientation;
+ print();
+ }
+
+ private void print() {
+ LOG.i("Angles changed:",
+ "sensorOffset:", mSensorOffset,
+ "displayOffset:", mDisplayOffset,
+ "deviceOrientation:", mDeviceOrientation);
}
/**
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglViewport.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglViewport.java
index 3496bf95..54114c9d 100644
--- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglViewport.java
+++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglViewport.java
@@ -145,6 +145,12 @@ public class EglViewport extends EglElement {
GLES20.glUseProgram(mProgramHandle);
check("glUseProgram");
+ // enable blending, from: http://www.learnopengles.com/android-lesson-five-an-introduction-to-blending/
+ GLES20.glDisable(GLES20.GL_CULL_FACE);
+ GLES20.glDisable(GLES20.GL_DEPTH_TEST);
+ GLES20.glEnable(GLES20.GL_BLEND);
+ GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);
+
// Set the texture.
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(mTextureTarget, textureId);
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/Pool.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/Pool.java
index b59a0819..4a0408b9 100644
--- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/Pool.java
+++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/Pool.java
@@ -63,17 +63,17 @@ public class Pool {
T item = mQueue.poll();
if (item != null) {
activeCount++; // poll decreases, this fixes
- LOG.v("GET: Reusing recycled item.", this);
+ LOG.v("GET - Reusing recycled item.", this);
return item;
}
if (isEmpty()) {
- LOG.v("GET: Returning null. Too much items requested.", this);
+ LOG.v("GET - Returning null. Too much items requested.", this);
return null;
}
activeCount++;
- LOG.v("GET: Creating a new item.", this);
+ LOG.v("GET - Creating a new item.", this);
return factory.create();
}
@@ -84,7 +84,7 @@ public class Pool {
* @param item used item
*/
public void recycle(@NonNull T item) {
- LOG.v("RECYCLE: Recycling item.", this);
+ LOG.v("RECYCLE - Recycling item.", this);
if (--activeCount < 0) {
throw new IllegalStateException("Trying to recycle an item which makes activeCount < 0." +
"This means that this or some previous items being recycled were not coming from " +
@@ -112,6 +112,7 @@ public class Pool {
*
* @return count
*/
+ @SuppressWarnings("WeakerAccess")
public final int count() {
return activeCount() + recycledCount();
}
@@ -122,6 +123,7 @@ public class Pool {
*
* @return active count
*/
+ @SuppressWarnings("WeakerAccess")
public final int activeCount() {
return activeCount;
}
@@ -133,6 +135,7 @@ public class Pool {
*
* @return recycled count
*/
+ @SuppressWarnings("WeakerAccess")
public final int recycledCount() {
return mQueue.size();
}
@@ -140,6 +143,6 @@ public class Pool {
@NonNull
@Override
public String toString() {
- return getClass().getSimpleName() + " -- count:" + count() + ", active:" + activeCount() + ", recycled:" + recycledCount();
+ return getClass().getSimpleName() + " - count:" + count() + ", active:" + activeCount() + ", recycled:" + recycledCount();
}
}
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/overlay/Overlay.java b/cameraview/src/main/java/com/otaliastudios/cameraview/overlay/Overlay.java
new file mode 100644
index 00000000..44e1661d
--- /dev/null
+++ b/cameraview/src/main/java/com/otaliastudios/cameraview/overlay/Overlay.java
@@ -0,0 +1,33 @@
+package com.otaliastudios.cameraview.overlay;
+
+import android.graphics.Canvas;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Base interface for overlays.
+ */
+public interface Overlay {
+
+ enum Target {
+ PREVIEW, PICTURE_SNAPSHOT, VIDEO_SNAPSHOT
+ }
+
+ /**
+ * Called for this overlay to draw itself on the specified target and canvas.
+ *
+ * @param target target
+ * @param canvas target canvas
+ */
+ void drawOn(@NonNull Target target, @NonNull Canvas canvas);
+
+ /**
+ * Called to understand if this overlay would like to draw onto the given
+ * target or not. If true is returned, {@link #drawOn(Target, Canvas)} can be
+ * called at a future time.
+ *
+ * @param target the target
+ * @return true to draw on it
+ */
+ boolean drawsOn(@NonNull Target target);
+}
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/overlay/OverlayLayout.java b/cameraview/src/main/java/com/otaliastudios/cameraview/overlay/OverlayLayout.java
new file mode 100644
index 00000000..931a5905
--- /dev/null
+++ b/cameraview/src/main/java/com/otaliastudios/cameraview/overlay/OverlayLayout.java
@@ -0,0 +1,211 @@
+package com.otaliastudios.cameraview.overlay;
+
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.otaliastudios.cameraview.CameraLogger;
+import com.otaliastudios.cameraview.R;
+
+
+@SuppressLint("CustomViewStyleable")
+public class OverlayLayout extends FrameLayout implements Overlay {
+
+ private static final String TAG = OverlayLayout.class.getSimpleName();
+ private static final CameraLogger LOG = CameraLogger.create(TAG);
+
+ @VisibleForTesting Target currentTarget = Target.PREVIEW;
+
+ /**
+ * We set {@link #setWillNotDraw(boolean)} to false even if we don't draw anything.
+ * This ensures that the View system will call {@link #draw(Canvas)} on us instead
+ * of short-circuiting to {@link #dispatchDraw(Canvas)}.
+ *
+ * That would be a problem for us since we use {@link #draw(Canvas)} to understand if
+ * we are currently drawing on the preview or not.
+ *
+ * @param context a context
+ */
+ public OverlayLayout(@NonNull Context context) {
+ super(context);
+ setWillNotDraw(false);
+ }
+
+ /**
+ * Returns true if this {@link AttributeSet} belongs to an overlay.
+ * @param set an attribute set
+ * @return true if overlay
+ */
+ public boolean isOverlay(@Nullable AttributeSet set) {
+ if (set == null) return false;
+ TypedArray a = getContext().obtainStyledAttributes(set, R.styleable.CameraView_Layout);
+ boolean isOverlay =
+ a.hasValue(R.styleable.CameraView_Layout_layout_drawOnPreview)
+ || a.hasValue(R.styleable.CameraView_Layout_layout_drawOnPictureSnapshot)
+ || a.hasValue(R.styleable.CameraView_Layout_layout_drawOnVideoSnapshot);
+ a.recycle();
+ return isOverlay;
+ }
+
+ /**
+ * Returns true if this {@link ViewGroup.LayoutParams} belongs to an overlay.
+ * @param params a layout params
+ * @return true if overlay
+ */
+ public boolean isOverlay(@NonNull ViewGroup.LayoutParams params) {
+ return params instanceof LayoutParams;
+ }
+
+ /**
+ * Generates our own overlay layout params.
+ * @param attrs input attrs
+ * @return our params
+ */
+ @Override
+ public OverlayLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new LayoutParams(getContext(), attrs);
+ }
+
+ /**
+ * This is called by the View hierarchy, so at this point we are
+ * likely drawing on the preview.
+ * @param canvas View canvas
+ */
+ @SuppressLint("MissingSuperCall")
+ @Override
+ public void draw(Canvas canvas) {
+ LOG.i("normal draw called.");
+ if (drawsOn(Target.PREVIEW)) {
+ drawOn(Target.PREVIEW, canvas);
+ }
+ }
+
+ @Override
+ public boolean drawsOn(@NonNull Target target) {
+ for (int i = 0; i < getChildCount(); i++) {
+ LayoutParams params = (LayoutParams) getChildAt(i).getLayoutParams();
+ if (params.drawsOn(target)) return true;
+ }
+ return false;
+ }
+
+ /**
+ * For {@link Target#PREVIEW}, this method is called by the View hierarchy. We will
+ * just forward the call to super.
+ *
+ * For {@link Target#PICTURE_SNAPSHOT} and {@link Target#VIDEO_SNAPSHOT},
+ * this method is called by the overlay drawer. We call {@link #dispatchDraw(Canvas)}
+ * to draw our children only.
+ *
+ * @param target the draw target
+ * @param canvas the canvas
+ */
+ @Override
+ public void drawOn(@NonNull Target target, @NonNull Canvas canvas) {
+ synchronized (this) {
+ currentTarget = target;
+ switch (target) {
+ case PREVIEW:
+ super.draw(canvas);
+ break;
+ case VIDEO_SNAPSHOT:
+ case PICTURE_SNAPSHOT:
+ canvas.save();
+ // The input canvas size is that of the preview stream, cropped to match
+ // the view aspect ratio (this op is done by picture & video recorder).
+ // So the aspect ratio is guaranteed to be the same, but we might have
+ // to apply some scale (typically > 1).
+ float widthScale = canvas.getWidth() / (float) getWidth();
+ float heightScale = canvas.getHeight() / (float) getHeight();
+ LOG.i("draw",
+ "target:", target,
+ "canvas:", canvas.getWidth() + "x" + canvas.getHeight(),
+ "view:", getWidth() + "x" + getHeight(),
+ "widthScale:", widthScale,
+ "heightScale:", heightScale
+ );
+ canvas.scale(widthScale, heightScale);
+ dispatchDraw(canvas);
+ canvas.restore();
+ break;
+ }
+ }
+ }
+
+ /**
+ * We end up here in all three cases, and should filter out
+ * views that are not meant to be drawn on that specific surface.
+ */
+ @Override
+ protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
+ LayoutParams params = (LayoutParams) child.getLayoutParams();
+ if (params.drawsOn(currentTarget)) {
+ LOG.v("Performing drawing for view:", child.getClass().getSimpleName(),
+ "target:", currentTarget,
+ "params:", params);
+ return doDrawChild(canvas, child, drawingTime);
+ } else {
+ LOG.v("Skipping drawing for view:", child.getClass().getSimpleName(),
+ "target:", currentTarget,
+ "params:", params);
+ return false;
+ }
+ }
+
+ @VisibleForTesting
+ boolean doDrawChild(Canvas canvas, View child, long drawingTime) {
+ return super.drawChild(canvas, child, drawingTime);
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ public static class LayoutParams extends FrameLayout.LayoutParams {
+
+ @SuppressWarnings("unused")
+ public boolean drawOnPreview = false;
+ public boolean drawOnPictureSnapshot = false;
+ public boolean drawOnVideoSnapshot = false;
+
+ public LayoutParams(int width, int height) {
+ super(width, height);
+ }
+
+ public LayoutParams(@NonNull Context context, @NonNull AttributeSet attrs) {
+ super(context, attrs);
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CameraView_Layout);
+ try {
+ drawOnPreview = a.getBoolean(R.styleable.CameraView_Layout_layout_drawOnPreview, false);
+ drawOnPictureSnapshot = a.getBoolean(R.styleable.CameraView_Layout_layout_drawOnPictureSnapshot, false);
+ drawOnVideoSnapshot = a.getBoolean(R.styleable.CameraView_Layout_layout_drawOnVideoSnapshot, false);
+ } finally {
+ a.recycle();
+ }
+ }
+
+ @VisibleForTesting
+ boolean drawsOn(@NonNull Target target) {
+ return ((target == Target.PREVIEW && drawOnPreview)
+ || (target == Target.VIDEO_SNAPSHOT && drawOnVideoSnapshot)
+ || (target == Target.PICTURE_SNAPSHOT && drawOnPictureSnapshot));
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return getClass().getName() + "["
+ + "drawOnPreview:" + drawOnPreview
+ + ",drawOnPictureSnapshot:" + drawOnPictureSnapshot
+ + ",drawOnVideoSnapshot:" + drawOnVideoSnapshot
+ + "]";
+ }
+ }
+}
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotGlPictureRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotGlPictureRecorder.java
index e969a67a..d13ef3a9 100644
--- a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotGlPictureRecorder.java
+++ b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotGlPictureRecorder.java
@@ -2,6 +2,9 @@ package com.otaliastudios.cameraview.picture;
import android.annotation.TargetApi;
import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.opengl.EGL14;
@@ -11,8 +14,10 @@ import android.os.Build;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.PictureResult;
+import com.otaliastudios.cameraview.overlay.Overlay;
import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.engine.CameraEngine;
+import com.otaliastudios.cameraview.engine.offset.Axis;
import com.otaliastudios.cameraview.engine.offset.Reference;
import com.otaliastudios.cameraview.internal.egl.EglCore;
import com.otaliastudios.cameraview.internal.egl.EglViewport;
@@ -26,7 +31,9 @@ import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import android.view.Surface;
public class SnapshotGlPictureRecorder extends PictureRecorder {
@@ -37,15 +44,21 @@ public class SnapshotGlPictureRecorder extends PictureRecorder {
private GlCameraPreview mPreview;
private AspectRatio mOutputRatio;
+ private Overlay mOverlay;
+ private boolean mHasOverlay;
+
public SnapshotGlPictureRecorder(
@NonNull PictureResult.Stub stub,
@NonNull CameraEngine engine,
@NonNull GlCameraPreview preview,
- @NonNull AspectRatio outputRatio) {
+ @NonNull AspectRatio outputRatio,
+ @Nullable Overlay overlay) {
super(stub, engine);
mEngine = engine;
mPreview = preview;
mOutputRatio = outputRatio;
+ mOverlay = overlay;
+ mHasOverlay = overlay != null && overlay.drawsOn(Overlay.Target.PICTURE_SNAPSHOT);
}
@TargetApi(Build.VERSION_CODES.KITKAT)
@@ -57,15 +70,31 @@ public class SnapshotGlPictureRecorder extends PictureRecorder {
SurfaceTexture mSurfaceTexture;
float[] mTransform;
+ int mOverlayTextureId = 0;
+ SurfaceTexture mOverlaySurfaceTexture;
+ Surface mOverlaySurface;
+ float[] mOverlayTransform;
+
+ EglViewport mViewport;
+
@RendererThread
public void onRendererTextureCreated(int textureId) {
mTextureId = textureId;
+ mViewport = new EglViewport();
mSurfaceTexture = new SurfaceTexture(mTextureId, true);
// Need to crop the size.
Rect crop = CropHelper.computeCrop(mResult.size, mOutputRatio);
mResult.size = new Size(crop.width(), crop.height());
mSurfaceTexture.setDefaultBufferSize(mResult.size.getWidth(), mResult.size.getHeight());
mTransform = new float[16];
+
+ if (mHasOverlay) {
+ mOverlayTextureId = mViewport.createTexture();
+ mOverlaySurfaceTexture = new SurfaceTexture(mOverlayTextureId, true);
+ mOverlaySurfaceTexture.setDefaultBufferSize(mResult.size.getWidth(), mResult.size.getHeight());
+ mOverlaySurface = new Surface(mOverlaySurfaceTexture);
+ mOverlayTransform = new float[16];
+ }
}
@RendererThread
@@ -97,14 +126,14 @@ public class SnapshotGlPictureRecorder extends PictureRecorder {
WorkerHandler.execute(new Runnable() {
@Override
public void run() {
+ // 1. Get latest texture
EglWindowSurface surface = new EglWindowSurface(core, mSurfaceTexture);
surface.makeCurrent();
- EglViewport viewport = new EglViewport();
mSurfaceTexture.updateTexImage();
mSurfaceTexture.getTransformMatrix(mTransform);
- // Apply scale and crop:
- // NOTE: scaleX and scaleY are in REF_VIEW, while our input appears to be in REF_SENSOR.
+ // 2. Apply scale and crop:
+ // scaleX and scaleY are in REF_VIEW, while our input appears to be in REF_SENSOR.
boolean flip = mEngine.getAngles().flip(Reference.VIEW, Reference.SENSOR);
float realScaleX = flip ? scaleY : scaleX;
float realScaleY = flip ? scaleX : scaleY;
@@ -113,38 +142,62 @@ public class SnapshotGlPictureRecorder extends PictureRecorder {
Matrix.translateM(mTransform, 0, scaleTranslX, scaleTranslY, 0);
Matrix.scaleM(mTransform, 0, realScaleX, realScaleY, 1);
- // Fix rotation:
- // Not sure why we need the minus here... It makes no sense to me.
- LOG.w("Recording frame. Rotation:", mResult.rotation, "Actual:", -mResult.rotation);
- int rotation = -mResult.rotation;
- mResult.rotation = 0;
-
- // Go back to 0,0 so that rotate and flip work well.
+ // 3. Go back to 0,0 so that rotate and flip work well.
Matrix.translateM(mTransform, 0, 0.5F, 0.5F, 0);
- // Apply rotation:
- Matrix.rotateM(mTransform, 0, rotation, 0, 0, 1);
+ // 4. Apply rotation:
+ // Not sure why we need the minus here.
+ Matrix.rotateM(mTransform, 0, -mResult.rotation, 0, 0, 1);
+ mResult.rotation = 0;
- // Flip horizontally for front camera:
+ // 5. Flip horizontally for front camera:
if (mResult.facing == Facing.FRONT) {
Matrix.scaleM(mTransform, 0, -1, 1, 1);
}
- // Go back to old position.
+ // 6. Go back to old position.
Matrix.translateM(mTransform, 0, -0.5F, -0.5F, 0);
- // Future note: passing scale values to the viewport?
- // They are simply realScaleX and realScaleY.
- viewport.drawFrame(mTextureId, mTransform);
+ // 7. Do pretty much the same for overlays, though with
+ // some differences.
+ if (mHasOverlay) {
+ // 1. First we must draw on the texture and get latest image.
+ try {
+ final Canvas surfaceCanvas = mOverlaySurface.lockCanvas(null);
+ surfaceCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
+ mOverlay.drawOn(Overlay.Target.PICTURE_SNAPSHOT, surfaceCanvas);
+ mOverlaySurface.unlockCanvasAndPost(surfaceCanvas);
+ } catch (Surface.OutOfResourcesException e) {
+ LOG.w("Got Surface.OutOfResourcesException while drawing picture overlays", e);
+ }
+ mOverlaySurfaceTexture.updateTexImage();
+ mOverlaySurfaceTexture.getTransformMatrix(mOverlayTransform);
+
+ // 2. Then we can apply the transformations.
+ int rotation = mEngine.getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE);
+ Matrix.translateM(mOverlayTransform, 0, 0.5F, 0.5F, 0);
+ Matrix.rotateM(mOverlayTransform, 0, rotation, 0, 0, 1);
+ // No need to flip the x axis for front camera, but need to flip the y axis always.
+ Matrix.scaleM(mOverlayTransform, 0, 1, -1, 1);
+ Matrix.translateM(mOverlayTransform, 0, -0.5F, -0.5F, 0);
+ }
+
+ // 8. Draw and save
+ mViewport.drawFrame(mTextureId, mTransform);
+ if (mHasOverlay) mViewport.drawFrame(mOverlayTextureId, mOverlayTransform);
// don't - surface.swapBuffers();
mResult.data = surface.saveFrameTo(Bitmap.CompressFormat.JPEG);
mResult.format = PictureResult.FORMAT_JPEG;
- mSurfaceTexture.releaseTexImage();
- // EGL14.eglMakeCurrent(oldDisplay, oldSurface, oldSurface, eglContext);
+ // 9. Cleanup
+ mSurfaceTexture.releaseTexImage();
surface.release();
- viewport.release();
+ mViewport.release();
mSurfaceTexture.release();
+ if (mHasOverlay) {
+ mOverlaySurface.release();
+ mOverlaySurfaceTexture.release();
+ }
core.release();
dispatchResult();
}
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/preview/CameraPreview.java b/cameraview/src/main/java/com/otaliastudios/cameraview/preview/CameraPreview.java
index a184731d..29470b7c 100644
--- a/cameraview/src/main/java/com/otaliastudios/cameraview/preview/CameraPreview.java
+++ b/cameraview/src/main/java/com/otaliastudios/cameraview/preview/CameraPreview.java
@@ -1,14 +1,21 @@
package com.otaliastudios.cameraview.preview;
import android.content.Context;
+
+import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
import androidx.annotation.VisibleForTesting;
+import android.os.Handler;
+import android.os.Looper;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
+import com.google.android.gms.tasks.TaskCompletionSource;
+import com.google.android.gms.tasks.Tasks;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.engine.CameraEngine;
import com.otaliastudios.cameraview.internal.utils.Op;
@@ -240,7 +247,32 @@ public abstract class CameraPreview {
* Called by the hosting {@link com.otaliastudios.cameraview.CameraView},
* this is a lifecycle event.
*/
+ @CallSuper
public void onDestroy() {
+ if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
+ onDestroyView();
+ } else {
+ // Do this on the UI thread and wait.
+ Handler ui = new Handler(Looper.getMainLooper());
+ final TaskCompletionSource task = new TaskCompletionSource<>();
+ ui.post(new Runnable() {
+ @Override
+ public void run() {
+ onDestroyView();
+ task.setResult(null);
+ }
+ });
+ try { Tasks.await(task.getTask()); } catch (Exception ignore) {}
+ }
+ }
+
+ /**
+ * At this point we undo the work that was done during {@link #onCreateView(Context, ViewGroup)},
+ * which basically means removing the root view from the hierarchy.
+ */
+ @SuppressWarnings("WeakerAccess")
+ @UiThread
+ protected void onDestroyView() {
View root = getRootView();
ViewParent parent = root.getParent();
if (parent instanceof ViewGroup) {
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java b/cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java
index 947114e0..54f62371 100644
--- a/cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java
+++ b/cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java
@@ -5,7 +5,6 @@ import android.graphics.SurfaceTexture;
import android.opengl.GLSurfaceView;
import android.opengl.Matrix;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import android.view.LayoutInflater;
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java
index 83eab2dd..92b43341 100644
--- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java
+++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java
@@ -1,14 +1,19 @@
package com.otaliastudios.cameraview.video;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
import android.graphics.SurfaceTexture;
import android.opengl.EGL14;
import android.os.Build;
+import android.view.Surface;
import com.otaliastudios.cameraview.CameraLogger;
+import com.otaliastudios.cameraview.overlay.Overlay;
import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.engine.CameraEngine;
-import com.otaliastudios.cameraview.engine.offset.Reference;
+import com.otaliastudios.cameraview.internal.egl.EglViewport;
import com.otaliastudios.cameraview.preview.GlCameraPreview;
import com.otaliastudios.cameraview.preview.RendererFrameCallback;
import com.otaliastudios.cameraview.preview.RendererThread;
@@ -40,25 +45,33 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
private static final int STATE_NOT_RECORDING = 1;
private MediaEncoderEngine mEncoderEngine;
- private CameraEngine mEngine;
private GlCameraPreview mPreview;
- private boolean mFlipped;
private int mCurrentState = STATE_NOT_RECORDING;
private int mDesiredState = STATE_NOT_RECORDING;
private int mTextureId = 0;
+ private int mOverlayTextureId = 0;
+ private SurfaceTexture mOverlaySurfaceTexture;
+ private Surface mOverlaySurface;
+ private Overlay mOverlay;
+ private boolean mHasOverlay;
+ private int mOverlayRotation;
+
public SnapshotVideoRecorder(@NonNull CameraEngine engine,
- @NonNull GlCameraPreview preview) {
+ @NonNull GlCameraPreview preview,
+ @Nullable Overlay overlay,
+ int overlayRotation) {
super(engine);
mPreview = preview;
- mEngine = engine;
+ mOverlay = overlay;
+ mHasOverlay = overlay != null && overlay.drawsOn(Overlay.Target.VIDEO_SNAPSHOT);
+ mOverlayRotation = overlayRotation;
}
@Override
protected void onStart() {
mPreview.addRendererFrameCallback(this);
- mFlipped = mEngine.getAngles().flip(Reference.SENSOR, Reference.VIEW);
mDesiredState = STATE_RECORDING;
dispatchVideoRecordingStart();
}
@@ -72,6 +85,13 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
@Override
public void onRendererTextureCreated(int textureId) {
mTextureId = textureId;
+ if (mHasOverlay) {
+ EglViewport temp = new EglViewport();
+ mOverlayTextureId = temp.createTexture();
+ mOverlaySurfaceTexture = new SurfaceTexture(mOverlayTextureId);
+ mOverlaySurfaceTexture.setDefaultBufferSize(mResult.size.getWidth(), mResult.size.getHeight());
+ mOverlaySurface = new Surface(mOverlaySurfaceTexture);
+ }
}
@RendererThread
@@ -104,8 +124,9 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
mResult.rotation,
type, mTextureId,
scaleX, scaleY,
- mFlipped,
- EGL14.eglGetCurrentContext()
+ EGL14.eglGetCurrentContext(),
+ mHasOverlay ? mOverlayTextureId : TextureMediaEncoder.NO_TEXTURE,
+ mOverlayRotation
);
TextureMediaEncoder videoEncoder = new TextureMediaEncoder(config);
@@ -129,6 +150,21 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
TextureMediaEncoder.TextureFrame textureFrame = textureEncoder.acquireFrame();
textureFrame.timestamp = surfaceTexture.getTimestamp();
surfaceTexture.getTransformMatrix(textureFrame.transform);
+
+ // get overlay
+ if (mHasOverlay) {
+ try {
+ final Canvas surfaceCanvas = mOverlaySurface.lockCanvas(null);
+ surfaceCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
+ mOverlay.drawOn(Overlay.Target.VIDEO_SNAPSHOT, surfaceCanvas);
+ mOverlaySurface.unlockCanvasAndPost(surfaceCanvas);
+ } catch (Surface.OutOfResourcesException e) {
+ LOG.w("Got Surface.OutOfResourcesException while drawing video overlays", e);
+ }
+ mOverlaySurfaceTexture.updateTexImage();
+ mOverlaySurfaceTexture.getTransformMatrix(textureFrame.overlayTransform);
+ }
+
if (mEncoderEngine != null) {
// can happen on teardown
mEncoderEngine.notify(TextureMediaEncoder.FRAME_EVENT, textureFrame);
@@ -142,28 +178,41 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
mEncoderEngine = null;
mPreview.removeRendererFrameCallback(SnapshotVideoRecorder.this);
mPreview = null;
+ if (mOverlaySurfaceTexture != null) {
+ mOverlaySurfaceTexture.release();
+ mOverlaySurfaceTexture = null;
+ }
+ if (mOverlaySurface != null) {
+ mOverlaySurface.release();
+ mOverlaySurface = null;
+ }
}
}
+ @Override
+ public void onEncodingStart() {
+ // Do nothing.
+ }
+
@EncoderThread
@Override
- public void onEncoderStop(int stopReason, @Nullable Exception e) {
+ public void onEncodingEnd(int stopReason, @Nullable Exception e) {
// If something failed, undo the result, since this is the mechanism
// to notify Camera1Engine about this.
if (e != null) {
- LOG.e("Error onEncoderStop", e);
+ LOG.e("Error onEncodingEnd", e);
mResult = null;
mError = e;
} else {
- if (stopReason == MediaEncoderEngine.STOP_BY_MAX_DURATION) {
- LOG.i("onEncoderStop because of max duration.");
+ if (stopReason == MediaEncoderEngine.END_BY_MAX_DURATION) {
+ LOG.i("onEncodingEnd because of max duration.");
mResult.endReason = VideoResult.REASON_MAX_DURATION_REACHED;
- } else if (stopReason == MediaEncoderEngine.STOP_BY_MAX_SIZE) {
- LOG.i("onEncoderStop because of max size.");
+ } else if (stopReason == MediaEncoderEngine.END_BY_MAX_SIZE) {
+ LOG.i("onEncodingEnd because of max size.");
mResult.endReason = VideoResult.REASON_MAX_SIZE_REACHED;
} else {
- LOG.i("onEncoderStop because of user.");
+ LOG.i("onEncodingEnd because of user.");
}
}
// Cleanup
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioMediaEncoder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioMediaEncoder.java
index c2e15d2e..5978629c 100644
--- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioMediaEncoder.java
+++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioMediaEncoder.java
@@ -44,17 +44,27 @@ public class AudioMediaEncoder extends MediaEncoder {
private static final int SAMPLE_SIZE = 2; // byte/sample/channel
private static final int BYTE_RATE_PER_CHANNEL = SAMPLING_FREQUENCY * SAMPLE_SIZE; // byte/sec/channel
private static final int BYTE_RATE = BYTE_RATE_PER_CHANNEL * CHANNELS_COUNT; // byte/sec
-
- static final int BIT_RATE = BYTE_RATE * 8; // bit/sec
+ @SuppressWarnings("unused")
+ private static final int BIT_RATE = BYTE_RATE * 8; // bit/sec
// We call FRAME here the chunk of data that we want to read at each loop cycle
private static final int FRAME_SIZE_PER_CHANNEL = 1024; // bytes/frame/channel [AAC constant]
private static final int FRAME_SIZE = FRAME_SIZE_PER_CHANNEL * CHANNELS_COUNT; // bytes/frame
- // We allocate buffers of 1KB each, which is not so much. I would say that allocating
- // at most 200 of them is a reasonable value. With the current setup, in device tests,
- // we manage to use 50 at most.
- private static final int BUFFER_POOL_MAX_SIZE = 200;
+ // We allocate buffers of 1KB each, which is not so much. This value indicates the maximum
+ // number of these buffers that we can allocate at a given instant.
+ // This value is the number of runnables that the encoder thread is allowed to be 'behind'
+ // the recorder thread. It's not safe to have it very large or we can end encoding A LOT AFTER
+ // the actual recording. It's better to reduce this and skip recording at all.
+ private static final int BUFFER_POOL_MAX_SIZE = 60;
+
+ private static long bytesToUs(int bytes) {
+ return (1000000L * bytes) / BYTE_RATE;
+ }
+
+ private static long bytesToUs(long bytes) {
+ return (1000000L * bytes) / BYTE_RATE;
+ }
private boolean mRequestStop = false;
private AudioEncodingHandler mEncoder;
@@ -157,7 +167,7 @@ public class AudioMediaEncoder extends MediaEncoder {
while (!mRequestStop) {
read(false);
}
- LOG.w("RECORDER: Stop was requested. We're out of the loop. Will post an endOfStream.");
+ LOG.w("Stop was requested. We're out of the loop. Will post an endOfStream.");
// Last input with 0 length. This will signal the endOfStream.
// Can't use drain(true); it is only available when writing to the codec InputSurface.
read(true);
@@ -169,20 +179,21 @@ public class AudioMediaEncoder extends MediaEncoder {
private void read(boolean endOfStream) {
mCurrentBuffer = mByteBufferPool.get();
if (mCurrentBuffer == null) {
- LOG.e("Skipping audio frame, encoding is too slow.");
- // TODO should fix the next presentation time here. However this is
- // extremely unlikely based on my tests. The mByteBufferPool should be big enough.
+ LOG.e("read thread - eos:", endOfStream, "- Skipping audio frame, encoding is too slow.");
+ // Should fix the next presentation time here, but
} else {
mCurrentBuffer.clear();
mReadBytes = mAudioRecord.read(mCurrentBuffer, FRAME_SIZE);
+ LOG.i("read thread - eos:", endOfStream, "- Read new audio frame. Bytes:", mReadBytes);
if (mReadBytes > 0) { // Good read: increase PTS.
- increaseTime(mReadBytes);
+ mLastTimeUs = increaseTime(mReadBytes);
+ LOG.i("read thread - eos:", endOfStream, "- Frame PTS:", mLastTimeUs);
mCurrentBuffer.limit(mReadBytes);
onBuffer(endOfStream);
} else if (mReadBytes == AudioRecord.ERROR_INVALID_OPERATION) {
- LOG.e("Got AudioRecord.ERROR_INVALID_OPERATION");
+ LOG.e("read thread - eos:", endOfStream, "- Got AudioRecord.ERROR_INVALID_OPERATION");
} else if (mReadBytes == AudioRecord.ERROR_BAD_VALUE) {
- LOG.e("Got AudioRecord.ERROR_BAD_VALUE");
+ LOG.e("read thread - eos:", endOfStream, "- Got AudioRecord.ERROR_BAD_VALUE");
}
}
}
@@ -193,12 +204,12 @@ public class AudioMediaEncoder extends MediaEncoder {
* to the consumer.
*/
private void onBuffer(boolean endOfStream) {
+ LOG.v("read thread - Sending buffer to encoder thread.");
mEncoder.sendInputBuffer(mCurrentBuffer, mLastTimeUs, endOfStream);
}
- private void increaseTime(int readBytes) {
- increaseTime3(readBytes);
- LOG.v("Read", readBytes, "bytes, increasing PTS to", mLastTimeUs);
+ private long increaseTime(int readBytes) {
+ return increaseTime3(readBytes);
}
/**
@@ -206,49 +217,61 @@ public class AudioMediaEncoder extends MediaEncoder {
* It will use System.nanoTime() just once, as the starting point.
* Of course we don't as there are things going on in this thread.
*/
- private void increaseTime1(int readBytes) {
- mLastTimeUs += (1000000L * readBytes) / BYTE_RATE;
+ @SuppressWarnings("unused")
+ private long increaseTime1(int readBytes) {
+ return mLastTimeUs + bytesToUs(readBytes);
}
/**
* Just for testing, this method will use Api 24 method to retrieve the timestamp.
* This way we let the platform choose instead of making assumptions.
*/
+ @SuppressWarnings("unused")
@RequiresApi(24)
- private void increaseTime2(int readBytes) {
+ private long increaseTime2(int readBytes) {
if (mApi24Timestamp == null) {
mApi24Timestamp = new AudioTimestamp();
}
mAudioRecord.getTimestamp(mApi24Timestamp, AudioTimestamp.TIMEBASE_MONOTONIC);
- mLastTimeUs = mApi24Timestamp.nanoTime / 1000;
+ return mApi24Timestamp.nanoTime / 1000;
}
private AudioTimestamp mApi24Timestamp;
/**
* This method looks like an improvement over {@link #increaseTime1(int)} as it
* accounts for the current time as well. Adapted & improved. from Kickflip.
+ *
+ * This creates regular timestamps unless we accumulate a lot of delay (greater than
+ * twice the buffer duration), in which case it creates a gap and starts again trying
+ * to be regular from the new point.
*/
- private void increaseTime3(int readBytes) {
- long currentTime = System.nanoTime() / 1000;
- long correctedTime;
- long bufferDuration = (1000000 * readBytes) / BYTE_RATE;
- long bufferTime = currentTime - bufferDuration; // delay of acquiring the audio buffer
- if (mTotalReadBytes == 0) {
- mStartTimeUs = bufferTime;
- }
+ private long increaseTime3(int readBytes) {
+ long bufferDurationUs = bytesToUs(readBytes);
+ long bufferEndTimeUs = System.nanoTime() / 1000; // now
+ long bufferStartTimeUs = bufferEndTimeUs - bufferDurationUs;
+
+ // If this is the first time, the base time is the buffer start time.
+ if (mBytesSinceBaseTime == 0) mBaseTimeUs = bufferStartTimeUs;
+
// Recompute time assuming that we are respecting the sampling frequency.
- // However, if the correction is too big (> 2*bufferDuration), reset to this point.
- correctedTime = mStartTimeUs + (1000000 * mTotalReadBytes) / BYTE_RATE;
- if(bufferTime - correctedTime >= 2 * bufferDuration) {
- mStartTimeUs = bufferTime;
- mTotalReadBytes = 0;
- correctedTime = mStartTimeUs;
+ // This puts the time at the end of last read buffer, which means, where we
+ // should be if we had no delay / missed buffers.
+ long correctedTimeUs = mBaseTimeUs + bytesToUs(mBytesSinceBaseTime);
+ long correctionUs = bufferStartTimeUs - correctedTimeUs;
+
+ // However, if the correction is too big (> 2*bufferDurationUs), reset to this point.
+ // This is triggered if we lose buffers and are recording/encoding at a slower rate.
+ if (correctionUs >= 2L * bufferDurationUs) {
+ mBaseTimeUs = bufferStartTimeUs;
+ mBytesSinceBaseTime = readBytes;
+ return mBaseTimeUs;
+ } else {
+ mBytesSinceBaseTime += readBytes;
+ return correctedTimeUs;
}
- mTotalReadBytes += readBytes;
- mLastTimeUs = correctedTime;
}
- private long mStartTimeUs;
- private long mTotalReadBytes;
+ private long mBaseTimeUs;
+ private long mBytesSinceBaseTime;
}
/**
@@ -278,9 +301,11 @@ public class AudioMediaEncoder extends MediaEncoder {
super.handleMessage(msg);
boolean endOfStream = msg.what == 1;
long timestamp = (((long) msg.arg1) << 32) | (((long) msg.arg2) & 0xffffffffL);
+ LOG.i("encoding thread - got buffer. timestamp:", timestamp, "eos:", endOfStream);
ByteBuffer buffer = (ByteBuffer) msg.obj;
int readBytes = buffer.remaining();
InputBuffer inputBuffer = mInputBufferPool.get();
+ //noinspection ConstantConditions
inputBuffer.source = buffer;
inputBuffer.timestamp = timestamp;
inputBuffer.length = readBytes;
@@ -290,7 +315,7 @@ public class AudioMediaEncoder extends MediaEncoder {
}
private void performPendingOps(boolean force) {
- LOG.v("Performing", mPendingOps.size(), "Pending operations.");
+ LOG.i("encoding thread - performing", mPendingOps.size(), "pending operations. force:", force);
InputBuffer buffer;
while ((buffer = mPendingOps.peek()) != null) {
if (force) {
@@ -305,17 +330,43 @@ public class AudioMediaEncoder extends MediaEncoder {
}
private void performPendingOp(InputBuffer buffer) {
- buffer.data.put(buffer.source);
+ LOG.i("encoding thread - performing pending operation for timestamp:", buffer.timestamp, "- encoding.");
+ buffer.data.put(buffer.source); // TODO this copy is prob. the worst part here for performance
mByteBufferPool.recycle(buffer.source);
mPendingOps.remove(buffer);
encodeInputBuffer(buffer);
boolean eos = buffer.isEndOfStream;
mInputBufferPool.recycle(buffer);
- drainOutput(eos);
- if (eos) {
- mInputBufferPool.clear();
- WorkerHandler.get("AudioEncodingHandler").getThread().interrupt();
+ if (eos) mInputBufferPool.clear();
+ LOG.i("encoding thread - performing pending operation for timestamp:", buffer.timestamp, "- draining.");
+ // NOTE: can consider calling this drainOutput on yet another thread, which would let us
+ // use an even smaller BUFFER_POOL_MAX_SIZE without losing audio frames. But this way
+ // we can accumulate delay on this new thread without noticing (no pool getting empty).
+ if (true) {
+ drainOutput(eos);
+ if (eos) WorkerHandler.get("AudioEncodingHandler").getThread().interrupt();
+ } else {
+ // Testing the option above.
+ WorkerHandler.get("AudioEncodingDrainer").remove(drainRunnable);
+ WorkerHandler.get("AudioEncodingDrainer").remove(drainRunnableEos);
+ WorkerHandler.get("AudioEncodingDrainer").post(eos ? drainRunnableEos : drainRunnable);
}
}
+
+ private final Runnable drainRunnable = new Runnable() {
+ @Override
+ public void run() {
+ drainOutput(false);
+ }
+ };
+
+ private final Runnable drainRunnableEos = new Runnable() {
+ @Override
+ public void run() {
+ drainOutput(true);
+ WorkerHandler.get("AudioEncodingHandler").getThread().interrupt();
+ WorkerHandler.get("AudioEncodingDrainer").getThread().interrupt();
+ }
+ };
}
}
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoder.java
index 2fa7b0f0..b7881416 100644
--- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoder.java
+++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoder.java
@@ -88,11 +88,11 @@ abstract class MediaEncoder {
* NOTE: it's important to call {@link WorkerHandler#post(Runnable)} instead of run()!
*/
final void start() {
- LOG.i(getName(), "Start was called. Posting.");
+ LOG.w(getName(), "Start was called. Posting.");
mWorker.post(new Runnable() {
@Override
public void run() {
- LOG.i(getName(), "Start was called. Executing.");
+ LOG.w(getName(), "Start was called. Executing.");
onStart();
}
});
@@ -108,11 +108,11 @@ abstract class MediaEncoder {
* @param data object
*/
final void notify(final @NonNull String event, final @Nullable Object data) {
- LOG.i(getName(), "Notify was called. Posting.");
+ LOG.v(getName(), "Notify was called. Posting.");
mWorker.post(new Runnable() {
@Override
public void run() {
- LOG.i(getName(), "Notify was called. Executing.");
+ LOG.v(getName(), "Notify was called. Executing.");
onEvent(event, data);
}
});
@@ -124,11 +124,11 @@ abstract class MediaEncoder {
* NOTE: it's important to call {@link WorkerHandler#post(Runnable)} instead of run()!
*/
final void stop() {
- LOG.i(getName(), "Stop was called. Posting.");
+ LOG.w(getName(), "Stop was called. Posting.");
mWorker.post(new Runnable() {
@Override
public void run() {
- LOG.i(getName(), "Stop was called. Executing.");
+ LOG.w(getName(), "Stop was called. Executing.");
onStop();
}
});
@@ -175,8 +175,9 @@ abstract class MediaEncoder {
* parameters, might also be through an input buffer flag).
*/
private void release() {
- LOG.w("Subclass", getName(), "Notified that it is released.");
- mController.requestRelease(mTrackIndex);
+ LOG.w(getName(), "is being released. Notifying controller and releasing codecs.");
+ // TODO should we notify after this method?
+ mController.notifyReleased(mTrackIndex);
mMediaCodec.stop();
mMediaCodec.release();
mMediaCodec = null;
@@ -217,7 +218,7 @@ abstract class MediaEncoder {
/**
* Returns a new input buffer and index, waiting indefinitely if none is available.
- * The buffer should be written into, then the index should be passed to {@link #encodeInputBuffer(InputBuffer)}.
+ * The buffer should be written into, then be passed to {@link #encodeInputBuffer(InputBuffer)}.
*
* @param holder the input buffer holder
*/
@@ -233,7 +234,7 @@ abstract class MediaEncoder {
*/
@SuppressWarnings("WeakerAccess")
protected void encodeInputBuffer(InputBuffer buffer) {
- LOG.w("ENCODING:", getName(), "Buffer:", buffer.index, "Bytes:", buffer.length, "Presentation:", buffer.timestamp);
+ LOG.v(getName(), "ENCODING - Buffer:", buffer.index, "Bytes:", buffer.length, "Presentation:", buffer.timestamp);
if (buffer.isEndOfStream) { // send EOS
mMediaCodec.queueInputBuffer(buffer.index, 0, 0,
buffer.timestamp, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
@@ -266,7 +267,7 @@ abstract class MediaEncoder {
@SuppressLint("LogNotTimber")
@SuppressWarnings("WeakerAccess")
protected void drainOutput(boolean drainAll) {
- LOG.w("DRAINING:", getName(), "EOS:", drainAll);
+ LOG.v(getName(), "DRAINING - EOS:", drainAll);
if (mMediaCodec == null) {
LOG.e("drain() was called before prepare() or after releasing.");
return;
@@ -315,14 +316,12 @@ abstract class MediaEncoder {
// and should be used for offsets only.
// TODO find a better way, this causes sync issues. (+ note: this sends pts=0 at first)
// mBufferInfo.presentationTimeUs = mLastPresentationTimeUs - mStartPresentationTimeUs;
- LOG.i("DRAINING:", getName(), "Dispatching write(). Presentation:", mBufferInfo.presentationTimeUs);
+ LOG.v(getName(), "DRAINING - About to write(). Presentation:", mBufferInfo.presentationTimeUs);
// TODO fix the mBufferInfo being the same, then implement delayed writing in Controller
// and remove the isStarted() check here.
OutputBuffer buffer = mOutputBufferPool.get();
- if (buffer == null) {
- throw new IllegalStateException("buffer is null!");
- }
+ //noinspection ConstantConditions
buffer.info = mBufferInfo;
buffer.trackIndex = mTrackIndex;
buffer.data = encodedData;
@@ -336,17 +335,18 @@ abstract class MediaEncoder {
&& !mMaxLengthReached
&& mStartPresentationTimeUs != Long.MIN_VALUE
&& mLastPresentationTimeUs - mStartPresentationTimeUs > mMaxLengthMillis * 1000) {
- LOG.w("DRAINING: Reached maxLength! mLastPresentationTimeUs:", mLastPresentationTimeUs,
+ LOG.w(getName(), "DRAINING - Reached maxLength! mLastPresentationTimeUs:", mLastPresentationTimeUs,
"mStartPresentationTimeUs:", mStartPresentationTimeUs,
"mMaxLengthUs:", mMaxLengthMillis * 1000);
mMaxLengthReached = true;
+ LOG.w(getName(), "DRAINING - Requesting a stop.");
mController.requestStop(mTrackIndex);
break;
}
// Check for the EOS flag so we can release the encoder.
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
- LOG.w("DRAINING:", getName(), "Dispatching release().");
+ LOG.w(getName(), "DRAINING - Got EOS. Releasing the codec.");
release();
break;
}
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoderEngine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoderEngine.java
index 8ba29f56..cdc079c1 100644
--- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoderEngine.java
+++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoderEngine.java
@@ -26,34 +26,40 @@ public class MediaEncoderEngine {
*/
public interface Listener {
+ /**
+ * Called when encoding started.
+ */
+ @EncoderThread
+ void onEncodingStart();
+
/**
* Called when encoding stopped for some reason.
* If there's an exception, it failed.
- * @param stopReason the reason
+ * @param reason the reason
* @param e the error, if present
*/
@EncoderThread
- void onEncoderStop(int stopReason, @Nullable Exception e);
+ void onEncodingEnd(int reason, @Nullable Exception e);
}
private final static String TAG = MediaEncoderEngine.class.getSimpleName();
private final static CameraLogger LOG = CameraLogger.create(TAG);
@SuppressWarnings("WeakerAccess")
- public final static int STOP_BY_USER = 0;
- public final static int STOP_BY_MAX_DURATION = 1;
- public final static int STOP_BY_MAX_SIZE = 2;
+ public final static int END_BY_USER = 0;
+ public final static int END_BY_MAX_DURATION = 1;
+ public final static int END_BY_MAX_SIZE = 2;
private ArrayList mEncoders;
private MediaMuxer mMediaMuxer;
private int mStartedEncodersCount;
- private int mStoppedEncodersCount;
+ private int mReleasedEncodersCount;
private boolean mMediaMuxerStarted;
@SuppressWarnings("FieldCanBeLocal")
private Controller mController;
private Listener mListener;
- private int mStopReason = STOP_BY_USER;
- private int mPossibleStopReason;
+ private int mEndReason = END_BY_USER;
+ private int mPossibleEndReason;
private final Object mControllerLock = new Object();
/**
@@ -87,7 +93,7 @@ public class MediaEncoderEngine {
}
mStartedEncodersCount = 0;
mMediaMuxerStarted = false;
- mStoppedEncodersCount = 0;
+ mReleasedEncodersCount = 0;
// Trying to convert the size constraints to duration constraints,
// because they are super easy to check.
@@ -101,13 +107,13 @@ public class MediaEncoderEngine {
long finalMaxDuration = Long.MAX_VALUE;
if (maxSize > 0 && maxDuration > 0) {
- mPossibleStopReason = sizeMaxDuration < maxDuration ? STOP_BY_MAX_SIZE : STOP_BY_MAX_DURATION;
+ mPossibleEndReason = sizeMaxDuration < maxDuration ? END_BY_MAX_SIZE : END_BY_MAX_DURATION;
finalMaxDuration = Math.min(sizeMaxDuration, maxDuration);
} else if (maxSize > 0) {
- mPossibleStopReason = STOP_BY_MAX_SIZE;
+ mPossibleEndReason = END_BY_MAX_SIZE;
finalMaxDuration = sizeMaxDuration;
} else if (maxDuration > 0) {
- mPossibleStopReason = STOP_BY_MAX_DURATION;
+ mPossibleEndReason = END_BY_MAX_DURATION;
finalMaxDuration = maxDuration;
}
LOG.w("Computed a max duration of", (finalMaxDuration / 1000F));
@@ -120,6 +126,7 @@ public class MediaEncoderEngine {
* Asks encoders to start (each one on its own track).
*/
public final void start() {
+ LOG.i("Passing event to encoders:", "START");
for (MediaEncoder encoder : mEncoders) {
encoder.start();
}
@@ -133,6 +140,7 @@ public class MediaEncoderEngine {
*/
@SuppressWarnings("SameParameterValue")
public final void notify(final String event, final Object data) {
+ LOG.i("Passing event to encoders:", event);
for (MediaEncoder encoder : mEncoders) {
encoder.notify(event, data);
}
@@ -140,21 +148,23 @@ public class MediaEncoderEngine {
/**
* Asks encoders to stop. This is not sync, of course we will ask for encoders
- * to call {@link Controller#requestRelease(int)} before actually stop the muxer.
- * When all encoders request a release, {@link #release()} is called to do cleanup
+ * to call {@link Controller#notifyReleased(int)} before actually stop the muxer.
+ * When all encoders request a release, {@link #end()} is called to do cleanup
* and notify the listener.
*/
public final void stop() {
+ LOG.i("Passing event to encoders:", "STOP");
for (MediaEncoder encoder : mEncoders) {
encoder.stop();
}
}
/**
- * Called after all encoders have requested a release using {@link Controller#requestRelease(int)}.
+ * Called after all encoders have requested a release using {@link Controller#notifyReleased(int)}.
* At this point we will do cleanup and notify the listener.
*/
- private void release() {
+ private void end() {
+ LOG.i("end:", "Releasing muxer after all encoders have been released.");
Exception error = null;
if (mMediaMuxer != null) {
// stop() throws an exception if you haven't fed it any data.
@@ -168,14 +178,16 @@ public class MediaEncoderEngine {
}
mMediaMuxer = null;
}
+ LOG.w("end:", "Dispatching end to listener - reason:", mEndReason, "error:", error);
if (mListener != null) {
- mListener.onEncoderStop(mStopReason, error);
+ mListener.onEncodingEnd(mEndReason, error);
mListener = null;
}
- mStopReason = STOP_BY_USER;
+ mEndReason = END_BY_USER;
mStartedEncodersCount = 0;
- mStoppedEncodersCount = 0;
+ mReleasedEncodersCount = 0;
mMediaMuxerStarted = false;
+ LOG.i("end:", "Completed.");
}
/**
@@ -219,10 +231,14 @@ public class MediaEncoderEngine {
throw new IllegalStateException("Trying to start but muxer started already");
}
int track = mMediaMuxer.addTrack(format);
- LOG.w("Controller:", "Assigned track", track, "to format", format.getString(MediaFormat.KEY_MIME));
+ LOG.w("requestStart:", "Assigned track", track, "to format", format.getString(MediaFormat.KEY_MIME));
if (++mStartedEncodersCount == mEncoders.size()) {
+ LOG.w("requestStart:", "All encoders have started. Starting muxer and dispatching onEncodingStart().");
mMediaMuxer.start();
mMediaMuxerStarted = true;
+ if (mListener != null) {
+ mListener.onEncodingStart();
+ }
}
return track;
}
@@ -251,7 +267,7 @@ public class MediaEncoderEngine {
// This is a bad idea and causes crashes.
// if (info.presentationTimeUs < mLastTimestampUs) info.presentationTimeUs = mLastTimestampUs;
// mLastTimestampUs = info.presentationTimeUs;
- LOG.v("Writing for track", buffer.trackIndex, ". Presentation:", buffer.info.presentationTimeUs);
+ LOG.v("write:", "Writing OutputBuffer - track:", buffer.trackIndex, "presentation:", buffer.info.presentationTimeUs);
mMediaMuxer.writeSampleData(buffer.trackIndex, buffer.data, buffer.info);
pool.recycle(buffer);
}
@@ -264,10 +280,11 @@ public class MediaEncoderEngine {
* When this succeeds, {@link MediaEncoder#stop()} is called.
*/
void requestStop(int track) {
- LOG.i("RequestStop was called for track", track);
synchronized (mControllerLock) {
+ LOG.w("requestStop:", "Called for track", track);
if (--mStartedEncodersCount == 0) {
- mStopReason = mPossibleStopReason;
+ LOG.w("requestStop:", "All encoders have requested a stop. Stopping them.");
+ mEndReason = mPossibleEndReason;
stop();
}
}
@@ -277,11 +294,12 @@ public class MediaEncoderEngine {
* Notifies that the encoder was stopped. After this is called by all encoders,
* we will actually stop the muxer.
*/
- void requestRelease(int track) {
- LOG.i("requestRelease was called for track", track);
+ void notifyReleased(int track) {
synchronized (mControllerLock) {
- if (++mStoppedEncodersCount == mEncoders.size()) {
- release();
+ LOG.w("notifyReleased:", "Called for track", track);
+ if (++mReleasedEncodersCount == mEncoders.size()) {
+ LOG.w("requestStop:", "All encoders have been released. Stopping the muxer.");
+ end();
}
}
}
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/TextureMediaEncoder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/TextureMediaEncoder.java
index 8d6652a4..2e9e275b 100644
--- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/TextureMediaEncoder.java
+++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/TextureMediaEncoder.java
@@ -24,17 +24,24 @@ public class TextureMediaEncoder extends VideoMediaEncoder extends Med
int rotation;
String mimeType;
- Config(int width, int height, int bitRate, int frameRate, int rotation, String mimeType) {
+ Config(int width, int height, int bitRate, int frameRate, int rotation, @NonNull String mimeType) {
this.width = width;
this.height = height;
this.bitRate = bitRate;
diff --git a/cameraview/src/main/res/values/attrs.xml b/cameraview/src/main/res/values/attrs.xml
index e680f319..a6efb1ac 100644
--- a/cameraview/src/main/res/values/attrs.xml
+++ b/cameraview/src/main/res/values/attrs.xml
@@ -22,12 +22,12 @@
-
-
-
+
+
+
@@ -58,6 +58,17 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -83,15 +94,6 @@
-
-
-
-
-
-
-
-
-
@@ -102,16 +104,14 @@
-
-
-
-
+
+
+
+
+
-
-
-
-
+
@@ -125,13 +125,20 @@
-
-
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java b/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java
index 9b888ade..c9ce8220 100644
--- a/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java
+++ b/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java
@@ -1,16 +1,21 @@
package com.otaliastudios.cameraview.demo;
+import android.animation.Animator;
+import android.animation.ValueAnimator;
+import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import androidx.annotation.NonNull;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import androidx.appcompat.app.AppCompatActivity;
+import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
+import android.widget.TextView;
import android.widget.Toast;
import com.otaliastudios.cameraview.CameraException;
@@ -24,6 +29,7 @@ import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.controls.Preview;
import java.io.File;
+import java.util.Arrays;
import java.util.List;
@@ -31,8 +37,6 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis
private CameraView camera;
private ViewGroup controlPanel;
-
- // To show stuff in the callback
private long mCaptureTime;
@Override
@@ -65,9 +69,41 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis
controlPanel = findViewById(R.id.controls);
ViewGroup group = (ViewGroup) controlPanel.getChildAt(0);
- List