* Use onStopVideo callback for restoring preview

* Fix takeVideoSnapshot without duration

* Add tests

* Add comments

* Remove extra log line
pull/562/head
Mattia Iavarone 5 years ago committed by GitHub
parent 445b742455
commit eeec7ac222
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 41
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java
  2. 13
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java
  3. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java
  4. 4
      cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioMediaEncoder.java
  5. 22
      cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoder.java
  6. 20
      cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoderEngine.java
  7. 6
      cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/TextureMediaEncoder.java
  8. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/VideoMediaEncoder.java

@ -6,6 +6,7 @@ import android.graphics.Canvas;
import android.graphics.PointF; import android.graphics.PointF;
import android.hardware.Camera; import android.hardware.Camera;
import android.os.Build; import android.os.Build;
import android.os.Handler;
import com.otaliastudios.cameraview.BaseTest; import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.CameraException; import com.otaliastudios.cameraview.CameraException;
@ -526,9 +527,6 @@ public abstract class CameraIntegrationTest extends BaseTest {
@Test @Test
public void testStartEndVideo() { public void testStartEndVideo() {
// Fails on Travis. Some emulators can't deal with MediaRecorder,
// Error while starting MediaRecorder. java.lang.RuntimeException: start failed.
// as documented. This works locally though.
camera.setMode(Mode.VIDEO); camera.setMode(Mode.VIDEO);
openSync(true); openSync(true);
takeVideoSync(true, 4000); takeVideoSync(true, 4000);
@ -543,6 +541,43 @@ public abstract class CameraIntegrationTest extends BaseTest {
waitForVideoResult(true); waitForVideoResult(true);
} }
@Test
public void testStartEndVideo_withManualStop() {
camera.setMode(Mode.VIDEO);
openSync(true);
takeVideoSync(true);
uiSync(new Runnable() {
@Override
public void run() {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
camera.stopVideo();
}
}, 5000);
}
});
waitForVideoResult(true);
}
@Test
public void testStartEndVideoSnapshot_withManualStop() {
openSync(true);
takeVideoSnapshotSync(true);
uiSync(new Runnable() {
@Override
public void run() {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
camera.stopVideo();
}
}, 5000);
}
});
waitForVideoResult(true);
}
@Test @Test
public void testEndVideo_withoutStarting() { public void testEndVideo_withoutStarting() {
camera.setMode(Mode.VIDEO); camera.setMode(Mode.VIDEO);

@ -726,11 +726,13 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
} }
@Override @Override
public void onVideoResult(@Nullable VideoResult.Stub result, @Nullable Exception exception) { protected void onStopVideo() {
boolean wasRecordingFullVideo = mVideoRecorder instanceof Full2VideoRecorder; // When video ends, we have to restart the repeating request for TEMPLATE_PREVIEW,
super.onVideoResult(result, exception); // this time without the video recorder surface. We do this before stopping the
if (wasRecordingFullVideo) { // recorder. If we stop first, the camera will try to fill an "abandoned" Surface
// We have to stop all repeating requests and restart them. // and, on some devices with a poor internal implementation, this crashes. See #549
boolean isFullVideo = mVideoRecorder instanceof Full2VideoRecorder;
if (isFullVideo) {
try { try {
createRepeatingRequestBuilder(CameraDevice.TEMPLATE_PREVIEW); createRepeatingRequestBuilder(CameraDevice.TEMPLATE_PREVIEW);
addRepeatingRequestBuilderSurfaces(); addRepeatingRequestBuilderSurfaces();
@ -739,6 +741,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
throw createCameraException(e); throw createCameraException(e);
} }
} }
super.onStopVideo();
} }
//endregion //endregion

@ -1188,13 +1188,17 @@ public abstract class CameraEngine implements
@Override @Override
public void run() { public void run() {
LOG.i("stopVideo", "executing.", "isTakingVideo?", isTakingVideo()); LOG.i("stopVideo", "executing.", "isTakingVideo?", isTakingVideo());
onStopVideo();
}
});
}
protected void onStopVideo() {
if (mVideoRecorder != null) { if (mVideoRecorder != null) {
mVideoRecorder.stop(false); mVideoRecorder.stop(false);
mVideoRecorder = null; mVideoRecorder = null;
} }
} }
});
}
@CallSuper @CallSuper
@Override @Override

@ -62,7 +62,7 @@ public class AudioMediaEncoder extends MediaEncoder {
@EncoderThread @EncoderThread
@Override @Override
protected void onPrepare(@NonNull MediaEncoderEngine.Controller controller, long maxLengthMillis) { protected void onPrepare(@NonNull MediaEncoderEngine.Controller controller, long maxLengthUs) {
final MediaFormat audioFormat = MediaFormat.createAudioFormat( final MediaFormat audioFormat = MediaFormat.createAudioFormat(
mConfig.mimeType, mConfig.mimeType,
mConfig.samplingFrequency, mConfig.samplingFrequency,
@ -248,7 +248,7 @@ public class AudioMediaEncoder extends MediaEncoder {
// See if we reached the max length value. // See if we reached the max length value.
if (!hasReachedMaxLength()) { if (!hasReachedMaxLength()) {
boolean didReachMaxLength = (mLastTimeUs - mFirstTimeUs) > getMaxLengthMillis() * 1000L; boolean didReachMaxLength = (mLastTimeUs - mFirstTimeUs) > getMaxLengthUs();
if (didReachMaxLength && !endOfStream) { if (didReachMaxLength && !endOfStream) {
LOG.w("read thread - this frame reached the maxLength! deltaUs:", mLastTimeUs - mFirstTimeUs); LOG.w("read thread - this frame reached the maxLength! deltaUs:", mLastTimeUs - mFirstTimeUs);
notifyMaxLengthReached(); notifyMaxLengthReached();

@ -122,7 +122,7 @@ public abstract class MediaEncoder {
private MediaCodecBuffers mBuffers; private MediaCodecBuffers mBuffers;
private final Map<String, AtomicInteger> mPendingEvents = new HashMap<>(); private final Map<String, AtomicInteger> mPendingEvents = new HashMap<>();
private long mMaxLengthMillis; private long mMaxLengthUs;
private boolean mMaxLengthReached; private boolean mMaxLengthReached;
private long mStartTimeMillis = 0; // In System.currentTimeMillis() private long mStartTimeMillis = 0; // In System.currentTimeMillis()
@ -171,14 +171,14 @@ public abstract class MediaEncoder {
* works, we might have {@link #onStop()} or {@link #onStopped()} to be executed before * works, we might have {@link #onStop()} or {@link #onStopped()} to be executed before
* the previous step has completed. * the previous step has completed.
*/ */
final void prepare(@NonNull final MediaEncoderEngine.Controller controller, final long maxLengthMillis) { final void prepare(@NonNull final MediaEncoderEngine.Controller controller, final long maxLengthUs) {
if (mState >= STATE_PREPARING) { if (mState >= STATE_PREPARING) {
LOG.e(mName, "Wrong state while preparing. Aborting.", mState); LOG.e(mName, "Wrong state while preparing. Aborting.", mState);
return; return;
} }
mController = controller; mController = controller;
mBufferInfo = new MediaCodec.BufferInfo(); mBufferInfo = new MediaCodec.BufferInfo();
mMaxLengthMillis = maxLengthMillis; mMaxLengthUs = maxLengthUs;
mWorker = WorkerHandler.get(mName); mWorker = WorkerHandler.get(mName);
mWorker.getThread().setPriority(Thread.MAX_PRIORITY); mWorker.getThread().setPriority(Thread.MAX_PRIORITY);
LOG.i(mName, "Prepare was called. Posting."); LOG.i(mName, "Prepare was called. Posting.");
@ -187,7 +187,7 @@ public abstract class MediaEncoder {
public void run() { public void run() {
LOG.i(mName, "Prepare was called. Executing."); LOG.i(mName, "Prepare was called. Executing.");
setState(STATE_PREPARING); setState(STATE_PREPARING);
onPrepare(controller, maxLengthMillis); onPrepare(controller, maxLengthUs);
setState(STATE_PREPARED); setState(STATE_PREPARED);
} }
}); });
@ -277,10 +277,10 @@ public 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 * @param maxLengthUs the maxLength in microseconds
*/ */
@EncoderThread @EncoderThread
protected abstract void onPrepare(@NonNull final MediaEncoderEngine.Controller controller, final long maxLengthMillis); protected abstract void onPrepare(@NonNull MediaEncoderEngine.Controller controller, long maxLengthUs);
/** /**
* Start recording. This might be a lightweight operation * Start recording. This might be a lightweight operation
@ -465,11 +465,11 @@ public abstract class MediaEncoder {
if (!drainAll if (!drainAll
&& !mMaxLengthReached && !mMaxLengthReached
&& mFirstTimeUs != Long.MIN_VALUE && mFirstTimeUs != Long.MIN_VALUE
&& mLastTimeUs - mFirstTimeUs > mMaxLengthMillis * 1000) { && mLastTimeUs - mFirstTimeUs > mMaxLengthUs) {
LOG.w(mName, "DRAINING - Reached maxLength! mLastTimeUs:", mLastTimeUs, LOG.w(mName, "DRAINING - Reached maxLength! mLastTimeUs:", mLastTimeUs,
"mStartTimeUs:", mFirstTimeUs, "mStartTimeUs:", mFirstTimeUs,
"mDeltaUs:", mLastTimeUs - mFirstTimeUs, "mDeltaUs:", mLastTimeUs - mFirstTimeUs,
"mMaxLengthUs:", mMaxLengthMillis * 1000); "mMaxLengthUs:", mMaxLengthUs);
onMaxLengthReached(); onMaxLengthReached();
break; break;
} }
@ -492,7 +492,7 @@ public abstract class MediaEncoder {
protected abstract int getEncodedBitRate(); protected abstract int getEncodedBitRate();
/** /**
* Returns the max length setting, in milliseconds, which can be used * Returns the max length setting, in microseconds, which can be used
* to compute the current state and eventually call {@link #notifyMaxLengthReached()}. * to compute the current state and eventually call {@link #notifyMaxLengthReached()}.
* This is not a requirement for subclasses - we do this check anyway when draining, * This is not a requirement for subclasses - we do this check anyway when draining,
* but doing so might be better. * but doing so might be better.
@ -500,8 +500,8 @@ public abstract class MediaEncoder {
* @return the max length setting * @return the max length setting
*/ */
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
protected long getMaxLengthMillis() { protected long getMaxLengthUs() {
return mMaxLengthMillis; return mMaxLengthUs;
} }
/** /**

@ -146,23 +146,23 @@ public class MediaEncoderEngine {
for (MediaEncoder encoder : mEncoders) { for (MediaEncoder encoder : mEncoders) {
bitRate += encoder.getEncodedBitRate(); bitRate += encoder.getEncodedBitRate();
} }
int bytePerSecond = bitRate / 8; int byteRate = bitRate / 8;
long sizeMaxDuration = (maxSize / bytePerSecond) * 1000L; long sizeMaxDurationUs = (maxSize / byteRate) * 1000L * 1000L;
long maxDurationUs = maxDuration * 1000L;
long finalMaxDuration = Long.MAX_VALUE; long finalMaxDurationUs = Long.MAX_VALUE;
if (maxSize > 0 && maxDuration > 0) { if (maxSize > 0 && maxDuration > 0) {
mPossibleEndReason = sizeMaxDuration < maxDuration ? END_BY_MAX_SIZE : END_BY_MAX_DURATION; mPossibleEndReason = sizeMaxDurationUs < maxDurationUs ? END_BY_MAX_SIZE : END_BY_MAX_DURATION;
finalMaxDuration = Math.min(sizeMaxDuration, maxDuration); finalMaxDurationUs = Math.min(sizeMaxDurationUs, maxDurationUs);
} else if (maxSize > 0) { } else if (maxSize > 0) {
mPossibleEndReason = END_BY_MAX_SIZE; mPossibleEndReason = END_BY_MAX_SIZE;
finalMaxDuration = sizeMaxDuration; finalMaxDurationUs = sizeMaxDurationUs;
} else if (maxDuration > 0) { } else if (maxDuration > 0) {
mPossibleEndReason = END_BY_MAX_DURATION; mPossibleEndReason = END_BY_MAX_DURATION;
finalMaxDuration = maxDuration; finalMaxDurationUs = maxDurationUs;
} }
LOG.w("Computed a max duration of", (finalMaxDuration / 1000F)); LOG.w("Computed a max duration of", (finalMaxDurationUs / 1000000F));
for (MediaEncoder encoder : mEncoders) { for (MediaEncoder encoder : mEncoders) {
encoder.prepare(mController, finalMaxDuration); encoder.prepare(mController, finalMaxDurationUs);
} }
} }

@ -89,12 +89,12 @@ public class TextureMediaEncoder extends VideoMediaEncoder<TextureConfig> {
@EncoderThread @EncoderThread
@Override @Override
protected void onPrepare(@NonNull MediaEncoderEngine.Controller controller, long maxLengthMillis) { protected void onPrepare(@NonNull MediaEncoderEngine.Controller controller, long maxLengthUs) {
// We rotate the texture using transformRotation. Pass rotation=0 to super so that // We rotate the texture using transformRotation. Pass rotation=0 to super so that
// no rotation metadata is written into the output file. // no rotation metadata is written into the output file.
mTransformRotation = mConfig.rotation; mTransformRotation = mConfig.rotation;
mConfig.rotation = 0; mConfig.rotation = 0;
super.onPrepare(controller, maxLengthMillis); super.onPrepare(controller, maxLengthUs);
mEglCore = new EglCore(mConfig.eglContext, EglCore.FLAG_RECORDABLE); mEglCore = new EglCore(mConfig.eglContext, EglCore.FLAG_RECORDABLE);
mWindow = new EglWindowSurface(mEglCore, mSurface, true); mWindow = new EglWindowSurface(mEglCore, mSurface, true);
mWindow.makeCurrent(); mWindow.makeCurrent();
@ -156,7 +156,7 @@ public class TextureMediaEncoder extends VideoMediaEncoder<TextureConfig> {
// Notify we have reached the max length value. // Notify we have reached the max length value.
if (mFirstTimeUs == Long.MIN_VALUE) mFirstTimeUs = frame.timestampUs(); if (mFirstTimeUs == Long.MIN_VALUE) mFirstTimeUs = frame.timestampUs();
if (!hasReachedMaxLength()) { if (!hasReachedMaxLength()) {
boolean didReachMaxLength = (frame.timestampUs() - mFirstTimeUs) > getMaxLengthMillis() * 1000L; boolean didReachMaxLength = (frame.timestampUs() - mFirstTimeUs) > getMaxLengthUs();
if (didReachMaxLength) { if (didReachMaxLength) {
LOG.w("onEvent -", LOG.w("onEvent -",
"frameNumber:", mFrameNumber, "frameNumber:", mFrameNumber,

@ -53,7 +53,7 @@ abstract class VideoMediaEncoder<C extends VideoConfig> extends MediaEncoder {
@EncoderThread @EncoderThread
@Override @Override
protected void onPrepare(@NonNull MediaEncoderEngine.Controller controller, long maxLengthMillis) { protected void onPrepare(@NonNull MediaEncoderEngine.Controller controller, long maxLengthUs) {
MediaFormat format = MediaFormat.createVideoFormat(mConfig.mimeType, mConfig.width, mConfig.height); MediaFormat format = MediaFormat.createVideoFormat(mConfig.mimeType, mConfig.width, mConfig.height);
// Failing to specify some of these can cause the MediaCodec configure() call to throw an unhelpful exception. // Failing to specify some of these can cause the MediaCodec configure() call to throw an unhelpful exception.

Loading…
Cancel
Save