diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/Pool.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/Pool.java index 4a0408b9..76df6ccb 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/Pool.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/Pool.java @@ -9,7 +9,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; /** - * Base class for pools of recycleable objects. + * Base class for thread-safe pools of recycleable objects. * @param the object type */ public class Pool { @@ -19,8 +19,9 @@ public class Pool { private int maxPoolSize; private int activeCount; - private LinkedBlockingQueue mQueue; + private LinkedBlockingQueue queue; private Factory factory; + private final Object lock = new Object(); /** * Used to create new instances of objects when needed. @@ -37,7 +38,7 @@ public class Pool { */ public Pool(int maxPoolSize, @NonNull Factory factory) { this.maxPoolSize = maxPoolSize; - this.mQueue = new LinkedBlockingQueue<>(maxPoolSize); + this.queue = new LinkedBlockingQueue<>(maxPoolSize); this.factory = factory; } @@ -48,7 +49,9 @@ public class Pool { * @return whether the pool is empty */ public boolean isEmpty() { - return count() >= maxPoolSize; + synchronized (lock) { + return count() >= maxPoolSize; + } } /** @@ -60,21 +63,23 @@ public class Pool { */ @Nullable public T get() { - T item = mQueue.poll(); - if (item != null) { - activeCount++; // poll decreases, this fixes - LOG.v("GET - Reusing recycled item.", this); - return item; - } - - if (isEmpty()) { - LOG.v("GET - Returning null. Too much items requested.", this); - return null; + synchronized (lock) { + T item = queue.poll(); + if (item != null) { + activeCount++; // poll decreases, this fixes + LOG.v("GET - Reusing recycled item.", this); + return item; + } + + if (isEmpty()) { + LOG.v("GET - Returning null. Too much items requested.", this); + return null; + } + + activeCount++; + LOG.v("GET - Creating a new item.", this); + return factory.create(); } - - activeCount++; - LOG.v("GET - Creating a new item.", this); - return factory.create(); } /** @@ -84,16 +89,18 @@ public class Pool { * @param item used item */ public void recycle(@NonNull T item) { - LOG.v("RECYCLE - Recycling item.", this); - if (--activeCount < 0) { - throw new IllegalStateException("Trying to recycle an item which makes activeCount < 0." + - "This means that this or some previous items being recycled were not coming from " + - "this pool, or some item was recycled more than once. " + this); - } - if (!mQueue.offer(item)) { - throw new IllegalStateException("Trying to recycle an item while the queue is full. " + - "This means that this or some previous items being recycled were not coming from " + - "this pool, or some item was recycled more than once. " + this); + synchronized (lock) { + LOG.v("RECYCLE - Recycling item.", this); + if (--activeCount < 0) { + throw new IllegalStateException("Trying to recycle an item which makes activeCount < 0." + + "This means that this or some previous items being recycled were not coming from " + + "this pool, or some item was recycled more than once. " + this); + } + if (!queue.offer(item)) { + throw new IllegalStateException("Trying to recycle an item while the queue is full. " + + "This means that this or some previous items being recycled were not coming from " + + "this pool, or some item was recycled more than once. " + this); + } } } @@ -102,7 +109,9 @@ public class Pool { */ @CallSuper public void clear() { - mQueue.clear(); + synchronized (lock) { + queue.clear(); + } } /** @@ -114,7 +123,9 @@ public class Pool { */ @SuppressWarnings("WeakerAccess") public final int count() { - return activeCount() + recycledCount(); + synchronized (lock) { + return activeCount() + recycledCount(); + } } /** @@ -125,7 +136,9 @@ public class Pool { */ @SuppressWarnings("WeakerAccess") public final int activeCount() { - return activeCount; + synchronized (lock) { + return activeCount; + } } /** @@ -137,7 +150,9 @@ public class Pool { */ @SuppressWarnings("WeakerAccess") public final int recycledCount() { - return mQueue.size(); + synchronized (lock) { + return queue.size(); + } } @NonNull diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioMediaEncoder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioMediaEncoder.java index d10ae194..f2f025d2 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioMediaEncoder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioMediaEncoder.java @@ -56,7 +56,7 @@ public class AudioMediaEncoder extends MediaEncoder { // This value is the number of runnables that the encoder thread is allowed to be 'behind' // 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 final int BUFFER_POOL_MAX_SIZE = 100; private static long bytesToUs(int bytes) { return (1000000L * bytes) / BYTE_RATE; @@ -175,8 +175,19 @@ public class AudioMediaEncoder extends MediaEncoder { private void read(boolean endOfStream) { mCurrentBuffer = mByteBufferPool.get(); if (mCurrentBuffer == null) { - LOG.e("read thread - eos:", endOfStream, "- Skipping audio frame, encoding is too slow."); - // Should fix the next presentation time here, but + // This can happen and it means that encoding is slow with respect to recording. + // One might be tempted to fix precisely the next frame presentation time when this happens, + // but this is not needed because the current increaseTime() algorithm will consider delays + // when they get large. + // Sleeping before returning is a good way of balancing the two operations. + // However, if endOfStream, we CAN'T lose this frame! + if (endOfStream) { + LOG.v("read thread - eos:", endOfStream, "- No buffer, retrying."); + read(true); // try again + } else { + LOG.w("read thread - eos:", endOfStream, "- Skipping audio frame, encoding is too slow."); + sleep(); + } } else { mCurrentBuffer.clear(); mReadBytes = mAudioRecord.read(mCurrentBuffer, FRAME_SIZE); @@ -185,7 +196,7 @@ public class AudioMediaEncoder extends MediaEncoder { mLastTimeUs = increaseTime(mReadBytes); LOG.i("read thread - eos:", endOfStream, "- Frame PTS:", mLastTimeUs); mCurrentBuffer.limit(mReadBytes); - onBuffer(endOfStream); + mEncoder.sendInputBuffer(mCurrentBuffer, mLastTimeUs, endOfStream); } else if (mReadBytes == AudioRecord.ERROR_INVALID_OPERATION) { LOG.e("read thread - eos:", endOfStream, "- Got AudioRecord.ERROR_INVALID_OPERATION"); } else if (mReadBytes == AudioRecord.ERROR_BAD_VALUE) { @@ -195,13 +206,13 @@ public class AudioMediaEncoder extends MediaEncoder { } /** - * New data at position buffer.position() of size buffer.remaining() - * has been written into this buffer. This method should pass the data - * to the consumer. + * Sleeps for a frame duration, to skip it. This can be used to slow down + * the recording operation to balance it with encoding. */ - private void onBuffer(boolean endOfStream) { - LOG.v("read thread - Sending buffer to encoder thread."); - mEncoder.sendInputBuffer(mCurrentBuffer, mLastTimeUs, endOfStream); + private void sleep() { + try { + Thread.sleep(bytesToUs(FRAME_SIZE) / 1000); + } catch (InterruptedException ignore) {} } private long increaseTime(int readBytes) { @@ -309,18 +320,14 @@ public class AudioMediaEncoder extends MediaEncoder { inputBuffer.length = readBytes; inputBuffer.isEndOfStream = endOfStream; mPendingOps.add(inputBuffer); - performPendingOps(endOfStream); - } - private void performPendingOps(boolean force) { - LOG.i("encoding thread - performing", mPendingOps.size(), "pending operations. force:", force); - InputBuffer buffer; - while ((buffer = mPendingOps.peek()) != null) { - if (force) { - acquireInputBuffer(buffer); - performPendingOp(buffer); - } else if (tryAcquireInputBuffer(buffer)) { - performPendingOp(buffer); + LOG.i("encoding thread - performing", mPendingOps.size(), "pending operations."); + while ((inputBuffer = mPendingOps.peek()) != null) { + if (endOfStream) { + acquireInputBuffer(inputBuffer); + performPendingOp(inputBuffer); + } else if (tryAcquireInputBuffer(inputBuffer)) { + performPendingOp(inputBuffer); } else { break; // Will try later. } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoder.java index 77f6fa55..f90b0822 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoder.java @@ -138,10 +138,9 @@ abstract class MediaEncoder { */ final void prepare(@NonNull final MediaEncoderEngine.Controller controller, final long maxLengthMillis) { if (mState >= STATE_PREPARING) { - LOG.e("Wrong state while preparing. Aborting.", mState); + LOG.e(mName, "Wrong state while preparing. Aborting.", mState); return; } - setState(STATE_PREPARING); mController = controller; mBufferInfo = new MediaCodec.BufferInfo(); mMaxLengthMillis = maxLengthMillis; @@ -151,6 +150,7 @@ abstract class MediaEncoder { @Override public void run() { LOG.i(mName, "Prepare was called. Executing."); + setState(STATE_PREPARING); onPrepare(controller, maxLengthMillis); setState(STATE_PREPARED); } @@ -168,15 +168,15 @@ abstract class MediaEncoder { * NOTE: it's important to call {@link WorkerHandler#post(Runnable)} instead of run()! */ final void start() { - if (mState < STATE_PREPARED || mState >= STATE_STARTING) { - LOG.e("Wrong state while starting. Aborting.", mState); - return; - } - setState(STATE_STARTING); LOG.w(mName, "Start was called. Posting."); mWorker.post(new Runnable() { @Override public void run() { + if (mState < STATE_PREPARED || mState >= STATE_STARTING) { + LOG.e(mName, "Wrong state while starting. Aborting.", mState); + return; + } + setState(STATE_STARTING); LOG.w(mName, "Start was called. Executing."); onStart(); } @@ -213,8 +213,8 @@ abstract class MediaEncoder { * NOTE: it's important to call {@link WorkerHandler#post(Runnable)} instead of run()! */ final void stop() { - if (mState >= STATE_LIMIT_REACHED) { - LOG.e("Wrong state while stopping. Aborting.", mState); + if (mState >= STATE_STOPPING) { + LOG.e(mName, "Wrong state while stopping. Aborting.", mState); return; } setState(STATE_STOPPING);