/* <!-- +++
package com.almalence.opencam_plus.ui;
+++ --> */
//<!-- -+-
package com.almalence.opencam.ui;
//-+- -->
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import android.annotation.SuppressLint;
import android.graphics.SurfaceTexture;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaFormat;
import android.media.MediaMuxer;
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.opengl.GLES11Ext;
import android.opengl.GLES20;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.view.Surface;
@SuppressLint("NewApi")
public class EglEncoder implements Runnable
{
private static final String TAG = "EglEncoder";
private static final boolean VERBOSE = false; // lots
// of
// logging
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_QUIT = 5;
private Object mReadyFence = new Object(); // guards
// ready/running
private boolean mReady;
private boolean mRunning;
static EGLContext mEglContext = null;
private int mTextureId;
private volatile EncoderHandler mHandler;
private static final String SHADER_VERTEX_WITH_ROTATION = "attribute vec2 vPosition;\n"
+ "attribute vec4 vTexCoordinate;\n"
+ "uniform mat4 textureTransform;\n"
+ "varying vec2 v_TexCoordinate;\n"
+ "void main() {\n"
+ " v_TexCoordinate = (textureTransform * vTexCoordinate).xy;\n"
+ " gl_Position = vec4 ( vPosition.x, vPosition.y, 1.0, 1.0 );\n"
+ "}";
private static final String SHADER_VERTEX = "attribute vec2 vPosition;\n"
+ "attribute vec2 vTexCoordinate;\n"
+ "varying vec2 v_TexCoordinate;\n"
+ "void main() {\n"
+ " v_TexCoordinate = vTexCoordinate;\n"
+ " gl_Position = vec4 ( vPosition.x, vPosition.y, 1.0, 1.0 );\n"
+ "}";
private static final String SHADER_FRAGMENT = "#extension GL_OES_EGL_image_external:enable\n"
+ "precision mediump float;\n"
+ "uniform samplerExternalOES sTexture;\n"
+ "varying vec2 v_TexCoordinate;\n"
+ "void main() {\n"
+ " gl_FragColor = texture2D(sTexture, v_TexCoordinate);\n"
+ "}";
private static final FloatBuffer VERTEX_BUFFER;
static
{
final float[] vtmp = { 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, -1.0f };
VERTEX_BUFFER = ByteBuffer.allocateDirect(8 * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
VERTEX_BUFFER.put(vtmp);
VERTEX_BUFFER.position(0);
}
private static int loadShader(final String vss, final String fss)
{
int vshader = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);
GLES20.glShaderSource(vshader, vss);
GLES20.glCompileShader(vshader);
final int[] compiled = new int[1];
GLES20.glGetShaderiv(vshader, GLES20.GL_COMPILE_STATUS, compiled, 0);
if (compiled[0] == 0)
{
Log.d(TAG, "Could not compile vertex shader: " + GLES20.glGetShaderInfoLog(vshader));
GLES20.glDeleteShader(vshader);
vshader = 0;
}
int fshader = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER);
GLES20.glShaderSource(fshader, fss);
GLES20.glCompileShader(fshader);
GLES20.glGetShaderiv(fshader, GLES20.GL_COMPILE_STATUS, compiled, 0);
if (compiled[0] == 0)
{
Log.d(TAG, "Could not compile fragment shader: " + GLES20.glGetShaderInfoLog(fshader));
GLES20.glDeleteShader(fshader);
fshader = 0;
}
final int program = GLES20.glCreateProgram();
GLES20.glAttachShader(program, vshader);
GLES20.glAttachShader(program, fshader);
GLES20.glLinkProgram(program);
return program;
}
// parameters for the encoder
private static final String MIME_TYPE = "video/avc"; // H.264
// Advanced
// Video
// Coding
private static final int IFRAME_INTERVAL = 10; // 10
// seconds
// between
// I-frames
private static final int EGL_RECORDABLE_ANDROID = 0x3142;
public static final int[] EGL_ATTRIB_LIST = { EGL14.EGL_RED_SIZE, 8, EGL14.EGL_GREEN_SIZE, 8,
EGL14.EGL_BLUE_SIZE, 8, EGL14.EGL_ALPHA_SIZE, 8, EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
EGL_RECORDABLE_ANDROID, 1, EGL14.EGL_NONE };
private final String outputPath;
private final int mWidth;
private final int mHeight;
private final int definedFPS;
private final int mBitRate;
// encoder / muxer state
private MediaCodec mEncoder;
private CodecInputSurface mInputSurface;
private MediaMuxer mMuxer;
private int mTrackIndex;
private boolean mMuxerStarted;
// allocate one of these up front so we don't need to do it every time
private MediaCodec.BufferInfo mBufferInfo;
private long timeLast = -1;
private long timeTotal = 0;
private boolean open = true;
private int hProgram;
private int hProgramWithRotation;
private final FloatBuffer UV_BUFFER;
private AudioRecorder audioRecorder;
private volatile boolean paused = true;
public EglEncoder(final String outputPath, final int width, final int height, final int fps, final int bitrate,
int orientation, EGLContext sharedContext)
{
this.outputPath = outputPath;
this.mBitRate = bitrate;
this.definedFPS = fps;
final float[] ttmp;
if (orientation == 0)
{
this.mWidth = width;
this.mHeight = height;
ttmp = new float[] { 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f };
} else if (orientation == 90)
{
this.mWidth = height;
this.mHeight = width;
ttmp = new float[] { 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f };
} else if (orientation == 180)
{
this.mWidth = width;
this.mHeight = height;
ttmp = new float[] { 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f };
} else if (orientation == 270)
{
this.mWidth = height;
this.mHeight = width;
ttmp = new float[] { 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f };
} else
{
throw new IllegalArgumentException("Orientation can only be 0, 90, 180 or 270");
}
this.UV_BUFFER = ByteBuffer.allocateDirect(8 * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
this.UV_BUFFER.put(ttmp);
this.UV_BUFFER.position(0);
mEglContext = sharedContext;
// Start encoder in it's own thread.
synchronized (mReadyFence)
{
if (mRunning)
{
Log.w(TAG, "Encoder thread already running");
return;
}
mRunning = true;
new Thread(this, "EglEncoder").start();
while (!mReady)
{
try
{
mReadyFence.wait();
} catch (InterruptedException ie)
{
// ignore
}
}
}
mHandler.sendMessage(mHandler.obtainMessage(MSG_START_RECORDING, null));
this.hProgram = loadShader(SHADER_VERTEX, SHADER_FRAGMENT);
this.hProgramWithRotation = loadShader(SHADER_VERTEX_WITH_ROTATION, SHADER_FRAGMENT);
}
public String getPath()
{
return this.outputPath;
}
@Override
public void finalize() throws Throwable
{
try
{
this.close();
} catch (final IllegalStateException e)
{
// Totally normal
}
super.finalize();
}
private boolean checkPaused()
{
if (this.paused)
{
if (this.mMuxerStarted)
{
this.paused = false;
this.audioRecorder.record(true);
}
return true;
} else
{
return false;
}
}
public void pause()
{
this.paused = true;
this.audioRecorder.record(false);
}
public void encode(final int texture)
{
final long time = System.nanoTime();
final long timeDiff;
if (this.checkPaused())
{
timeDiff = 0;
} else
{
timeDiff = time - this.timeLast;
}
if (this.timeLast >= 0)
{
this.timeTotal += timeDiff;
}
this.timeLast = time;
this.audioRecorder.updateTime(this.timeTotal);
this.drawEncode(texture, null);
}
public void encode(final int texture, final long nanoSec)
{
if (nanoSec < 0)
{
throw new IllegalArgumentException("Time shift can't be negative.");
}
this.checkPaused();
this.timeLast = System.nanoTime();
this.timeTotal += nanoSec;
this.audioRecorder.updateTime(this.timeTotal);
this.drawEncode(texture, null);
}
public void encode(float[] transform, long timestamp)
{
final long time = System.nanoTime();
final long timeDiff;
if (this.checkPaused())
{
timeDiff = 0;
} else
{
timeDiff = time - this.timeLast;
}
if (this.timeLast >= 0)
{
this.timeTotal += timeDiff;
}
this.timeLast = time;
this.audioRecorder.updateTime(this.timeTotal);
this.drawEncode(mTextureId, transform);
}
private void drawEncode(final int texture, float[] transform)
{
this.mInputSurface.makeCurrent();
this.drainEncoder(false);
this.drawTexture(texture, transform);
this.mInputSurface.setPresentationTime(this.timeTotal);
this.mInputSurface.swapBuffers();
this.mInputSurface.unmakeCurrent();
}
private void drawTexture(final int texture, float[] transform) {
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
GLES20.glViewport(0, 0, this.mWidth, this.mHeight);
if (transform == null) {
GLES20.glUseProgram(this.hProgram);
} else{
GLES20.glUseProgram(this.hProgramWithRotation);
}
final int ph;
final int th;
final int tch;
if (transform == null) {
GLES20.glUseProgram(this.hProgram);
ph = GLES20.glGetAttribLocation(this.hProgram, "vPosition");
th = GLES20.glGetUniformLocation(this.hProgram, "sTexture");
tch = GLES20.glGetAttribLocation(this.hProgram, "vTexCoordinate");
} else {
GLES20.glUseProgram(this.hProgramWithRotation);
ph = GLES20.glGetAttribLocation(this.hProgramWithRotation, "vPosition");
th = GLES20.glGetUniformLocation(this.hProgramWithRotation, "sTexture");
tch = GLES20.glGetAttribLocation(this.hProgramWithRotation, "vTexCoordinate");
}
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texture);
GLES20.glUniform1i(th, 0);
if (transform != null) {
int tth = GLES20.glGetUniformLocation(this.hProgramWithRotation, "textureTransform");
GLES20.glUniformMatrix4fv(tth, 1, false, transform, 0);
}
GLES20.glVertexAttribPointer(ph, 2, GLES20.GL_FLOAT, false, 4 * 2,
VERTEX_BUFFER);
GLES20.glVertexAttribPointer(tch, 2, GLES20.GL_FLOAT, false, 4 * 2,
this.UV_BUFFER);
GLES20.glEnableVertexAttribArray(ph);
GLES20.glEnableVertexAttribArray(tch);
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
// GLES20.glFlush();
// GLES20.glFinish();
GLES20.glDisableVertexAttribArray(ph);
GLES20.glDisableVertexAttribArray(tch);
}
public void close()
{
if (this.open)
{
this.open = false;
this.drainEncoder(true);
this.releaseEncoder();
} else
{
throw new IllegalStateException("Already closed.");
}
}
/**
* Returns the first codec capable of encoding the specified MIME type, or
* null if no match was found.
*/
private static MediaCodecInfo selectCodec(String mimeType)
{
int numCodecs = MediaCodecList.getCodecCount();
for (int i = 0; i < numCodecs; i++)
{
MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
if (!codecInfo.isEncoder())
{
continue;
}
String[] types = codecInfo.getSupportedTypes();
for (int j = 0; j < types.length; j++)
{
if (types[j].equalsIgnoreCase(mimeType))
{
return codecInfo;
}
}
}
return null;
}
/**
* Returns a color format that is supported by the codec and by this test
* code. If no match is found, this throws a test failure -- the set of
* formats known to the test should be expanded for new platforms.
*/
public static int selectColorFormat(MediaCodecInfo codecInfo, String mimeType)
{
MediaCodecInfo.CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(mimeType);
for (int i = 0; i < capabilities.colorFormats.length; i++)
{
int colorFormat = capabilities.colorFormats[i];
if (isRecognizedFormat(colorFormat))
{
return colorFormat;
}
}
Log.d(TAG, "couldn't find a good color format for " + codecInfo.getName() + " / " + mimeType);
return 0; // not reached
}
/**
* Returns true if this is a color format that this test code understands
* (i.e. we know how to read and generate frames in this format).
*/
private static boolean isRecognizedFormat(int colorFormat)
{
switch (colorFormat)
{
// these are the formats we know how to handle for this test
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar:
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar:
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar:
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar:
case MediaCodecInfo.CodecCapabilities.COLOR_TI_FormatYUV420PackedSemiPlanar:
return true;
default:
return false;
}
}
/**
* Configures encoder and muxer state, and prepares the input Surface.
*/
private void prepareEncoder()
{
this.mBufferInfo = new MediaCodec.BufferInfo();
final MediaCodecInfo codecInfo = selectCodec(MIME_TYPE);
if (codecInfo == null)
{
// Don't fail CTS if they don't have an AVC codec (not here,
// anyway).
Log.d(TAG, "Unable to find an appropriate codec for " + MIME_TYPE);
throw new RuntimeException("No codec found.");
}
final MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, this.mWidth, this.mHeight);
// Set some properties. Failing to specify some of these can cause the
// MediaCodec
// configure() call to throw an unhelpful exception.
// Video
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_BIT_RATE, this.mBitRate);
format.setInteger(MediaFormat.KEY_FRAME_RATE, this.definedFPS);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);
format.setInteger(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, 0);
format.setInteger(MediaFormat.KEY_PUSH_BLANK_BUFFERS_ON_STOP, 0);
// Audio
format.setInteger(MediaFormat.KEY_SAMPLE_RATE, 64000);
format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
if (VERBOSE)
Log.d(TAG, "format: " + format);
// Create a MediaCodec encoder, and configure it with our format. Get a
// Surface
// we can use for input and wrap it with a class that handles the EGL
// work.
//
// If you want to have two EGL contexts -- one for display, one for
// recording --
// you will likely want to defer instantiation of CodecInputSurface
// until after the
// "display" EGL context is created, then modify the eglCreateContext
// call to
// take eglGetCurrentContext() as the share_context argument.
try
{
this.mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);
} catch (IOException e1)
{
// TODO Auto-generated catch block
e1.printStackTrace();
}
this.mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
this.mInputSurface = new CodecInputSurface(this.mEncoder.createInputSurface());
this.mEncoder.start();
// Create a MediaMuxer. We can't add the video track and start() the
// muxer here,
// because our MediaFormat doesn't have the Magic Goodies. These can
// only be
// obtained from the encoder after it has started processing data.
//
// We're not actually interested in multiplexing audio. We just want to
// convert
// the raw H.264 elementary stream we get from MediaCodec into a .mp4
// file.
try
{
this.mMuxer = new MediaMuxer(this.outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
this.audioRecorder = new AudioRecorder(this.mMuxer);
this.audioRecorder.start();
} catch (final IOException e)
{
e.printStackTrace();
throw new RuntimeException("MediaMuxer creation failed");
}
this.mTrackIndex = -1;
this.mMuxerStarted = false;
}
/**
* Releases encoder resources. May be called after partial / failed
* initialization.
*/
private void releaseEncoder()
{
if (VERBOSE)
Log.d(TAG, "releasing encoder objects");
if (this.audioRecorder != null)
{
this.audioRecorder.stop();
this.audioRecorder = null;
}
if (this.mEncoder != null)
{
this.mEncoder.stop();
this.mEncoder.release();
this.mEncoder = null;
}
if (this.mInputSurface != null)
{
this.mInputSurface.release();
this.mInputSurface = null;
}
if (this.mMuxer != null)
{
this.mMuxer.stop();
this.mMuxer.release();
this.mMuxer = null;
}
}
/**
* Extracts all pending data from the encoder.
* <p>
* 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.
*/
private void drainEncoder(boolean endOfStream)
{
final int TIMEOUT_USEC = 10000;
if (VERBOSE)
Log.d(TAG, "drainEncoder(" + endOfStream + ")");
if (endOfStream)
{
if (VERBOSE)
Log.d(TAG, "sending EOS to encoder");
this.mEncoder.signalEndOfInputStream();
}
ByteBuffer[] encoderOutputBuffers = mEncoder.getOutputBuffers();
while (true)
{
final int encoderStatus = this.mEncoder.dequeueOutputBuffer(this.mBufferInfo, TIMEOUT_USEC);
if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER)
{
// no output available yet
if (!endOfStream)
{
break; // out of while
} else
{
if (VERBOSE)
Log.d(TAG, "no output available, spinning to await EOS");
}
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED)
{
// not expected for an encoder
encoderOutputBuffers = this.mEncoder.getOutputBuffers();
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED)
{
// should happen before receiving buffers, and should only
// happen once
if (this.mMuxerStarted)
{
throw new RuntimeException("format changed twice");
}
final MediaFormat newFormat = this.mEncoder.getOutputFormat();
Log.d(TAG, "encoder output format changed: " + newFormat);
// now that we have the Magic Goodies, start the muxer
this.mTrackIndex = this.mMuxer.addTrack(newFormat);
this.mMuxer.start();
this.mMuxerStarted = true;
} else if (encoderStatus < 0)
{
Log.w(TAG, "unexpected result from encoder.dequeueOutputBuffer: " + encoderStatus);
// let's ignore it
} else
{
final ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
if (encodedData == null)
{
throw new RuntimeException("encoderOutputBuffer " + encoderStatus + " was null");
}
if ((this.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)
Log.d(TAG, "ignoring BUFFER_FLAG_CODEC_CONFIG");
this.mBufferInfo.size = 0;
}
if (this.mBufferInfo.size != 0)
{
if (!this.mMuxerStarted)
{
throw new RuntimeException("muxer hasn't started");
}
// adjust the ByteBuffer values to match BufferInfo (not
// needed?)
encodedData.position(this.mBufferInfo.offset);
encodedData.limit(this.mBufferInfo.offset + this.mBufferInfo.size);
this.mMuxer.writeSampleData(this.mTrackIndex, encodedData, this.mBufferInfo);
if (VERBOSE)
Log.d(TAG, "sent " + this.mBufferInfo.size + " bytes to muxer");
}
this.mEncoder.releaseOutputBuffer(encoderStatus, false);
if ((this.mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0)
{
if (!endOfStream)
{
Log.w(TAG, "reached end of stream unexpectedly");
} else
{
if (VERBOSE)
Log.d(TAG, "end of stream reached");
}
break; // out of while
}
}
}
}
/**
* Holds state associated with a Surface used for MediaCodec encoder input.
* <p>
* The constructor takes a Surface obtained from
* MediaCodec.createInputSurface(), and uses that to create an EGL window
* surface. Calls to eglSwapBuffers() cause a frame of data to be sent to
* the video encoder.
* <p>
* This object owns the Surface -- releasing this will release the Surface
* too.
*/
private static class CodecInputSurface
{
private EGLDisplay eglDisplay = EGL14.EGL_NO_DISPLAY;
private EGLContext eglContext = EGL14.EGL_NO_CONTEXT;
private EGLSurface eglSurfaceDraw = EGL14.EGL_NO_SURFACE;
private EGLSurface eglSurfaceRead = EGL14.EGL_NO_SURFACE;
private EGLSurface eglSurfaceSwap = EGL14.EGL_NO_SURFACE;
private Surface mSurface;
/**
* Creates a CodecInputSurface from a Surface.
*/
public CodecInputSurface(final Surface surface)
{
if (surface == null)
{
throw new NullPointerException();
}
this.mSurface = surface;
this.eglSetup();
}
public static final int FLAG_RECORDABLE = 0x01;
private EGLConfig getConfig(int flags, int version, EGLDisplay mEGLDisplay)
{
EGLConfig[] configs = new EGLConfig[1];
int[] numConfigs = new int[1];
if (!EGL14.eglChooseConfig(mEGLDisplay, EGL_ATTRIB_LIST, 0, configs, 0, configs.length, numConfigs, 0))
{
Log.w(TAG, "unable to find RGB8888 / " + version + " EGLConfig");
return null;
}
return configs[0];
}
/**
* Prepares EGL. We want a GLES 2.0 context and a surface that supports
* recording.
*/
private void eglSetup()
{
this.eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
if (this.eglDisplay == EGL14.EGL_NO_DISPLAY)
{
throw new RuntimeException("unable to get EGL14 display");
}
// this.eglContext = EGL14.eglGetCurrentContext();
int[] attrib2_list = { EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE };
EGLConfig config = getConfig(FLAG_RECORDABLE, 2, eglDisplay);
this.eglContext = EGL14.eglCreateContext(eglDisplay, config, mEglContext, attrib2_list, 0);
if (this.eglContext == EGL14.EGL_NO_CONTEXT)
{
throw new RuntimeException("unable to get EGL14 context");
}
this.eglSurfaceDraw = EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW);
this.eglSurfaceRead = EGL14.eglGetCurrentSurface(EGL14.EGL_READ);
EGLConfig[] configs = new EGLConfig[1];
int[] numConfigs = new int[1];
EGL14.eglChooseConfig(this.eglDisplay, EGL_ATTRIB_LIST, 0, configs, 0, configs.length, numConfigs, 0);
checkEglError("eglCreateContext RGB888+recordable ES2");
// Create a window surface, and attach it to the Surface we
// received.
final int[] surfaceAttribs = { EGL14.EGL_NONE };
this.eglSurfaceSwap = EGL14.eglCreateWindowSurface(this.eglDisplay, configs[0], this.mSurface,
surfaceAttribs, 0);
checkEglError("eglCreateWindowSurface");
}
/**
* Discards all resources held by this class, notably the EGL context.
* Also releases the Surface that was passed to our constructor.
*/
public void release()
{
if (this.eglDisplay != EGL14.EGL_NO_DISPLAY)
{
EGL14.eglDestroySurface(this.eglDisplay, this.eglSurfaceSwap);
}
this.mSurface.release();
this.mSurface = null;
this.eglDisplay = EGL14.EGL_NO_DISPLAY;
this.eglContext = EGL14.EGL_NO_CONTEXT;
this.eglSurfaceRead = EGL14.EGL_NO_SURFACE;
this.eglSurfaceDraw = EGL14.EGL_NO_SURFACE;
this.eglSurfaceSwap = EGL14.EGL_NO_SURFACE;
}
/**
* Makes our EGL context and surface current.
*/
public void makeCurrent()
{
EGL14.eglMakeCurrent(this.eglDisplay, this.eglSurfaceSwap, this.eglSurfaceSwap, this.eglContext);
checkEglError("eglMakeCurrent");
}
public void unmakeCurrent()
{
EGL14.eglMakeCurrent(this.eglDisplay, this.eglSurfaceDraw, this.eglSurfaceRead, this.eglContext);
}
/**
* Calls eglSwapBuffers. Use this to "publish" the current frame.
*/
public boolean swapBuffers()
{
final boolean result = EGL14.eglSwapBuffers(this.eglDisplay, this.eglSurfaceSwap);
checkEglError("eglSwapBuffers");
return result;
}
/**
* Sends the presentation time stamp to EGL. Time is expressed in
* nanoseconds.
*/
public void setPresentationTime(final long nsecs)
{
EGLExt.eglPresentationTimeANDROID(this.eglDisplay, this.eglSurfaceSwap, nsecs);
checkEglError("eglPresentationTimeANDROID");
}
/**
* Checks for EGL errors. Throws an exception if one is found.
*/
private static void checkEglError(String msg)
{
int error;
if ((error = EGL14.eglGetError()) != EGL14.EGL_SUCCESS)
{
throw new RuntimeException(msg + ": EGL error: 0x" + Integer.toHexString(error));
}
}
}
// EglEncoder working in it's own thread.
@Override
public void run()
{
Looper.prepare();
synchronized (mReadyFence)
{
mHandler = new EncoderHandler(this);
mReady = true;
mReadyFence.notify();
}
Looper.loop();
Log.d(TAG, "Encoder thread exiting");
synchronized (mReadyFence)
{
mReady = mRunning = false;
mHandler = null;
}
}
float[] transform = new float[16];
/**
* Tells the video recorder new frame is available. (Call from non-encoder
* thread)
*/
public void frameAvailable(SurfaceTexture st, boolean filtering)
{
synchronized (mReadyFence)
{
if (!mReady)
{
return;
}
}
st.getTransformMatrix(transform);
long timestamp = st.getTimestamp();
if (timestamp == 0)
{
Log.w(TAG, "HEY: got SurfaceTexture with timestamp of zero");
return;
}
mHandler.sendMessage(mHandler.obtainMessage(MSG_FRAME_AVAILABLE, (int) (timestamp >> 32), (int) timestamp,
filtering ? null : transform.clone()));
}
/**
* Sets the texture name that SurfaceTexture will use when frames are
* received.
*/
private void handleSetTexture(int id)
{
// Log.d(TAG, "handleSetTexture " + id);
mTextureId = id;
}
/**
* 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));
}
/**
* Tells the video recorder to stop recording. (Call from non-encoder
* thread)
*/
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.
}
/**
* Handles encoder state change requests. The handler is created on the
* encoder thread.
*/
private static class EncoderHandler extends Handler
{
private WeakReference<EglEncoder> mWeakEncoder;
public EncoderHandler(EglEncoder encoder)
{
mWeakEncoder = new WeakReference<EglEncoder>(encoder);
}
@Override
// runs on encoder thread
public void handleMessage(Message inputMessage)
{
int what = inputMessage.what;
Object obj = inputMessage.obj;
EglEncoder encoder = mWeakEncoder.get();
if (encoder == null)
{
Log.w(TAG, "EncoderHandler.handleMessage: encoder is null");
return;
}
switch (what)
{
case MSG_START_RECORDING:
encoder.prepareEncoder();
break;
case MSG_STOP_RECORDING:
encoder.close();
break;
case MSG_FRAME_AVAILABLE:
long timestamp = (((long) inputMessage.arg1) << 32) | (((long) inputMessage.arg2) & 0xffffffffL);
encoder.encode((float[]) obj, timestamp);
break;
case MSG_SET_TEXTURE_ID:
encoder.handleSetTexture(inputMessage.arg1);
break;
case MSG_QUIT:
Looper.myLooper().quit();
break;
default:
throw new RuntimeException("Unhandled msg what=" + what);
}
}
}
}