|
|
@ -1,8 +1,10 @@ |
|
|
|
package com.otaliastudios.cameraview; |
|
|
|
package com.otaliastudios.cameraview; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import android.annotation.SuppressLint; |
|
|
|
import android.media.MediaCodec; |
|
|
|
import android.media.MediaCodec; |
|
|
|
import android.media.MediaFormat; |
|
|
|
import android.media.MediaFormat; |
|
|
|
import android.os.Build; |
|
|
|
import android.os.Build; |
|
|
|
|
|
|
|
|
|
|
|
import androidx.annotation.NonNull; |
|
|
|
import androidx.annotation.NonNull; |
|
|
|
import androidx.annotation.Nullable; |
|
|
|
import androidx.annotation.Nullable; |
|
|
|
import androidx.annotation.RequiresApi; |
|
|
|
import androidx.annotation.RequiresApi; |
|
|
@ -14,17 +16,107 @@ import java.nio.ByteBuffer; |
|
|
|
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) |
|
|
|
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) |
|
|
|
abstract class MediaEncoder { |
|
|
|
abstract class MediaEncoder { |
|
|
|
|
|
|
|
|
|
|
|
private final static int TIMEOUT_USEC = 10000; // 10 msec
|
|
|
|
private final static String TAG = MediaEncoder.class.getSimpleName(); |
|
|
|
|
|
|
|
private final static CameraLogger LOG = CameraLogger.create(TAG); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Did some test to see which value would maximize our performance in the current setup (infinite audio pool).
|
|
|
|
|
|
|
|
// Measured the time it would take to write a 30 seconds video. Based on this, we'll go with TIMEOUT=0 for now.
|
|
|
|
|
|
|
|
// INPUT_TIMEOUT_US 10000: 46 seconds
|
|
|
|
|
|
|
|
// INPUT_TIMEOUT_US 1000: 37 seconds
|
|
|
|
|
|
|
|
// INPUT_TIMEOUT_US 100: 33 seconds
|
|
|
|
|
|
|
|
// INPUT_TIMEOUT_US 0: 32 seconds
|
|
|
|
|
|
|
|
private final static int INPUT_TIMEOUT_US = 0; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 0 also seems to be the best, although it does not change so much.
|
|
|
|
|
|
|
|
// Can't go too high or this is a bottleneck for the audio encoder.
|
|
|
|
|
|
|
|
private final static int OUTPUT_TIMEOUT_US = 0; |
|
|
|
|
|
|
|
|
|
|
|
@SuppressWarnings("WeakerAccess") |
|
|
|
@SuppressWarnings("WeakerAccess") |
|
|
|
protected MediaCodec mMediaCodec; |
|
|
|
protected MediaCodec mMediaCodec; |
|
|
|
|
|
|
|
|
|
|
|
private MediaCodec.BufferInfo mBufferInfo; |
|
|
|
@SuppressWarnings("WeakerAccess") |
|
|
|
|
|
|
|
protected WorkerHandler mWorker; |
|
|
|
|
|
|
|
|
|
|
|
private MediaEncoderEngine.Controller mController; |
|
|
|
private MediaEncoderEngine.Controller mController; |
|
|
|
private int mTrackIndex; |
|
|
|
private int mTrackIndex; |
|
|
|
|
|
|
|
private OutputBufferPool mOutputBufferPool; |
|
|
|
|
|
|
|
private MediaCodec.BufferInfo mBufferInfo; |
|
|
|
|
|
|
|
private MediaCodecBuffers mBuffers; |
|
|
|
private long mMaxLengthMillis; |
|
|
|
private long mMaxLengthMillis; |
|
|
|
private boolean mMaxLengthReached; |
|
|
|
private boolean mMaxLengthReached; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* A readable name for the thread. |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
@NonNull |
|
|
|
|
|
|
|
abstract String getName(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* This encoder was attached to the engine. Keep the controller |
|
|
|
|
|
|
|
* and run the internal thread. |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
final void prepare(@NonNull final MediaEncoderEngine.Controller controller, final long maxLengthMillis) { |
|
|
|
|
|
|
|
mController = controller; |
|
|
|
|
|
|
|
mBufferInfo = new MediaCodec.BufferInfo(); |
|
|
|
|
|
|
|
mMaxLengthMillis = maxLengthMillis; |
|
|
|
|
|
|
|
mWorker = WorkerHandler.get(getName()); |
|
|
|
|
|
|
|
LOG.i(getName(), "Prepare was called. Posting."); |
|
|
|
|
|
|
|
mWorker.post(new Runnable() { |
|
|
|
|
|
|
|
@Override |
|
|
|
|
|
|
|
public void run() { |
|
|
|
|
|
|
|
LOG.i(getName(), "Prepare was called. Executing."); |
|
|
|
|
|
|
|
onPrepare(controller, maxLengthMillis); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Start recording. This might be a lightweight operation |
|
|
|
|
|
|
|
* in case the encoder needs to wait for a certain event |
|
|
|
|
|
|
|
* like a "frame available". |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
final void start() { |
|
|
|
|
|
|
|
LOG.i(getName(), "Start was called. Posting."); |
|
|
|
|
|
|
|
mWorker.post(new Runnable() { |
|
|
|
|
|
|
|
@Override |
|
|
|
|
|
|
|
public void run() { |
|
|
|
|
|
|
|
LOG.i(getName(), "Start was called. Executing."); |
|
|
|
|
|
|
|
onStart(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* The caller notifying of a certain event occurring. |
|
|
|
|
|
|
|
* Should analyze the string and see if the event is important. |
|
|
|
|
|
|
|
* @param event what happened |
|
|
|
|
|
|
|
* @param data object |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
final void notify(final @NonNull String event, final @Nullable Object data) { |
|
|
|
|
|
|
|
LOG.i(getName(), "Notify was called. Posting."); |
|
|
|
|
|
|
|
mWorker.post(new Runnable() { |
|
|
|
|
|
|
|
@Override |
|
|
|
|
|
|
|
public void run() { |
|
|
|
|
|
|
|
LOG.i(getName(), "Notify was called. Executing."); |
|
|
|
|
|
|
|
onEvent(event, data); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Stop recording. |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
final void stop() { |
|
|
|
|
|
|
|
LOG.i(getName(), "Stop was called. Posting."); |
|
|
|
|
|
|
|
mWorker.post(new Runnable() { |
|
|
|
|
|
|
|
@Override |
|
|
|
|
|
|
|
public void run() { |
|
|
|
|
|
|
|
LOG.i(getName(), "Stop was called. Executing."); |
|
|
|
|
|
|
|
onStop(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* Called to prepare this encoder before starting. |
|
|
|
* Called to prepare this encoder before starting. |
|
|
|
* Any initialization should be done here as it does not interfere with the original |
|
|
|
* Any initialization should be done here as it does not interfere with the original |
|
|
@ -33,13 +125,10 @@ abstract class MediaEncoder { |
|
|
|
* At this point subclasses MUST create the {@link #mMediaCodec} object. |
|
|
|
* At this point subclasses MUST create the {@link #mMediaCodec} object. |
|
|
|
* |
|
|
|
* |
|
|
|
* @param controller the muxer controller |
|
|
|
* @param controller the muxer controller |
|
|
|
|
|
|
|
* @param maxLengthMillis the maxLength in millis |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
@EncoderThread |
|
|
|
@EncoderThread |
|
|
|
void prepare(@NonNull MediaEncoderEngine.Controller controller, long maxLengthMillis) { |
|
|
|
abstract void onPrepare(@NonNull final MediaEncoderEngine.Controller controller, final long maxLengthMillis); |
|
|
|
mController = controller; |
|
|
|
|
|
|
|
mBufferInfo = new MediaCodec.BufferInfo(); |
|
|
|
|
|
|
|
mMaxLengthMillis = maxLengthMillis; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* Start recording. This might be a lightweight operation |
|
|
|
* Start recording. This might be a lightweight operation |
|
|
@ -47,7 +136,7 @@ abstract class MediaEncoder { |
|
|
|
* like a "frame available". |
|
|
|
* like a "frame available". |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
@EncoderThread |
|
|
|
@EncoderThread |
|
|
|
abstract void start(); |
|
|
|
abstract void onStart(); |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* The caller notifying of a certain event occurring. |
|
|
|
* The caller notifying of a certain event occurring. |
|
|
@ -56,97 +145,130 @@ abstract class MediaEncoder { |
|
|
|
* @param data object |
|
|
|
* @param data object |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
@EncoderThread |
|
|
|
@EncoderThread |
|
|
|
abstract void notify(@NonNull String event, @Nullable Object data); |
|
|
|
abstract void onEvent(@NonNull String event, @Nullable Object data); |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* Stop recording. |
|
|
|
* Stop recording. |
|
|
|
* This MUST happen SYNCHRONOUSLY! |
|
|
|
|
|
|
|
*/ |
|
|
|
*/ |
|
|
|
@EncoderThread |
|
|
|
@EncoderThread |
|
|
|
abstract void stop(); |
|
|
|
abstract void onStop(); |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* Release resources here. |
|
|
|
* Called by {@link #drainOutput(boolean)} when we get an EOS signal (not necessarily in the |
|
|
|
|
|
|
|
* parameters, might also be through an input buffer flag). |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
@EncoderThread |
|
|
|
private void release() { |
|
|
|
void release() { |
|
|
|
LOG.w("Subclass", getName(), "Notified that it is released."); |
|
|
|
if (mMediaCodec != null) { |
|
|
|
mController.requestRelease(mTrackIndex); |
|
|
|
mMediaCodec.stop(); |
|
|
|
mMediaCodec.stop(); |
|
|
|
mMediaCodec.release(); |
|
|
|
mMediaCodec.release(); |
|
|
|
mMediaCodec = null; |
|
|
|
mMediaCodec = null; |
|
|
|
|
|
|
|
mOutputBufferPool.clear(); |
|
|
|
|
|
|
|
mOutputBufferPool = null; |
|
|
|
|
|
|
|
mBuffers = null; |
|
|
|
|
|
|
|
onRelease(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* This is called when we are stopped. |
|
|
|
|
|
|
|
* It is a good moment to release all resources, although the muxer |
|
|
|
|
|
|
|
* might still be alive (we wait for the other Encoder, see Controller). |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
abstract void onRelease(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Returns a new input buffer and index, waiting at most {@link #INPUT_TIMEOUT_US} if none is available. |
|
|
|
|
|
|
|
* Callers should check the boolean result - true if the buffer was filled. |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
@SuppressWarnings("WeakerAccess") |
|
|
|
|
|
|
|
protected boolean tryAcquireInputBuffer(@NonNull InputBuffer holder) { |
|
|
|
|
|
|
|
if (mBuffers == null) { |
|
|
|
|
|
|
|
mBuffers = new MediaCodecBuffers(mMediaCodec); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
int inputBufferIndex = mMediaCodec.dequeueInputBuffer(INPUT_TIMEOUT_US); |
|
|
|
|
|
|
|
if (inputBufferIndex < 0) { |
|
|
|
|
|
|
|
return false; |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
holder.index = inputBufferIndex; |
|
|
|
|
|
|
|
holder.data = mBuffers.getInputBuffer(inputBufferIndex); |
|
|
|
|
|
|
|
return true; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Returns a new input buffer and index, waiting indefinitely if none is available. |
|
|
|
|
|
|
|
* The buffer should be written into, then the index should be passed to {@link #encodeInputBuffer(InputBuffer)}. |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
@SuppressWarnings({"StatementWithEmptyBody", "WeakerAccess"}) |
|
|
|
|
|
|
|
protected void acquireInputBuffer(@NonNull InputBuffer holder) { |
|
|
|
|
|
|
|
while (!tryAcquireInputBuffer(holder)) {} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* Encode data into the {@link #mMediaCodec}. |
|
|
|
* Encode data into the {@link #mMediaCodec}. |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
@SuppressWarnings("WeakerAccess") |
|
|
|
@SuppressWarnings("WeakerAccess") |
|
|
|
protected void encode(@Nullable final ByteBuffer buffer, final int length, final long presentationTimeUs) { |
|
|
|
protected void encodeInputBuffer(InputBuffer buffer) { |
|
|
|
final ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers(); |
|
|
|
LOG.w("ENCODING:", getName(), "Buffer:", buffer.index, "Bytes:", buffer.length, "Presentation:", buffer.timestamp); |
|
|
|
while (true) { |
|
|
|
if (buffer.isEndOfStream) { // send EOS
|
|
|
|
final int inputBufferIndex = mMediaCodec.dequeueInputBuffer(TIMEOUT_USEC); |
|
|
|
mMediaCodec.queueInputBuffer(buffer.index, 0, 0, |
|
|
|
if (inputBufferIndex >= 0) { |
|
|
|
buffer.timestamp, MediaCodec.BUFFER_FLAG_END_OF_STREAM); |
|
|
|
final ByteBuffer inputBuffer = inputBuffers[inputBufferIndex]; |
|
|
|
|
|
|
|
inputBuffer.clear(); |
|
|
|
|
|
|
|
if (buffer != null) { |
|
|
|
|
|
|
|
inputBuffer.put(buffer); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (length <= 0) { // send EOS
|
|
|
|
|
|
|
|
mMediaCodec.queueInputBuffer(inputBufferIndex, 0, 0, |
|
|
|
|
|
|
|
presentationTimeUs, MediaCodec.BUFFER_FLAG_END_OF_STREAM); |
|
|
|
|
|
|
|
} else { |
|
|
|
} else { |
|
|
|
mMediaCodec.queueInputBuffer(inputBufferIndex, 0, length, |
|
|
|
mMediaCodec.queueInputBuffer(buffer.index, 0, buffer.length, |
|
|
|
presentationTimeUs, 0); |
|
|
|
buffer.timestamp, 0); |
|
|
|
} |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
} else if (inputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) { |
|
|
|
|
|
|
|
// wait for MediaCodec encoder is ready to encode
|
|
|
|
|
|
|
|
// nothing to do here because MediaCodec#dequeueInputBuffer(TIMEOUT_USEC)
|
|
|
|
|
|
|
|
// will wait for maximum TIMEOUT_USEC(10msec) on each call
|
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Signals the end of input stream. This is a Video only API, as in the normal case, |
|
|
|
|
|
|
|
* we use input buffers to signal the end. In the video case, we don't have input buffers |
|
|
|
|
|
|
|
* because we use an input surface instead. |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
@SuppressWarnings("WeakerAccess") |
|
|
|
|
|
|
|
protected void signalEndOfInputStream() { |
|
|
|
|
|
|
|
mMediaCodec.signalEndOfInputStream(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* Extracts all pending data that was written and encoded into {@link #mMediaCodec}, |
|
|
|
* Extracts all pending data that was written and encoded into {@link #mMediaCodec}, |
|
|
|
* and forwards it to the muxer. |
|
|
|
* and forwards it to the muxer. |
|
|
|
* <p> |
|
|
|
* |
|
|
|
* If endOfStream is not set, this returns when there is no more data to drain. If it |
|
|
|
* If drainAll is not set, this returns after TIMEOUT_USEC if there is no more data to drain. |
|
|
|
* is set, we send EOS to the encoder, and then iterate until we see EOS on the output. |
|
|
|
* If drainAll is set, we wait until we see EOS on the output. |
|
|
|
* Calling this with endOfStream set should be done once, right before stopping the muxer. |
|
|
|
* Calling this with drainAll set should be done once, right before stopping the muxer. |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
|
|
|
|
@SuppressLint("LogNotTimber") |
|
|
|
@SuppressWarnings("WeakerAccess") |
|
|
|
@SuppressWarnings("WeakerAccess") |
|
|
|
protected void drain(boolean endOfStream) { |
|
|
|
protected void drainOutput(boolean drainAll) { |
|
|
|
if (endOfStream) { |
|
|
|
LOG.w("DRAINING:", getName(), "EOS:", drainAll); |
|
|
|
mMediaCodec.signalEndOfInputStream(); |
|
|
|
if (mMediaCodec == null) { |
|
|
|
|
|
|
|
LOG.e("drain() was called before prepare() or after releasing."); |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (mBuffers == null) { |
|
|
|
|
|
|
|
mBuffers = new MediaCodecBuffers(mMediaCodec); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
ByteBuffer[] encoderOutputBuffers = mMediaCodec.getOutputBuffers(); |
|
|
|
|
|
|
|
while (true) { |
|
|
|
while (true) { |
|
|
|
int encoderStatus = mMediaCodec.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC); |
|
|
|
int encoderStatus = mMediaCodec.dequeueOutputBuffer(mBufferInfo, OUTPUT_TIMEOUT_US); |
|
|
|
if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) { |
|
|
|
if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) { |
|
|
|
// no output available yet
|
|
|
|
// no output available yet
|
|
|
|
if (!endOfStream) break; // out of while
|
|
|
|
if (!drainAll) break; // out of while
|
|
|
|
|
|
|
|
|
|
|
|
} 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 = mMediaCodec.getOutputBuffers(); |
|
|
|
mBuffers.onOutputBuffersChanged(); |
|
|
|
|
|
|
|
|
|
|
|
} 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 (mController.isStarted()) throw new RuntimeException("format changed twice"); |
|
|
|
if (mController.isStarted()) throw new RuntimeException("MediaFormat changed twice."); |
|
|
|
MediaFormat newFormat = mMediaCodec.getOutputFormat(); |
|
|
|
MediaFormat newFormat = mMediaCodec.getOutputFormat(); |
|
|
|
|
|
|
|
mTrackIndex = mController.requestStart(newFormat); |
|
|
|
// now that we have the Magic Goodies, start the muxer
|
|
|
|
mOutputBufferPool = new OutputBufferPool(mTrackIndex); |
|
|
|
mTrackIndex = mController.start(newFormat); |
|
|
|
|
|
|
|
} else if (encoderStatus < 0) { |
|
|
|
} else if (encoderStatus < 0) { |
|
|
|
Log.w("VideoMediaEncoder", "unexpected result from encoder.dequeueOutputBuffer: " + encoderStatus); |
|
|
|
LOG.e("Unexpected result from dequeueOutputBuffer: " + encoderStatus); |
|
|
|
// let's ignore it
|
|
|
|
// let's ignore it
|
|
|
|
} else { |
|
|
|
} else { |
|
|
|
ByteBuffer encodedData = encoderOutputBuffers[encoderStatus]; |
|
|
|
ByteBuffer encodedData = mBuffers.getOutputBuffer(encoderStatus); |
|
|
|
if (encodedData == null) { |
|
|
|
|
|
|
|
throw new RuntimeException("encoderOutputBuffer " + encoderStatus + " was null"); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Codec config means that config data was pulled out and fed to the muxer when we got
|
|
|
|
// Codec config means that config data was pulled out and fed to the muxer when we got
|
|
|
|
// the INFO_OUTPUT_FORMAT_CHANGED status. Ignore it.
|
|
|
|
// the INFO_OUTPUT_FORMAT_CHANGED status. Ignore it.
|
|
|
@ -155,41 +277,56 @@ abstract class MediaEncoder { |
|
|
|
// adjust the ByteBuffer values to match BufferInfo (not needed?)
|
|
|
|
// adjust the ByteBuffer values to match BufferInfo (not needed?)
|
|
|
|
encodedData.position(mBufferInfo.offset); |
|
|
|
encodedData.position(mBufferInfo.offset); |
|
|
|
encodedData.limit(mBufferInfo.offset + mBufferInfo.size); |
|
|
|
encodedData.limit(mBufferInfo.offset + mBufferInfo.size); |
|
|
|
mController.write(mTrackIndex, encodedData, mBufferInfo); |
|
|
|
// Store startPresentationTime and lastPresentationTime, useful for example to
|
|
|
|
mLastPresentationTime = mBufferInfo.presentationTimeUs; |
|
|
|
// detect the mMaxLengthReached and stop recording.
|
|
|
|
if (mStartPresentationTime == 0) { |
|
|
|
if (mStartPresentationTimeUs == Long.MIN_VALUE) { |
|
|
|
mStartPresentationTime = mLastPresentationTime; |
|
|
|
mStartPresentationTimeUs = mBufferInfo.presentationTimeUs; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
mLastPresentationTimeUs = mBufferInfo.presentationTimeUs; |
|
|
|
|
|
|
|
// Pass presentation times as offets with respect to the mStartPresentationTimeUs.
|
|
|
|
|
|
|
|
// This ensures consistency between audio pts (coming from System.nanoTime()) and
|
|
|
|
|
|
|
|
// video pts (coming from SurfaceTexture) both of which have no meaningful time-base
|
|
|
|
|
|
|
|
// and should be used for offsets only.
|
|
|
|
|
|
|
|
// TODO find a better way, this causes sync issues. (+ note: this sends pts=0 at first)
|
|
|
|
|
|
|
|
// mBufferInfo.presentationTimeUs = mLastPresentationTimeUs - mStartPresentationTimeUs;
|
|
|
|
|
|
|
|
LOG.i("DRAINING:", getName(), "Dispatching write(). Presentation:", mBufferInfo.presentationTimeUs); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// TODO fix the mBufferInfo being the same, then implement delayed writing in Controller
|
|
|
|
|
|
|
|
// and remove the isStarted() check here.
|
|
|
|
|
|
|
|
OutputBuffer buffer = mOutputBufferPool.get(); |
|
|
|
|
|
|
|
buffer.info = mBufferInfo; |
|
|
|
|
|
|
|
buffer.trackIndex = mTrackIndex; |
|
|
|
|
|
|
|
buffer.data = encodedData; |
|
|
|
|
|
|
|
mController.write(mOutputBufferPool, buffer); |
|
|
|
} |
|
|
|
} |
|
|
|
mMediaCodec.releaseOutputBuffer(encoderStatus, false); |
|
|
|
mMediaCodec.releaseOutputBuffer(encoderStatus, false); |
|
|
|
if (!mMaxLengthReached) { |
|
|
|
|
|
|
|
if (mLastPresentationTime / 1000 - mStartPresentationTime / 1000 > mMaxLengthMillis) { |
|
|
|
// Check for the maxLength constraint (with appropriate conditions)
|
|
|
|
|
|
|
|
// Not needed if drainAll because we already were asked to stop
|
|
|
|
|
|
|
|
if (!drainAll |
|
|
|
|
|
|
|
&& !mMaxLengthReached |
|
|
|
|
|
|
|
&& mStartPresentationTimeUs != Long.MIN_VALUE |
|
|
|
|
|
|
|
&& mLastPresentationTimeUs - mStartPresentationTimeUs > mMaxLengthMillis * 1000) { |
|
|
|
|
|
|
|
LOG.w("DRAINING: Reached maxLength! mLastPresentationTimeUs:", mLastPresentationTimeUs, |
|
|
|
|
|
|
|
"mStartPresentationTimeUs:", mStartPresentationTimeUs, |
|
|
|
|
|
|
|
"mMaxLengthUs:", mMaxLengthMillis * 1000); |
|
|
|
mMaxLengthReached = true; |
|
|
|
mMaxLengthReached = true; |
|
|
|
// Log.e("MediaEncoder", this.getClass().getSimpleName() + " requested stop at " + (mLastPresentationTime * 1000 * 1000));
|
|
|
|
mController.requestStop(mTrackIndex); |
|
|
|
mController.requestStop(); |
|
|
|
|
|
|
|
break; |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Check for the EOS flag so we can release the encoder.
|
|
|
|
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { |
|
|
|
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { |
|
|
|
break; // out of while
|
|
|
|
LOG.w("DRAINING:", getName(), "Dispatching release()."); |
|
|
|
|
|
|
|
release(); |
|
|
|
|
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private long mStartPresentationTime = 0; |
|
|
|
private long mStartPresentationTimeUs = Long.MIN_VALUE; |
|
|
|
private long mLastPresentationTime = 0; |
|
|
|
private long mLastPresentationTimeUs = 0; |
|
|
|
|
|
|
|
|
|
|
|
long getPresentationTime() { |
|
|
|
|
|
|
|
long result = System.nanoTime() / 1000L; |
|
|
|
|
|
|
|
// presentationTimeUs should be monotonic
|
|
|
|
|
|
|
|
// otherwise muxer fail to write
|
|
|
|
|
|
|
|
if (result < mLastPresentationTime) { |
|
|
|
|
|
|
|
result = (mLastPresentationTime - result) + result; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return result; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
abstract int getBitRate(); |
|
|
|
abstract int getEncodedBitRate(); |
|
|
|
} |
|
|
|
} |
|
|
|