Create new MediaEncoder interfaces to support audio

v2
Mattia Iavarone 6 years ago
parent c669308ba8
commit d7e0c3beed
  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. 22
      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. 89
      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 - JpegQuality: both cameraJpegQuality and setJpegQuality() have been removed, because
they were working only with specific setups. We'll use the default quality provided 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. * TODO: tweak the API (esp. textureId) so it's less awkward for simple use cases.
*/ */
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
class VideoTextureEncoder implements Runnable { class OldMediaEncoder implements Runnable {
private static final String TAG = VideoTextureEncoder.class.getSimpleName(); private static final String TAG = OldMediaEncoder.class.getSimpleName();
private static final int MSG_START_RECORDING = 0; private static final int MSG_START_RECORDING = 0;
private static final int MSG_STOP_RECORDING = 1; private static final int MSG_STOP_RECORDING = 1;
@ -73,7 +73,7 @@ class VideoTextureEncoder implements Runnable {
private EglViewport mFullScreen; private EglViewport mFullScreen;
private int mTextureId; private int mTextureId;
private int mFrameNum = -1; // Important private int mFrameNum = -1; // Important
private VideoCoreEncoder mVideoEncoder; private OldMediaEncoderCore mVideoEncoder;
private float mTransformationScaleX = 1F; private float mTransformationScaleX = 1F;
private float mTransformationScaleY = 1F; private float mTransformationScaleY = 1F;
private int mTransformationRotation = 0; private int mTransformationRotation = 0;
@ -131,12 +131,12 @@ class VideoTextureEncoder implements Runnable {
} }
private void prepareEncoder(Config config) { 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 { try {
mVideoEncoder = new VideoCoreEncoder(config.mWidth, config.mHeight, mVideoEncoder = new OldMediaEncoderCore(videoConfig, config.mOutputFile);
config.mBitRate, config.mFrameRate,
0, // The video encoder rotation does not work, so we apply it here using Matrix.rotateM().
config.mOutputFile,
config.mMimeType);
} catch (IOException ioe) { } catch (IOException ioe) {
throw new RuntimeException(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. * Handles encoder state change requests. The handler is created on the encoder thread.
*/ */
private static class EncoderHandler extends Handler { 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); mWeakEncoder = new WeakReference<>(encoder);
} }
@ -303,7 +303,7 @@ class VideoTextureEncoder implements Runnable {
int what = inputMessage.what; int what = inputMessage.what;
Object obj = inputMessage.obj; Object obj = inputMessage.obj;
VideoTextureEncoder encoder = mWeakEncoder.get(); OldMediaEncoder encoder = mWeakEncoder.get();
if (encoder == null) { if (encoder == null) {
Log.w(TAG, "EncoderHandler.handleMessage: encoder is null"); Log.w(TAG, "EncoderHandler.handleMessage: encoder is null");
return; return;

@ -43,40 +43,55 @@ import java.nio.ByteBuffer;
* on one thread, and drain the output on a different thread. * on one thread, and drain the output on a different thread.
*/ */
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
class VideoCoreEncoder { class OldMediaEncoderCore {
private Surface mInputSurface;
private MediaMuxer mMuxer; private MediaMuxer mMuxer;
private MediaCodec mEncoder; private boolean mMuxerStarted;
private MediaCodec mVideoEncoder;
private Surface mVideoInputSurface;
private MediaCodec.BufferInfo mBufferInfo; private MediaCodec.BufferInfo mBufferInfo;
private int mTrackIndex; 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. * 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) OldMediaEncoderCore(VideoConfig videoConfig, File outputFile) throws IOException {
throws IOException {
mBufferInfo = new MediaCodec.BufferInfo(); 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 // Set some properties. Failing to specify some of these can cause the MediaCodec
// configure() call to throw an unhelpful exception. // configure() call to throw an unhelpful exception.
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); format.setInteger(MediaFormat.KEY_BIT_RATE, videoConfig.bitRate);
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); format.setInteger(MediaFormat.KEY_FRAME_RATE, videoConfig.frameRate);
format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5); 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 // 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. // we can use for input and wrap it with a class that handles the EGL work.
mEncoder = MediaCodec.createEncoderByType(mimeType); mVideoEncoder = MediaCodec.createEncoderByType(videoConfig.mimeType);
mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); mVideoEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mInputSurface = mEncoder.createInputSurface(); mVideoInputSurface = mVideoEncoder.createInputSurface();
mEncoder.start(); mVideoEncoder.start();
// Create a MediaMuxer. We can't add the video track and start() the muxer here, // 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 // 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 // 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. // the raw H.264 elementary stream we get from MediaCodec into a .mp4 file.
mMuxer = new MediaMuxer(outputFile.toString(), mMuxer = new MediaMuxer(outputFile.toString(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
mTrackIndex = -1; mTrackIndex = -1;
mMuxerStarted = false; mMuxerStarted = false;
@ -95,17 +109,17 @@ class VideoCoreEncoder {
* Returns the encoder's input surface. * Returns the encoder's input surface.
*/ */
public Surface getInputSurface() { public Surface getInputSurface() {
return mInputSurface; return mVideoInputSurface;
} }
/** /**
* Releases encoder resources. * Releases encoder resources.
*/ */
public void release() { public void release() {
if (mEncoder != null) { if (mVideoEncoder != null) {
mEncoder.stop(); mVideoEncoder.stop();
mEncoder.release(); mVideoEncoder.release();
mEncoder = null; mVideoEncoder = null;
} }
if (mMuxer != null) { if (mMuxer != null) {
// TODO: stop() throws an exception if you haven't fed it any data. Keep track // 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; final int TIMEOUT_USEC = 10000;
if (endOfStream) { if (endOfStream) {
mEncoder.signalEndOfInputStream(); mVideoEncoder.signalEndOfInputStream();
} }
ByteBuffer[] encoderOutputBuffers = mEncoder.getOutputBuffers(); ByteBuffer[] encoderOutputBuffers = mVideoEncoder.getOutputBuffers();
while (true) { while (true) {
int encoderStatus = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC); int encoderStatus = mVideoEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) { if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
// no output available yet // no output available yet
if (!endOfStream) { if (!endOfStream) {
@ -143,20 +157,20 @@ class VideoCoreEncoder {
} }
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { } else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
// not expected for an encoder // not expected for an encoder
encoderOutputBuffers = mEncoder.getOutputBuffers(); encoderOutputBuffers = mVideoEncoder.getOutputBuffers();
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// should happen before receiving buffers, and should only happen once // should happen before receiving buffers, and should only happen once
if (mMuxerStarted) { if (mMuxerStarted) {
throw new RuntimeException("format changed twice"); throw new RuntimeException("format changed twice");
} }
MediaFormat newFormat = mEncoder.getOutputFormat(); MediaFormat newFormat = mVideoEncoder.getOutputFormat();
// now that we have the Magic Goodies, start the muxer // now that we have the Magic Goodies, start the muxer
mTrackIndex = mMuxer.addTrack(newFormat); mTrackIndex = mMuxer.addTrack(newFormat);
mMuxer.start(); mMuxer.start();
mMuxerStarted = true; mMuxerStarted = true;
} else if (encoderStatus < 0) { } 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 // let's ignore it
} else { } else {
ByteBuffer encodedData = encoderOutputBuffers[encoderStatus]; ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
@ -182,11 +196,11 @@ class VideoCoreEncoder {
mMuxer.writeSampleData(mTrackIndex, encodedData, mBufferInfo); mMuxer.writeSampleData(mTrackIndex, encodedData, mBufferInfo);
} }
mEncoder.releaseOutputBuffer(encoderStatus, false); mVideoEncoder.releaseOutputBuffer(encoderStatus, false);
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
if (!endOfStream) { if (!endOfStream) {
Log.w("VideoCoreEncoder", "reached end of stream unexpectedly"); Log.w("OldMediaEncoderCore", "reached end of stream unexpectedly");
} }
break; // out of while 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 String TAG = SnapshotVideoRecorder.class.getSimpleName();
private static final CameraLogger LOG = CameraLogger.create(TAG); 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_RECORDING = 0;
private static final int STATE_NOT_RECORDING = 1; private static final int STATE_NOT_RECORDING = 1;
private VideoTextureEncoder mEncoder; private OldMediaEncoder mEncoder;
private MediaEncoderEngine mEncoderEngine;
private GLCameraPreview mPreview; private GLCameraPreview mPreview;
private int mCurrentState = STATE_NOT_RECORDING; private int mCurrentState = STATE_NOT_RECORDING;
@ -31,7 +33,9 @@ class SnapshotVideoRecorder extends VideoRecorder implements GLCameraPreview.Ren
SnapshotVideoRecorder(VideoResult stub, VideoResultListener listener, GLCameraPreview preview) { SnapshotVideoRecorder(VideoResult stub, VideoResultListener listener, GLCameraPreview preview) {
super(stub, listener); super(stub, listener);
mEncoder = new VideoTextureEncoder(); if (USE_OLD_ENCODER) {
mEncoder = new OldMediaEncoder();
}
mPreview = preview; mPreview = preview;
mPreview.addRendererFrameCallback(this); mPreview.addRendererFrameCallback(this);
} }
@ -76,39 +80,72 @@ class SnapshotVideoRecorder extends VideoRecorder implements GLCameraPreview.Ren
case H_264: type = "video/avc"; break; // MediaFormat.MIMETYPE_VIDEO_AVC: case H_264: type = "video/avc"; break; // MediaFormat.MIMETYPE_VIDEO_AVC:
case DEVICE_DEFAULT: type = "video/avc"; break; case DEVICE_DEFAULT: type = "video/avc"; break;
} }
VideoTextureEncoder.Config configuration = new VideoTextureEncoder.Config( if (USE_OLD_ENCODER) {
mResult.getFile(), OldMediaEncoder.Config configuration = new OldMediaEncoder.Config(
width, mResult.getFile(),
height, width,
1000000, height,
30, 1000000,
mResult.getRotation(), 30,
scaleX, mResult.getRotation(),
scaleY, scaleX,
type, scaleY,
EGL14.eglGetCurrentContext() 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();
}
mResult.rotation = 0; // We will rotate the result instead. mResult.rotation = 0; // We will rotate the result instead.
mEncoder.startRecording(configuration);
mEncoder.setTextureId(mTextureId);
mCurrentState = STATE_RECORDING; mCurrentState = STATE_RECORDING;
} }
if (mCurrentState == STATE_RECORDING) { if (mCurrentState == STATE_RECORDING) {
mEncoder.frameAvailable(surfaceTexture); 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 (mCurrentState == STATE_RECORDING && mDesiredState == STATE_NOT_RECORDING) {
mEncoder.stopRecording(new Runnable() { if (USE_OLD_ENCODER) {
@Override mEncoder.stopRecording(new Runnable() {
public void run() { @Override
// We are in the encoder thread. public void run() {
dispatchResult(); // 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;
}
mCurrentState = STATE_NOT_RECORDING; mCurrentState = STATE_NOT_RECORDING;
mEncoder = null;
mPreview.removeRendererFrameCallback(SnapshotVideoRecorder.this); mPreview.removeRendererFrameCallback(SnapshotVideoRecorder.this);
mPreview = null; mPreview = null;
} }

Loading…
Cancel
Save