diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseEglTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseEglTest.java new file mode 100644 index 00000000..0bb74e0d --- /dev/null +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseEglTest.java @@ -0,0 +1,56 @@ +package com.otaliastudios.cameraview; + + +import android.graphics.Canvas; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.otaliastudios.cameraview.internal.egl.EglBaseSurface; +import com.otaliastudios.cameraview.internal.egl.EglCore; +import com.otaliastudios.cameraview.internal.egl.EglViewport; +import com.otaliastudios.cameraview.overlay.Overlay; +import com.otaliastudios.cameraview.overlay.OverlayDrawer; +import com.otaliastudios.cameraview.size.Size; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + + +@SuppressWarnings("WeakerAccess") +public abstract class BaseEglTest extends BaseTest { + + protected final static int WIDTH = 100; + protected final static int HEIGHT = 100; + + protected EglCore eglCore; + protected EglBaseSurface eglSurface; + + @Before + public void setUp() { + eglCore = new EglCore(null, EglCore.FLAG_RECORDABLE); + eglSurface = new EglBaseSurface(eglCore); + eglSurface.createOffscreenSurface(WIDTH, HEIGHT); + eglSurface.makeCurrent(); + } + + @After + public void tearDown() { + eglSurface.releaseEglSurface(); + eglSurface = null; + eglCore.release(); + eglCore = null; + } +} diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/BaseFilterTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/BaseFilterTest.java new file mode 100644 index 00000000..d6614f52 --- /dev/null +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/BaseFilterTest.java @@ -0,0 +1,177 @@ +package com.otaliastudios.cameraview.filter; + + +import android.opengl.GLES20; + +import androidx.annotation.NonNull; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.otaliastudios.cameraview.BaseEglTest; +import com.otaliastudios.cameraview.BaseTest; +import com.otaliastudios.cameraview.internal.GlUtils; +import com.otaliastudios.cameraview.internal.egl.EglViewport; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.HashSet; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class BaseFilterTest extends BaseEglTest { + + public static class TestFilter extends BaseFilter implements TwoParameterFilter { + + private float param1; + private float param2; + + @NonNull + @Override + public String getFragmentShader() { + return createDefaultFragmentShader(); + } + + @Override + public void setParameter1(float value) { + param1 = value; + } + + @Override + public void setParameter2(float value) { + param2 = value; + } + + @Override + public float getParameter1() { + return param1; + } + + @Override + public float getParameter2() { + return param2; + } + } + + private TestFilter filter; + + @Test + public void testCreateDefaultFragmentShader() { + filter = new TestFilter(); + filter.fragmentTextureCoordinateName = "XXX"; + String defaultFragmentShader = filter.createDefaultFragmentShader(); + assertNotNull(defaultFragmentShader); + assertTrue(defaultFragmentShader.contains(filter.fragmentTextureCoordinateName)); + } + + @Test + public void testCreateDefaultVertexShader() { + filter = new TestFilter(); + filter.vertexModelViewProjectionMatrixName = "AAA"; + filter.vertexPositionName = "BBB"; + filter.vertexTextureCoordinateName = "CCC"; + filter.vertexTransformMatrixName = "DDD"; + filter.fragmentTextureCoordinateName = "EEE"; + String defaultVertexShader = filter.createDefaultVertexShader(); + assertNotNull(defaultVertexShader); + assertTrue(defaultVertexShader.contains(filter.vertexModelViewProjectionMatrixName)); + assertTrue(defaultVertexShader.contains(filter.vertexPositionName)); + assertTrue(defaultVertexShader.contains(filter.vertexTextureCoordinateName)); + assertTrue(defaultVertexShader.contains(filter.vertexTransformMatrixName)); + assertTrue(defaultVertexShader.contains(filter.fragmentTextureCoordinateName)); + } + + @Test + public void testOnProgramCreate() { + filter = new TestFilter(); + int handle = GlUtils.createProgram(filter.getVertexShader(), filter.getFragmentShader()); + filter.onCreate(handle); + assertTrue(filter.programHandle >= 0); + filter.onDestroy(); + assertTrue(filter.programHandle < 0); + GLES20.glDeleteProgram(handle); + } + + @Test + public void testDraw_whenInvalid() { + filter = spy(new TestFilter()); + float[] matrix = new float[16]; + filter.draw(matrix); + verify(filter, never()).onPreDraw(matrix); + verify(filter, never()).onDraw(); + verify(filter, never()).onPostDraw(); + } + + @Test + public void testDraw() { + // Use an EglViewport which cares about GL setup. + filter = spy(new TestFilter()); + EglViewport viewport = new EglViewport(filter); + int texture = viewport.createTexture(); + + float[] matrix = new float[16]; + viewport.drawFrame(texture, matrix); + verify(filter, times(1)).onPreDraw(matrix); + verify(filter, times(1)).onDraw(); + verify(filter, times(1)).onPostDraw(); + + viewport.release(); + } + + @Test(expected = RuntimeException.class) + public void testOnCopy_invalid() { + // Anonymous inner classes do not have a public constructor. + Filter filter = new BaseFilter() { + @NonNull + @Override + public String getFragmentShader() { + return "whatever"; + } + }; + filter.copy(); + } + + @Test + public void testOnCopy() { + filter = new TestFilter(); + BaseFilter other = filter.copy(); + assertTrue(other instanceof TestFilter); + } + + @Test + public void testCopy_withSize() { + filter = new TestFilter(); + filter.setSize(WIDTH, HEIGHT); + BaseFilter other = filter.copy(); + assertEquals(WIDTH, other.size.getWidth()); + assertEquals(HEIGHT, other.size.getHeight()); + } + + @Test + public void testCopy_withParameter1() { + filter = new TestFilter(); + filter.setParameter1(0.5F); + TestFilter other = (TestFilter) filter.copy(); + assertEquals(filter.getParameter1(), other.getParameter1(), 0.001F); + } + + @Test + public void testCopy_withParameter2() { + filter = new TestFilter(); + filter.setParameter2(0.5F); + TestFilter other = (TestFilter) filter.copy(); + assertEquals(filter.getParameter2(), other.getParameter2(), 0.001F); + } +} diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/FiltersTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/FiltersTest.java new file mode 100644 index 00000000..b251fd71 --- /dev/null +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/FiltersTest.java @@ -0,0 +1,30 @@ +package com.otaliastudios.cameraview.filter; + + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.otaliastudios.cameraview.BaseTest; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class FiltersTest extends BaseTest { + + @Test + public void testNewInstance() { + // At least tests that all our default filters have a no-args constructor. + Filters[] filtersArray = Filters.values(); + for (Filters filters : filtersArray) { + Filter filter = filters.newInstance(); + assertNotNull(filter); + } + } +} diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/NoFilterTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/NoFilterTest.java new file mode 100644 index 00000000..72fb14e6 --- /dev/null +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/NoFilterTest.java @@ -0,0 +1,35 @@ +package com.otaliastudios.cameraview.filter; + + +import androidx.annotation.NonNull; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.otaliastudios.cameraview.BaseTest; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NoFilterTest extends BaseTest { + + public static class DummyFilter extends BaseFilter { + @NonNull + @Override + public String getFragmentShader() { + return "whatever"; + } + } + + @Test + public void testGetFragmentShader() { + NoFilter filter = new NoFilter(); + String defaultFragmentShader = new DummyFilter().createDefaultFragmentShader(); + assertEquals(defaultFragmentShader, filter.getFragmentShader()); + } +} diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/SimpleFilterTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/SimpleFilterTest.java new file mode 100644 index 00000000..1673584d --- /dev/null +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/SimpleFilterTest.java @@ -0,0 +1,43 @@ +package com.otaliastudios.cameraview.filter; + + +import android.content.res.TypedArray; + +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.R; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static junit.framework.TestCase.assertNotNull; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class SimpleFilterTest extends BaseTest { + + @Test + public void testGetFragmentShader() { + String shader = "shader"; + SimpleFilter filter = new SimpleFilter(shader); + assertEquals(shader, filter.getFragmentShader()); + } + + @Test + public void testCopy() { + String shader = "shader"; + SimpleFilter filter = new SimpleFilter(shader); + BaseFilter copy = filter.copy(); + assertTrue(copy instanceof SimpleFilter); + SimpleFilter simpleCopy = (SimpleFilter) copy; + assertEquals(shader, simpleCopy.getFragmentShader()); + } +} diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/overlay/OverlayDrawerTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/overlay/OverlayDrawerTest.java index de620f05..81d69666 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/overlay/OverlayDrawerTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/overlay/OverlayDrawerTest.java @@ -13,6 +13,7 @@ import androidx.test.annotation.UiThreadTest; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.otaliastudios.cameraview.BaseEglTest; import com.otaliastudios.cameraview.BaseTest; import com.otaliastudios.cameraview.internal.egl.EglBaseSurface; import com.otaliastudios.cameraview.internal.egl.EglCore; @@ -45,29 +46,7 @@ import static org.mockito.Mockito.when; @RunWith(AndroidJUnit4.class) @SmallTest -public class OverlayDrawerTest extends BaseTest { - - private final static int WIDTH = 100; - private final static int HEIGHT = 100; - - private EglCore eglCore; - private EglBaseSurface eglSurface; - - @Before - public void setUp() { - eglCore = new EglCore(null, EglCore.FLAG_RECORDABLE); - eglSurface = new EglBaseSurface(eglCore); - eglSurface.createOffscreenSurface(WIDTH, HEIGHT); - eglSurface.makeCurrent(); - } - - @After - public void tearDown() { - eglSurface.releaseEglSurface(); - eglSurface = null; - eglCore.release(); - eglCore = null; - } +public class OverlayDrawerTest extends BaseEglTest { @Test public void testDraw() { diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filter/BaseFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filter/BaseFilter.java index 9016a8ab..1cfa900b 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filter/BaseFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filter/BaseFilter.java @@ -3,6 +3,7 @@ package com.otaliastudios.cameraview.filter; import android.opengl.GLES20; import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.internal.GlUtils; @@ -96,8 +97,8 @@ public abstract class BaseFilter implements Filter { private int vertexTranformMatrixLocation = -1; private int vertexPositionLocation = -1; private int vertexTextureCoordinateLocation = -1; - private int programHandle = -1; - private Size size; + @VisibleForTesting int programHandle = -1; + @VisibleForTesting Size size; @SuppressWarnings("WeakerAccess") protected String vertexPositionName = DEFAULT_VERTEX_POSITION_NAME; @@ -120,7 +121,6 @@ public abstract class BaseFilter implements Filter { fragmentTextureCoordinateName); } - @SuppressWarnings("WeakerAccess") @NonNull protected String createDefaultFragmentShader() { return createDefaultFragmentShader(fragmentTextureCoordinateName); @@ -210,7 +210,9 @@ public abstract class BaseFilter implements Filter { @Override public final BaseFilter copy() { BaseFilter copy = onCopy(); - copy.setSize(size.getWidth(), size.getHeight()); + if (size != null) { + copy.setSize(size.getWidth(), size.getHeight()); + } if (this instanceof OneParameterFilter) { ((OneParameterFilter) copy).setParameter1(((OneParameterFilter) this).getParameter1()); } @@ -220,7 +222,6 @@ public abstract class BaseFilter implements Filter { return copy; } - @SuppressWarnings("WeakerAccess") protected BaseFilter onCopy() { try { return getClass().newInstance(); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filter/NoFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filter/NoFilter.java index c801da28..c5a673a8 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filter/NoFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filter/NoFilter.java @@ -4,7 +4,10 @@ import androidx.annotation.NonNull; import com.otaliastudios.cameraview.filter.BaseFilter; -public class NoFilter extends BaseFilter { +/** + * A {@link Filter} that draws frames without any modification. + */ +public final class NoFilter extends BaseFilter { @NonNull @Override diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filter/SimpleFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filter/SimpleFilter.java new file mode 100644 index 00000000..eda5ec3e --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filter/SimpleFilter.java @@ -0,0 +1,40 @@ +package com.otaliastudios.cameraview.filter; + +import androidx.annotation.NonNull; + +/** + * The simplest possible filter that accepts a fragment shader in its constructor. + * This can be used when your fragment shader is static and has no 'runtime' parameters + * that influence its behavior. + * + * The given fragment shader should respect the default variable names, as listed + * in the {@link BaseFilter} class. + * + * NOTE: SimpleFilter is not meant to be subclassed! + * Subclassing it would require you to override {@link #onCopy()}, which would make + * this class useless. Instead, you can extend {@link BaseFilter} directly. + */ +public final class SimpleFilter extends BaseFilter { + + private final String fragmentShader; + + /** + * Creates a new filter with the given fragment shader. + * @param fragmentShader a fragment shader + */ + @SuppressWarnings("WeakerAccess") + public SimpleFilter(@NonNull String fragmentShader) { + this.fragmentShader = fragmentShader; + } + + @NonNull + @Override + public String getFragmentShader() { + return fragmentShader; + } + + @Override + protected BaseFilter onCopy() { + return new SimpleFilter(fragmentShader); + } +} diff --git a/docs/_posts/2019-08-06-filters.md b/docs/_posts/2019-08-06-filters.md index 099ad559..e1a864e4 100644 --- a/docs/_posts/2019-08-06-filters.md +++ b/docs/_posts/2019-08-06-filters.md @@ -96,6 +96,17 @@ camera.mapGesture(Gesture.SCROLL_VERTICAL, GestureAction.FILTER_CONTROL_2); Advanced users with OpenGL experience can create their own filters by implementing the `Filter` interface and passing in a fragment shader and a vertex shader that will be used for drawing. +#### Simple filters + +For very simple filters that have a static fragment shader, you can create a working filter +implementation by simply creating an instance of `SimpleFilter`: + +```java +Filter filter = new SimpleFilter(myFragmentShader); +``` + +#### More complex filters + We recommend: - Subclassing `BaseFilter` instead of implementing `Filter`, since that takes care of most of the work