summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobert Henigan <robert.henigan@livio.io>2021-02-01 16:16:18 -0500
committerGitHub <noreply@github.com>2021-02-01 16:16:18 -0500
commit7f6db0821a3595745cff3ed34029e8a5f2552338 (patch)
treed55ac13a0f09babf02813ba6b657b80ec64c26a2
parentef2f86c997f608fe2eb977223dfe578b34cea10e (diff)
parent4add79ec1888cc517ff2010d56d795d8b9a27884 (diff)
downloadsdl_android-7f6db0821a3595745cff3ed34029e8a5f2552338.tar.gz
Merge pull request #1614 from smartdevicelink/integration/stable_frame_rate
Integration/stable frame rate
-rw-r--r--android/sdl_android/src/androidTest/java/com/android/grafika/gles/OffscreenSurfaceTest.java105
-rw-r--r--android/sdl_android/src/androidTest/java/com/smartdevicelink/managers/lifecycle/SystemCapabilityManagerTests.java2
-rw-r--r--android/sdl_android/src/androidTest/java/com/smartdevicelink/test/rpc/datatypes/VideoStreamingCapabilityTests.java7
-rw-r--r--android/sdl_android/src/main/java/com/android/grafika/gles/Drawable2d.java197
-rw-r--r--android/sdl_android/src/main/java/com/android/grafika/gles/EglCore.java372
-rw-r--r--android/sdl_android/src/main/java/com/android/grafika/gles/EglSurfaceBase.java197
-rw-r--r--android/sdl_android/src/main/java/com/android/grafika/gles/FullFrameRect.java91
-rw-r--r--android/sdl_android/src/main/java/com/android/grafika/gles/GlUtil.java195
-rw-r--r--android/sdl_android/src/main/java/com/android/grafika/gles/OffscreenSurface.java39
-rw-r--r--android/sdl_android/src/main/java/com/android/grafika/gles/Texture2dProgram.java344
-rw-r--r--android/sdl_android/src/main/java/com/android/grafika/gles/WindowSurface.java90
-rw-r--r--android/sdl_android/src/main/java/com/smartdevicelink/encoder/VirtualDisplayEncoder.java293
-rw-r--r--android/sdl_android/src/main/java/com/smartdevicelink/managers/video/VideoStreamManager.java55
-rw-r--r--base/src/main/java/com/smartdevicelink/proxy/rpc/VideoStreamingCapability.java17
-rw-r--r--base/src/main/java/com/smartdevicelink/streaming/video/VideoStreamingParameters.java60
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();
}