width / height
.
+ */
+ public void setAspectRatio(double aspectRatio) {
+ if (aspectRatio < 0) {
+ throw new IllegalArgumentException();
+ }
+ Timber.d("Setting aspect ratio to %f (was %f)", aspectRatio, mTargetAspect);
+ if (mTargetAspect != aspectRatio) {
+ mTargetAspect = aspectRatio;
+ requestLayout();
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // Target aspect ratio will be < 0 if it hasn't been set yet. In that case,
+ // we just use whatever we've been handed.
+ if (mTargetAspect > 0) {
+ int initialWidth = MeasureSpec.getSize(widthMeasureSpec);
+ int initialHeight = MeasureSpec.getSize(heightMeasureSpec);
+
+ // factor the padding out
+ int horizPadding = getPaddingLeft() + getPaddingRight();
+ int vertPadding = getPaddingTop() + getPaddingBottom();
+ initialWidth -= horizPadding;
+ initialHeight -= vertPadding;
+
+ double viewAspectRatio = (double) initialWidth / initialHeight;
+ double aspectDiff = mTargetAspect / viewAspectRatio - 1;
+
+ if (Math.abs(aspectDiff) >= 0.01) {
+ if (aspectDiff > 0) {
+ // limited by narrow width; restrict height
+ initialHeight = (int) (initialWidth / mTargetAspect);
+ } else {
+ // limited by short height; restrict width
+ initialWidth = (int) (initialHeight * mTargetAspect);
+ }
+ initialWidth += horizPadding;
+ initialHeight += vertPadding;
+ widthMeasureSpec = MeasureSpec.makeMeasureSpec(initialWidth, MeasureSpec.EXACTLY);
+ heightMeasureSpec = MeasureSpec.makeMeasureSpec(initialHeight, MeasureSpec.EXACTLY);
+ }
+ }
+
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+}
diff --git a/app/src/main/java/io/a3dv/VIRec/Camera2Proxy.java b/app/src/main/java/io/a3dv/VIRec/Camera2Proxy.java
new file mode 100644
index 0000000..32b747b
--- /dev/null
+++ b/app/src/main/java/io/a3dv/VIRec/Camera2Proxy.java
@@ -0,0 +1,565 @@
+package io.a3dv.VIRec;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraManager;
+import android.hardware.camera2.CameraMetadata;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.TotalCaptureResult;
+import android.hardware.camera2.params.MeteringRectangle;
+import android.hardware.camera2.params.StreamConfigurationMap;
+import android.media.ImageReader;
+import android.media.MediaRecorder;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import androidx.preference.PreferenceManager;
+import androidx.annotation.NonNull;
+
+import android.util.Size;
+import android.util.SizeF;
+import android.view.Surface;
+
+import java.io.BufferedWriter;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+
+import java.util.List;
+
+import timber.log.Timber;
+
+public class Camera2Proxy {
+ private final Activity mActivity;
+ private static SharedPreferences mSharedPreferences;
+ private String mCameraIdStr = "";
+ private final boolean mSecondCamera;
+ private Size mPreviewSize;
+ private Size mVideoSize;
+ private final CameraManager mCameraManager;
+ private CameraDevice mCameraDevice;
+ private CameraCaptureSession mCaptureSession;
+ private CaptureRequest.Builder mPreviewRequestBuilder;
+ private Rect sensorArraySize;
+ private Integer mTimeSourceValue;
+
+ private CaptureRequest mPreviewRequest;
+ private Handler mBackgroundHandler;
+ private HandlerThread mBackgroundThread;
+ private ImageReader mImageReader;
+ private Surface mPreviewSurface;
+ private SurfaceTexture mPreviewSurfaceTexture = null;
+
+ /**
+ * Camera state: Showing camera preview.
+ */
+ private static final int STATE_PREVIEW = 0;
+
+ /**
+ * Wait until the CONTROL_AF_MODE is in auto.
+ */
+ private static final int STATE_WAITING_AUTO = 1;
+
+ /**
+ * Trigger auto focus algorithm.
+ */
+ private static final int STATE_TRIGGER_AUTO = 2;
+
+ /**
+ * Camera state: Waiting for the focus to be locked.
+ */
+ private static final int STATE_WAITING_LOCK = 3;
+
+ /**
+ * Camera state: Focus distance is locked.
+ */
+ private static final int STATE_FOCUS_LOCKED = 4;
+ /**
+ * The current state of camera state for taking pictures.
+ *
+ * @see #mFocusCaptureCallback
+ */
+ private int mState = STATE_PREVIEW;
+
+ private BufferedWriter mFrameMetadataWriter = null;
+
+ private volatile boolean mRecordingMetadata = false;
+
+ private final FocalLengthHelper mFocalLengthHelper = new FocalLengthHelper();
+
+ private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {
+ @Override
+ public void onOpened(@NonNull CameraDevice camera) {
+ Timber.d("onOpened");
+ mCameraDevice = camera;
+ initPreviewRequest();
+ }
+
+ @Override
+ public void onDisconnected(@NonNull CameraDevice camera) {
+ Timber.d("onDisconnected");
+ releaseCamera();
+ }
+
+ @Override
+ public void onError(@NonNull CameraDevice camera, int error) {
+ Timber.w("Camera Open failed with error %d", error);
+ releaseCamera();
+ }
+ };
+
+ public Integer getmTimeSourceValue() {
+ return mTimeSourceValue;
+ }
+
+ public Size getmVideoSize() {
+ return mVideoSize;
+ }
+
+ public void startRecordingCaptureResult(String captureResultFile) {
+ try {
+ if (mFrameMetadataWriter != null) {
+ try {
+ mFrameMetadataWriter.flush();
+ mFrameMetadataWriter.close();
+ Timber.d("Flushing results!");
+ } catch (IOException err) {
+ Timber.e(err, "IOException in closing an earlier frameMetadataWriter.");
+ }
+ }
+ mFrameMetadataWriter = new BufferedWriter(
+ new FileWriter(captureResultFile, true));
+ String header = "Timestamp[nanosec],fx[px],fy[px],Frame No.," +
+ "Exposure time[nanosec],Sensor frame duration[nanosec]," +
+ "Frame readout time[nanosec]," +
+ "ISO,Focal length,Focus distance,AF mode,Unix time[nanosec]";
+
+ mFrameMetadataWriter.write(header + "\n");
+ mRecordingMetadata = true;
+ } catch (IOException err) {
+ Timber.e(err, "IOException in opening frameMetadataWriter at %s",
+ captureResultFile);
+ }
+ }
+
+// public void resumeRecordingCaptureResult() {
+// mRecordingMetadata = true;
+// }
+//
+// public void pauseRecordingCaptureResult() {
+// mRecordingMetadata = false;
+// }
+
+ public void stopRecordingCaptureResult() {
+ if (mRecordingMetadata) {
+ mRecordingMetadata = false;
+ }
+ if (mFrameMetadataWriter != null) {
+ try {
+ mFrameMetadataWriter.flush();
+ mFrameMetadataWriter.close();
+ } catch (IOException err) {
+ Timber.e(err, "IOException in closing frameMetadataWriter.");
+ }
+ mFrameMetadataWriter = null;
+ }
+ }
+
+ public Camera2Proxy(Activity activity, boolean secondCamera) {
+ mActivity = activity;
+ mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(mActivity);
+ mCameraManager = (CameraManager) mActivity.getSystemService(Context.CAMERA_SERVICE);
+ mSecondCamera = secondCamera; // If it's the second camera
+ }
+
+ public Size configureCamera() {
+ try {
+ if (mSecondCamera) {
+ mCameraIdStr = mSharedPreferences.getString("prefCamera2", "1");
+ } else {
+ mCameraIdStr = mSharedPreferences.getString("prefCamera", "0");
+ }
+
+ CameraCharacteristics mCameraCharacteristics = mCameraManager.getCameraCharacteristics(mCameraIdStr);
+
+ String imageSize = mSharedPreferences.getString("prefSizeRaw",
+ DesiredCameraSetting.mDesiredFrameSize);
+ int width = Integer.parseInt(imageSize.substring(0, imageSize.lastIndexOf("x")));
+ int height = Integer.parseInt(imageSize.substring(imageSize.lastIndexOf("x") + 1));
+
+ sensorArraySize = mCameraCharacteristics.get(
+ CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
+ mTimeSourceValue = mCameraCharacteristics.get(
+ CameraCharacteristics.SENSOR_INFO_TIMESTAMP_SOURCE);
+
+ StreamConfigurationMap map = mCameraCharacteristics.get(CameraCharacteristics
+ .SCALER_STREAM_CONFIGURATION_MAP);
+
+ Size[] videoSizeChoices = map.getOutputSizes(MediaRecorder.class);
+ mVideoSize = CameraUtils.chooseVideoSize(videoSizeChoices, width, height, width);
+
+ mFocalLengthHelper.setLensParams(mCameraCharacteristics);
+ mFocalLengthHelper.setmImageSize(mVideoSize);
+
+ mPreviewSize = CameraUtils.chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class),
+ width, height, mVideoSize);
+ Timber.d("Video size %s preview size %s.",
+ mVideoSize.toString(), mPreviewSize.toString());
+
+ } catch (CameraAccessException e) {
+ Timber.e(e);
+ }
+ return mPreviewSize;
+ }
+
+ @SuppressLint("MissingPermission")
+ public void openCamera() {
+ Timber.v("openCamera");
+ startBackgroundThread();
+ if (mCameraIdStr.isEmpty()) {
+ configureCamera();
+ }
+ try {
+ mCameraManager.openCamera(mCameraIdStr, mStateCallback, mBackgroundHandler);
+ } catch (CameraAccessException e) {
+ Timber.e(e);
+ }
+ }
+
+ public void releaseCamera() {
+ Timber.v("releaseCamera");
+ if (null != mCaptureSession) {
+ mCaptureSession.close();
+ mCaptureSession = null;
+ }
+ if (mCameraDevice != null) {
+ mCameraDevice.close();
+ mCameraDevice = null;
+ }
+ if (mImageReader != null) {
+ mImageReader.close();
+ mImageReader = null;
+ }
+ mPreviewSurfaceTexture = null;
+ mCameraIdStr = "";
+ stopRecordingCaptureResult();
+ stopBackgroundThread();
+ }
+
+ public void setPreviewSurfaceTexture(SurfaceTexture surfaceTexture) {
+ mPreviewSurfaceTexture = surfaceTexture;
+ }
+
+ private static class NumExpoIso {
+ public Long mNumber;
+ public Long mExposureNanos;
+ public Integer mIso;
+
+ public NumExpoIso(Long number, Long expoNanos, Integer iso) {
+ mNumber = number;
+ mExposureNanos = expoNanos;
+ mIso = iso;
+ }
+ }
+
+ private final int kMaxExpoSamples = 10;
+ private final ArrayList+ * For best results, call this *after* disabling Camera preview. + */ + public void notifyPausing() { + if (mSurfaceTexture != null) { + Timber.d("renderer pausing -- releasing SurfaceTexture"); + mSurfaceTexture.release(); + mSurfaceTexture = null; + } + if (mFullScreen != null) { + mFullScreen.release(false); // assume the GLSurfaceView EGL context is about + mFullScreen = null; // to be destroyed + } + mIncomingWidth = mIncomingHeight = -1; + mVideoFrameWidth = mVideoFrameHeight = -1; + } + + /** + * Notifies the renderer that we want to stop or start recording. + */ + public void changeRecordingState(boolean isRecording) { + Timber.d("changeRecordingState: was %b now %b", mRecordingEnabled, isRecording); + mRecordingEnabled = isRecording; + } + + /** + * Changes the filter that we're applying to the camera preview. + */ + public void changeFilterMode(int filter) { + mNewFilter = filter; + } + + /** + * Updates the filter program. + */ + public void updateFilter() { + Texture2dProgram.ProgramType programType; + float[] kernel = null; + float colorAdj = 0.0f; + + Timber.d("Updating filter to %d", mNewFilter); + switch (mNewFilter) { + case CameraActivity.FILTER_NONE: + programType = Texture2dProgram.ProgramType.TEXTURE_EXT; + break; + case CameraActivity.FILTER_BLACK_WHITE: + programType = Texture2dProgram.ProgramType.TEXTURE_EXT_BW; + break; + case CameraActivity.FILTER_BLUR: + programType = Texture2dProgram.ProgramType.TEXTURE_EXT_FILT_VIEW; + kernel = new float[]{ + 1f / 16f, 2f / 16f, 1f / 16f, + 2f / 16f, 4f / 16f, 2f / 16f, + 1f / 16f, 2f / 16f, 1f / 16f}; + break; + case CameraActivity.FILTER_SHARPEN: + programType = Texture2dProgram.ProgramType.TEXTURE_EXT_FILT_VIEW; + kernel = new float[]{ + 0f, -1f, 0f, + -1f, 5f, -1f, + 0f, -1f, 0f}; + break; + case CameraActivity.FILTER_EDGE_DETECT: + programType = Texture2dProgram.ProgramType.TEXTURE_EXT_FILT_VIEW; + kernel = new float[]{ + -1f, -1f, -1f, + -1f, 8f, -1f, + -1f, -1f, -1f}; + break; + case CameraActivity.FILTER_EMBOSS: + programType = Texture2dProgram.ProgramType.TEXTURE_EXT_FILT_VIEW; + kernel = new float[]{ + 2f, 0f, 0f, + 0f, -1f, 0f, + 0f, 0f, -1f}; + colorAdj = 0.5f; + break; + default: + throw new RuntimeException("Unknown filter mode " + mNewFilter); + } + + // Do we need a whole new program? (We want to avoid doing this if we don't have + // too -- compiling a program could be expensive.) + if (programType != mFullScreen.getProgram().getProgramType()) { + mFullScreen.changeProgram(new Texture2dProgram(programType)); + // If we created a new program, we need to initialize the texture width/height. + mIncomingSizeUpdated = true; + } + + // Update the filter kernel (if any). + if (kernel != null) { + mFullScreen.getProgram().setKernel(kernel, colorAdj); + } + + mCurrentFilter = mNewFilter; + } + + /** + * Records the size of the incoming camera preview frames. + *
+ * It's not clear whether this is guaranteed to execute before or after onSurfaceCreated(),
+ * so we assume it could go either way. (Fortunately they both run on the same thread,
+ * so we at least know that they won't execute concurrently.)
+ */
+ public void setCameraPreviewSize(int width, int height) {
+ Timber.d("setCameraPreviewSize");
+ mIncomingWidth = width;
+ mIncomingHeight = height;
+ mIncomingSizeUpdated = true;
+ }
+
+ public void setVideoFrameSize(int width, int height) {
+ mVideoFrameWidth = width;
+ mVideoFrameHeight = height;
+ }
+
+ @Override
+ public void onSurfaceCreated(GL10 unused, EGLConfig config) {
+ Timber.d("onSurfaceCreated");
+
+ // We're starting up or coming back. Either way we've got a new EGLContext that will
+ // need to be shared with the video encoder, so figure out if a recording is already
+ // in progress.
+ mRecordingEnabled = mVideoEncoder.isRecording();
+ if (mRecordingEnabled) {
+ mRecordingStatus = RECORDING_RESUMED;
+ } else {
+ mRecordingStatus = RECORDING_OFF;
+ }
+
+ // Set up the texture glitter that will be used for on-screen display. This
+ // is *not* applied to the recording, because that uses a separate shader.
+ mFullScreen = new FullFrameRect(
+ new Texture2dProgram(Texture2dProgram.ProgramType.TEXTURE_EXT));
+
+ mTextureId = mFullScreen.createTextureObject();
+
+ // Create a SurfaceTexture, with an external texture, in this EGL context. We don't
+ // have a Looper in this thread -- GLSurfaceView doesn't create one -- so the frame
+ // available messages will arrive on the main thread.
+ mSurfaceTexture = new SurfaceTexture(mTextureId);
+
+ // Tell the UI thread to enable the camera preview.
+ if (mCameraId == 0) {
+ mCameraHandler.sendMessage(mCameraHandler.obtainMessage(
+ CameraHandler.MSG_SET_SURFACE_TEXTURE, mSurfaceTexture));
+ } else {
+ mCameraHandler.sendMessage(mCameraHandler.obtainMessage(
+ CameraHandler.MSG_SET_SURFACE_TEXTURE2, mSurfaceTexture));
+ }
+ }
+
+ @Override
+ public void onSurfaceChanged(GL10 unused, int width, int height) {
+ Timber.d("onSurfaceChanged %dx%d", width, height);
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
+ @Override
+ public void onDrawFrame(GL10 unused) {
+ if (VERBOSE) Timber.d("onDrawFrame tex=%d", mTextureId);
+ boolean showBox;
+
+ // Latch the latest frame. If there isn't anything new, we'll just re-use whatever
+ // was there before.
+ mSurfaceTexture.updateTexImage();
+
+ // If the recording state is changing, take care of it here. Ideally we wouldn't
+ // be doing all this in onDrawFrame(), but the EGLContext sharing with GLSurfaceView
+ // makes it hard to do elsewhere.
+ if (mRecordingEnabled) {
+ switch (mRecordingStatus) {
+ case RECORDING_OFF:
+ if (mVideoFrameWidth <= 0 || mVideoFrameHeight <= 0) {
+ Timber.i("Start recording before setting video frame size; skipping");
+ break;
+ }
+ Timber.d("Start recording outputFile: %s", mOutputFile);
+ // The output video has a size e.g., 720x1280. Video of the same size is recorded in
+ // the portrait mode of the complex CameraRecorder-android at
+ // https://github.com/MasayukiSuda/CameraRecorder-android.
+ mVideoEncoder.startRecording(
+ new TextureMovieEncoder.EncoderConfig(
+ mOutputFile, mVideoFrameHeight, mVideoFrameWidth,
+ CameraUtils.calcBitRate(mVideoFrameWidth, mVideoFrameHeight,
+ VideoEncoderCore.FRAME_RATE),
+ EGL14.eglGetCurrentContext(),
+ mFullScreen.getProgram(), mMetadataFile));
+ mRecordingStatus = RECORDING_ON;
+ break;
+ case RECORDING_RESUMED:
+ Timber.d("Resume recording");
+ mVideoEncoder.updateSharedContext(EGL14.eglGetCurrentContext());
+ mRecordingStatus = RECORDING_ON;
+ break;
+ case RECORDING_ON:
+ break;
+ default:
+ throw new RuntimeException("unknown status " + mRecordingStatus);
+ }
+ } else {
+ switch (mRecordingStatus) {
+ case RECORDING_ON:
+ case RECORDING_RESUMED:
+ Timber.d("Stop recording");
+ mVideoEncoder.stopRecording();
+ mRecordingStatus = RECORDING_OFF;
+ break;
+ case RECORDING_OFF:
+ break;
+ default:
+ throw new RuntimeException("unknown status " + mRecordingStatus);
+ }
+ }
+
+ // Set the video encoder's texture name. We only need to do this once, but in the
+ // current implementation it has to happen after the video encoder is started, so
+ // we just do it here.
+ mVideoEncoder.setTextureId(mTextureId);
+
+ // Tell the video encoder thread that a new frame is available.
+ // This will be ignored if we're not actually recording.
+ mVideoEncoder.frameAvailable(mSurfaceTexture);
+
+ if (mIncomingWidth <= 0 || mIncomingHeight <= 0) {
+ // Texture size isn't set yet. This is only used for the filters, but to be
+ // safe we can just skip drawing while we wait for the various races to resolve.
+ // (This seems to happen if you toggle the screen off/on with power button.)
+ Timber.i("Drawing before incoming texture size set; skipping");
+ return;
+ }
+
+ // Update the filter, if necessary.
+ if (mCurrentFilter != mNewFilter) {
+ updateFilter();
+ }
+
+ if (mIncomingSizeUpdated) {
+ mFullScreen.getProgram().setTexSize(mIncomingWidth, mIncomingHeight);
+ mIncomingSizeUpdated = false;
+ }
+
+ // Draw the video frame.
+ mSurfaceTexture.getTransformMatrix(mSTMatrix);
+ mFullScreen.drawFrame(mTextureId, mSTMatrix);
+
+ // Draw a flashing box if we're recording. This only appears on screen.
+ showBox = (mRecordingStatus == RECORDING_ON);
+ if (showBox && (++mFrameCount & 0x04) == 0) {
+ drawBox();
+ }
+ }
+
+ /**
+ * Draws a red box in the corner.
+ */
+ private void drawBox() {
+ GLES20.glEnable(GLES20.GL_SCISSOR_TEST);
+ GLES20.glScissor(0, 0, 50, 50);
+ GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
+ GLES20.glDisable(GLES20.GL_SCISSOR_TEST);
+ }
+}
diff --git a/app/src/main/java/io/a3dv/VIRec/CameraUtils.java b/app/src/main/java/io/a3dv/VIRec/CameraUtils.java
new file mode 100644
index 0000000..fdd9b36
--- /dev/null
+++ b/app/src/main/java/io/a3dv/VIRec/CameraUtils.java
@@ -0,0 +1,88 @@
+package io.a3dv.VIRec;
+
+import android.util.Size;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import timber.log.Timber;
+
+/**
+ * Camera-related utility functions.
+ */
+public class CameraUtils {
+ private static final float BPP = 0.25f;
+
+ /**
+ * In this sample, we choose a video size with 3x4 aspect ratio. Also, we don't use sizes
+ * larger than 1080p, since MediaRecorder cannot handle such a high-resolution video.
+ *
+ * @param choices The list of available sizes
+ * @return The video size
+ */
+ public static Size chooseVideoSize(
+ Size[] choices, int wScale, int hScale, int maxWidth) {
+ for (Size size : choices) {
+ if (size.getWidth() == size.getHeight() * wScale / hScale &&
+ size.getWidth() <= maxWidth) {
+ return size;
+ }
+ }
+ Timber.e("Couldn't find any suitable video size");
+ return choices[choices.length - 1];
+ }
+
+ /**
+ * Compares two {@code Size}s based on their areas.
+ */
+ static class CompareSizesByArea implements Comparator
+ * The object wraps an encoder running on a dedicated thread. The various control messages
+ * may be sent from arbitrary threads (typically the app UI thread). The encoder thread
+ * manages both sides of the encoder (feeding and draining); the only external input is
+ * the GL texture.
+ *
+ * The design is complicated slightly by the need to create an EGL context that shares state
+ * with a view that gets restarted if (say) the device orientation changes. When the view
+ * in question is a GLSurfaceView, we don't have full control over the EGL context creation
+ * on that side, so we have to bend a bit backwards here.
+ *
+ * To use:
+ *
+ */
+public class TextureMovieEncoder implements Runnable {
+ private static final boolean VERBOSE = false;
+
+ private static final int MSG_START_RECORDING = 0;
+ private static final int MSG_STOP_RECORDING = 1;
+ private static final int MSG_FRAME_AVAILABLE = 2;
+ private static final int MSG_SET_TEXTURE_ID = 3;
+ private static final int MSG_UPDATE_SHARED_CONTEXT = 4;
+ private static final int MSG_QUIT = 5;
+
+ // ----- accessed exclusively by encoder thread -----
+ private WindowSurface mInputWindowSurface;
+ private EglCore mEglCore;
+ private FullFrameRect mFullScreen;
+ private int mTextureId;
+ private int mFrameNum;
+ private VideoEncoderCore mVideoEncoder;
+
+ // ----- accessed by multiple threads -----
+ private volatile EncoderHandler mHandler;
+
+ private final Object mReadyFence = new Object(); // guards ready/running
+ private boolean mReady;
+ private boolean mRunning;
+ private Long mLastFrameTimeNs = null;
+ public Float mFrameRate = 15.f;
+ private final float[] STMatrix = new float[16];
+
+ /**
+ * Encoder configuration.
+ *
+ * Object is immutable, which means we can safely pass it between threads without
+ * explicit synchronization (and don't need to worry about it getting tweaked out from
+ * under us).
+ *
+ * with reasonable defaults for those and bit rate.
+ */
+ public static class EncoderConfig {
+ final String mOutputFile;
+ final int mWidth;
+ final int mHeight;
+ final int mBitRate;
+ final EGLContext mEglContext;
+ final Texture2dProgram mProgram;
+ final String mMetadataFile;
+
+ public EncoderConfig(String outputFile, int width, int height, int bitRate,
+ EGLContext sharedEglContext, Texture2dProgram program, String metaFile) {
+ mOutputFile = outputFile;
+ mWidth = width;
+ mHeight = height;
+ mBitRate = bitRate;
+ mEglContext = sharedEglContext;
+ mProgram = program;
+ mMetadataFile = metaFile;
+ }
+
+ @Override
+ public String toString() {
+ return "EncoderConfig: " + mWidth + "x" + mHeight + " @" + mBitRate +
+ " to '" + mOutputFile + "' ctxt=" + mEglContext;
+ }
+ }
+
+ /**
+ * Tells the video recorder to start recording. (Call from non-encoder thread.)
+ *
+ * Creates a new thread, which will create an encoder using the provided configuration.
+ *
+ * Returns after the recorder thread has started and is ready to accept Messages. The
+ * encoder may not yet be fully configured.
+ */
+ public void startRecording(EncoderConfig config) {
+ Timber.d("Encoder: startRecording()");
+ synchronized (mReadyFence) {
+ if (mRunning) {
+ Timber.w("Encoder thread already running");
+ return;
+ }
+ mRunning = true;
+
+ new Thread(this, "TextureMovieEncoder").start();
+
+ while (!mReady) {
+ try {
+ mReadyFence.wait();
+ } catch (InterruptedException ie) {
+ // ignore
+ }
+ }
+ }
+
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_START_RECORDING, config));
+ }
+
+ /**
+ * Tells the video recorder to stop recording. (Call from non-encoder thread.)
+ *
+ * Returns immediately; the encoder/muxer may not yet be finished creating the movie.
+ *
+ * so we can provide reasonable status UI (and let the caller know that movie encoding
+ * has completed).
+ */
+ public void stopRecording() {
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_STOP_RECORDING));
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_QUIT));
+ // We don't know when these will actually finish (or even start). We don't want to
+ // delay the UI thread though, so we return immediately.
+ }
+
+ /**
+ * Returns true if recording has been started.
+ */
+ public boolean isRecording() {
+ synchronized (mReadyFence) {
+ return mRunning;
+ }
+ }
+
+ /**
+ * Tells the video recorder to refresh its EGL surface. (Call from non-encoder thread.)
+ */
+ public void updateSharedContext(EGLContext sharedContext) {
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_SHARED_CONTEXT, sharedContext));
+ }
+
+ /**
+ * Tells the video recorder that a new frame is available. (Call from non-encoder thread.)
+ *
+ * This function sends a message and returns immediately. This isn't sufficient -- we
+ * don't want the caller to latch a new frame until we're done with this one -- but we
+ * can get away with it so long as the input frame rate is reasonable and the encoder
+ * thread doesn't stall.
+ *
+ * or have a separate "block if still busy" method that the caller can execute immediately
+ * before it calls updateTexImage(). The latter is preferred because we don't want to
+ * stall the caller while this thread does work.
+ */
+ public void frameAvailable(SurfaceTexture st) {
+ synchronized (mReadyFence) {
+ if (!mReady) {
+ return;
+ }
+ }
+
+ st.getTransformMatrix(STMatrix);
+ long timestamp = st.getTimestamp();
+ if (timestamp == 0) {
+ // Seeing this after device is toggled off/on with power button. The
+ // first frame back has a zero timestamp.
+ //
+ // MPEG4Writer thinks this is cause to abort() in native code, so it's very
+ // important that we just ignore the frame.
+ Timber.w("HEY: got SurfaceTexture with timestamp of zero");
+ return;
+ }
+
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_FRAME_AVAILABLE,
+ (int) (timestamp >> 32), (int) timestamp, STMatrix));
+ }
+
+ /**
+ * Tells the video recorder what texture name to use. This is the external texture that
+ * we're receiving camera previews in. (Call from non-encoder thread.)
+ *
+ */
+ public void setTextureId(int id) {
+ synchronized (mReadyFence) {
+ if (!mReady) {
+ return;
+ }
+ }
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_SET_TEXTURE_ID, id, 0, null));
+ }
+
+ /**
+ * Encoder thread entry point. Establishes Looper/Handler and waits for messages.
+ *
+ *
+ * @see java.lang.Thread#run()
+ */
+ @Override
+ public void run() {
+ // Establish a Looper for this thread, and define a Handler for it.
+ Looper.prepare();
+ synchronized (mReadyFence) {
+ mHandler = new EncoderHandler(this);
+ mReady = true;
+ mReadyFence.notify();
+ }
+ Looper.loop();
+
+ Timber.d("Encoder thread exiting");
+ synchronized (mReadyFence) {
+ mReady = mRunning = false;
+ mHandler = null;
+ }
+ }
+
+
+ /**
+ * Handles encoder state change requests. The handler is created on the encoder thread.
+ */
+ private static class EncoderHandler extends Handler {
+ private final WeakReference
+ * The texture is rendered onto the encoder's input surface, along with a moving
+ * box (just because we can).
+ *
+ *
+ * @param transform The texture transform, from SurfaceTexture.
+ * @param timestampNanos The frame's timestamp, from SurfaceTexture.
+ */
+ private void handleFrameAvailable(float[] transform, long timestampNanos) {
+ if (VERBOSE) Timber.d("handleFrameAvailable tr=%f", transform[0]);
+ mVideoEncoder.drainEncoder(false);
+ mFullScreen.drawFrame(mTextureId, transform);
+
+ mInputWindowSurface.setPresentationTime(timestampNanos);
+ mInputWindowSurface.swapBuffers();
+
+ if (mLastFrameTimeNs != null) {
+ long gapNs = timestampNanos - mLastFrameTimeNs;
+ mFrameRate = mFrameRate * 0.3f + (float) (1000000000.0 / gapNs * 0.7);
+ }
+ mLastFrameTimeNs = timestampNanos;
+ }
+
+ /**
+ * Handles a request to stop encoding.
+ */
+ private void handleStopRecording() {
+ Timber.d("handleStopRecording");
+ mVideoEncoder.drainEncoder(true);
+ releaseEncoder();
+ }
+
+ /**
+ * Sets the texture name that SurfaceTexture will use when frames are received.
+ */
+ private void handleSetTexture(int id) {
+ mTextureId = id;
+ }
+
+ /**
+ * Tears down the EGL surface and context we've been using to feed the MediaCodec input
+ * surface, and replaces it with a new one that shares with the new context.
+ *
+ * This is useful if the old context we were sharing with went away (maybe a GLSurfaceView
+ * that got torn down) and we need to hook up with the new one.
+ */
+ private void handleUpdateSharedContext(EGLContext newSharedContext) {
+ Timber.d("handleUpdatedSharedContext %s", newSharedContext.toString());
+
+ // Release the EGLSurface and EGLContext.
+ mInputWindowSurface.releaseEglSurface();
+ mFullScreen.release(false);
+ mEglCore.release();
+
+ // Create a new EGLContext and recreate the window surface.
+ mEglCore = new EglCore(newSharedContext, EglCore.FLAG_RECORDABLE);
+ mInputWindowSurface.recreate(mEglCore);
+ mInputWindowSurface.makeCurrent();
+
+ // Create new programs and such for the new context.
+ mFullScreen = new FullFrameRect(
+ new Texture2dProgram(Texture2dProgram.ProgramType.TEXTURE_EXT));
+ }
+
+ private void prepareEncoder(EGLContext sharedContext, int width, int height, int bitRate,
+ String outputFile, Texture2dProgram program, String metaFile) {
+ try {
+ mVideoEncoder = new VideoEncoderCore(
+ width, height, bitRate, outputFile, metaFile);
+ } catch (IOException ioe) {
+ throw new RuntimeException(ioe);
+ }
+ mEglCore = new EglCore(sharedContext, EglCore.FLAG_RECORDABLE);
+ mInputWindowSurface = new WindowSurface(mEglCore, mVideoEncoder.getInputSurface(), true);
+ mInputWindowSurface.makeCurrent();
+
+ if (program.getProgramType() == Texture2dProgram.ProgramType.TEXTURE_EXT_FILT_VIEW) {
+ Texture2dProgram newProgram = new Texture2dProgram(Texture2dProgram.ProgramType.TEXTURE_EXT_FILT);
+ newProgram.setKernel(program.getKernel(), program.getColorAdjust());
+ mFullScreen = new FullFrameRect(newProgram);
+ } else {
+ mFullScreen = new FullFrameRect(program);
+ }
+ }
+
+ private void releaseEncoder() {
+ mVideoEncoder.release();
+ if (mInputWindowSurface != null) {
+ mInputWindowSurface.release();
+ mInputWindowSurface = null;
+ }
+ if (mFullScreen != null) {
+ mFullScreen.release(false);
+ mFullScreen = null;
+ }
+ if (mEglCore != null) {
+ mEglCore.release();
+ mEglCore = null;
+ }
+ }
+}
diff --git a/app/src/main/java/io/a3dv/VIRec/TimeBaseManager.java b/app/src/main/java/io/a3dv/VIRec/TimeBaseManager.java
new file mode 100644
index 0000000..e74ba1c
--- /dev/null
+++ b/app/src/main/java/io/a3dv/VIRec/TimeBaseManager.java
@@ -0,0 +1,84 @@
+package io.a3dv.VIRec;
+
+import android.hardware.camera2.CameraCharacteristics;
+import android.os.SystemClock;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+
+import timber.log.Timber;
+
+public class TimeBaseManager {
+ public String mTimeBaseHint;
+
+ private BufferedWriter mDataWriter = null;
+
+ public TimeBaseManager() {
+ }
+
+ public void startRecording(String captureResultFile, Integer timeSourceValue) {
+ mDataWriter = FileHelper.createBufferedWriter(captureResultFile);
+ long sysElapsedNs = SystemClock.elapsedRealtimeNanos();
+ long sysNs = System.nanoTime();
+ long diff = sysElapsedNs - sysNs;
+ setCameraTimestampSource(timeSourceValue);
+ try {
+ mDataWriter.write(mTimeBaseHint + "\n");
+ mDataWriter.write("#IMU data clock\tSENSOR_INFO_TIMESTAMP_SOURCE_UNKNOWN camera clock\tDifference\n");
+ mDataWriter.write("#elapsedRealtimeNanos()\tnanoTime()\tDifference\n");
+ mDataWriter.write(sysElapsedNs + "\t" + sysNs + "\t" + diff + "\n");
+ } catch (IOException ioe) {
+ Timber.e(ioe);
+ }
+ }
+
+ public void stopRecording() {
+ long sysElapsedNs = SystemClock.elapsedRealtimeNanos();
+ long sysNs = System.nanoTime();
+ long diff = sysElapsedNs - sysNs;
+ try {
+ mDataWriter.write(sysElapsedNs + "\t" + sysNs + "\t" + diff + "\n");
+ } catch (IOException ioe) {
+ Timber.e(ioe);
+ }
+ FileHelper.closeBufferedWriter(mDataWriter);
+ mDataWriter = null;
+ }
+
+ private void createHeader(String timestampSource) {
+ mTimeBaseHint = "#Camera frame timestamp source according to CameraCharacteristics.SENSOR_INFO_" +
+ "TIMESTAMP_SOURCE is " + timestampSource + ".\n#" +
+ "If SENSOR_INFO_TIMESTAMP_SOURCE is SENSOR_INFO_TIMESTAMP_SOURCE_REALTIME, then " +
+ "camera frame timestamps of the attribute CaptureResult.SENSOR_TIMESTAMP\n#" +
+ "and IMU reading timestamps of the field SensorEvent.timestamp " +
+ "are on the same timebase CLOCK_BOOTTIME which is " +
+ "used by elapsedRealtimeNanos().\n#" +
+ "In this case, no offline sync is necessary.\n#" +
+ "Otherwise, the camera frame timestamp is " +
+ "assumed to be on the timebase of CLOCK_MONOTONIC" +
+ " which is generally used by nanoTime().\n#" +
+ "In this case, offline sync is usually necessary unless the difference " +
+ "is really small, e.g., <1000 nanoseconds.\n#" +
+ "To help sync camera frames to " +
+ "the IMU offline, the timestamps" +
+ " according to the two time basis at the start and end" +
+ " of a recording session are recorded.";
+ }
+
+ private void setCameraTimestampSource(Integer timestampSource) {
+ String warn_msg = "The camera timestamp source is unreliable to synchronize with motion sensors";
+ String src_type = "SENSOR_INFO_TIMESTAMP_SOURCE_UNKNOWN";
+ if (timestampSource != null) {
+ if (timestampSource == CameraCharacteristics.SENSOR_INFO_TIMESTAMP_SOURCE_UNKNOWN) {
+ src_type = "SENSOR_INFO_TIMESTAMP_SOURCE_UNKNOWN";
+ Timber.d("%s:%s", warn_msg, src_type);
+ } else if (timestampSource == CameraCharacteristics.SENSOR_INFO_TIMESTAMP_SOURCE_REALTIME) {
+ src_type = "SENSOR_INFO_TIMESTAMP_SOURCE_REALTIME";
+ } else {
+ src_type = "SENSOR_INFO_TIMESTAMP_SOURCE_UNKNOWN (" + timestampSource + ")";
+ Timber.d("%s:%s", warn_msg, src_type);
+ }
+ }
+ createHeader(src_type);
+ }
+}
diff --git a/app/src/main/java/io/a3dv/VIRec/VideoEncoderCore.java b/app/src/main/java/io/a3dv/VIRec/VideoEncoderCore.java
new file mode 100644
index 0000000..fc549eb
--- /dev/null
+++ b/app/src/main/java/io/a3dv/VIRec/VideoEncoderCore.java
@@ -0,0 +1,243 @@
+package io.a3dv.VIRec;
+
+import android.media.MediaCodec;
+import android.media.MediaCodecInfo;
+import android.media.MediaFormat;
+import android.media.MediaMuxer;
+import android.view.Surface;
+
+import androidx.annotation.NonNull;
+
+import java.io.BufferedWriter;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+
+import timber.log.Timber;
+
+/**
+ * This class wraps up the core components used for surface-input video encoding.
+ *
+ * Once created, frames are fed to the input surface. Remember to provide the presentation
+ * time stamp, and always call drainEncoder() before swapBuffers() to ensure that the
+ * producer side doesn't get backed up.
+ *
+ * This class is not thread-safe, with one exception: it is valid to use the input surface
+ * on one thread, and drain the output on a different thread.
+ */
+public class VideoEncoderCore {
+ private static final boolean VERBOSE = false;
+
+ private static final String MIME_TYPE = "video/avc"; // H.264 Advanced Video Coding
+ public static final int FRAME_RATE = 30; // 30fps
+ private static final int IFRAME_INTERVAL = 1; // seconds between I-frames
+
+ private final Surface mInputSurface;
+ private MediaMuxer mMuxer;
+ private MediaCodec mEncoder;
+ private boolean mEncoderInExecutingState;
+ private final MediaCodec.BufferInfo mBufferInfo;
+ private int mTrackIndex;
+ private boolean mMuxerStarted;
+ private BufferedWriter mFrameMetadataWriter = null;
+
+ static class TimePair {
+ public Long sensorTimeMicros;
+ public long unixTimeMillis;
+
+ public TimePair(Long sensorTime, long unixTime) {
+ sensorTimeMicros = sensorTime;
+ unixTimeMillis = unixTime;
+ }
+
+ @NonNull
+ public String toString() {
+ String delimiter = ",";
+ return sensorTimeMicros + "000" + delimiter + unixTimeMillis + "000000";
+ }
+ }
+
+ private final ArrayList
+ * If endOfStream is not set, this returns when there is no more data to drain. If it
+ * is set, we send EOS to the encoder, and then iterate until we see EOS on the output.
+ * Calling this with endOfStream set should be done once, right before stopping the muxer.
+ *
+ * We're just using the muxer to get a .mp4 file (instead of a raw H.264 stream). We're
+ * not recording audio.
+ */
+ public void drainEncoder(boolean endOfStream) {
+ if (VERBOSE) Timber.d("drainEncoder(%b)", endOfStream);
+
+ if (endOfStream) {
+ if (VERBOSE) Timber.d("sending EOS to encoder");
+ mEncoder.signalEndOfInputStream();
+ }
+
+ while (mEncoderInExecutingState) {
+ int encoderStatus = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
+ if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
+ // no output available yet
+ if (!endOfStream) {
+ break; // out of while
+ } else {
+ if (VERBOSE) Timber.d("no output available, spinning to await EOS");
+ }
+ } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
+ // should happen before receiving buffers, and should only happen once
+ if (mMuxerStarted) {
+ throw new RuntimeException("format changed twice");
+ }
+ MediaFormat newFormat = mEncoder.getOutputFormat();
+ Timber.d("encoder output format changed: %s", newFormat.toString());
+
+ // now that we have the Magic Goodies, start the muxer
+ mTrackIndex = mMuxer.addTrack(newFormat);
+ mMuxer.start();
+ mMuxerStarted = true;
+ } else if (encoderStatus < 0) {
+ Timber.w("unexpected result from encoder.dequeueOutputBuffer: %d", encoderStatus);
+ // let's ignore it
+ } else {
+ ByteBuffer encodedData = mEncoder.getOutputBuffer(encoderStatus);
+// MediaFormat bufferFormat = mEncoder.getOutputFormat(encoderStatus);
+ // bufferFormat is identical to newFormat
+ if (encodedData == null) {
+ throw new RuntimeException("encoderOutputBuffer " + encoderStatus + " was null");
+ }
+
+ if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
+ // The codec config data was pulled out and fed to the muxer when we got
+ // the INFO_OUTPUT_FORMAT_CHANGED status. Ignore it.
+ if (VERBOSE) Timber.d("ignoring BUFFER_FLAG_CODEC_CONFIG");
+ mBufferInfo.size = 0;
+ }
+
+ if (mBufferInfo.size != 0) {
+ if (!mMuxerStarted) {
+ throw new RuntimeException("muxer hasn't started");
+ }
+
+ // adjust the ByteBuffer values to match BufferInfo (not needed?)
+ encodedData.position(mBufferInfo.offset);
+ encodedData.limit(mBufferInfo.offset + mBufferInfo.size);
+ mTimeArray.add(new TimePair(mBufferInfo.presentationTimeUs,
+ System.currentTimeMillis()));
+ mMuxer.writeSampleData(mTrackIndex, encodedData, mBufferInfo);
+ if (VERBOSE) {
+ Timber.d("sent %d bytes to muxer, ts=%d",
+ mBufferInfo.size, mBufferInfo.presentationTimeUs);
+ }
+ }
+
+ mEncoder.releaseOutputBuffer(encoderStatus, false);
+
+ if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
+ if (!endOfStream) {
+ Timber.w("reached end of stream unexpectedly");
+ } else {
+ if (VERBOSE) Timber.d("end of stream reached");
+ }
+ break; // out of while
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/io/a3dv/VIRec/gles/Drawable2d.java b/app/src/main/java/io/a3dv/VIRec/gles/Drawable2d.java
new file mode 100644
index 0000000..afdfd5b
--- /dev/null
+++ b/app/src/main/java/io/a3dv/VIRec/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 io.a3dv.VIRec.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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * To avoid allocations, this returns internal state. The caller must not modify it.
+ */
+ public FloatBuffer getVertexArray() {
+ return mVertexArray;
+ }
+
+ /**
+ * Returns the array of texture coordinates.
+ *
+ * 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/app/src/main/java/io/a3dv/VIRec/gles/EglCore.java b/app/src/main/java/io/a3dv/VIRec/gles/EglCore.java
new file mode 100644
index 0000000..6a99a75
--- /dev/null
+++ b/app/src/main/java/io/a3dv/VIRec/gles/EglCore.java
@@ -0,0 +1,373 @@
+/*
+ * 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 io.a3dv.VIRec.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.view.Surface;
+
+import timber.log.Timber;
+
+/**
+ * Core EGL state (display, context, config).
+ *
+ * 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 = GlUtil.TAG;
+
+ /**
+ * 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.
+ *
+ * Equivalent to EglCore(null, 0).
+ */
+ public EglCore() {
+ this(null, 0);
+ }
+
+ /**
+ * Prepares EGL display and context.
+ *
+ * @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) {
+ //Timber.d("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) {
+ //Timber.d("Got GLES 3 config");
+ mEGLConfig = config;
+ mEGLContext = context;
+ mGlVersion = 3;
+ }
+ }
+ }
+ if (mEGLContext == EGL14.EGL_NO_CONTEXT) { // GLES 2 only, or GLES 3 attempt failed
+ //Timber.d("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);
+ Timber.d("EGLContext created, client version %d", 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)) {
+ Timber.w("unable to find RGB8888 / %d EGLConfig", version);
+ 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.
+ *
+ * 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.
+ Timber.w("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.
+ *
+ * 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?
+ Timber.d("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?
+ Timber.d("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);
+ Timber.i("Current EGL (%s): display=%s, context=%s, surface=%s",
+ msg, display.toString(), context.toString(), surface.toString());
+ }
+
+ /**
+ * 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/app/src/main/java/io/a3dv/VIRec/gles/EglSurfaceBase.java b/app/src/main/java/io/a3dv/VIRec/gles/EglSurfaceBase.java
new file mode 100644
index 0000000..a4f2d13
--- /dev/null
+++ b/app/src/main/java/io/a3dv/VIRec/gles/EglSurfaceBase.java
@@ -0,0 +1,186 @@
+/*
+ * 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 io.a3dv.VIRec.gles;
+
+import android.graphics.Bitmap;
+import android.opengl.EGL14;
+import android.opengl.EGLSurface;
+import android.opengl.GLES20;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+import timber.log.Timber;
+
+/**
+ * Common base class for EGL surfaces.
+ *
+ * There can be multiple surfaces associated with a single context.
+ */
+public class 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.
+ *
+ * @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.
+ *
+ * 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);
+ }
+
+ /**
+ * Calls eglSwapBuffers. Use this to "publish" the current frame.
+ *
+ */
+ public void swapBuffers() {
+ boolean result = mEglCore.swapBuffers(mEGLSurface);
+ if (!result) {
+ Timber.d("WARNING: swapBuffers() failed");
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * 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();
+ }
+ Timber.d("Saved %dx%d frame as '%s'", width, height, filename);
+ }
+}
diff --git a/app/src/main/java/io/a3dv/VIRec/gles/FullFrameRect.java b/app/src/main/java/io/a3dv/VIRec/gles/FullFrameRect.java
new file mode 100644
index 0000000..4928d58
--- /dev/null
+++ b/app/src/main/java/io/a3dv/VIRec/gles/FullFrameRect.java
@@ -0,0 +1,89 @@
+/*
+ * 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 io.a3dv.VIRec.gles;
+
+/**
+ * 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.
+ *
+ * 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.
+ *
+ * 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/app/src/main/java/io/a3dv/VIRec/gles/GlUtil.java b/app/src/main/java/io/a3dv/VIRec/gles/GlUtil.java
new file mode 100644
index 0000000..9e29839
--- /dev/null
+++ b/app/src/main/java/io/a3dv/VIRec/gles/GlUtil.java
@@ -0,0 +1,144 @@
+/*
+ * 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 io.a3dv.VIRec.gles;
+
+import android.opengl.GLES20;
+import android.opengl.Matrix;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+
+import timber.log.Timber;
+
+/**
+ * Some OpenGL utility functions.
+ */
+public class GlUtil {
+ public static final String TAG = "Grafika";
+
+ /**
+ * 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) {
+ Timber.e("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) {
+ Timber.e("Could not link program: ");
+ Timber.e(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) {
+ Timber.e("Could not compile shader %d: %s",
+ shaderType, 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);
+ Timber.e(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.
+ *
+ * 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");
+ }
+ }
+
+ /**
+ * 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;
+ }
+
+}
diff --git a/app/src/main/java/io/a3dv/VIRec/gles/OffscreenSurface.java b/app/src/main/java/io/a3dv/VIRec/gles/OffscreenSurface.java
new file mode 100644
index 0000000..737fe86
--- /dev/null
+++ b/app/src/main/java/io/a3dv/VIRec/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 io.a3dv.VIRec.gles;
+
+/**
+ * Off-screen EGL surface (pbuffer).
+ *
+ * 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/app/src/main/java/io/a3dv/VIRec/gles/Texture2dProgram.java b/app/src/main/java/io/a3dv/VIRec/gles/Texture2dProgram.java
new file mode 100644
index 0000000..7999d44
--- /dev/null
+++ b/app/src/main/java/io/a3dv/VIRec/gles/Texture2dProgram.java
@@ -0,0 +1,374 @@
+/*
+ * 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 io.a3dv.VIRec.gles;
+
+import android.opengl.GLES11Ext;
+import android.opengl.GLES20;
+
+import java.nio.FloatBuffer;
+
+import timber.log.Timber;
+
+/**
+ * GL program and supporting functions for textured 2D shapes.
+ */
+public class Texture2dProgram {
+ public enum ProgramType {
+ TEXTURE_2D, TEXTURE_EXT, TEXTURE_EXT_BW, TEXTURE_EXT_FILT, TEXTURE_EXT_FILT_VIEW
+ }
+
+ // 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_VIEW =
+ "#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 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" +
+ " 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" +
+ " gl_FragColor = sum;\n" +
+ "}\n";
+
+ private final ProgramType mProgramType;
+
+ // Handles to the GL program and various components of it.
+ private int mProgramHandle;
+ private final int muMVPMatrixLoc;
+ private final int muTexMatrixLoc;
+ private int muKernelLoc;
+ private final int muTexOffsetLoc;
+ private final int muColorAdjustLoc;
+ private final int maPositionLoc;
+ private final int maTextureCoordLoc;
+
+ private final int mTextureTarget;
+
+ private final 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;
+ case TEXTURE_EXT_FILT_VIEW:
+ mTextureTarget = GLES11Ext.GL_TEXTURE_EXTERNAL_OES;
+ mProgramHandle = GlUtil.createProgram(VERTEX_SHADER, FRAGMENT_SHADER_EXT_FILT_VIEW);
+ break;
+ default:
+ throw new RuntimeException("Unhandled type " + programType);
+ }
+ if (mProgramHandle == 0) {
+ throw new RuntimeException("Unable to create program");
+ }
+ Timber.d("Created program %d (%s)", mProgramHandle, programType.toString());
+
+ // 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.
+ *
+ * The appropriate EGL context must be current (i.e. the one that was used to create
+ * the program).
+ */
+ public void release() {
+ Timber.d("deleting program %d", 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.
+ *
+ * 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;
+ // Timber.d("filt kernel: %s, adj=%f", Arrays.toString(mKernel), colorAdj);
+ }
+
+ public float[] getKernel() {
+ return mKernel;
+ }
+
+ public float getColorAdjust() {
+ return mColorAdjust;
+ }
+
+ /**
+ * 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
+ };
+ // Timber.d("filt size: %dx%d: %s", width, 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/app/src/main/java/io/a3dv/VIRec/gles/WindowSurface.java b/app/src/main/java/io/a3dv/VIRec/gles/WindowSurface.java
new file mode 100644
index 0000000..3d38a28
--- /dev/null
+++ b/app/src/main/java/io/a3dv/VIRec/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 io.a3dv.VIRec.gles;
+
+import android.graphics.SurfaceTexture;
+import android.view.Surface;
+
+/**
+ * Recordable EGL window surface.
+ *
+ * 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.
+ *
+ * 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).
+ *
+ * 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().
+ *
+ * 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.)
+ *
+ * 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/app/src/main/res/drawable/ic_baseline_fiber_manual_record_24.xml b/app/src/main/res/drawable/ic_baseline_fiber_manual_record_24.xml
new file mode 100644
index 0000000..83e0ffd
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_fiber_manual_record_24.xml
@@ -0,0 +1,10 @@
+
+ *
+ *