// started from https://github.com/google/grafika/blob/f3c8c3dee60153f471312e21acac8b3a3cddd7dc/src/com/android/grafika/TextureMovieEncoder.java /* * 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.cine.android.streaming; import android.graphics.SurfaceTexture; import android.opengl.EGLContext; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.util.Log; import java.io.File; import java.io.IOException; import java.lang.ref.WeakReference; import io.cine.android.streaming.gles.EglCore; import io.cine.android.streaming.gles.FullFrameRect; import io.cine.android.streaming.gles.Texture2dProgram; import io.cine.android.streaming.gles.WindowSurface; /** * Encode a movie from frames rendered from an external texture image. * <p/> * 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. * <p/> * 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. * <p/> * To use: * <ul> * <li>create TextureMovieEncoder object * <li>create an EncoderConfig * <li>call TextureMovieEncoder#startRecording() with the config * <li>call TextureMovieEncoder#setTextureId() with the texture object that receives frames * <li>for each frame, after latching it with SurfaceTexture#updateTexImage(), * call TextureMovieEncoder#frameAvailable(). * </ul> * <p/> * TODO: tweak the API (esp. textureId) so it's less awkward for simple use cases. */ public class TextureMovieEncoder implements Runnable { private static final String TAG = "TextureMovieEncoder"; 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; public static final int MSG_ENCODER_SAVEFRAME = 6; // ----- 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 Object mReadyFence = new Object(); // guards ready/running private boolean mReady; private boolean mRunning; /** * Tells the video recorder to start recording. (Call from non-encoder thread.) * <p/> * Creates a new thread, which will create an encoder using the provided configuration. * <p/> * 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) { Log.d(TAG, "Encoder: startRecording()"); synchronized (mReadyFence) { if (mRunning) { Log.w(TAG, "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.) * <p/> * Returns immediately; the encoder/muxer may not yet be finished creating the movie. * <p/> * TODO: have the encoder thread invoke a callback on the UI thread just before it shuts down * 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.) * <p/> * 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. * <p/> * TODO: either block here until the texture has been rendered onto the encoder surface, * 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; } } float[] transform = new float[16]; // TODO - avoid alloc every frame st.getTransformMatrix(transform); 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. Log.w(TAG, "HEY: got SurfaceTexture with timestamp of zero"); return; } mHandler.sendMessage(mHandler.obtainMessage(MSG_FRAME_AVAILABLE, (int) (timestamp >> 32), (int) timestamp, transform)); } /** * 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.) * <p/> * TODO: do something less clumsy */ 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. * <p/> * * @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(); Log.d(TAG, "Encoder thread exiting"); synchronized (mReadyFence) { mReady = mRunning = false; mHandler = null; } } /** * Starts recording. */ private void handleStartRecording(EncoderConfig config) { Log.d(TAG, "handleStartRecording " + config); mFrameNum = 0; prepareEncoder(config.mEglContext, config.mMuxer); } /** * This checks for the existence of an inputsurface * and if it exists, simply calls the inputWindowSurface saveframe method. * @param screenShot */ public void saveFrame(ScreenShot screenShot){ if (mInputWindowSurface!= null) { try { mInputWindowSurface.saveFrame(screenShot); } catch (IOException e) { e.printStackTrace(); } } } /** * Handles notification of an available frame. * <p/> * The texture is rendered onto the encoder's input surface, along with a moving * box (just because we can). * <p/> * * @param transform The texture transform, from SurfaceTexture. * @param timestampNanos The frame's timestamp, from SurfaceTexture. */ public void handleFrameAvailable(float[] transform, long timestampNanos) { if (VERBOSE) Log.d(TAG, "handleFrameAvailable tr=" + transform); mVideoEncoder.drainEncoder(false); mFullScreen.drawFrame(mTextureId, transform); mInputWindowSurface.setPresentationTime(timestampNanos); mInputWindowSurface.swapBuffers(); } /** * Handles a request to stop encoding. */ private void handleStopRecording() { Log.d(TAG, "handleStopRecording"); mVideoEncoder.signalEndOfStream(); mVideoEncoder.drainEncoder(true); releaseEncoder(); } /** * Sets the texture name that SurfaceTexture will use when frames are received. */ private void handleSetTexture(int id) { //Log.d(TAG, "handleSetTexture " + 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. * <p/> * 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) { Log.d(TAG, "handleUpdatedSharedContext " + newSharedContext); // 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, Muxer muxer) { try { mVideoEncoder = new VideoEncoderCore(muxer); } catch (IOException ioe) { throw new RuntimeException(ioe); } mEglCore = new EglCore(sharedContext, EglCore.FLAG_RECORDABLE); mInputWindowSurface = new WindowSurface(mEglCore, mVideoEncoder.getInputSurface(), true); mInputWindowSurface.makeCurrent(); mFullScreen = new FullFrameRect( new Texture2dProgram(Texture2dProgram.ProgramType.TEXTURE_EXT)); } 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; } } //We need to access the encoderHandler, for instance to capture frames public EncoderHandler getHandler(){ return mHandler; } /** * Encoder configuration. * <p/> * 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). * <p/> * TODO: make frame rate and iframe interval configurable? Maybe use builder pattern * with reasonable defaults for those and bit rate. */ public static class EncoderConfig { final Muxer mMuxer; final EGLContext mEglContext; public EncoderConfig(Muxer muxer, EGLContext sharedEglContext) { mMuxer = muxer; mEglContext = sharedEglContext; } @Override public String toString() { return mMuxer.toString() + "' ctxt=" + mEglContext; } } /** * Handles encoder state change requests. The handler is created on the encoder thread. */ public static class EncoderHandler extends Handler { private WeakReference<TextureMovieEncoder> mWeakEncoder; public EncoderHandler(TextureMovieEncoder encoder) { mWeakEncoder = new WeakReference<TextureMovieEncoder>(encoder); } @Override // runs on encoder thread public void handleMessage(Message inputMessage) { int what = inputMessage.what; Object obj = inputMessage.obj; TextureMovieEncoder encoder = mWeakEncoder.get(); if (encoder == null) { Log.w(TAG, "EncoderHandler.handleMessage: encoder is null"); return; } switch (what) { case MSG_START_RECORDING: encoder.handleStartRecording((EncoderConfig) obj); break; case MSG_STOP_RECORDING: encoder.handleStopRecording(); break; case MSG_FRAME_AVAILABLE: long timestamp = (((long) inputMessage.arg1) << 32) | (((long) inputMessage.arg2) & 0xffffffffL); encoder.handleFrameAvailable((float[]) obj, timestamp); break; case MSG_SET_TEXTURE_ID: encoder.handleSetTexture(inputMessage.arg1); break; case MSG_UPDATE_SHARED_CONTEXT: encoder.handleUpdateSharedContext((EGLContext) inputMessage.obj); break; case MSG_QUIT: Looper.myLooper().quit(); break; case MSG_ENCODER_SAVEFRAME: encoder.saveFrame((ScreenShot) obj); break; default: throw new RuntimeException("Unhandled msg what=" + what); } } } }