/* <!-- +++ 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); } } } }