parent
1f0341256a
commit
d08c56c821
@ -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. |
|
||||||
* <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 Config |
|
||||||
* <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> |
|
||||||
* |
|
||||||
* TODO: tweak the API (esp. textureId) so it's less awkward for simple use cases. |
|
||||||
*/ |
|
||||||
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) |
|
||||||
class OldMediaEncoder implements Runnable { |
|
||||||
private static final String TAG = OldMediaEncoder.class.getSimpleName(); |
|
||||||
|
|
||||||
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 = 4; |
|
||||||
|
|
||||||
// ----- accessed exclusively by encoder thread -----
|
|
||||||
private EglWindowSurface mInputWindowSurface; |
|
||||||
private EglCore mEglCore; |
|
||||||
private EglViewport mFullScreen; |
|
||||||
private int mTextureId; |
|
||||||
private int mFrameNum = -1; // Important
|
|
||||||
private OldMediaEncoderCore mVideoEncoder; |
|
||||||
private float mTransformationScaleX = 1F; |
|
||||||
private float mTransformationScaleY = 1F; |
|
||||||
private int mTransformationRotation = 0; |
|
||||||
|
|
||||||
// ----- accessed by multiple threads -----
|
|
||||||
private volatile EncoderHandler mHandler; |
|
||||||
|
|
||||||
private final Object mLooperReadyLock = new Object(); // guards ready/running
|
|
||||||
private boolean mLooperReady; |
|
||||||
private boolean mRunning; |
|
||||||
|
|
||||||
/** |
|
||||||
* 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> |
|
||||||
*/ |
|
||||||
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.) |
|
||||||
* <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(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.) |
|
||||||
* <p> |
|
||||||
* Returns immediately; the encoder/muxer may not yet be finished creating the movie. |
|
||||||
* <p> |
|
||||||
*/ |
|
||||||
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.) |
|
||||||
* <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 (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.) |
|
||||||
* <p> |
|
||||||
* 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. |
|
||||||
* <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 (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<OldMediaEncoder> mWeakEncoder; |
|
||||||
|
|
||||||
public EncoderHandler(OldMediaEncoder encoder) { |
|
||||||
mWeakEncoder = new WeakReference<>(encoder); |
|
||||||
} |
|
||||||
|
|
||||||
@Override // runs on encoder thread
|
|
||||||
public void handleMessage(Message inputMessage) { |
|
||||||
int what = inputMessage.what; |
|
||||||
Object obj = inputMessage.obj; |
|
||||||
|
|
||||||
OldMediaEncoder encoder = mWeakEncoder.get(); |
|
||||||
if (encoder == null) { |
|
||||||
Log.w(TAG, "EncoderHandler.handleMessage: encoder is null"); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
switch (what) { |
|
||||||
case MSG_START_RECORDING: |
|
||||||
encoder.mFrameNum = 0; |
|
||||||
Config config = (Config) obj; |
|
||||||
encoder.prepareEncoder(config); |
|
||||||
break; |
|
||||||
case MSG_STOP_RECORDING: |
|
||||||
encoder.mFrameNum = -1; |
|
||||||
encoder.mVideoEncoder.drainEncoder(true); |
|
||||||
encoder.releaseEncoder(); |
|
||||||
((Runnable) obj).run(); |
|
||||||
break; |
|
||||||
case MSG_FRAME_AVAILABLE: |
|
||||||
if (encoder.mFrameNum < 0) break; |
|
||||||
encoder.mFrameNum++; |
|
||||||
long timestamp = (((long) inputMessage.arg1) << 32) | (((long) inputMessage.arg2) & 0xffffffffL); |
|
||||||
float[] transform = (float[]) obj; |
|
||||||
|
|
||||||
// We must scale this matrix like GlCameraPreview does, because it might have some cropping.
|
|
||||||
// Scaling takes place with respect to the (0, 0, 0) point, so we must apply a Translation to compensate.
|
|
||||||
|
|
||||||
float scaleX = encoder.mTransformationScaleX; |
|
||||||
float scaleY = encoder.mTransformationScaleY; |
|
||||||
float scaleTranslX = (1F - scaleX) / 2F; |
|
||||||
float scaleTranslY = (1F - scaleY) / 2F; |
|
||||||
Matrix.translateM(transform, 0, scaleTranslX, scaleTranslY, 0); |
|
||||||
Matrix.scaleM(transform, 0, scaleX, scaleY, 1); |
|
||||||
|
|
||||||
// We also must rotate this matrix. In GlCameraPreview it is not needed because it is a live
|
|
||||||
// stream, but the output video, must be correctly rotated based on the device rotation at the moment.
|
|
||||||
// Rotation also takes place with respect to the origin (the Z axis), so we must
|
|
||||||
// translate to origin, rotate, then back to where we were.
|
|
||||||
|
|
||||||
Matrix.translateM(transform, 0, 0.5F, 0.5F, 0); |
|
||||||
Matrix.rotateM(transform, 0, encoder.mTransformationRotation, 0, 0, 1); |
|
||||||
Matrix.translateM(transform, 0, -0.5F, -0.5F, 0); |
|
||||||
|
|
||||||
encoder.mVideoEncoder.drainEncoder(false); |
|
||||||
encoder.mFullScreen.drawFrame(encoder.mTextureId, transform); |
|
||||||
encoder.mInputWindowSurface.setPresentationTime(timestamp); |
|
||||||
encoder.mInputWindowSurface.swapBuffers(); |
|
||||||
break; |
|
||||||
case MSG_SET_TEXTURE_ID: |
|
||||||
encoder.mTextureId = inputMessage.arg1; |
|
||||||
break; |
|
||||||
case MSG_QUIT: |
|
||||||
Looper.myLooper().quit(); |
|
||||||
break; |
|
||||||
default: |
|
||||||
throw new RuntimeException("Unhandled msg what=" + what); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
@ -1,211 +0,0 @@ |
|||||||
/* |
|
||||||
* 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 com.otaliastudios.cameraview; |
|
||||||
|
|
||||||
|
|
||||||
import android.media.MediaCodec; |
|
||||||
import android.media.MediaCodecInfo; |
|
||||||
import android.media.MediaFormat; |
|
||||||
import android.media.MediaMuxer; |
|
||||||
import android.os.Build; |
|
||||||
import android.support.annotation.RequiresApi; |
|
||||||
import android.util.Log; |
|
||||||
import android.view.Surface; |
|
||||||
|
|
||||||
import java.io.File; |
|
||||||
import java.io.IOException; |
|
||||||
import java.nio.ByteBuffer; |
|
||||||
|
|
||||||
/** |
|
||||||
* -- From grafika VideoEncoderCore.java -- |
|
||||||
* |
|
||||||
* This class wraps up the core components used for surface-input video encoding. |
|
||||||
* <p> |
|
||||||
* 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. |
|
||||||
* <p> |
|
||||||
* 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. |
|
||||||
* <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. |
|
||||||
* <p> |
|
||||||
* 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
|
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
Loading…
Reference in new issue