diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseTest.java index 03ab2945..f7b53309 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseTest.java @@ -33,8 +33,6 @@ import static org.mockito.Mockito.mock; public class BaseTest { - public static CameraLogger LOG = CameraLogger.create("Test"); - private static KeyguardManager.KeyguardLock keyguardLock; private static PowerManager.WakeLock wakeLock; diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java index 14787e17..3b9fd5a2 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java @@ -10,6 +10,7 @@ import android.os.Build; import com.otaliastudios.cameraview.BaseTest; import com.otaliastudios.cameraview.CameraException; import com.otaliastudios.cameraview.CameraListener; +import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.CameraOptions; import com.otaliastudios.cameraview.CameraUtils; import com.otaliastudios.cameraview.CameraView; @@ -62,7 +63,9 @@ import static org.mockito.Mockito.when; public abstract class CameraIntegrationTest extends BaseTest { - private final static long DELAY = 9000; + private final static CameraLogger LOG = CameraLogger.create(CameraIntegrationTest.class.getSimpleName()); + private final static long DELAY = 8000; + private final static long VIDEO_DELAY = 16000; @Rule public ActivityTestRule rule = new ActivityTestRule<>(TestActivity.class); @@ -82,6 +85,7 @@ public abstract class CameraIntegrationTest extends BaseTest { @Before public void setUp() { + LOG.e("Test started. Setting up camera."); WorkerHandler.destroy(); ui(new Runnable() { @@ -120,7 +124,7 @@ public abstract class CameraIntegrationTest extends BaseTest { @After public void tearDown() { - camera.stopVideo(); + LOG.e("Test ended. Tearing down camera."); camera.destroy(); WorkerHandler.destroy(); } @@ -141,7 +145,6 @@ public abstract class CameraIntegrationTest extends BaseTest { assertNotNull("Can open", result); // Extra wait for the bind state. // TODO fix this and other while {} in this class in a more elegant way. - //noinspection StatementWithEmptyBody while (controller.getBindState() != CameraEngine.STATE_STARTED) {} } else { assertNull("Should not open", result); @@ -172,7 +175,7 @@ public abstract class CameraIntegrationTest extends BaseTest { return argument.getReason() == CameraException.REASON_VIDEO_FAILED; } })); - VideoResult result = video.await(DELAY); + VideoResult result = video.await(VIDEO_DELAY); if (expectSuccess) { assertNotNull("Should end video", result); } else { @@ -624,7 +627,7 @@ public abstract class CameraIntegrationTest extends BaseTest { camera.takePicture(); boolean did = latch.await(4, TimeUnit.SECONDS); assertFalse(did); - assertEquals(latch.getCount(), 1); + assertEquals(1, latch.getCount()); } @SuppressWarnings("StatementWithEmptyBody") 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 64b3aff8..5978629c 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 @@ -44,17 +44,27 @@ public class AudioMediaEncoder extends MediaEncoder { 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 = BYTE_RATE_PER_CHANNEL * CHANNELS_COUNT; // byte/sec - - static final int BIT_RATE = BYTE_RATE * 8; // bit/sec + @SuppressWarnings("unused") + 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 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 - // We allocate buffers of 1KB each, which is not so much. I would say that allocating - // at most 200 of them is a reasonable value. With the current setup, in device tests, - // we manage to use 50 at most. - private static final int BUFFER_POOL_MAX_SIZE = 200; + // We allocate buffers of 1KB each, which is not so much. This value indicates the maximum + // number of these buffers that we can allocate at a given instant. + // 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 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 AudioEncodingHandler mEncoder; @@ -169,21 +179,21 @@ public class AudioMediaEncoder extends MediaEncoder { private void read(boolean endOfStream) { mCurrentBuffer = mByteBufferPool.get(); if (mCurrentBuffer == null) { - LOG.e("read thread - Skipping audio frame, encoding is too slow."); - // TODO should fix the next presentation time here. + LOG.e("read thread - eos:", endOfStream, "- Skipping audio frame, encoding is too slow."); + // Should fix the next presentation time here, but } else { mCurrentBuffer.clear(); 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. mLastTimeUs = increaseTime(mReadBytes); - LOG.v("read thread - Increasing PTS to", mLastTimeUs); + LOG.i("read thread - eos:", endOfStream, "- Frame PTS:", mLastTimeUs); mCurrentBuffer.limit(mReadBytes); onBuffer(endOfStream); } 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) { - 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); } - 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) { return increaseTime3(readBytes); } @@ -238,6 +240,10 @@ public class AudioMediaEncoder extends MediaEncoder { /** * This method looks like an improvement over {@link #increaseTime1(int)} as it * 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) { long bufferDurationUs = bytesToUs(readBytes); @@ -295,10 +301,11 @@ public class AudioMediaEncoder extends MediaEncoder { super.handleMessage(msg); boolean endOfStream = msg.what == 1; 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; int readBytes = buffer.remaining(); InputBuffer inputBuffer = mInputBufferPool.get(); + //noinspection ConstantConditions inputBuffer.source = buffer; inputBuffer.timestamp = timestamp; inputBuffer.length = readBytes; @@ -308,7 +315,7 @@ public class AudioMediaEncoder extends MediaEncoder { } 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; while ((buffer = mPendingOps.peek()) != null) { if (force) { @@ -323,20 +330,43 @@ public class AudioMediaEncoder extends MediaEncoder { } private void performPendingOp(InputBuffer buffer) { - LOG.v("encoding thread - performing pending operation for timestamp:", buffer.timestamp); - buffer.data.put(buffer.source); + LOG.i("encoding thread - performing pending operation for timestamp:", buffer.timestamp, "- encoding."); + buffer.data.put(buffer.source); // TODO this copy is prob. the worst part here for performance mByteBufferPool.recycle(buffer.source); mPendingOps.remove(buffer); - LOG.v("encoding thread - performing pending operation for timestamp:", buffer.timestamp, "- encoding."); encodeInputBuffer(buffer); boolean eos = buffer.isEndOfStream; mInputBufferPool.recycle(buffer); - LOG.v("encoding thread - performing pending operation for timestamp:", buffer.timestamp, "- draining."); - drainOutput(eos); - if (eos) { - mInputBufferPool.clear(); - WorkerHandler.get("AudioEncodingHandler").getThread().interrupt(); + if (eos) mInputBufferPool.clear(); + LOG.i("encoding thread - performing pending operation for timestamp:", buffer.timestamp, "- draining."); + // NOTE: can consider calling this drainOutput on yet another thread, which would let us + // use an even smaller BUFFER_POOL_MAX_SIZE without losing audio frames. But this way + // 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(); + } + }; } } 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 f54277cb..b7881416 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 @@ -108,11 +108,11 @@ abstract class MediaEncoder { * @param data object */ final void notify(final @NonNull String event, final @Nullable Object data) { - LOG.i(getName(), "Notify was called. Posting."); + LOG.v(getName(), "Notify was called. Posting."); mWorker.post(new Runnable() { @Override public void run() { - LOG.i(getName(), "Notify was called. Executing."); + LOG.v(getName(), "Notify was called. Executing."); onEvent(event, data); } }); @@ -321,9 +321,7 @@ abstract class MediaEncoder { // TODO fix the mBufferInfo being the same, then implement delayed writing in Controller // and remove the isStarted() check here. OutputBuffer buffer = mOutputBufferPool.get(); - if (buffer == null) { - throw new IllegalStateException("buffer is null!"); - } + //noinspection ConstantConditions buffer.info = mBufferInfo; buffer.trackIndex = mTrackIndex; buffer.data = encodedData; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoderEngine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoderEngine.java index 3666db54..cdc079c1 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoderEngine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoderEngine.java @@ -149,7 +149,7 @@ public class MediaEncoderEngine { /** * Asks encoders to stop. This is not sync, of course we will ask for encoders * to call {@link Controller#notifyReleased(int)} before actually stop the muxer. - * When all encoders request a release, {@link #release()} is called to do cleanup + * When all encoders request a release, {@link #end()} is called to do cleanup * and notify the listener. */ public final void stop() { @@ -163,8 +163,8 @@ public class MediaEncoderEngine { * Called after all encoders have requested a release using {@link Controller#notifyReleased(int)}. * At this point we will do cleanup and notify the listener. */ - private void release() { - LOG.i("release:", "Releasing muxer after all encoders have been released."); + private void end() { + LOG.i("end:", "Releasing muxer after all encoders have been released."); Exception error = null; if (mMediaMuxer != null) { // stop() throws an exception if you haven't fed it any data. @@ -178,7 +178,7 @@ public class MediaEncoderEngine { } mMediaMuxer = null; } - LOG.w("release:", "Dispatching end to listener - reason:", mEndReason, "error:", error); + LOG.w("end:", "Dispatching end to listener - reason:", mEndReason, "error:", error); if (mListener != null) { mListener.onEncodingEnd(mEndReason, error); mListener = null; @@ -187,7 +187,7 @@ public class MediaEncoderEngine { mStartedEncodersCount = 0; mReleasedEncodersCount = 0; mMediaMuxerStarted = false; - LOG.i("release:", "Completed."); + LOG.i("end:", "Completed."); } /** @@ -299,7 +299,7 @@ public class MediaEncoderEngine { LOG.w("notifyReleased:", "Called for track", track); if (++mReleasedEncodersCount == mEncoders.size()) { LOG.w("requestStop:", "All encoders have been released. Stopping the muxer."); - release(); + end(); } } }