parent
439e49d513
commit
cfc46afaf2
@ -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; |
||||
} |
||||
} |
||||
}); |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue