diff --git a/cameraview/src/main/gles/com/otaliastudios/cameraview/OldMediaEncoder.java b/cameraview/src/main/gles/com/otaliastudios/cameraview/OldMediaEncoder.java deleted file mode 100644 index 1c9251c6..00000000 --- a/cameraview/src/main/gles/com/otaliastudios/cameraview/OldMediaEncoder.java +++ /dev/null @@ -1,366 +0,0 @@ -/* - * Copyright 2013 Google Inc. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.otaliastudios.cameraview; - -import android.graphics.SurfaceTexture; -import android.opengl.EGLContext; -import android.opengl.Matrix; -import android.os.Build; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.support.annotation.RequiresApi; -import android.util.Log; - -import java.io.File; -import java.io.IOException; -import java.lang.ref.WeakReference; - - -/** - * -- from grafika -- - * - * Encode a movie from frames rendered from an external texture image. - *
- * 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: - *
- * 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). - *
- */ - static class Config { - final File mOutputFile; - final int mWidth; - final int mHeight; - final int mBitRate; - final int mFrameRate; - final int mRotation; - final float mScaleX; - final float mScaleY; - final EGLContext mEglContext; - final String mMimeType; - - Config(File outputFile, int width, int height, - int bitRate, int frameRate, - int rotation, - float scaleX, float scaleY, - String mimeType, - EGLContext sharedEglContext) { - mOutputFile = outputFile; - mWidth = width; - mHeight = height; - mBitRate = bitRate; - mFrameRate = frameRate; - mEglContext = sharedEglContext; - mScaleX = scaleX; - mScaleY = scaleY; - mRotation = rotation; - mMimeType = mimeType; - } - - @Override - public String toString() { - return "Config: " + mWidth + "x" + mHeight + " @" + mBitRate + - " to '" + mOutputFile.toString() + "' ctxt=" + mEglContext; - } - } - - private void prepareEncoder(Config config) { - OldMediaEncoderCore.VideoConfig videoConfig = new OldMediaEncoderCore.VideoConfig( - config.mWidth, config.mHeight, config.mBitRate, config.mFrameRate, - 0, // The video encoder rotation does not work, so we apply it here using Matrix.rotateM(). - config.mMimeType); - try { - mVideoEncoder = new OldMediaEncoderCore(videoConfig, config.mOutputFile); - } catch (IOException ioe) { - throw new RuntimeException(ioe); - } - mEglCore = new EglCore(config.mEglContext, EglCore.FLAG_RECORDABLE); - mInputWindowSurface = new EglWindowSurface(mEglCore, mVideoEncoder.getInputSurface(), true); - mInputWindowSurface.makeCurrent(); // drawing will happen on the InputWindowSurface, which - // is backed by mVideoEncoder.getInputSurface() - mFullScreen = new EglViewport(); - mTransformationScaleX = config.mScaleX; - mTransformationScaleY = config.mScaleY; - mTransformationRotation = config.mRotation; - } - - private void releaseEncoder() { - mVideoEncoder.release(); - if (mInputWindowSurface != null) { - mInputWindowSurface.release(); - mInputWindowSurface = null; - } - if (mFullScreen != null) { - mFullScreen.release(true); - mFullScreen = null; - } - if (mEglCore != null) { - mEglCore.release(); - mEglCore = null; - } - } - - /** - * 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(Config config) { - Log.d(TAG, "Encoder: startRecording()"); - synchronized (mLooperReadyLock) { - if (mRunning) { - Log.w(TAG, "Encoder thread already running"); - return; - } - mRunning = true; - new Thread(this, "TextureMovieEncoder").start(); - while (!mLooperReady) { - try { - mLooperReadyLock.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. - *
- */ - public void stopRecording(Runnable onStop) { - mHandler.sendMessage(mHandler.obtainMessage(MSG_STOP_RECORDING, onStop)); - 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 (mLooperReadyLock) { - return mRunning; - } - } - - /** - * 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. - *
- * 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 (mLooperReadyLock) { - if (!mLooperReady) { - return; - } - } - - float[] transform = new float[16]; // TODO - avoid alloc every frame. Not easy, need a pool - 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.) - *
- * TODO: do something less clumsy - */ - public void setTextureId(int id) { - synchronized (mLooperReadyLock) { - if (!mLooperReady) 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 (mLooperReadyLock) {
- mHandler = new EncoderHandler(this);
- mLooperReady = true;
- mLooperReadyLock.notify();
- }
- Looper.loop();
- Log.d(TAG, "Encoder thread exiting");
- synchronized (mLooperReadyLock) {
- mLooperReady = mRunning = false;
- mHandler = null;
- }
- }
-
- /**
- * Handles encoder state change requests. The handler is created on the encoder thread.
- */
- private static class EncoderHandler extends Handler {
- private WeakReference
- * 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.
- */
-@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
-class OldMediaEncoderCore {
-
- private MediaMuxer mMuxer;
- private boolean mMuxerStarted;
- private MediaCodec mVideoEncoder;
- private Surface mVideoInputSurface;
- private MediaCodec.BufferInfo mBufferInfo;
- private int mTrackIndex;
-
-
- static class VideoConfig {
- int width;
- int height;
- int bitRate;
- int frameRate;
- int rotation;
- String mimeType;
-
- VideoConfig(int width, int height, int bitRate, int frameRate, int rotation, String mimeType) {
- this.width = width;
- this.height = height;
- this.bitRate = bitRate;
- this.frameRate = frameRate;
- this.rotation = rotation;
- this.mimeType = mimeType;
- }
- }
- /**
- * Configures encoder and muxer state, and prepares the input Surface.
- */
- OldMediaEncoderCore(VideoConfig videoConfig, File outputFile) throws IOException {
- mBufferInfo = new MediaCodec.BufferInfo();
-
- MediaFormat format = MediaFormat.createVideoFormat(videoConfig.mimeType, videoConfig.width, videoConfig.height);
-
- // Set some properties. Failing to specify some of these can cause the MediaCodec
- // configure() call to throw an unhelpful exception.
- format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
- format.setInteger(MediaFormat.KEY_BIT_RATE, videoConfig.bitRate);
- format.setInteger(MediaFormat.KEY_FRAME_RATE, videoConfig.frameRate);
- format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5);
- format.setInteger("rotation-degrees", videoConfig.rotation);
-
- // 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.
- mVideoEncoder = MediaCodec.createEncoderByType(videoConfig.mimeType);
- mVideoEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
- mVideoInputSurface = mVideoEncoder.createInputSurface();
- mVideoEncoder.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.
- mMuxer = new MediaMuxer(outputFile.toString(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
-
- mTrackIndex = -1;
- mMuxerStarted = false;
- }
-
- /**
- * Returns the encoder's input surface.
- */
- public Surface getInputSurface() {
- return mVideoInputSurface;
- }
-
- /**
- * Releases encoder resources.
- */
- public void release() {
- if (mVideoEncoder != null) {
- mVideoEncoder.stop();
- mVideoEncoder.release();
- mVideoEncoder = null;
- }
- if (mMuxer != null) {
- // TODO: stop() throws an exception if you haven't fed it any data. Keep track
- // of frames submitted, and don't call stop() if we haven't written anything.
- mMuxer.stop();
- mMuxer.release();
- mMuxer = null;
- }
- }
-
- /**
- * Extracts all pending data from the encoder and forwards it to the muxer.
- *
- * 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) {
- final int TIMEOUT_USEC = 10000;
-
- if (endOfStream) {
- mVideoEncoder.signalEndOfInputStream();
- }
-
- ByteBuffer[] encoderOutputBuffers = mVideoEncoder.getOutputBuffers();
- while (true) {
- int encoderStatus = mVideoEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
- if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
- // no output available yet
- if (!endOfStream) {
- break; // out of while
- }
- } else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
- // not expected for an encoder
- encoderOutputBuffers = mVideoEncoder.getOutputBuffers();
- } 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 = mVideoEncoder.getOutputFormat();
-
- // now that we have the Magic Goodies, start the muxer
- mTrackIndex = mMuxer.addTrack(newFormat);
- mMuxer.start();
- mMuxerStarted = true;
- } else if (encoderStatus < 0) {
- Log.w("OldMediaEncoderCore", "unexpected result from encoder.dequeueOutputBuffer: " + encoderStatus);
- // let's ignore it
- } else {
- ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
- 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.
- 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);
-
- mMuxer.writeSampleData(mTrackIndex, encodedData, mBufferInfo);
- }
-
- mVideoEncoder.releaseOutputBuffer(encoderStatus, false);
-
- if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
- if (!endOfStream) {
- Log.w("OldMediaEncoderCore", "reached end of stream unexpectedly");
- }
- break; // out of while
- }
- }
- }
- }
-}
-
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/SnapshotVideoRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/SnapshotVideoRecorder.java
index ea7dc79c..b4022cb0 100644
--- a/cameraview/src/main/java/com/otaliastudios/cameraview/SnapshotVideoRecorder.java
+++ b/cameraview/src/main/java/com/otaliastudios/cameraview/SnapshotVideoRecorder.java
@@ -18,12 +18,10 @@ class SnapshotVideoRecorder extends VideoRecorder implements GlCameraPreview.Ren
private static final String TAG = SnapshotVideoRecorder.class.getSimpleName();
private static final CameraLogger LOG = CameraLogger.create(TAG);
- private static final boolean USE_OLD_ENCODER = false;
private static final int STATE_RECORDING = 0;
private static final int STATE_NOT_RECORDING = 1;
- private OldMediaEncoder mEncoder;
private MediaEncoderEngine mEncoderEngine;
private GlCameraPreview mPreview;
@@ -33,9 +31,6 @@ class SnapshotVideoRecorder extends VideoRecorder implements GlCameraPreview.Ren
SnapshotVideoRecorder(VideoResult stub, VideoResultListener listener, GlCameraPreview preview) {
super(stub, listener);
- if (USE_OLD_ENCODER) {
- mEncoder = new OldMediaEncoder();
- }
mPreview = preview;
mPreview.addRendererFrameCallback(this);
}
@@ -82,71 +77,39 @@ class SnapshotVideoRecorder extends VideoRecorder implements GlCameraPreview.Ren
case H_264: type = "video/avc"; break; // MediaFormat.MIMETYPE_VIDEO_AVC:
case DEVICE_DEFAULT: type = "video/avc"; break;
}
- if (USE_OLD_ENCODER) {
- OldMediaEncoder.Config configuration = new OldMediaEncoder.Config(
- mResult.getFile(),
- width,
- height,
- 1000000,
- 30,
- mResult.getRotation(),
- scaleX,
- scaleY,
- type,
- EGL14.eglGetCurrentContext()
- );
- mEncoder.startRecording(configuration);
- mEncoder.setTextureId(mTextureId);
- } else {
- TextureMediaEncoder.Config config = new TextureMediaEncoder.Config(
- width, height,
- 1000000,
- 30,
- mResult.getRotation(),
- type, mTextureId,
- scaleX, scaleY,
- EGL14.eglGetCurrentContext()
- );
- TextureMediaEncoder videoEncoder = new TextureMediaEncoder(config);
- mEncoderEngine = new MediaEncoderEngine(mResult.file, videoEncoder, null);
- mEncoderEngine.start();
- }
+ TextureMediaEncoder.Config config = new TextureMediaEncoder.Config(
+ width, height,
+ 1000000,
+ 30,
+ mResult.getRotation(),
+ type, mTextureId,
+ scaleX, scaleY,
+ EGL14.eglGetCurrentContext()
+ );
+ TextureMediaEncoder videoEncoder = new TextureMediaEncoder(config);
+ mEncoderEngine = new MediaEncoderEngine(mResult.file, videoEncoder, null);
+ mEncoderEngine.start();
mResult.rotation = 0; // We will rotate the result instead.
mCurrentState = STATE_RECORDING;
}
if (mCurrentState == STATE_RECORDING) {
- if (USE_OLD_ENCODER) {
- mEncoder.frameAvailable(surfaceTexture);
- } else {
- TextureMediaEncoder.Frame frame = new TextureMediaEncoder.Frame();
- frame.timestamp = surfaceTexture.getTimestamp();
- frame.transform = new float[16];
- surfaceTexture.getTransformMatrix(frame.transform);
- mEncoderEngine.notify(TextureMediaEncoder.FRAME_EVENT, frame);
- }
+ TextureMediaEncoder.Frame frame = new TextureMediaEncoder.Frame();
+ frame.timestamp = surfaceTexture.getTimestamp();
+ frame.transform = new float[16];
+ surfaceTexture.getTransformMatrix(frame.transform);
+ mEncoderEngine.notify(TextureMediaEncoder.FRAME_EVENT, frame);
}
if (mCurrentState == STATE_RECORDING && mDesiredState == STATE_NOT_RECORDING) {
- if (USE_OLD_ENCODER) {
- mEncoder.stopRecording(new Runnable() {
- @Override
- public void run() {
- // We are in the encoder thread.
- dispatchResult();
- }
- });
- mEncoder = null;
- } else {
- mEncoderEngine.stop(new Runnable() {
- @Override
- public void run() {
- // We are in the encoder thread.
- dispatchResult();
- }
- });
- mEncoderEngine = null;
- }
+ mEncoderEngine.stop(new Runnable() {
+ @Override
+ public void run() {
+ // We are in the encoder thread.
+ dispatchResult();
+ }
+ });
+ mEncoderEngine = null;
mCurrentState = STATE_NOT_RECORDING;
mPreview.removeRendererFrameCallback(SnapshotVideoRecorder.this);
mPreview = null;