parent
c669308ba8
commit
d7e0c3beed
@ -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