diff options
author | Robert Henigan <robert.henigan@livio.io> | 2021-02-01 16:16:18 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-02-01 16:16:18 -0500 |
commit | 7f6db0821a3595745cff3ed34029e8a5f2552338 (patch) | |
tree | d55ac13a0f09babf02813ba6b657b80ec64c26a2 | |
parent | ef2f86c997f608fe2eb977223dfe578b34cea10e (diff) | |
parent | 4add79ec1888cc517ff2010d56d795d8b9a27884 (diff) | |
download | sdl_android-7f6db0821a3595745cff3ed34029e8a5f2552338.tar.gz |
Merge pull request #1614 from smartdevicelink/integration/stable_frame_rate
Integration/stable frame rate
15 files changed, 2026 insertions, 38 deletions
diff --git a/android/sdl_android/src/androidTest/java/com/android/grafika/gles/OffscreenSurfaceTest.java b/android/sdl_android/src/androidTest/java/com/android/grafika/gles/OffscreenSurfaceTest.java new file mode 100644 index 000000000..2f93c8307 --- /dev/null +++ b/android/sdl_android/src/androidTest/java/com/android/grafika/gles/OffscreenSurfaceTest.java @@ -0,0 +1,105 @@ +package com.android.grafika.gles; + +import android.opengl.GLES20; +import android.os.Environment; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import android.util.Log; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import static junit.framework.TestCase.assertTrue; + +@RunWith(AndroidJUnit4.class) +public class OffscreenSurfaceTest { + + private final String TAG = OffscreenSurfaceTest.class.getSimpleName(); + private final int mWidth = 1280; + private final int mHeight = 720; + private final int mIterations = 100; + + @Test + public void testReadPixels() { + EglCore eglCore = new EglCore(null, 0); + OffscreenSurface offscreenSurface = new OffscreenSurface(eglCore, mWidth, mHeight); + float time = runReadPixelsTest(offscreenSurface); + Log.d(TAG, "runReadPixelsTest returns " + time + " msec"); + } + + // HELPER test method + /** + * Does a simple bit of rendering and then reads the pixels back. + * + * @return total time (msec order) spent on glReadPixels() + */ + private float runReadPixelsTest(OffscreenSurface eglSurface) { + long totalTime = 0; + + eglSurface.makeCurrent(); + + ByteBuffer pixelBuf = ByteBuffer.allocateDirect(mWidth * mHeight * 4); + pixelBuf.order(ByteOrder.LITTLE_ENDIAN); + + Log.d(TAG, "Running..."); + float colorMult = 1.0f / mIterations; + for (int i = 0; i < mIterations; i++) { + if ((i % (mIterations / 8)) == 0) { + Log.d(TAG, "iteration " + i); + } + + // Clear the screen to a solid color, then add a rectangle. Change the color + // each time. + float r = i * colorMult; + float g = 1.0f - r; + float b = (r + g) / 2.0f; + GLES20.glClearColor(r, g, b, 1.0f); + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + + GLES20.glEnable(GLES20.GL_SCISSOR_TEST); + GLES20.glScissor(mWidth / 4, mHeight / 4, mWidth / 2, mHeight / 2); + GLES20.glClearColor(b, g, r, 1.0f); + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + GLES20.glDisable(GLES20.GL_SCISSOR_TEST); + + // Try to ensure that rendering has finished. + GLES20.glFinish(); + GLES20.glReadPixels(0, 0, 1, 1, + GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, pixelBuf); + + // Time individual extraction. Ideally we'd be timing a bunch of these calls + // and measuring the aggregate time, but we want the isolated time, and if we + // just read the same buffer repeatedly we might get some sort of cache effect. + long startWhen = System.nanoTime(); + GLES20.glReadPixels(0, 0, mWidth, mHeight, + GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, pixelBuf); + totalTime += System.nanoTime() - startWhen; + } + Log.d(TAG, "done"); + + // It's not the good idea to request external strage permission in unit test. + boolean requireStoragePermission = false; + if (requireStoragePermission) { + long startWhen = System.nanoTime(); + File file = new File(Environment.getExternalStorageDirectory(), + "test.png"); + try { + eglSurface.saveFrame(file); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + Log.d(TAG, "Saved frame in " + ((System.nanoTime() - startWhen) / 1000000) + "ms"); + assertTrue(file.exists()); + } else { + // here' we can recognize Unit Test succeeded, but anyway checks to see totalTime and buffer capacity. + assertTrue(pixelBuf.capacity() > 0 && totalTime > 0); + } + + return (float)totalTime / 1000000f; + } + +} diff --git a/android/sdl_android/src/androidTest/java/com/smartdevicelink/managers/lifecycle/SystemCapabilityManagerTests.java b/android/sdl_android/src/androidTest/java/com/smartdevicelink/managers/lifecycle/SystemCapabilityManagerTests.java index 8d7df7689..928b7bd09 100644 --- a/android/sdl_android/src/androidTest/java/com/smartdevicelink/managers/lifecycle/SystemCapabilityManagerTests.java +++ b/android/sdl_android/src/androidTest/java/com/smartdevicelink/managers/lifecycle/SystemCapabilityManagerTests.java @@ -98,6 +98,7 @@ public class SystemCapabilityManagerTests { videoStreamingCapability.setMaxBitrate(TestValues.GENERAL_INT); videoStreamingCapability.setPreferredResolution(TestValues.GENERAL_IMAGERESOLUTION); videoStreamingCapability.setSupportedFormats(TestValues.GENERAL_VIDEOSTREAMINGFORMAT_LIST); + videoStreamingCapability.setPreferredFPS(TestValues.GENERAL_INTEGER); systemCapability.setCapabilityForType(SystemCapabilityType.VIDEO_STREAMING, videoStreamingCapability); } @@ -214,6 +215,7 @@ public class SystemCapabilityManagerTests { vsCapability.setMaxBitrate(TestValues.GENERAL_INT); vsCapability.setPreferredResolution(TestValues.GENERAL_IMAGERESOLUTION); vsCapability.setSupportedFormats(TestValues.GENERAL_VIDEOSTREAMINGFORMAT_LIST); + vsCapability.setPreferredFPS(TestValues.GENERAL_INTEGER); SystemCapability cap = new SystemCapability(); cap.setSystemCapabilityType(SystemCapabilityType.VIDEO_STREAMING); diff --git a/android/sdl_android/src/androidTest/java/com/smartdevicelink/test/rpc/datatypes/VideoStreamingCapabilityTests.java b/android/sdl_android/src/androidTest/java/com/smartdevicelink/test/rpc/datatypes/VideoStreamingCapabilityTests.java index 422cd5ddc..0799645b6 100644 --- a/android/sdl_android/src/androidTest/java/com/smartdevicelink/test/rpc/datatypes/VideoStreamingCapabilityTests.java +++ b/android/sdl_android/src/androidTest/java/com/smartdevicelink/test/rpc/datatypes/VideoStreamingCapabilityTests.java @@ -4,6 +4,7 @@ import com.smartdevicelink.marshal.JsonRPCMarshaller; import com.smartdevicelink.proxy.rpc.ImageResolution; import com.smartdevicelink.proxy.rpc.VideoStreamingCapability; import com.smartdevicelink.proxy.rpc.VideoStreamingFormat; +import com.smartdevicelink.streaming.video.VideoStreamingParameters; import com.smartdevicelink.test.JsonUtils; import com.smartdevicelink.test.TestValues; import com.smartdevicelink.test.Validator; @@ -33,6 +34,7 @@ public class VideoStreamingCapabilityTests extends TestCase { msg.setDiagonalScreenSize(TestValues.GENERAL_DOUBLE); msg.setPixelPerInch(TestValues.GENERAL_DOUBLE); msg.setScale(TestValues.GENERAL_DOUBLE); + msg.setPreferredFPS(TestValues.GENERAL_INTEGER); } /** @@ -47,6 +49,7 @@ public class VideoStreamingCapabilityTests extends TestCase { Double diagonalScreenSize = msg.getDiagonalScreenSize(); Double pixelPerInch = msg.getPixelPerInch(); Double scale = msg.getScale(); + Integer preferredFPS = msg.getPreferredFPS(); // Valid Tests assertEquals(TestValues.MATCH, (List<VideoStreamingFormat>) TestValues.GENERAL_VIDEOSTREAMINGFORMAT_LIST, format); @@ -68,6 +71,7 @@ public class VideoStreamingCapabilityTests extends TestCase { assertNull(TestValues.NULL, msg.getDiagonalScreenSize()); assertNull(TestValues.NULL, msg.getPixelPerInch()); assertNull(TestValues.NULL, msg.getScale()); + assertNull(TestValues.NULL, msg.getPreferredFPS()); } public void testJson() { @@ -81,6 +85,7 @@ public class VideoStreamingCapabilityTests extends TestCase { reference.put(VideoStreamingCapability.KEY_DIAGONAL_SCREEN_SIZE, TestValues.GENERAL_DOUBLE); reference.put(VideoStreamingCapability.KEY_PIXEL_PER_INCH, TestValues.GENERAL_DOUBLE); reference.put(VideoStreamingCapability.KEY_SCALE, TestValues.GENERAL_DOUBLE); + reference.put(VideoStreamingCapability.KEY_PREFERRED_FPS, TestValues.GENERAL_INTEGER); JSONObject underTest = msg.serializeJSON(); assertEquals(TestValues.MATCH, reference.length(), underTest.length()); @@ -89,7 +94,7 @@ public class VideoStreamingCapabilityTests extends TestCase { while (iterator.hasNext()) { String key = (String) iterator.next(); - if (key.equals(VideoStreamingCapability.KEY_MAX_BITRATE) || key.equals(VideoStreamingCapability.KEY_HAPTIC_SPATIAL_DATA_SUPPORTED)) { + if (key.equals(VideoStreamingCapability.KEY_MAX_BITRATE) || key.equals(VideoStreamingCapability.KEY_HAPTIC_SPATIAL_DATA_SUPPORTED) || key.equals(VideoStreamingCapability.KEY_PREFERRED_FPS)) { assertTrue(TestValues.TRUE, JsonUtils.readIntegerFromJsonObject(reference, key) == JsonUtils.readIntegerFromJsonObject(underTest, key)); } else if (key.equals(VideoStreamingCapability.KEY_PREFERRED_RESOLUTION)) { ImageResolution irReference = (ImageResolution) JsonUtils.readObjectFromJsonObject(reference, key); diff --git a/android/sdl_android/src/main/java/com/android/grafika/gles/Drawable2d.java b/android/sdl_android/src/main/java/com/android/grafika/gles/Drawable2d.java new file mode 100644 index 000000000..52fcabf1f --- /dev/null +++ b/android/sdl_android/src/main/java/com/android/grafika/gles/Drawable2d.java @@ -0,0 +1,197 @@ +/* + * Copyright 2014 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.grafika.gles; + +import java.nio.FloatBuffer; + +/** + * Base class for stuff we like to draw. + */ +public class Drawable2d { + private static final int SIZEOF_FLOAT = 4; + + /** + * Simple equilateral triangle (1.0 per side). Centered on (0,0). + */ + private static final float TRIANGLE_COORDS[] = { + 0.0f, 0.577350269f, // 0 top + -0.5f, -0.288675135f, // 1 bottom left + 0.5f, -0.288675135f // 2 bottom right + }; + private static final float TRIANGLE_TEX_COORDS[] = { + 0.5f, 0.0f, // 0 top center + 0.0f, 1.0f, // 1 bottom left + 1.0f, 1.0f, // 2 bottom right + }; + private static final FloatBuffer TRIANGLE_BUF = + GlUtil.createFloatBuffer(TRIANGLE_COORDS); + private static final FloatBuffer TRIANGLE_TEX_BUF = + GlUtil.createFloatBuffer(TRIANGLE_TEX_COORDS); + + /** + * Simple square, specified as a triangle strip. The square is centered on (0,0) and has + * a size of 1x1. + * <p> + * Triangles are 0-1-2 and 2-1-3 (counter-clockwise winding). + */ + private static final float RECTANGLE_COORDS[] = { + -0.5f, -0.5f, // 0 bottom left + 0.5f, -0.5f, // 1 bottom right + -0.5f, 0.5f, // 2 top left + 0.5f, 0.5f, // 3 top right + }; + private static final float RECTANGLE_TEX_COORDS[] = { + 0.0f, 1.0f, // 0 bottom left + 1.0f, 1.0f, // 1 bottom right + 0.0f, 0.0f, // 2 top left + 1.0f, 0.0f // 3 top right + }; + private static final FloatBuffer RECTANGLE_BUF = + GlUtil.createFloatBuffer(RECTANGLE_COORDS); + private static final FloatBuffer RECTANGLE_TEX_BUF = + GlUtil.createFloatBuffer(RECTANGLE_TEX_COORDS); + + /** + * A "full" square, extending from -1 to +1 in both dimensions. When the model/view/projection + * matrix is identity, this will exactly cover the viewport. + * <p> + * The texture coordinates are Y-inverted relative to RECTANGLE. (This seems to work out + * right with external textures from SurfaceTexture.) + */ + private static final float FULL_RECTANGLE_COORDS[] = { + -1.0f, -1.0f, // 0 bottom left + 1.0f, -1.0f, // 1 bottom right + -1.0f, 1.0f, // 2 top left + 1.0f, 1.0f, // 3 top right + }; + private static final float FULL_RECTANGLE_TEX_COORDS[] = { + 0.0f, 0.0f, // 0 bottom left + 1.0f, 0.0f, // 1 bottom right + 0.0f, 1.0f, // 2 top left + 1.0f, 1.0f // 3 top right + }; + private static final FloatBuffer FULL_RECTANGLE_BUF = + GlUtil.createFloatBuffer(FULL_RECTANGLE_COORDS); + private static final FloatBuffer FULL_RECTANGLE_TEX_BUF = + GlUtil.createFloatBuffer(FULL_RECTANGLE_TEX_COORDS); + + + private FloatBuffer mVertexArray; + private FloatBuffer mTexCoordArray; + private int mVertexCount; + private int mCoordsPerVertex; + private int mVertexStride; + private int mTexCoordStride; + private Prefab mPrefab; + + /** + * Enum values for constructor. + */ + public enum Prefab { + TRIANGLE, RECTANGLE, FULL_RECTANGLE + } + + /** + * Prepares a drawable from a "pre-fabricated" shape definition. + * <p> + * Does no EGL/GL operations, so this can be done at any time. + */ + public Drawable2d(Prefab shape) { + switch (shape) { + case TRIANGLE: + mVertexArray = TRIANGLE_BUF; + mTexCoordArray = TRIANGLE_TEX_BUF; + mCoordsPerVertex = 2; + mVertexStride = mCoordsPerVertex * SIZEOF_FLOAT; + mVertexCount = TRIANGLE_COORDS.length / mCoordsPerVertex; + break; + case RECTANGLE: + mVertexArray = RECTANGLE_BUF; + mTexCoordArray = RECTANGLE_TEX_BUF; + mCoordsPerVertex = 2; + mVertexStride = mCoordsPerVertex * SIZEOF_FLOAT; + mVertexCount = RECTANGLE_COORDS.length / mCoordsPerVertex; + break; + case FULL_RECTANGLE: + mVertexArray = FULL_RECTANGLE_BUF; + mTexCoordArray = FULL_RECTANGLE_TEX_BUF; + mCoordsPerVertex = 2; + mVertexStride = mCoordsPerVertex * SIZEOF_FLOAT; + mVertexCount = FULL_RECTANGLE_COORDS.length / mCoordsPerVertex; + break; + default: + throw new RuntimeException("Unknown shape " + shape); + } + mTexCoordStride = 2 * SIZEOF_FLOAT; + mPrefab = shape; + } + + /** + * Returns the array of vertices. + * <p> + * To avoid allocations, this returns internal state. The caller must not modify it. + */ + public FloatBuffer getVertexArray() { + return mVertexArray; + } + + /** + * Returns the array of texture coordinates. + * <p> + * To avoid allocations, this returns internal state. The caller must not modify it. + */ + public FloatBuffer getTexCoordArray() { + return mTexCoordArray; + } + + /** + * Returns the number of vertices stored in the vertex array. + */ + public int getVertexCount() { + return mVertexCount; + } + + /** + * Returns the width, in bytes, of the data for each vertex. + */ + public int getVertexStride() { + return mVertexStride; + } + + /** + * Returns the width, in bytes, of the data for each texture coordinate. + */ + public int getTexCoordStride() { + return mTexCoordStride; + } + + /** + * Returns the number of position coordinates per vertex. This will be 2 or 3. + */ + public int getCoordsPerVertex() { + return mCoordsPerVertex; + } + + @Override + public String toString() { + if (mPrefab != null) { + return "[Drawable2d: " + mPrefab + "]"; + } else { + return "[Drawable2d: ...]"; + } + } +} diff --git a/android/sdl_android/src/main/java/com/android/grafika/gles/EglCore.java b/android/sdl_android/src/main/java/com/android/grafika/gles/EglCore.java new file mode 100644 index 000000000..9e814a529 --- /dev/null +++ b/android/sdl_android/src/main/java/com/android/grafika/gles/EglCore.java @@ -0,0 +1,372 @@ +/* + * Copyright 2013 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.grafika.gles; + +import android.graphics.SurfaceTexture; +import android.opengl.EGL14; +import android.opengl.EGLConfig; +import android.opengl.EGLContext; +import android.opengl.EGLDisplay; +import android.opengl.EGLExt; +import android.opengl.EGLSurface; +import android.util.Log; +import android.view.Surface; + + +/** + * Core EGL state (display, context, config). + * <p> + * The EGLContext must only be attached to one thread at a time. This class is not thread-safe. + */ +public final class EglCore { + private static final String TAG = "EglCore"; + + /** + * Constructor flag: surface must be recordable. This discourages EGL from using a + * pixel format that cannot be converted efficiently to something usable by the video + * encoder. + */ + public static final int FLAG_RECORDABLE = 0x01; + + /** + * Constructor flag: ask for GLES3, fall back to GLES2 if not available. Without this + * flag, GLES2 is used. + */ + public static final int FLAG_TRY_GLES3 = 0x02; + + // Android-specific extension. + private static final int EGL_RECORDABLE_ANDROID = 0x3142; + + private EGLDisplay mEGLDisplay = EGL14.EGL_NO_DISPLAY; + private EGLContext mEGLContext = EGL14.EGL_NO_CONTEXT; + private EGLConfig mEGLConfig = null; + private int mGlVersion = -1; + + + /** + * Prepares EGL display and context. + * <p> + * Equivalent to EglCore(null, 0). + */ + public EglCore() { + this(null, 0); + } + + /** + * Prepares EGL display and context. + * <p> + * @param sharedContext The context to share, or null if sharing is not desired. + * @param flags Configuration bit flags, e.g. FLAG_RECORDABLE. + */ + public EglCore(EGLContext sharedContext, int flags) { + if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) { + throw new RuntimeException("EGL already set up"); + } + + if (sharedContext == null) { + sharedContext = EGL14.EGL_NO_CONTEXT; + } + + mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); + if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) { + throw new RuntimeException("unable to get EGL14 display"); + } + int[] version = new int[2]; + if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) { + mEGLDisplay = null; + throw new RuntimeException("unable to initialize EGL14"); + } + + // Try to get a GLES3 context, if requested. + if ((flags & FLAG_TRY_GLES3) != 0) { + //Log.d(TAG, "Trying GLES 3"); + EGLConfig config = getConfig(flags, 3); + if (config != null) { + int[] attrib3_list = { + EGL14.EGL_CONTEXT_CLIENT_VERSION, 3, + EGL14.EGL_NONE + }; + EGLContext context = EGL14.eglCreateContext(mEGLDisplay, config, sharedContext, + attrib3_list, 0); + + if (EGL14.eglGetError() == EGL14.EGL_SUCCESS) { + //Log.d(TAG, "Got GLES 3 config"); + mEGLConfig = config; + mEGLContext = context; + mGlVersion = 3; + } + } + } + if (mEGLContext == EGL14.EGL_NO_CONTEXT) { // GLES 2 only, or GLES 3 attempt failed + //Log.d(TAG, "Trying GLES 2"); + EGLConfig config = getConfig(flags, 2); + if (config == null) { + throw new RuntimeException("Unable to find a suitable EGLConfig"); + } + int[] attrib2_list = { + EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, + EGL14.EGL_NONE + }; + EGLContext context = EGL14.eglCreateContext(mEGLDisplay, config, sharedContext, + attrib2_list, 0); + checkEglError("eglCreateContext"); + mEGLConfig = config; + mEGLContext = context; + mGlVersion = 2; + } + + // Confirm with query. + int[] values = new int[1]; + EGL14.eglQueryContext(mEGLDisplay, mEGLContext, EGL14.EGL_CONTEXT_CLIENT_VERSION, + values, 0); + Log.d(TAG,"EGLContext created, client version " + values[0]); + } + + /** + * Finds a suitable EGLConfig. + * + * @param flags Bit flags from constructor. + * @param version Must be 2 or 3. + */ + private EGLConfig getConfig(int flags, int version) { + int renderableType = EGL14.EGL_OPENGL_ES2_BIT; + if (version >= 3) { + renderableType |= EGLExt.EGL_OPENGL_ES3_BIT_KHR; + } + + // The actual surface is generally RGBA or RGBX, so situationally omitting alpha + // doesn't really help. It can also lead to a huge performance hit on glReadPixels() + // when reading into a GL_RGBA buffer. + int[] attribList = { + EGL14.EGL_RED_SIZE, 8, + EGL14.EGL_GREEN_SIZE, 8, + EGL14.EGL_BLUE_SIZE, 8, + EGL14.EGL_ALPHA_SIZE, 8, + //EGL14.EGL_DEPTH_SIZE, 16, + //EGL14.EGL_STENCIL_SIZE, 8, + EGL14.EGL_RENDERABLE_TYPE, renderableType, + EGL14.EGL_NONE, 0, // placeholder for recordable [@-3] + EGL14.EGL_NONE + }; + if ((flags & FLAG_RECORDABLE) != 0) { + attribList[attribList.length - 3] = EGL_RECORDABLE_ANDROID; + attribList[attribList.length - 2] = 1; + } + EGLConfig[] configs = new EGLConfig[1]; + int[] numConfigs = new int[1]; + if (!EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, configs, 0, configs.length, + numConfigs, 0)) { + Log.d(TAG,"unable to find RGB8888 / " + version + " EGLConfig"); + return null; + } + return configs[0]; + } + + /** + * Discards all resources held by this class, notably the EGL context. This must be + * called from the thread where the context was created. + * <p> + * On completion, no context will be current. + */ + public void release() { + if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) { + // Android is unusual in that it uses a reference-counted EGLDisplay. So for + // every eglInitialize() we need an eglTerminate(). + EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, + EGL14.EGL_NO_CONTEXT); + EGL14.eglDestroyContext(mEGLDisplay, mEGLContext); + EGL14.eglReleaseThread(); + EGL14.eglTerminate(mEGLDisplay); + } + + mEGLDisplay = EGL14.EGL_NO_DISPLAY; + mEGLContext = EGL14.EGL_NO_CONTEXT; + mEGLConfig = null; + } + + @Override + protected void finalize() throws Throwable { + try { + if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) { + // We're limited here -- finalizers don't run on the thread that holds + // the EGL state, so if a surface or context is still current on another + // thread we can't fully release it here. Exceptions thrown from here + // are quietly discarded. Complain in the log file. + Log.e(TAG,"WARNING: EglCore was not explicitly released -- state may be leaked"); + release(); + } + } finally { + super.finalize(); + } + } + + /** + * Destroys the specified surface. Note the EGLSurface won't actually be destroyed if it's + * still current in a context. + */ + public void releaseSurface(EGLSurface eglSurface) { + EGL14.eglDestroySurface(mEGLDisplay, eglSurface); + } + + /** + * Creates an EGL surface associated with a Surface. + * <p> + * If this is destined for MediaCodec, the EGLConfig should have the "recordable" attribute. + */ + public EGLSurface createWindowSurface(Object surface) { + if (!(surface instanceof Surface) && !(surface instanceof SurfaceTexture)) { + throw new RuntimeException("invalid surface: " + surface); + } + + // Create a window surface, and attach it to the Surface we received. + int[] surfaceAttribs = { + EGL14.EGL_NONE + }; + EGLSurface eglSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, mEGLConfig, surface, + surfaceAttribs, 0); + checkEglError("eglCreateWindowSurface"); + if (eglSurface == null) { + throw new RuntimeException("surface was null"); + } + return eglSurface; + } + + /** + * Creates an EGL surface associated with an offscreen buffer. + */ + public EGLSurface createOffscreenSurface(int width, int height) { + int[] surfaceAttribs = { + EGL14.EGL_WIDTH, width, + EGL14.EGL_HEIGHT, height, + EGL14.EGL_NONE + }; + EGLSurface eglSurface = EGL14.eglCreatePbufferSurface(mEGLDisplay, mEGLConfig, + surfaceAttribs, 0); + checkEglError("eglCreatePbufferSurface"); + if (eglSurface == null) { + throw new RuntimeException("surface was null"); + } + return eglSurface; + } + + /** + * Makes our EGL context current, using the supplied surface for both "draw" and "read". + */ + public void makeCurrent(EGLSurface eglSurface) { + if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) { + // called makeCurrent() before create? + Log.d(TAG,"NOTE: makeCurrent w/o display"); + } + if (!EGL14.eglMakeCurrent(mEGLDisplay, eglSurface, eglSurface, mEGLContext)) { + throw new RuntimeException("eglMakeCurrent failed"); + } + } + + /** + * Makes our EGL context current, using the supplied "draw" and "read" surfaces. + */ + public void makeCurrent(EGLSurface drawSurface, EGLSurface readSurface) { + if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) { + // called makeCurrent() before create? + Log.d(TAG,"NOTE: makeCurrent w/o display"); + } + if (!EGL14.eglMakeCurrent(mEGLDisplay, drawSurface, readSurface, mEGLContext)) { + throw new RuntimeException("eglMakeCurrent(draw,read) failed"); + } + } + + /** + * Makes no context current. + */ + public void makeNothingCurrent() { + if (!EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, + EGL14.EGL_NO_CONTEXT)) { + throw new RuntimeException("eglMakeCurrent failed"); + } + } + + /** + * Calls eglSwapBuffers. Use this to "publish" the current frame. + * + * @return false on failure + */ + public boolean swapBuffers(EGLSurface eglSurface) { + return EGL14.eglSwapBuffers(mEGLDisplay, eglSurface); + } + + /** + * Sends the presentation time stamp to EGL. Time is expressed in nanoseconds. + */ + public void setPresentationTime(EGLSurface eglSurface, long nsecs) { + EGLExt.eglPresentationTimeANDROID(mEGLDisplay, eglSurface, nsecs); + } + + /** + * Returns true if our context and the specified surface are current. + */ + public boolean isCurrent(EGLSurface eglSurface) { + return mEGLContext.equals(EGL14.eglGetCurrentContext()) && + eglSurface.equals(EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW)); + } + + /** + * Performs a simple surface query. + */ + public int querySurface(EGLSurface eglSurface, int what) { + int[] value = new int[1]; + EGL14.eglQuerySurface(mEGLDisplay, eglSurface, what, value, 0); + return value[0]; + } + + /** + * Queries a string value. + */ + public String queryString(int what) { + return EGL14.eglQueryString(mEGLDisplay, what); + } + + /** + * Returns the GLES version this context is configured for (currently 2 or 3). + */ + public int getGlVersion() { + return mGlVersion; + } + + /** + * Writes the current display, context, and surface to the log. + */ + public static void logCurrent(String msg) { + EGLDisplay display; + EGLContext context; + EGLSurface surface; + + display = EGL14.eglGetCurrentDisplay(); + context = EGL14.eglGetCurrentContext(); + surface = EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW); + Log.i(TAG,"Current EGL (" + msg + "): display=" + display + ", context=" + context + ", surface=" + surface); + } + + /** + * Checks for EGL errors. Throws an exception if an error has been raised. + */ + private void checkEglError(String msg) { + int error; + if ((error = EGL14.eglGetError()) != EGL14.EGL_SUCCESS) { + throw new RuntimeException(msg + ": EGL error: 0x" + Integer.toHexString(error)); + } + } +} diff --git a/android/sdl_android/src/main/java/com/android/grafika/gles/EglSurfaceBase.java b/android/sdl_android/src/main/java/com/android/grafika/gles/EglSurfaceBase.java new file mode 100644 index 000000000..3223a4073 --- /dev/null +++ b/android/sdl_android/src/main/java/com/android/grafika/gles/EglSurfaceBase.java @@ -0,0 +1,197 @@ +/* + * Copyright 2013 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.grafika.gles; + +import android.graphics.Bitmap; +import android.opengl.EGL14; +import android.opengl.EGLSurface; +import android.opengl.GLES20; +import android.util.Log; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Common base class for EGL surfaces. + * <p> + * There can be multiple surfaces associated with a single context. + */ +public class EglSurfaceBase { + private static final String TAG = "EglSurfaceBase"; + + // EglCore object we're associated with. It may be associated with multiple surfaces. + protected EglCore mEglCore; + + private EGLSurface mEGLSurface = EGL14.EGL_NO_SURFACE; + private int mWidth = -1; + private int mHeight = -1; + + protected EglSurfaceBase(EglCore eglCore) { + mEglCore = eglCore; + } + + /** + * Creates a window surface. + * <p> + * @param surface May be a Surface or SurfaceTexture. + */ + public void createWindowSurface(Object surface) { + if (mEGLSurface != EGL14.EGL_NO_SURFACE) { + throw new IllegalStateException("surface already created"); + } + mEGLSurface = mEglCore.createWindowSurface(surface); + + // Don't cache width/height here, because the size of the underlying surface can change + // out from under us (see e.g. HardwareScalerActivity). + //mWidth = mEglCore.querySurface(mEGLSurface, EGL14.EGL_WIDTH); + //mHeight = mEglCore.querySurface(mEGLSurface, EGL14.EGL_HEIGHT); + } + + /** + * Creates an off-screen surface. + */ + public void createOffscreenSurface(int width, int height) { + if (mEGLSurface != EGL14.EGL_NO_SURFACE) { + throw new IllegalStateException("surface already created"); + } + mEGLSurface = mEglCore.createOffscreenSurface(width, height); + mWidth = width; + mHeight = height; + } + + /** + * Returns the surface's width, in pixels. + * <p> + * If this is called on a window surface, and the underlying surface is in the process + * of changing size, we may not see the new size right away (e.g. in the "surfaceChanged" + * callback). The size should match after the next buffer swap. + */ + public int getWidth() { + if (mWidth < 0) { + return mEglCore.querySurface(mEGLSurface, EGL14.EGL_WIDTH); + } else { + return mWidth; + } + } + + /** + * Returns the surface's height, in pixels. + */ + public int getHeight() { + if (mHeight < 0) { + return mEglCore.querySurface(mEGLSurface, EGL14.EGL_HEIGHT); + } else { + return mHeight; + } + } + + /** + * Release the EGL surface. + */ + public void releaseEglSurface() { + mEglCore.releaseSurface(mEGLSurface); + mEGLSurface = EGL14.EGL_NO_SURFACE; + mWidth = mHeight = -1; + } + + /** + * Makes our EGL context and surface current. + */ + public void makeCurrent() { + mEglCore.makeCurrent(mEGLSurface); + } + + /** + * Makes our EGL context and surface current for drawing, using the supplied surface + * for reading. + */ + public void makeCurrentReadFrom(EglSurfaceBase readSurface) { + mEglCore.makeCurrent(mEGLSurface, readSurface.mEGLSurface); + } + + /** + * Calls eglSwapBuffers. Use this to "publish" the current frame. + * + * @return false on failure + */ + public boolean swapBuffers() { + boolean result = mEglCore.swapBuffers(mEGLSurface); + if (!result) { + Log.d(TAG,"WARNING: swapBuffers() failed"); + } + return result; + } + + /** + * Sends the presentation time stamp to EGL. + * + * @param nsecs Timestamp, in nanoseconds. + */ + public void setPresentationTime(long nsecs) { + mEglCore.setPresentationTime(mEGLSurface, nsecs); + } + + /** + * Saves the EGL surface to a file. + * <p> + * Expects that this object's EGL surface is current. + */ + public void saveFrame(File file) throws IOException { + if (!mEglCore.isCurrent(mEGLSurface)) { + throw new RuntimeException("Expected EGL context/surface is not current"); + } + + // glReadPixels fills in a "direct" ByteBuffer with what is essentially big-endian RGBA + // data (i.e. a byte of red, followed by a byte of green...). While the Bitmap + // constructor that takes an int[] wants little-endian ARGB (blue/red swapped), the + // Bitmap "copy pixels" method wants the same format GL provides. + // + // Ideally we'd have some way to re-use the ByteBuffer, especially if we're calling + // here often. + // + // Making this even more interesting is the upside-down nature of GL, which means + // our output will look upside down relative to what appears on screen if the + // typical GL conventions are used. + + String filename = file.toString(); + + int width = getWidth(); + int height = getHeight(); + ByteBuffer buf = ByteBuffer.allocateDirect(width * height * 4); + buf.order(ByteOrder.LITTLE_ENDIAN); + GLES20.glReadPixels(0, 0, width, height, + GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buf); + GlUtil.checkGlError("glReadPixels"); + buf.rewind(); + + BufferedOutputStream bos = null; + try { + bos = new BufferedOutputStream(new FileOutputStream(filename)); + Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + bmp.copyPixelsFromBuffer(buf); + bmp.compress(Bitmap.CompressFormat.PNG, 90, bos); + bmp.recycle(); + } finally { + if (bos != null) bos.close(); + } + Log.d(TAG,"Saved " + width + "x" + height + " frame as '" + filename + "'"); + } +} diff --git a/android/sdl_android/src/main/java/com/android/grafika/gles/FullFrameRect.java b/android/sdl_android/src/main/java/com/android/grafika/gles/FullFrameRect.java new file mode 100644 index 000000000..74c8dac44 --- /dev/null +++ b/android/sdl_android/src/main/java/com/android/grafika/gles/FullFrameRect.java @@ -0,0 +1,91 @@ +/* + * Copyright 2014 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.grafika.gles; + +import android.opengl.Matrix; + +/** + * This class essentially represents a viewport-sized sprite that will be rendered with + * a texture, usually from an external source like the camera or video decoder. + */ +public class FullFrameRect { + private final Drawable2d mRectDrawable = new Drawable2d(Drawable2d.Prefab.FULL_RECTANGLE); + private Texture2dProgram mProgram; + + /** + * Prepares the object. + * + * @param program The program to use. FullFrameRect takes ownership, and will release + * the program when no longer needed. + */ + public FullFrameRect(Texture2dProgram program) { + mProgram = program; + } + + /** + * Releases resources. + * <p> + * This must be called with the appropriate EGL context current (i.e. the one that was + * current when the constructor was called). If we're about to destroy the EGL context, + * there's no value in having the caller make it current just to do this cleanup, so you + * can pass a flag that will tell this function to skip any EGL-context-specific cleanup. + */ + public void release(boolean doEglCleanup) { + if (mProgram != null) { + if (doEglCleanup) { + mProgram.release(); + } + mProgram = null; + } + } + + /** + * Returns the program currently in use. + */ + public Texture2dProgram getProgram() { + return mProgram; + } + + /** + * Changes the program. The previous program will be released. + * <p> + * The appropriate EGL context must be current. + */ + public void changeProgram(Texture2dProgram program) { + mProgram.release(); + mProgram = program; + } + + /** + * Creates a texture object suitable for use with drawFrame(). + */ + public int createTextureObject() { + return mProgram.createTextureObject(); + } + + /** + * Draws a viewport-filling rect, texturing it with the specified texture object. + */ + public void drawFrame(int textureId, float[] texMatrix) { + // Use the identity matrix for MVP so our 2x2 FULL_RECTANGLE covers the viewport. + mProgram.draw(GlUtil.IDENTITY_MATRIX, mRectDrawable.getVertexArray(), 0, + mRectDrawable.getVertexCount(), mRectDrawable.getCoordsPerVertex(), + mRectDrawable.getVertexStride(), + texMatrix, mRectDrawable.getTexCoordArray(), textureId, + mRectDrawable.getTexCoordStride()); + } +} diff --git a/android/sdl_android/src/main/java/com/android/grafika/gles/GlUtil.java b/android/sdl_android/src/main/java/com/android/grafika/gles/GlUtil.java new file mode 100644 index 000000000..4eed8d00c --- /dev/null +++ b/android/sdl_android/src/main/java/com/android/grafika/gles/GlUtil.java @@ -0,0 +1,195 @@ +/* + * Copyright 2014 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.grafika.gles; + +import android.opengl.GLES20; +import android.opengl.GLES30; +import android.opengl.Matrix; +import android.util.Log; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; + +/** + * Some OpenGL utility functions. + */ +public class GlUtil { + private static final String TAG = "GlUtil"; + + /** Identity matrix for general use. Don't modify or life will get weird. */ + public static final float[] IDENTITY_MATRIX; + static { + IDENTITY_MATRIX = new float[16]; + Matrix.setIdentityM(IDENTITY_MATRIX, 0); + } + + private static final int SIZEOF_FLOAT = 4; + + + private GlUtil() {} // do not instantiate + + /** + * Creates a new program from the supplied vertex and fragment shaders. + * + * @return A handle to the program, or 0 on failure. + */ + public static int createProgram(String vertexSource, String fragmentSource) { + int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource); + if (vertexShader == 0) { + return 0; + } + int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource); + if (pixelShader == 0) { + return 0; + } + + int program = GLES20.glCreateProgram(); + checkGlError("glCreateProgram"); + if (program == 0) { + Log.d(TAG,"Could not create program"); + } + GLES20.glAttachShader(program, vertexShader); + checkGlError("glAttachShader"); + GLES20.glAttachShader(program, pixelShader); + checkGlError("glAttachShader"); + GLES20.glLinkProgram(program); + int[] linkStatus = new int[1]; + GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0); + if (linkStatus[0] != GLES20.GL_TRUE) { + Log.e(TAG,"Could not link program: "); + Log.e(TAG,GLES20.glGetProgramInfoLog(program)); + GLES20.glDeleteProgram(program); + program = 0; + } + return program; + } + + /** + * Compiles the provided shader source. + * + * @return A handle to the shader, or 0 on failure. + */ + public static int loadShader(int shaderType, String source) { + int shader = GLES20.glCreateShader(shaderType); + checkGlError("glCreateShader type=" + shaderType); + GLES20.glShaderSource(shader, source); + GLES20.glCompileShader(shader); + int[] compiled = new int[1]; + GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0); + if (compiled[0] == 0) { + Log.e(TAG,"Could not compile shader " + shaderType + ":"); + Log.e(TAG," " + GLES20.glGetShaderInfoLog(shader)); + GLES20.glDeleteShader(shader); + shader = 0; + } + return shader; + } + + /** + * Checks to see if a GLES error has been raised. + */ + public static void checkGlError(String op) { + int error = GLES20.glGetError(); + if (error != GLES20.GL_NO_ERROR) { + String msg = op + ": glError 0x" + Integer.toHexString(error); + Log.e(TAG,msg); + throw new RuntimeException(msg); + } + } + + /** + * Checks to see if the location we obtained is valid. GLES returns -1 if a label + * could not be found, but does not set the GL error. + * <p> + * Throws a RuntimeException if the location is invalid. + */ + public static void checkLocation(int location, String label) { + if (location < 0) { + throw new RuntimeException("Unable to locate '" + label + "' in program"); + } + } + + /** + * Creates a texture from raw data. + * + * @param data Image data, in a "direct" ByteBuffer. + * @param width Texture width, in pixels (not bytes). + * @param height Texture height, in pixels. + * @param format Image data format (use constant appropriate for glTexImage2D(), e.g. GL_RGBA). + * @return Handle to texture. + */ + public static int createImageTexture(ByteBuffer data, int width, int height, int format) { + int[] textureHandles = new int[1]; + int textureHandle; + + GLES20.glGenTextures(1, textureHandles, 0); + textureHandle = textureHandles[0]; + GlUtil.checkGlError("glGenTextures"); + + // Bind the texture handle to the 2D texture target. + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureHandle); + + // Configure min/mag filtering, i.e. what scaling method do we use if what we're rendering + // is smaller or larger than the source image. + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, + GLES20.GL_LINEAR); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, + GLES20.GL_LINEAR); + GlUtil.checkGlError("loadImageTexture"); + + // Load the data from the buffer into the texture handle. + GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, /*level*/ 0, format, + width, height, /*border*/ 0, format, GLES20.GL_UNSIGNED_BYTE, data); + GlUtil.checkGlError("loadImageTexture"); + + return textureHandle; + } + + /** + * Allocates a direct float buffer, and populates it with the float array data. + */ + public static FloatBuffer createFloatBuffer(float[] coords) { + // Allocate a direct ByteBuffer, using 4 bytes per float, and copy coords into it. + ByteBuffer bb = ByteBuffer.allocateDirect(coords.length * SIZEOF_FLOAT); + bb.order(ByteOrder.nativeOrder()); + FloatBuffer fb = bb.asFloatBuffer(); + fb.put(coords); + fb.position(0); + return fb; + } + + /** + * Writes GL version info to the log. + */ + public static void logVersionInfo() { + Log.i(TAG,"vendor : " + GLES20.glGetString(GLES20.GL_VENDOR)); + Log.i(TAG,"renderer: " + GLES20.glGetString(GLES20.GL_RENDERER)); + Log.i(TAG,"version : " + GLES20.glGetString(GLES20.GL_VERSION)); + + if (false) { + int[] values = new int[1]; + GLES30.glGetIntegerv(GLES30.GL_MAJOR_VERSION, values, 0); + int majorVersion = values[0]; + GLES30.glGetIntegerv(GLES30.GL_MINOR_VERSION, values, 0); + int minorVersion = values[0]; + if (GLES30.glGetError() == GLES30.GL_NO_ERROR) { + Log.i(TAG,"iversion: " + majorVersion + "." + minorVersion); + } + } + } +} diff --git a/android/sdl_android/src/main/java/com/android/grafika/gles/OffscreenSurface.java b/android/sdl_android/src/main/java/com/android/grafika/gles/OffscreenSurface.java new file mode 100644 index 000000000..8b33d87a4 --- /dev/null +++ b/android/sdl_android/src/main/java/com/android/grafika/gles/OffscreenSurface.java @@ -0,0 +1,39 @@ +/* + * Copyright 2013 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.grafika.gles; + +/** + * Off-screen EGL surface (pbuffer). + * <p> + * It's good practice to explicitly release() the surface, preferably from a "finally" block. + */ +public class OffscreenSurface extends EglSurfaceBase { + /** + * Creates an off-screen surface with the specified width and height. + */ + public OffscreenSurface(EglCore eglCore, int width, int height) { + super(eglCore); + createOffscreenSurface(width, height); + } + + /** + * Releases any resources associated with the surface. + */ + public void release() { + releaseEglSurface(); + } +} diff --git a/android/sdl_android/src/main/java/com/android/grafika/gles/Texture2dProgram.java b/android/sdl_android/src/main/java/com/android/grafika/gles/Texture2dProgram.java new file mode 100644 index 000000000..1faed6776 --- /dev/null +++ b/android/sdl_android/src/main/java/com/android/grafika/gles/Texture2dProgram.java @@ -0,0 +1,344 @@ +/* + * Copyright 2014 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.grafika.gles; + +import android.opengl.GLES11Ext; +import android.opengl.GLES20; +import android.util.Log; + +import java.nio.FloatBuffer; +import java.util.Arrays; + +/** + * GL program and supporting functions for textured 2D shapes. + */ +public class Texture2dProgram { + private static final String TAG = "Texture2dProgram"; + + public enum ProgramType { + TEXTURE_2D, TEXTURE_EXT, TEXTURE_EXT_BW, TEXTURE_EXT_FILT + } + + // Simple vertex shader, used for all programs. + private static final String VERTEX_SHADER = + "uniform mat4 uMVPMatrix;\n" + + "uniform mat4 uTexMatrix;\n" + + "attribute vec4 aPosition;\n" + + "attribute vec4 aTextureCoord;\n" + + "varying vec2 vTextureCoord;\n" + + "void main() {\n" + + " gl_Position = uMVPMatrix * aPosition;\n" + + " vTextureCoord = (uTexMatrix * aTextureCoord).xy;\n" + + "}\n"; + + // Simple fragment shader for use with "normal" 2D textures. + private static final String FRAGMENT_SHADER_2D = + "precision mediump float;\n" + + "varying vec2 vTextureCoord;\n" + + "uniform sampler2D sTexture;\n" + + "void main() {\n" + + " gl_FragColor = texture2D(sTexture, vTextureCoord);\n" + + "}\n"; + + // Simple fragment shader for use with external 2D textures (e.g. what we get from + // SurfaceTexture). + private static final String FRAGMENT_SHADER_EXT = + "#extension GL_OES_EGL_image_external : require\n" + + "precision mediump float;\n" + + "varying vec2 vTextureCoord;\n" + + "uniform samplerExternalOES sTexture;\n" + + "void main() {\n" + + " gl_FragColor = texture2D(sTexture, vTextureCoord);\n" + + "}\n"; + + // Fragment shader that converts color to black & white with a simple transformation. + private static final String FRAGMENT_SHADER_EXT_BW = + "#extension GL_OES_EGL_image_external : require\n" + + "precision mediump float;\n" + + "varying vec2 vTextureCoord;\n" + + "uniform samplerExternalOES sTexture;\n" + + "void main() {\n" + + " vec4 tc = texture2D(sTexture, vTextureCoord);\n" + + " float color = tc.r * 0.3 + tc.g * 0.59 + tc.b * 0.11;\n" + + " gl_FragColor = vec4(color, color, color, 1.0);\n" + + "}\n"; + + // Fragment shader with a convolution filter. The upper-left half will be drawn normally, + // the lower-right half will have the filter applied, and a thin red line will be drawn + // at the border. + // + // This is not optimized for performance. Some things that might make this faster: + // - Remove the conditionals. They're used to present a half & half view with a red + // stripe across the middle, but that's only useful for a demo. + // - Unroll the loop. Ideally the compiler does this for you when it's beneficial. + // - Bake the filter kernel into the shader, instead of passing it through a uniform + // array. That, combined with loop unrolling, should reduce memory accesses. + public static final int KERNEL_SIZE = 9; + private static final String FRAGMENT_SHADER_EXT_FILT = + "#extension GL_OES_EGL_image_external : require\n" + + "#define KERNEL_SIZE " + KERNEL_SIZE + "\n" + + "precision highp float;\n" + + "varying vec2 vTextureCoord;\n" + + "uniform samplerExternalOES sTexture;\n" + + "uniform float uKernel[KERNEL_SIZE];\n" + + "uniform vec2 uTexOffset[KERNEL_SIZE];\n" + + "uniform float uColorAdjust;\n" + + "void main() {\n" + + " int i = 0;\n" + + " vec4 sum = vec4(0.0);\n" + + " if (vTextureCoord.x < vTextureCoord.y - 0.005) {\n" + + " for (i = 0; i < KERNEL_SIZE; i++) {\n" + + " vec4 texc = texture2D(sTexture, vTextureCoord + uTexOffset[i]);\n" + + " sum += texc * uKernel[i];\n" + + " }\n" + + " sum += uColorAdjust;\n" + + " } else if (vTextureCoord.x > vTextureCoord.y + 0.005) {\n" + + " sum = texture2D(sTexture, vTextureCoord);\n" + + " } else {\n" + + " sum.r = 1.0;\n" + + " }\n" + + " gl_FragColor = sum;\n" + + "}\n"; + + private ProgramType mProgramType; + + // Handles to the GL program and various components of it. + private int mProgramHandle; + private int muMVPMatrixLoc; + private int muTexMatrixLoc; + private int muKernelLoc; + private int muTexOffsetLoc; + private int muColorAdjustLoc; + private int maPositionLoc; + private int maTextureCoordLoc; + + private int mTextureTarget; + + private float[] mKernel = new float[KERNEL_SIZE]; + private float[] mTexOffset; + private float mColorAdjust; + + + /** + * Prepares the program in the current EGL context. + */ + public Texture2dProgram(ProgramType programType) { + mProgramType = programType; + + switch (programType) { + case TEXTURE_2D: + mTextureTarget = GLES20.GL_TEXTURE_2D; + mProgramHandle = GlUtil.createProgram(VERTEX_SHADER, FRAGMENT_SHADER_2D); + break; + case TEXTURE_EXT: + mTextureTarget = GLES11Ext.GL_TEXTURE_EXTERNAL_OES; + mProgramHandle = GlUtil.createProgram(VERTEX_SHADER, FRAGMENT_SHADER_EXT); + break; + case TEXTURE_EXT_BW: + mTextureTarget = GLES11Ext.GL_TEXTURE_EXTERNAL_OES; + mProgramHandle = GlUtil.createProgram(VERTEX_SHADER, FRAGMENT_SHADER_EXT_BW); + break; + case TEXTURE_EXT_FILT: + mTextureTarget = GLES11Ext.GL_TEXTURE_EXTERNAL_OES; + mProgramHandle = GlUtil.createProgram(VERTEX_SHADER, FRAGMENT_SHADER_EXT_FILT); + break; + default: + throw new RuntimeException("Unhandled type " + programType); + } + if (mProgramHandle == 0) { + throw new RuntimeException("Unable to create program"); + } + Log.e(TAG,"Created program " + mProgramHandle + " (" + programType + ")"); + + // get locations of attributes and uniforms + + maPositionLoc = GLES20.glGetAttribLocation(mProgramHandle, "aPosition"); + GlUtil.checkLocation(maPositionLoc, "aPosition"); + maTextureCoordLoc = GLES20.glGetAttribLocation(mProgramHandle, "aTextureCoord"); + GlUtil.checkLocation(maTextureCoordLoc, "aTextureCoord"); + muMVPMatrixLoc = GLES20.glGetUniformLocation(mProgramHandle, "uMVPMatrix"); + GlUtil.checkLocation(muMVPMatrixLoc, "uMVPMatrix"); + muTexMatrixLoc = GLES20.glGetUniformLocation(mProgramHandle, "uTexMatrix"); + GlUtil.checkLocation(muTexMatrixLoc, "uTexMatrix"); + muKernelLoc = GLES20.glGetUniformLocation(mProgramHandle, "uKernel"); + if (muKernelLoc < 0) { + // no kernel in this one + muKernelLoc = -1; + muTexOffsetLoc = -1; + muColorAdjustLoc = -1; + } else { + // has kernel, must also have tex offset and color adj + muTexOffsetLoc = GLES20.glGetUniformLocation(mProgramHandle, "uTexOffset"); + GlUtil.checkLocation(muTexOffsetLoc, "uTexOffset"); + muColorAdjustLoc = GLES20.glGetUniformLocation(mProgramHandle, "uColorAdjust"); + GlUtil.checkLocation(muColorAdjustLoc, "uColorAdjust"); + + // initialize default values + setKernel(new float[] {0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f}, 0f); + setTexSize(256, 256); + } + } + + /** + * Releases the program. + * <p> + * The appropriate EGL context must be current (i.e. the one that was used to create + * the program). + */ + public void release() { + Log.d(TAG,"deleting program " + mProgramHandle); + GLES20.glDeleteProgram(mProgramHandle); + mProgramHandle = -1; + } + + /** + * Returns the program type. + */ + public ProgramType getProgramType() { + return mProgramType; + } + + /** + * Creates a texture object suitable for use with this program. + * <p> + * On exit, the texture will be bound. + */ + public int createTextureObject() { + int[] textures = new int[1]; + GLES20.glGenTextures(1, textures, 0); + GlUtil.checkGlError("glGenTextures"); + + int texId = textures[0]; + GLES20.glBindTexture(mTextureTarget, texId); + GlUtil.checkGlError("glBindTexture " + texId); + + GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, + GLES20.GL_NEAREST); + GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, + GLES20.GL_LINEAR); + GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, + GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, + GLES20.GL_CLAMP_TO_EDGE); + GlUtil.checkGlError("glTexParameter"); + + return texId; + } + + /** + * Configures the convolution filter values. + * + * @param values Normalized filter values; must be KERNEL_SIZE elements. + */ + public void setKernel(float[] values, float colorAdj) { + if (values.length != KERNEL_SIZE) { + throw new IllegalArgumentException("Kernel size is " + values.length + + " vs. " + KERNEL_SIZE); + } + System.arraycopy(values, 0, mKernel, 0, KERNEL_SIZE); + mColorAdjust = colorAdj; + Log.d(TAG,"filt kernel: " + Arrays.toString(mKernel) + ", adj=" + colorAdj); + } + + /** + * Sets the size of the texture. This is used to find adjacent texels when filtering. + */ + public void setTexSize(int width, int height) { + float rw = 1.0f / width; + float rh = 1.0f / height; + + // Don't need to create a new array here, but it's syntactically convenient. + mTexOffset = new float[] { + -rw, -rh, 0f, -rh, rw, -rh, + -rw, 0f, 0f, 0f, rw, 0f, + -rw, rh, 0f, rh, rw, rh + }; + Log.d(TAG,"filt size: " + width + "x" + height + ": " + Arrays.toString(mTexOffset)); + } + + /** + * Issues the draw call. Does the full setup on every call. + * + * @param mvpMatrix The 4x4 projection matrix. + * @param vertexBuffer Buffer with vertex position data. + * @param firstVertex Index of first vertex to use in vertexBuffer. + * @param vertexCount Number of vertices in vertexBuffer. + * @param coordsPerVertex The number of coordinates per vertex (e.g. x,y is 2). + * @param vertexStride Width, in bytes, of the position data for each vertex (often + * vertexCount * sizeof(float)). + * @param texMatrix A 4x4 transformation matrix for texture coords. (Primarily intended + * for use with SurfaceTexture.) + * @param texBuffer Buffer with vertex texture data. + * @param texStride Width, in bytes, of the texture data for each vertex. + */ + public void draw(float[] mvpMatrix, FloatBuffer vertexBuffer, int firstVertex, + int vertexCount, int coordsPerVertex, int vertexStride, + float[] texMatrix, FloatBuffer texBuffer, int textureId, int texStride) { + GlUtil.checkGlError("draw start"); + + // Select the program. + GLES20.glUseProgram(mProgramHandle); + GlUtil.checkGlError("glUseProgram"); + + // Set the texture. + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLES20.glBindTexture(mTextureTarget, textureId); + + // Copy the model / view / projection matrix over. + GLES20.glUniformMatrix4fv(muMVPMatrixLoc, 1, false, mvpMatrix, 0); + GlUtil.checkGlError("glUniformMatrix4fv"); + + // Copy the texture transformation matrix over. + GLES20.glUniformMatrix4fv(muTexMatrixLoc, 1, false, texMatrix, 0); + GlUtil.checkGlError("glUniformMatrix4fv"); + + // Enable the "aPosition" vertex attribute. + GLES20.glEnableVertexAttribArray(maPositionLoc); + GlUtil.checkGlError("glEnableVertexAttribArray"); + + // Connect vertexBuffer to "aPosition". + GLES20.glVertexAttribPointer(maPositionLoc, coordsPerVertex, + GLES20.GL_FLOAT, false, vertexStride, vertexBuffer); + GlUtil.checkGlError("glVertexAttribPointer"); + + // Enable the "aTextureCoord" vertex attribute. + GLES20.glEnableVertexAttribArray(maTextureCoordLoc); + GlUtil.checkGlError("glEnableVertexAttribArray"); + + // Connect texBuffer to "aTextureCoord". + GLES20.glVertexAttribPointer(maTextureCoordLoc, 2, + GLES20.GL_FLOAT, false, texStride, texBuffer); + GlUtil.checkGlError("glVertexAttribPointer"); + + // Populate the convolution kernel, if present. + if (muKernelLoc >= 0) { + GLES20.glUniform1fv(muKernelLoc, KERNEL_SIZE, mKernel, 0); + GLES20.glUniform2fv(muTexOffsetLoc, KERNEL_SIZE, mTexOffset, 0); + GLES20.glUniform1f(muColorAdjustLoc, mColorAdjust); + } + + // Draw the rect. + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, firstVertex, vertexCount); + GlUtil.checkGlError("glDrawArrays"); + + // Done -- disable vertex array, texture, and program. + GLES20.glDisableVertexAttribArray(maPositionLoc); + GLES20.glDisableVertexAttribArray(maTextureCoordLoc); + GLES20.glBindTexture(mTextureTarget, 0); + GLES20.glUseProgram(0); + } +} diff --git a/android/sdl_android/src/main/java/com/android/grafika/gles/WindowSurface.java b/android/sdl_android/src/main/java/com/android/grafika/gles/WindowSurface.java new file mode 100644 index 000000000..adefce528 --- /dev/null +++ b/android/sdl_android/src/main/java/com/android/grafika/gles/WindowSurface.java @@ -0,0 +1,90 @@ +/* + * Copyright 2013 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.grafika.gles; + +import android.graphics.SurfaceTexture; +import android.view.Surface; + +/** + * Recordable EGL window surface. + * <p> + * It's good practice to explicitly release() the surface, preferably from a "finally" block. + */ +public class WindowSurface extends EglSurfaceBase { + private Surface mSurface; + private boolean mReleaseSurface; + + /** + * Associates an EGL surface with the native window surface. + * <p> + * Set releaseSurface to true if you want the Surface to be released when release() is + * called. This is convenient, but can interfere with framework classes that expect to + * manage the Surface themselves (e.g. if you release a SurfaceView's Surface, the + * surfaceDestroyed() callback won't fire). + */ + public WindowSurface(EglCore eglCore, Surface surface, boolean releaseSurface) { + super(eglCore); + createWindowSurface(surface); + mSurface = surface; + mReleaseSurface = releaseSurface; + } + + /** + * Associates an EGL surface with the SurfaceTexture. + */ + public WindowSurface(EglCore eglCore, SurfaceTexture surfaceTexture) { + super(eglCore); + createWindowSurface(surfaceTexture); + } + + /** + * Releases any resources associated with the EGL surface (and, if configured to do so, + * with the Surface as well). + * <p> + * Does not require that the surface's EGL context be current. + */ + public void release() { + releaseEglSurface(); + if (mSurface != null) { + if (mReleaseSurface) { + mSurface.release(); + } + mSurface = null; + } + } + + /** + * Recreate the EGLSurface, using the new EglBase. The caller should have already + * freed the old EGLSurface with releaseEglSurface(). + * <p> + * This is useful when we want to update the EGLSurface associated with a Surface. + * For example, if we want to share with a different EGLContext, which can only + * be done by tearing down and recreating the context. (That's handled by the caller; + * this just creates a new EGLSurface for the Surface we were handed earlier.) + * <p> + * If the previous EGLSurface isn't fully destroyed, e.g. it's still current on a + * context somewhere, the create call will fail with complaints from the Surface + * about already being connected. + */ + public void recreate(EglCore newEglCore) { + if (mSurface == null) { + throw new RuntimeException("not yet implemented for SurfaceTexture"); + } + mEglCore = newEglCore; // switch to new context + createWindowSurface(mSurface); // create new surface + } +} diff --git a/android/sdl_android/src/main/java/com/smartdevicelink/encoder/VirtualDisplayEncoder.java b/android/sdl_android/src/main/java/com/smartdevicelink/encoder/VirtualDisplayEncoder.java index 6a670dba3..7c8df5b98 100644 --- a/android/sdl_android/src/main/java/com/smartdevicelink/encoder/VirtualDisplayEncoder.java +++ b/android/sdl_android/src/main/java/com/smartdevicelink/encoder/VirtualDisplayEncoder.java @@ -34,15 +34,26 @@ package com.smartdevicelink.encoder; import android.annotation.TargetApi; import android.content.Context; +import android.graphics.SurfaceTexture; import android.hardware.display.DisplayManager; import android.hardware.display.VirtualDisplay; import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaFormat; +import android.opengl.GLES20; import android.os.Build; +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; import android.view.Display; import android.view.Surface; +import com.android.grafika.gles.EglCore; +import com.android.grafika.gles.FullFrameRect; +import com.android.grafika.gles.OffscreenSurface; +import com.android.grafika.gles.Texture2dProgram; +import com.android.grafika.gles.WindowSurface; import com.smartdevicelink.proxy.rpc.ImageResolution; import com.smartdevicelink.proxy.rpc.VideoStreamingFormat; import com.smartdevicelink.proxy.rpc.enums.VideoStreamingCodec; @@ -72,6 +83,14 @@ public class VirtualDisplayEncoder { //For older (<21) OS versions private Thread encoderThread; + private CaptureThread mCaptureThread; + private EglCore mEglCore; + private OffscreenSurface mDummySurface; + private int mTextureId = -1; + private SurfaceTexture mInterSurfaceTexture; + private Surface mInterSurface; + private FullFrameRect mFullFrameBlit; + private WindowSurface mEncoderSurface; /** * Initialization method for VirtualDisplayEncoder object. MUST be called before start() or shutdown() @@ -106,10 +125,29 @@ public class VirtualDisplayEncoder { return this.streamingParams; } + /** + * This method is deprecated; setStreamingParams with having stableFrameRate should be used. + */ + @Deprecated public void setStreamingParams(int displayDensity, ImageResolution resolution, int frameRate, int bitrate, int interval, VideoStreamingFormat format) { this.streamingParams = new VideoStreamingParameters(displayDensity, frameRate, bitrate, interval, resolution, format); } + /** + * setter of every parameter in streamingParams. + * @param displayDensity + * @param resolution + * @param frameRate + * @param bitrate + * @param interval + * @param format + * @param stableFramerate + */ + public void setStreamingParams(int displayDensity, ImageResolution resolution, int frameRate, int bitrate, int interval, VideoStreamingFormat format, boolean stableFramerate) { + this.streamingParams = new VideoStreamingParameters(displayDensity, frameRate, bitrate, interval, resolution, format, stableFramerate); + } + + @SuppressWarnings("unused") public void setStreamingParams(VideoStreamingParameters streamingParams) { this.streamingParams = streamingParams; } @@ -127,17 +165,50 @@ public class VirtualDisplayEncoder { return; } + int width = streamingParams.getResolution().getResolutionWidth(); + int height = streamingParams.getResolution().getResolutionHeight(); + if (streamingParams.isStableFrameRate()) { + setupGLES(width, height); + } + synchronized (STREAMING_LOCK) { try { - inputSurface = prepareVideoEncoder(); + if (streamingParams.isStableFrameRate()) { + // We use WindowSurface for the input of MediaCodec. + mEncoderSurface = new WindowSurface(mEglCore, prepareVideoEncoder(), true); + virtualDisplay = mDisplayManager.createVirtualDisplay(TAG, + width, height, streamingParams.getDisplayDensity(), mInterSurface, DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION); + + startEncoder(); + // also start capture thread. + final ConditionVariable cond = new ConditionVariable(); + mCaptureThread = new CaptureThread(mEglCore, mInterSurfaceTexture, mTextureId, + mEncoderSurface, mFullFrameBlit, width, height, streamingParams.getFrameRate(), new Runnable() { + @Override + public void run() { + cond.open(); + } + }); + mCaptureThread.start(); + cond.block(); // make sure Capture thread exists. + + // setup listener prior to the surface is attached to VirtualDisplay. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + mInterSurfaceTexture.setOnFrameAvailableListener(mCaptureThread, mCaptureThread.getHandler()); + } else { + mInterSurfaceTexture.setOnFrameAvailableListener(mCaptureThread); + } + } else { + inputSurface = prepareVideoEncoder(); - // Create a virtual display that will output to our encoder. - virtualDisplay = mDisplayManager.createVirtualDisplay(TAG, - streamingParams.getResolution().getResolutionWidth(), streamingParams.getResolution().getResolutionHeight(), - streamingParams.getDisplayDensity(), inputSurface, DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION); + // Create a virtual display that will output to our encoder. + virtualDisplay = mDisplayManager.createVirtualDisplay(TAG, + streamingParams.getResolution().getResolutionWidth(), streamingParams.getResolution().getResolutionHeight(), + streamingParams.getDisplayDensity(), inputSurface, DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION); - startEncoder(); + startEncoder(); + } } catch (Exception ex) { DebugTool.logError(TAG, "Unable to create Virtual Display."); @@ -152,6 +223,16 @@ public class VirtualDisplayEncoder { return; } try { + // cleanup GLES stuff + if (mCaptureThread != null) { + mCaptureThread.stopAsync(); + try { + mCaptureThread.join(); + } catch(InterruptedException e) { + + } + mCaptureThread = null; + } if (encoderThread != null) { encoderThread.interrupt(); encoderThread = null; @@ -177,6 +258,206 @@ public class VirtualDisplayEncoder { } } + /** + * setupGLES: create offscreen surface and surface texture. + * @param Width + * @param Height + */ + private void setupGLES(int Width, int Height) { + mEglCore = new EglCore(null, 0); + + // This 1x1 offscreen is created just to get the texture name (mTextureId). + // (To create a SurfaceTexture, we need a texture name. Texture name can be created by + // glGenTextures(), but for this method we need to acquire EGLContext. And to acquire + // EGLContext, we need to call eglMakeCurrent() on SurfaceTexture ... which is not created yet! + // So here, EGLContext is acquired by calling eglMakeCurrent() on a PBufferSurface which + // can be created without a texture name. That's why mDummySurface is not used anywhere.) + mDummySurface = new OffscreenSurface(mEglCore, 1, 1); + mDummySurface.makeCurrent(); + + mFullFrameBlit = new FullFrameRect(new Texture2dProgram(Texture2dProgram.ProgramType.TEXTURE_EXT)); + mTextureId = mFullFrameBlit.createTextureObject(); + + mInterSurfaceTexture = new SurfaceTexture(mTextureId); + mInterSurfaceTexture.setDefaultBufferSize(Width, Height); + mInterSurface = new Surface(mInterSurfaceTexture); + + // Some devices (e.g. Xperia Z4 with Android 5.0.1) do not allow eglMakeCurrent() called + // by multiple threads. (An EGLContext should be bound to a single thread.) Since the + // EGLContext will be accessed by CaptureThread from now on, unbind it from current thread. + mEglCore.makeNothingCurrent(); + } + + /** + * CatureThread: utilize OpenGl to capture the rendering thru intermediate surface and surface texture. + */ + private final class CaptureThread extends Thread implements SurfaceTexture.OnFrameAvailableListener { + + private static final int MSG_TICK = 1; + private static final int MSG_UPDATE_SURFACE = 2; + private static final int MSG_TERMINATE = -1; + + private static final long END_MARGIN_NSEC = 1000000; // 1 msec + private static final long LONG_SLEEP_THRES_NSEC = 16000000; // 16 msec + private static final long LONG_SLEEP_MSEC = 10; + + private Handler mHandler; + private Runnable mStartedCallback; + + private EglCore mEgl; + private SurfaceTexture mSourceSurfaceTexture; + private int mSourceTextureId; + private WindowSurface mDestSurface; + private FullFrameRect mBlit; + private int mWidth; + private int mHeight; + + private long mFrameIntervalNsec; + private long mStartNsec; + private long mNextTime; + private boolean mFirstInput; + private final float[] mMatrix = new float[16]; + + public CaptureThread(EglCore eglCore, SurfaceTexture sourceSurfaceTexture, int sourceTextureId, + WindowSurface destSurface, FullFrameRect blit, int width, int height, float fps, + Runnable onStarted) { + mEgl = eglCore; + mSourceSurfaceTexture = sourceSurfaceTexture; + mSourceTextureId = sourceTextureId; + mDestSurface = destSurface; + mBlit = blit; + mWidth = width; + mHeight = height; + mFrameIntervalNsec = (long)(1000000000 / fps); + mStartedCallback = onStarted; + } + + @Override + public void run() { + Looper.prepare(); + + // create a Handler for this thread + mHandler = new Handler() { + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_TICK: { + long now = System.nanoTime(); + if (now > mNextTime - END_MARGIN_NSEC) { + drawImage(now); + mNextTime += mFrameIntervalNsec; + } + + if (mNextTime - END_MARGIN_NSEC - now > LONG_SLEEP_THRES_NSEC) { + mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_TICK), LONG_SLEEP_MSEC); + } else { + mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_TICK), 1); + } + break; + } + // this is for KitKat and below + case MSG_UPDATE_SURFACE: + updateSurface(); + break; + case MSG_TERMINATE: { + removeCallbacksAndMessages(null); + Looper looper = Looper.myLooper(); + if (looper != null) { + looper.quit(); + } + break; + } + default: + break; + } + } + }; + + mStartNsec = -1; + mFirstInput = true; + + if (mStartedCallback != null) { + mStartedCallback.run(); + } + + Looper.loop(); + + // this is for safe (unbind EGLContext when terminating the thread) + mEgl.makeNothingCurrent(); + } + + // this may return null before mStartedCallback is called + public Handler getHandler() { + return mHandler; + } + + // make sure this is called after mStartedCallback is called + public void stopAsync() { + if (mHandler != null) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_TERMINATE)); + } + } + + @Override + public void onFrameAvailable(SurfaceTexture surfaceTexture) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + // With API level 21 and higher, setOnFrameAvailableListener(listener, handler) is used + // so this method is called on the CaptureThread. We can call updateTexImage() directly. + updateSurface(); + } else { + // With API level 20 and lower, setOnFrameAvailableListener(listener) is used, and + // this method is called on an "arbitrary" thread. (looks like the main thread is + // used for the most case.) So switch to CaptureThread before calling updateTexImage(). + mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_SURFACE)); + } + + if (mFirstInput) { + mFirstInput = false; + mNextTime = System.nanoTime(); + // start the loop + mHandler.sendMessage(mHandler.obtainMessage(MSG_TICK)); + } + } + + private void updateSurface() { + try { + mDestSurface.makeCurrent(); + } catch (RuntimeException e) { + DebugTool.logError(TAG, "Runtime exception in updateSurface: " + e); + return; + } + // Workaround for the issue Nexus6,5x(6.0 or 6.0.1) stuck. + // See https://github.com/google/grafika/issues/43 + // As in the comments, the nature of bug is still unclear... + // But it seems to have the effect to improve. + GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + // get latest image from VirtualDisplay + mSourceSurfaceTexture.updateTexImage(); + mSourceSurfaceTexture.getTransformMatrix(mMatrix); + } + + private void drawImage(long currentTime) { + if (mStartNsec < 0) { + // first frame + mStartNsec = currentTime; + } + + try { + mDestSurface.makeCurrent(); + // draw from mInterSurfaceTexture to mEncoderSurface + GLES20.glViewport(0, 0, mWidth, mHeight); + mBlit.drawFrame(mSourceTextureId, mMatrix); + } catch (RuntimeException e) { + DebugTool.logError(TAG, "Runtime exception in updateSurface: " + e); + return; + } + + // output to encoder + mDestSurface.setPresentationTime(currentTime - mStartNsec); + mDestSurface.swapBuffers(); + } + } + private Surface prepareVideoEncoder() { if (streamingParams == null || streamingParams.getResolution() == null || streamingParams.getFormat() == null) { diff --git a/android/sdl_android/src/main/java/com/smartdevicelink/managers/video/VideoStreamManager.java b/android/sdl_android/src/main/java/com/smartdevicelink/managers/video/VideoStreamManager.java index 714005041..a7ed506ab 100644 --- a/android/sdl_android/src/main/java/com/smartdevicelink/managers/video/VideoStreamManager.java +++ b/android/sdl_android/src/main/java/com/smartdevicelink/managers/video/VideoStreamManager.java @@ -294,38 +294,35 @@ public class VideoStreamManager extends BaseVideoStreamManager { stateMachine.transitionToState(StreamingStateMachine.ERROR); return; } - if (parameters == null) { - if (majorProtocolVersion >= 5) { - if (internalInterface.getSystemCapabilityManager() != null) { - internalInterface.getSystemCapabilityManager().getCapability(SystemCapabilityType.VIDEO_STREAMING, new OnSystemCapabilityListener() { - @Override - public void onCapabilityRetrieved(Object capability) { - VideoStreamingParameters params = new VideoStreamingParameters(); - params.update((VideoStreamingCapability) capability, vehicleMake); //Streaming parameters are ready time to stream - startStreaming(params, encrypted); - } + // regardless of VideoStreamingParameters are specified or not, we should refer to VideoStreamingCapability. + if (majorProtocolVersion >= 5) { + if (internalInterface.getSystemCapabilityManager() != null) { + final VideoStreamingParameters params = ( parameters == null) ? new VideoStreamingParameters() : new VideoStreamingParameters(parameters); + internalInterface.getSystemCapabilityManager().getCapability(SystemCapabilityType.VIDEO_STREAMING, new OnSystemCapabilityListener() { + @Override + public void onCapabilityRetrieved(Object capability) { + params.update((VideoStreamingCapability) capability, vehicleMake); //Streaming parameters are ready time to stream + startStreaming(params, encrypted); + } - @Override - public void onError(String info) { - stateMachine.transitionToState(StreamingStateMachine.ERROR); - DebugTool.logError(TAG, "Error retrieving video streaming capability: " + info); - } - }, false); - } - } else { - //We just use default video streaming params - VideoStreamingParameters params = new VideoStreamingParameters(); - DisplayCapabilities dispCap = null; - if (internalInterface.getSystemCapabilityManager() != null) { - dispCap = (DisplayCapabilities) internalInterface.getSystemCapabilityManager().getCapability(SystemCapabilityType.DISPLAY, null, false); - } - if (dispCap != null) { - params.setResolution(dispCap.getScreenParams().getImageResolution()); - } - startStreaming(params, encrypted); + @Override + public void onError(String info) { + stateMachine.transitionToState(StreamingStateMachine.ERROR); + DebugTool.logError(TAG, "Error retrieving video streaming capability: " + info); + } + }, false); } } else { - startStreaming(parameters, encrypted); + //We just use default video streaming params + VideoStreamingParameters params = (parameters == null) ? new VideoStreamingParameters() : new VideoStreamingParameters(parameters); + DisplayCapabilities dispCap = null; + if (internalInterface.getSystemCapabilityManager() != null) { + dispCap = (DisplayCapabilities) internalInterface.getSystemCapabilityManager().getCapability(SystemCapabilityType.DISPLAY, null, false); + } + if (dispCap != null) { + params.setResolution(dispCap.getScreenParams().getImageResolution()); + } + startStreaming(params, encrypted); } } diff --git a/base/src/main/java/com/smartdevicelink/proxy/rpc/VideoStreamingCapability.java b/base/src/main/java/com/smartdevicelink/proxy/rpc/VideoStreamingCapability.java index 88b18b802..199bd3999 100644 --- a/base/src/main/java/com/smartdevicelink/proxy/rpc/VideoStreamingCapability.java +++ b/base/src/main/java/com/smartdevicelink/proxy/rpc/VideoStreamingCapability.java @@ -49,6 +49,7 @@ public class VideoStreamingCapability extends RPCStruct { public static final String KEY_DIAGONAL_SCREEN_SIZE = "diagonalScreenSize"; public static final String KEY_PIXEL_PER_INCH = "pixelPerInch"; public static final String KEY_SCALE = "scale"; + public static final String KEY_PREFERRED_FPS = "preferredFPS"; public VideoStreamingCapability() { } @@ -180,4 +181,20 @@ public class VideoStreamingCapability extends RPCStruct { setValue(KEY_SCALE, scale); return this; } + + + /** + * @return the preferred frame rate per second (FPS) specified by head unit. + */ + public Integer getPreferredFPS() { + return getInteger(KEY_PREFERRED_FPS); + } + + /** + * @param preferredFPS preferred frame rate per second + */ + public VideoStreamingCapability setPreferredFPS(Integer preferredFPS) { + setValue(KEY_PREFERRED_FPS, preferredFPS); + return this; + } } diff --git a/base/src/main/java/com/smartdevicelink/streaming/video/VideoStreamingParameters.java b/base/src/main/java/com/smartdevicelink/streaming/video/VideoStreamingParameters.java index fa1f65489..0424639c5 100644 --- a/base/src/main/java/com/smartdevicelink/streaming/video/VideoStreamingParameters.java +++ b/base/src/main/java/com/smartdevicelink/streaming/video/VideoStreamingParameters.java @@ -66,6 +66,7 @@ public class VideoStreamingParameters { private int interval; private ImageResolution resolution; private VideoStreamingFormat format; + private boolean stableFrameRate; public VideoStreamingParameters() { displayDensity = DEFAULT_DENSITY; @@ -78,16 +79,49 @@ public class VideoStreamingParameters { format = new VideoStreamingFormat(); format.setProtocol(DEFAULT_PROTOCOL); format.setCodec(DEFAULT_CODEC); + stableFrameRate = true; } + /** + * deprecated constructor of VideoStreamingParameters. This constructor will be removed in the future version. + * @param displayDensity + * @param frameRate + * @param bitrate + * @param interval + * @param resolution + * @param format + */ + @Deprecated public VideoStreamingParameters(int displayDensity, int frameRate, int bitrate, int interval, ImageResolution resolution, VideoStreamingFormat format) { + this.displayDensity = displayDensity; + this.frameRate = frameRate; + this.bitrate = bitrate; + this.interval = interval; + this.resolution = resolution; + this.format = format; + this.stableFrameRate = true; + } + + /** + * new constructor of VideoStreamingParameters, which now has stableFrameRate param. + * @param displayDensity + * @param frameRate + * @param bitrate + * @param interval + * @param resolution + * @param format + * @param stableFrameRate + */ + public VideoStreamingParameters(int displayDensity, int frameRate, int bitrate, int interval, + ImageResolution resolution, VideoStreamingFormat format, boolean stableFrameRate) { this.displayDensity = displayDensity; this.frameRate = frameRate; this.bitrate = bitrate; this.interval = interval; this.resolution = resolution; this.format = format; + this.stableFrameRate = stableFrameRate; } /** @@ -132,6 +166,7 @@ public class VideoStreamingParameters { if (params.format != null) { this.format = params.format; } + this.stableFrameRate = params.stableFrameRate; } } @@ -146,9 +181,13 @@ public class VideoStreamingParameters { */ public void update(VideoStreamingCapability capability, String vehicleMake) { if (capability.getMaxBitrate() != null) { - this.bitrate = capability.getMaxBitrate() * 1000; + // Taking lower value as per SDL 0323 : + // https://github.com/smartdevicelink/sdl_evolution/blob/master/proposals/0323-align-VideoStreamingParameter-with-capability.md + int capableBitrateInKb = Math.min(Integer.MAX_VALUE / 1000, capability.getMaxBitrate()); + this.bitrate = Math.min(this.bitrate, capableBitrateInKb * 1000); } // NOTE: the unit of maxBitrate in getSystemCapability is kbps. double scale = DEFAULT_SCALE; + // For resolution and scale, the capability values should be taken rather than parameters specified by developers. if (capability.getScale() != null) { scale = capability.getScale(); } @@ -172,10 +211,17 @@ public class VideoStreamingParameters { this.resolution.setResolutionWidth((int) (resolution.getResolutionWidth() / scale)); } } + if (capability.getPreferredFPS() != null) { + // Taking lower value as per SDL 0323 + this.frameRate = Math.min(this.frameRate, capability.getPreferredFPS()); + } // This should be the last call as it will return out once a suitable format is found final List<VideoStreamingFormat> formats = capability.getSupportedFormats(); if (formats != null && formats.size() > 0) { + if (this.format != null && formats.contains(this.format)) { + return; // given format is supported, so no need to change. + } for (VideoStreamingFormat format : formats) { for (VideoStreamingFormat currentlySupportedFormat : currentlySupportedFormats) { if (currentlySupportedFormat.equals(format)) { @@ -240,7 +286,15 @@ public class VideoStreamingParameters { return resolution; } - @Override + public boolean isStableFrameRate() { + return stableFrameRate; + } + + public void setStableFrameRate(boolean isStable) { + stableFrameRate = isStable; + } + + @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("VideoStreamingParams - format: {"); @@ -257,6 +311,8 @@ public class VideoStreamingParameters { builder.append(bitrate); builder.append("}, IFrame interval{ "); builder.append(interval); + builder.append("}, stableFrameRate{"); + builder.append(stableFrameRate); builder.append("}"); return builder.toString(); } |