|
|
@ -44,17 +44,27 @@ public class AudioMediaEncoder extends MediaEncoder { |
|
|
|
private static final int SAMPLE_SIZE = 2; // byte/sample/channel
|
|
|
|
private static final int SAMPLE_SIZE = 2; // byte/sample/channel
|
|
|
|
private static final int BYTE_RATE_PER_CHANNEL = SAMPLING_FREQUENCY * SAMPLE_SIZE; // byte/sec/channel
|
|
|
|
private static final int BYTE_RATE_PER_CHANNEL = SAMPLING_FREQUENCY * SAMPLE_SIZE; // byte/sec/channel
|
|
|
|
private static final int BYTE_RATE = BYTE_RATE_PER_CHANNEL * CHANNELS_COUNT; // byte/sec
|
|
|
|
private static final int BYTE_RATE = BYTE_RATE_PER_CHANNEL * CHANNELS_COUNT; // byte/sec
|
|
|
|
|
|
|
|
@SuppressWarnings("unused") |
|
|
|
static final int BIT_RATE = BYTE_RATE * 8; // bit/sec
|
|
|
|
private static final int BIT_RATE = BYTE_RATE * 8; // bit/sec
|
|
|
|
|
|
|
|
|
|
|
|
// We call FRAME here the chunk of data that we want to read at each loop cycle
|
|
|
|
// We call FRAME here the chunk of data that we want to read at each loop cycle
|
|
|
|
private static final int FRAME_SIZE_PER_CHANNEL = 1024; // bytes/frame/channel [AAC constant]
|
|
|
|
private static final int FRAME_SIZE_PER_CHANNEL = 1024; // bytes/frame/channel [AAC constant]
|
|
|
|
private static final int FRAME_SIZE = FRAME_SIZE_PER_CHANNEL * CHANNELS_COUNT; // bytes/frame
|
|
|
|
private static final int FRAME_SIZE = FRAME_SIZE_PER_CHANNEL * CHANNELS_COUNT; // bytes/frame
|
|
|
|
|
|
|
|
|
|
|
|
// We allocate buffers of 1KB each, which is not so much. I would say that allocating
|
|
|
|
// We allocate buffers of 1KB each, which is not so much. This value indicates the maximum
|
|
|
|
// at most 200 of them is a reasonable value. With the current setup, in device tests,
|
|
|
|
// number of these buffers that we can allocate at a given instant.
|
|
|
|
// we manage to use 50 at most.
|
|
|
|
// This value is the number of runnables that the encoder thread is allowed to be 'behind'
|
|
|
|
private static final int BUFFER_POOL_MAX_SIZE = 200; |
|
|
|
// the recorder thread. It's not safe to have it very large or we can end encoding A LOT AFTER
|
|
|
|
|
|
|
|
// the actual recording. It's better to reduce this and skip recording at all.
|
|
|
|
|
|
|
|
private static final int BUFFER_POOL_MAX_SIZE = 60; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static long bytesToUs(int bytes) { |
|
|
|
|
|
|
|
return (1000000L * bytes) / BYTE_RATE; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static long bytesToUs(long bytes) { |
|
|
|
|
|
|
|
return (1000000L * bytes) / BYTE_RATE; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private boolean mRequestStop = false; |
|
|
|
private boolean mRequestStop = false; |
|
|
|
private AudioEncodingHandler mEncoder; |
|
|
|
private AudioEncodingHandler mEncoder; |
|
|
@ -169,21 +179,21 @@ public class AudioMediaEncoder extends MediaEncoder { |
|
|
|
private void read(boolean endOfStream) { |
|
|
|
private void read(boolean endOfStream) { |
|
|
|
mCurrentBuffer = mByteBufferPool.get(); |
|
|
|
mCurrentBuffer = mByteBufferPool.get(); |
|
|
|
if (mCurrentBuffer == null) { |
|
|
|
if (mCurrentBuffer == null) { |
|
|
|
LOG.e("read thread - Skipping audio frame, encoding is too slow."); |
|
|
|
LOG.e("read thread - eos:", endOfStream, "- Skipping audio frame, encoding is too slow."); |
|
|
|
// TODO should fix the next presentation time here.
|
|
|
|
// Should fix the next presentation time here, but
|
|
|
|
} else { |
|
|
|
} else { |
|
|
|
mCurrentBuffer.clear(); |
|
|
|
mCurrentBuffer.clear(); |
|
|
|
mReadBytes = mAudioRecord.read(mCurrentBuffer, FRAME_SIZE); |
|
|
|
mReadBytes = mAudioRecord.read(mCurrentBuffer, FRAME_SIZE); |
|
|
|
LOG.v("read thread - Read new audio frame. Bytes:", mReadBytes); |
|
|
|
LOG.i("read thread - eos:", endOfStream, "- Read new audio frame. Bytes:", mReadBytes); |
|
|
|
if (mReadBytes > 0) { // Good read: increase PTS.
|
|
|
|
if (mReadBytes > 0) { // Good read: increase PTS.
|
|
|
|
mLastTimeUs = increaseTime(mReadBytes); |
|
|
|
mLastTimeUs = increaseTime(mReadBytes); |
|
|
|
LOG.v("read thread - Increasing PTS to", mLastTimeUs); |
|
|
|
LOG.i("read thread - eos:", endOfStream, "- Frame PTS:", mLastTimeUs); |
|
|
|
mCurrentBuffer.limit(mReadBytes); |
|
|
|
mCurrentBuffer.limit(mReadBytes); |
|
|
|
onBuffer(endOfStream); |
|
|
|
onBuffer(endOfStream); |
|
|
|
} else if (mReadBytes == AudioRecord.ERROR_INVALID_OPERATION) { |
|
|
|
} else if (mReadBytes == AudioRecord.ERROR_INVALID_OPERATION) { |
|
|
|
LOG.e("read thread - Got AudioRecord.ERROR_INVALID_OPERATION"); |
|
|
|
LOG.e("read thread - eos:", endOfStream, "- Got AudioRecord.ERROR_INVALID_OPERATION"); |
|
|
|
} else if (mReadBytes == AudioRecord.ERROR_BAD_VALUE) { |
|
|
|
} else if (mReadBytes == AudioRecord.ERROR_BAD_VALUE) { |
|
|
|
LOG.e("read thread - Got AudioRecord.ERROR_BAD_VALUE"); |
|
|
|
LOG.e("read thread - eos:", endOfStream, "- Got AudioRecord.ERROR_BAD_VALUE"); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
@ -198,14 +208,6 @@ public class AudioMediaEncoder extends MediaEncoder { |
|
|
|
mEncoder.sendInputBuffer(mCurrentBuffer, mLastTimeUs, endOfStream); |
|
|
|
mEncoder.sendInputBuffer(mCurrentBuffer, mLastTimeUs, endOfStream); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private long bytesToUs(int bytes) { |
|
|
|
|
|
|
|
return (1000000L * bytes) / BYTE_RATE; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private long bytesToUs(long bytes) { |
|
|
|
|
|
|
|
return (1000000L * bytes) / BYTE_RATE; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private long increaseTime(int readBytes) { |
|
|
|
private long increaseTime(int readBytes) { |
|
|
|
return increaseTime3(readBytes); |
|
|
|
return increaseTime3(readBytes); |
|
|
|
} |
|
|
|
} |
|
|
@ -238,6 +240,10 @@ public class AudioMediaEncoder extends MediaEncoder { |
|
|
|
/** |
|
|
|
/** |
|
|
|
* This method looks like an improvement over {@link #increaseTime1(int)} as it |
|
|
|
* This method looks like an improvement over {@link #increaseTime1(int)} as it |
|
|
|
* accounts for the current time as well. Adapted & improved. from Kickflip. |
|
|
|
* accounts for the current time as well. Adapted & improved. from Kickflip. |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* This creates regular timestamps unless we accumulate a lot of delay (greater than |
|
|
|
|
|
|
|
* twice the buffer duration), in which case it creates a gap and starts again trying |
|
|
|
|
|
|
|
* to be regular from the new point. |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
private long increaseTime3(int readBytes) { |
|
|
|
private long increaseTime3(int readBytes) { |
|
|
|
long bufferDurationUs = bytesToUs(readBytes); |
|
|
|
long bufferDurationUs = bytesToUs(readBytes); |
|
|
@ -295,10 +301,11 @@ public class AudioMediaEncoder extends MediaEncoder { |
|
|
|
super.handleMessage(msg); |
|
|
|
super.handleMessage(msg); |
|
|
|
boolean endOfStream = msg.what == 1; |
|
|
|
boolean endOfStream = msg.what == 1; |
|
|
|
long timestamp = (((long) msg.arg1) << 32) | (((long) msg.arg2) & 0xffffffffL); |
|
|
|
long timestamp = (((long) msg.arg1) << 32) | (((long) msg.arg2) & 0xffffffffL); |
|
|
|
LOG.v("encoding thread - got buffer. timestamp:", timestamp, "eos:", endOfStream); |
|
|
|
LOG.i("encoding thread - got buffer. timestamp:", timestamp, "eos:", endOfStream); |
|
|
|
ByteBuffer buffer = (ByteBuffer) msg.obj; |
|
|
|
ByteBuffer buffer = (ByteBuffer) msg.obj; |
|
|
|
int readBytes = buffer.remaining(); |
|
|
|
int readBytes = buffer.remaining(); |
|
|
|
InputBuffer inputBuffer = mInputBufferPool.get(); |
|
|
|
InputBuffer inputBuffer = mInputBufferPool.get(); |
|
|
|
|
|
|
|
//noinspection ConstantConditions
|
|
|
|
inputBuffer.source = buffer; |
|
|
|
inputBuffer.source = buffer; |
|
|
|
inputBuffer.timestamp = timestamp; |
|
|
|
inputBuffer.timestamp = timestamp; |
|
|
|
inputBuffer.length = readBytes; |
|
|
|
inputBuffer.length = readBytes; |
|
|
@ -308,7 +315,7 @@ public class AudioMediaEncoder extends MediaEncoder { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private void performPendingOps(boolean force) { |
|
|
|
private void performPendingOps(boolean force) { |
|
|
|
LOG.v("encoding thread - performing", mPendingOps.size(), "pending operations."); |
|
|
|
LOG.i("encoding thread - performing", mPendingOps.size(), "pending operations. force:", force); |
|
|
|
InputBuffer buffer; |
|
|
|
InputBuffer buffer; |
|
|
|
while ((buffer = mPendingOps.peek()) != null) { |
|
|
|
while ((buffer = mPendingOps.peek()) != null) { |
|
|
|
if (force) { |
|
|
|
if (force) { |
|
|
@ -323,20 +330,43 @@ public class AudioMediaEncoder extends MediaEncoder { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private void performPendingOp(InputBuffer buffer) { |
|
|
|
private void performPendingOp(InputBuffer buffer) { |
|
|
|
LOG.v("encoding thread - performing pending operation for timestamp:", buffer.timestamp); |
|
|
|
LOG.i("encoding thread - performing pending operation for timestamp:", buffer.timestamp, "- encoding."); |
|
|
|
buffer.data.put(buffer.source); |
|
|
|
buffer.data.put(buffer.source); // TODO this copy is prob. the worst part here for performance
|
|
|
|
mByteBufferPool.recycle(buffer.source); |
|
|
|
mByteBufferPool.recycle(buffer.source); |
|
|
|
mPendingOps.remove(buffer); |
|
|
|
mPendingOps.remove(buffer); |
|
|
|
LOG.v("encoding thread - performing pending operation for timestamp:", buffer.timestamp, "- encoding."); |
|
|
|
|
|
|
|
encodeInputBuffer(buffer); |
|
|
|
encodeInputBuffer(buffer); |
|
|
|
boolean eos = buffer.isEndOfStream; |
|
|
|
boolean eos = buffer.isEndOfStream; |
|
|
|
mInputBufferPool.recycle(buffer); |
|
|
|
mInputBufferPool.recycle(buffer); |
|
|
|
LOG.v("encoding thread - performing pending operation for timestamp:", buffer.timestamp, "- draining."); |
|
|
|
if (eos) mInputBufferPool.clear(); |
|
|
|
drainOutput(eos); |
|
|
|
LOG.i("encoding thread - performing pending operation for timestamp:", buffer.timestamp, "- draining."); |
|
|
|
if (eos) { |
|
|
|
// NOTE: can consider calling this drainOutput on yet another thread, which would let us
|
|
|
|
mInputBufferPool.clear(); |
|
|
|
// use an even smaller BUFFER_POOL_MAX_SIZE without losing audio frames. But this way
|
|
|
|
WorkerHandler.get("AudioEncodingHandler").getThread().interrupt(); |
|
|
|
// we can accumulate delay on this new thread without noticing (no pool getting empty).
|
|
|
|
|
|
|
|
if (true) { |
|
|
|
|
|
|
|
drainOutput(eos); |
|
|
|
|
|
|
|
if (eos) WorkerHandler.get("AudioEncodingHandler").getThread().interrupt(); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
// Testing the option above.
|
|
|
|
|
|
|
|
WorkerHandler.get("AudioEncodingDrainer").remove(drainRunnable); |
|
|
|
|
|
|
|
WorkerHandler.get("AudioEncodingDrainer").remove(drainRunnableEos); |
|
|
|
|
|
|
|
WorkerHandler.get("AudioEncodingDrainer").post(eos ? drainRunnableEos : drainRunnable); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private final Runnable drainRunnable = new Runnable() { |
|
|
|
|
|
|
|
@Override |
|
|
|
|
|
|
|
public void run() { |
|
|
|
|
|
|
|
drainOutput(false); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private final Runnable drainRunnableEos = new Runnable() { |
|
|
|
|
|
|
|
@Override |
|
|
|
|
|
|
|
public void run() { |
|
|
|
|
|
|
|
drainOutput(true); |
|
|
|
|
|
|
|
WorkerHandler.get("AudioEncodingHandler").getThread().interrupt(); |
|
|
|
|
|
|
|
WorkerHandler.get("AudioEncodingDrainer").getThread().interrupt(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|