Create new MediaEncoder interfaces to support audio

pull/360/head
Mattia Iavarone 6 years ago
parent 439e49d513
commit cfc46afaf2
  1. 2
      MIGRATION.md
  2. 44
      cameraview/src/main/gles/com/otaliastudios/cameraview/AudioMediaEncoder.java
  3. 112
      cameraview/src/main/gles/com/otaliastudios/cameraview/MediaEncoder.java
  4. 87
      cameraview/src/main/gles/com/otaliastudios/cameraview/MediaEncoderEngine.java
  5. 20
      cameraview/src/main/gles/com/otaliastudios/cameraview/OldMediaEncoder.java
  6. 76
      cameraview/src/main/gles/com/otaliastudios/cameraview/OldMediaEncoderCore.java
  7. 127
      cameraview/src/main/gles/com/otaliastudios/cameraview/TextureMediaEncoder.java
  8. 90
      cameraview/src/main/gles/com/otaliastudios/cameraview/VideoMediaEncoder.java
  9. 49
      cameraview/src/main/java/com/otaliastudios/cameraview/SnapshotVideoRecorder.java

@ -1,4 +1,4 @@
# Migrating to v2
# Migrating to v2.X.X
- JpegQuality: both cameraJpegQuality and setJpegQuality() have been removed, because
they were working only with specific setups. We'll use the default quality provided

@ -0,0 +1,44 @@
package com.otaliastudios.cameraview;
import android.media.MediaMuxer;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.RequiresApi;
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
class AudioMediaEncoder extends MediaEncoder {
static class Config {
Config() { }
}
AudioMediaEncoder(@NonNull Config config) {
}
@Override
void prepare(MediaMuxer muxer) {
super.prepare(muxer);
}
@Override
void start() {
}
@Override
void notify(String event, Object data) {
}
@Override
void stop() {
}
@Override
void release() {
}
}

@ -0,0 +1,112 @@
package com.otaliastudios.cameraview;
import android.media.MediaCodec;
import android.media.MediaFormat;
import android.media.MediaMuxer;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.util.Log;
import java.nio.ByteBuffer;
// https://github.com/saki4510t/AudioVideoRecordingSample/blob/master/app/src/main/java/com/serenegiant/encoder/MediaEncoder.java
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
abstract class MediaEncoder {
private final static int TIMEOUT_USEC = 10000; // 10 msec
protected MediaCodec mMediaCodec;
private MediaCodec.BufferInfo mBufferInfo;
private MediaMuxer mMuxer;
private int mTrackIndex;
private boolean mTrackStarted;
MediaEncoder() {
}
void prepare(MediaMuxer muxer) {
mMuxer = muxer;
mBufferInfo = new MediaCodec.BufferInfo();
}
abstract void start();
abstract void notify(String event, Object data);
abstract void stop();
abstract void release();
/**
* 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.
*/
protected void drain(boolean endOfStream) {
if (endOfStream) {
mMediaCodec.signalEndOfInputStream();
}
ByteBuffer[] encoderOutputBuffers = mMediaCodec.getOutputBuffers();
while (true) {
int encoderStatus = mMediaCodec.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 = mMediaCodec.getOutputBuffers();
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// should happen before receiving buffers, and should only happen once
if (mTrackStarted) {
throw new RuntimeException("format changed twice");
}
MediaFormat newFormat = mMediaCodec.getOutputFormat();
// now that we have the Magic Goodies, start the muxer
mTrackIndex = mMuxer.addTrack(newFormat);
// TODO this is wrong. Look at the Github project: it is a muxer wrapper.
// If you have multiple encoders this breaks.
mMuxer.start();
mTrackStarted = true;
} else if (encoderStatus < 0) {
Log.w("VideoMediaEncoder", "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 (!mTrackStarted) {
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);
}
mMediaCodec.releaseOutputBuffer(encoderStatus, false);
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
if (!endOfStream) {
Log.w("VideoMediaEncoder", "reached end of stream unexpectedly");
}
break; // out of while
}
}
}
}
}

@ -0,0 +1,87 @@
package com.otaliastudios.cameraview;
import android.graphics.SurfaceTexture;
import android.media.MediaMuxer;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
class MediaEncoderEngine {
private WorkerHandler mWorker;
private ArrayList<MediaEncoder> mEncoders;
private MediaMuxer mMediaMuxer;
MediaEncoderEngine(@NonNull File file, @NonNull VideoMediaEncoder videoEncoder, @Nullable AudioMediaEncoder audioEncoder) {
mWorker = WorkerHandler.get("EncoderEngine");
mEncoders = new ArrayList<>();
mEncoders.add(videoEncoder);
if (audioEncoder != null) {
mEncoders.add(audioEncoder);
}
try {
mMediaMuxer = new MediaMuxer(file.toString(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
} catch (IOException e) {
throw new RuntimeException(e);
}
mWorker.post(new Runnable() {
@Override
public void run() {
for (MediaEncoder encoder : mEncoders) {
encoder.prepare(mMediaMuxer);
}
}
});
}
void start() {
mWorker.post(new Runnable() {
@Override
public void run() {
for (MediaEncoder encoder : mEncoders) {
encoder.start();
}
}
});
}
void notify(final String event, final Object data) {
mWorker.post(new Runnable() {
@Override
public void run() {
for (MediaEncoder encoder : mEncoders) {
encoder.notify(event, data);
}
}
});
}
void stop(final Runnable onStop) {
mWorker.post(new Runnable() {
@Override
public void run() {
for (MediaEncoder encoder : mEncoders) {
encoder.stop();
}
onStop.run();
for (MediaEncoder encoder : mEncoders) {
encoder.release();
}
if (mMediaMuxer != 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.
mMediaMuxer.stop();
mMediaMuxer.release();
mMediaMuxer = null;
}
}
});
}
}

@ -58,8 +58,8 @@ import java.lang.ref.WeakReference;
* TODO: tweak the API (esp. textureId) so it's less awkward for simple use cases.
*/
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
class VideoTextureEncoder implements Runnable {
private static final String TAG = VideoTextureEncoder.class.getSimpleName();
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;
@ -73,7 +73,7 @@ class VideoTextureEncoder implements Runnable {
private EglViewport mFullScreen;
private int mTextureId;
private int mFrameNum = -1; // Important
private VideoCoreEncoder mVideoEncoder;
private OldMediaEncoderCore mVideoEncoder;
private float mTransformationScaleX = 1F;
private float mTransformationScaleY = 1F;
private int mTransformationRotation = 0;
@ -131,12 +131,12 @@ class VideoTextureEncoder implements Runnable {
}
private void prepareEncoder(Config config) {
try {
mVideoEncoder = new VideoCoreEncoder(config.mWidth, config.mHeight,
config.mBitRate, config.mFrameRate,
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.mOutputFile,
config.mMimeType);
try {
mVideoEncoder = new OldMediaEncoderCore(videoConfig, config.mOutputFile);
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
@ -292,9 +292,9 @@ class VideoTextureEncoder implements Runnable {
* Handles encoder state change requests. The handler is created on the encoder thread.
*/
private static class EncoderHandler extends Handler {
private WeakReference<VideoTextureEncoder> mWeakEncoder;
private WeakReference<OldMediaEncoder> mWeakEncoder;
public EncoderHandler(VideoTextureEncoder encoder) {
public EncoderHandler(OldMediaEncoder encoder) {
mWeakEncoder = new WeakReference<>(encoder);
}
@ -303,7 +303,7 @@ class VideoTextureEncoder implements Runnable {
int what = inputMessage.what;
Object obj = inputMessage.obj;
VideoTextureEncoder encoder = mWeakEncoder.get();
OldMediaEncoder encoder = mWeakEncoder.get();
if (encoder == null) {
Log.w(TAG, "EncoderHandler.handleMessage: encoder is null");
return;

@ -43,40 +43,55 @@ import java.nio.ByteBuffer;
* on one thread, and drain the output on a different thread.
*/
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
class VideoCoreEncoder {
class OldMediaEncoderCore {
private Surface mInputSurface;
private MediaMuxer mMuxer;
private MediaCodec mEncoder;
private boolean mMuxerStarted;
private MediaCodec mVideoEncoder;
private Surface mVideoInputSurface;
private MediaCodec.BufferInfo mBufferInfo;
private int mTrackIndex;
private boolean mMuxerStarted;
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.
*/
public VideoCoreEncoder(int width, int height, int bitRate, int frameRate, int rotation, File outputFile, String mimeType)
throws IOException {
OldMediaEncoderCore(VideoConfig videoConfig, File outputFile) throws IOException {
mBufferInfo = new MediaCodec.BufferInfo();
MediaFormat format = MediaFormat.createVideoFormat(mimeType, width, height);
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, bitRate);
format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
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", rotation);
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.
mEncoder = MediaCodec.createEncoderByType(mimeType);
mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mInputSurface = mEncoder.createInputSurface();
mEncoder.start();
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
@ -84,8 +99,7 @@ class VideoCoreEncoder {
//
// 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);
mMuxer = new MediaMuxer(outputFile.toString(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
mTrackIndex = -1;
mMuxerStarted = false;
@ -95,17 +109,17 @@ class VideoCoreEncoder {
* Returns the encoder's input surface.
*/
public Surface getInputSurface() {
return mInputSurface;
return mVideoInputSurface;
}
/**
* Releases encoder resources.
*/
public void release() {
if (mEncoder != null) {
mEncoder.stop();
mEncoder.release();
mEncoder = null;
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
@ -130,12 +144,12 @@ class VideoCoreEncoder {
final int TIMEOUT_USEC = 10000;
if (endOfStream) {
mEncoder.signalEndOfInputStream();
mVideoEncoder.signalEndOfInputStream();
}
ByteBuffer[] encoderOutputBuffers = mEncoder.getOutputBuffers();
ByteBuffer[] encoderOutputBuffers = mVideoEncoder.getOutputBuffers();
while (true) {
int encoderStatus = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
int encoderStatus = mVideoEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
// no output available yet
if (!endOfStream) {
@ -143,20 +157,20 @@ class VideoCoreEncoder {
}
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
// not expected for an encoder
encoderOutputBuffers = mEncoder.getOutputBuffers();
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 = mEncoder.getOutputFormat();
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("VideoCoreEncoder", "unexpected result from encoder.dequeueOutputBuffer: " + encoderStatus);
Log.w("OldMediaEncoderCore", "unexpected result from encoder.dequeueOutputBuffer: " + encoderStatus);
// let's ignore it
} else {
ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
@ -182,11 +196,11 @@ class VideoCoreEncoder {
mMuxer.writeSampleData(mTrackIndex, encodedData, mBufferInfo);
}
mEncoder.releaseOutputBuffer(encoderStatus, false);
mVideoEncoder.releaseOutputBuffer(encoderStatus, false);
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
if (!endOfStream) {
Log.w("VideoCoreEncoder", "reached end of stream unexpectedly");
Log.w("OldMediaEncoderCore", "reached end of stream unexpectedly");
}
break; // out of while
}

@ -0,0 +1,127 @@
package com.otaliastudios.cameraview;
import android.graphics.SurfaceTexture;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.media.MediaMuxer;
import android.opengl.EGLContext;
import android.opengl.Matrix;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.RequiresApi;
import android.util.Log;
import android.view.Surface;
import java.io.IOException;
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
class TextureMediaEncoder extends VideoMediaEncoder<TextureMediaEncoder.Config> {
public final static String FRAME_EVENT = "frame";
static class Frame {
float[] transform;
long timestamp;
}
static class Config extends VideoMediaEncoder.Config {
int textureId;
float scaleX;
float scaleY;
EGLContext eglContext;
Config(int width, int height, int bitRate, int frameRate, int rotation, String mimeType,
int textureId, float scaleX, float scaleY, EGLContext eglContext) {
super(width, height, bitRate, frameRate, rotation, mimeType);
this.textureId = textureId;
this.scaleX = scaleX;
this.scaleY = scaleY;
this.eglContext = eglContext;
}
}
private EglCore mEglCore;
private EglWindowSurface mWindow;
private EglViewport mViewport;
public TextureMediaEncoder(@NonNull Config config) {
super(config);
}
@Override
void prepare(MediaMuxer muxer) {
super.prepare(muxer);
mEglCore = new EglCore(mConfig.eglContext, EglCore.FLAG_RECORDABLE);
mWindow = new EglWindowSurface(mEglCore, mSurface, true);
mWindow.makeCurrent(); // drawing will happen on the InputWindowSurface, which
// is backed by mVideoEncoder.getInputSurface()
mViewport = new EglViewport();
}
@Override
void release() {
super.release();
if (mWindow != null) {
mWindow.release();
mWindow = null;
}
if (mViewport != null) {
mViewport.release(true);
mViewport = null;
}
if (mEglCore != null) {
mEglCore.release();
mEglCore = null;
}
}
@Override
void start() {
super.start();
// Nothing to do here. Waiting for the first frame.
}
@Override
void notify(String event, Object data) {
if (event.equals(FRAME_EVENT)) {
Frame frame = (Frame) data;
// 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.
if (frame.timestamp == 0) return;
if (mFrameNum < 0) return;
mFrameNum++;
int arg1 = (int) (frame.timestamp >> 32);
int arg2 = (int) frame.timestamp;
long timestamp = (((long) arg1) << 32) | (((long) arg2) & 0xffffffffL);
float[] transform = frame.transform;
// 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 = mConfig.scaleX;
float scaleY = mConfig.scaleY;
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, mConfig.rotation, 0, 0, 1);
Matrix.translateM(transform, 0, -0.5F, -0.5F, 0);
drain(false);
mViewport.drawFrame(mConfig.textureId, transform);
mWindow.setPresentationTime(timestamp);
mWindow.swapBuffers();
}
}
}

@ -0,0 +1,90 @@
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.NonNull;
import android.support.annotation.RequiresApi;
import android.util.Log;
import android.view.Surface;
import java.io.IOException;
import java.nio.ByteBuffer;
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
abstract class VideoMediaEncoder<C extends VideoMediaEncoder.Config> extends MediaEncoder {
protected C mConfig;
protected Surface mSurface;
protected int mFrameNum = -1;
static class Config {
int width;
int height;
int bitRate;
int frameRate;
int rotation;
String mimeType;
Config(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;
}
}
VideoMediaEncoder(@NonNull C config) {
mConfig = config;
}
@Override
void prepare(MediaMuxer muxer) {
super.prepare(muxer);
MediaFormat format = MediaFormat.createVideoFormat(mConfig.mimeType, mConfig.width, mConfig.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, mConfig.bitRate);
format.setInteger(MediaFormat.KEY_FRAME_RATE, mConfig.frameRate);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5);
format.setInteger("rotation-degrees", mConfig.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.
try {
mMediaCodec = MediaCodec.createEncoderByType(mConfig.mimeType);
} catch (IOException e) {
throw new RuntimeException(e);
}
mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mSurface = mMediaCodec.createInputSurface();
mMediaCodec.start();
}
@Override
void start() {
// Nothing to do here. Waiting for the first frame.
mFrameNum = 0;
}
@Override
void stop() {
mFrameNum = -1;
drain(true);
}
@Override
void release() {
if (mMediaCodec != null) {
mMediaCodec.stop();
mMediaCodec.release();
mMediaCodec = null;
}
}
}

@ -18,11 +18,13 @@ 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 VideoTextureEncoder mEncoder;
private OldMediaEncoder mEncoder;
private MediaEncoderEngine mEncoderEngine;
private GLCameraPreview mPreview;
private int mCurrentState = STATE_NOT_RECORDING;
@ -31,7 +33,9 @@ class SnapshotVideoRecorder extends VideoRecorder implements GLCameraPreview.Ren
SnapshotVideoRecorder(VideoResult stub, VideoResultListener listener, GLCameraPreview preview) {
super(stub, listener);
mEncoder = new VideoTextureEncoder();
if (USE_OLD_ENCODER) {
mEncoder = new OldMediaEncoder();
}
mPreview = preview;
mPreview.addRendererFrameCallback(this);
}
@ -76,7 +80,8 @@ 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;
}
VideoTextureEncoder.Config configuration = new VideoTextureEncoder.Config(
if (USE_OLD_ENCODER) {
OldMediaEncoder.Config configuration = new OldMediaEncoder.Config(
mResult.getFile(),
width,
height,
@ -88,17 +93,40 @@ class SnapshotVideoRecorder extends VideoRecorder implements GLCameraPreview.Ren
type,
EGL14.eglGetCurrentContext()
);
mResult.rotation = 0; // We will rotate the result instead.
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();
}
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);
}
}
if (mCurrentState == STATE_RECORDING && mDesiredState == STATE_NOT_RECORDING) {
if (USE_OLD_ENCODER) {
mEncoder.stopRecording(new Runnable() {
@Override
public void run() {
@ -106,9 +134,18 @@ class SnapshotVideoRecorder extends VideoRecorder implements GLCameraPreview.Ren
dispatchResult();
}
});
mCurrentState = STATE_NOT_RECORDING;
mEncoder = null;
} else {
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;
}

Loading…
Cancel
Save