Commit 94f3b1bf by kimvde Committed by microkatz

Add muxer timer to detect when generating an output sample is too slow

This allows to throw when the Transformer is stuck or is too slow.

PiperOrigin-RevId: 484179037
(cherry picked from commit 376ee77f)
parent 861cd9a0
...@@ -27,10 +27,27 @@ public final class DefaultMuxer implements Muxer { ...@@ -27,10 +27,27 @@ public final class DefaultMuxer implements Muxer {
/** A {@link Muxer.Factory} for {@link DefaultMuxer}. */ /** A {@link Muxer.Factory} for {@link DefaultMuxer}. */
public static final class Factory implements Muxer.Factory { public static final class Factory implements Muxer.Factory {
/** The default value returned by {@link #getMaxDelayBetweenSamplesMs()}. */
public static final long DEFAULT_MAX_DELAY_BETWEEN_SAMPLES_MS = 3000;
private final Muxer.Factory muxerFactory; private final Muxer.Factory muxerFactory;
/**
* Creates an instance with {@link Muxer#getMaxDelayBetweenSamplesMs() maxDelayBetweenSamplesMs}
* set to {@link #DEFAULT_MAX_DELAY_BETWEEN_SAMPLES_MS}.
*/
public Factory() { public Factory() {
this.muxerFactory = new FrameworkMuxer.Factory(); this.muxerFactory = new FrameworkMuxer.Factory(DEFAULT_MAX_DELAY_BETWEEN_SAMPLES_MS);
}
/**
* Creates an instance.
*
* @param maxDelayBetweenSamplesMs See {@link Muxer#getMaxDelayBetweenSamplesMs()}.
*/
public Factory(long maxDelayBetweenSamplesMs) {
this.muxerFactory = new FrameworkMuxer.Factory(maxDelayBetweenSamplesMs);
} }
@Override @Override
...@@ -71,4 +88,9 @@ public final class DefaultMuxer implements Muxer { ...@@ -71,4 +88,9 @@ public final class DefaultMuxer implements Muxer {
public void release(boolean forCancellation) throws MuxerException { public void release(boolean forCancellation) throws MuxerException {
muxer.release(forCancellation); muxer.release(forCancellation);
} }
@Override
public long getMaxDelayBetweenSamplesMs() {
return muxer.getMaxDelayBetweenSamplesMs();
}
} }
...@@ -55,10 +55,17 @@ import java.nio.ByteBuffer; ...@@ -55,10 +55,17 @@ import java.nio.ByteBuffer;
/** {@link Muxer.Factory} for {@link FrameworkMuxer}. */ /** {@link Muxer.Factory} for {@link FrameworkMuxer}. */
public static final class Factory implements Muxer.Factory { public static final class Factory implements Muxer.Factory {
private final long maxDelayBetweenSamplesMs;
public Factory(long maxDelayBetweenSamplesMs) {
this.maxDelayBetweenSamplesMs = maxDelayBetweenSamplesMs;
}
@Override @Override
public FrameworkMuxer create(String path) throws IOException { public FrameworkMuxer create(String path) throws IOException {
MediaMuxer mediaMuxer = new MediaMuxer(path, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); MediaMuxer mediaMuxer = new MediaMuxer(path, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
return new FrameworkMuxer(mediaMuxer); return new FrameworkMuxer(mediaMuxer, maxDelayBetweenSamplesMs);
} }
@RequiresApi(26) @RequiresApi(26)
...@@ -68,7 +75,7 @@ import java.nio.ByteBuffer; ...@@ -68,7 +75,7 @@ import java.nio.ByteBuffer;
new MediaMuxer( new MediaMuxer(
parcelFileDescriptor.getFileDescriptor(), parcelFileDescriptor.getFileDescriptor(),
MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
return new FrameworkMuxer(mediaMuxer); return new FrameworkMuxer(mediaMuxer, maxDelayBetweenSamplesMs);
} }
@Override @Override
...@@ -83,13 +90,15 @@ import java.nio.ByteBuffer; ...@@ -83,13 +90,15 @@ import java.nio.ByteBuffer;
} }
private final MediaMuxer mediaMuxer; private final MediaMuxer mediaMuxer;
private final long maxDelayBetweenSamplesMs;
private final MediaCodec.BufferInfo bufferInfo; private final MediaCodec.BufferInfo bufferInfo;
private final SparseLongArray trackIndexToLastPresentationTimeUs; private final SparseLongArray trackIndexToLastPresentationTimeUs;
private boolean isStarted; private boolean isStarted;
private FrameworkMuxer(MediaMuxer mediaMuxer) { private FrameworkMuxer(MediaMuxer mediaMuxer, long maxDelayBetweenSamplesMs) {
this.mediaMuxer = mediaMuxer; this.mediaMuxer = mediaMuxer;
this.maxDelayBetweenSamplesMs = maxDelayBetweenSamplesMs;
bufferInfo = new MediaCodec.BufferInfo(); bufferInfo = new MediaCodec.BufferInfo();
trackIndexToLastPresentationTimeUs = new SparseLongArray(); trackIndexToLastPresentationTimeUs = new SparseLongArray();
} }
...@@ -183,6 +192,11 @@ import java.nio.ByteBuffer; ...@@ -183,6 +192,11 @@ import java.nio.ByteBuffer;
} }
} }
@Override
public long getMaxDelayBetweenSamplesMs() {
return maxDelayBetweenSamplesMs;
}
// Accesses MediaMuxer state via reflection to ensure that muxer resources can be released even // Accesses MediaMuxer state via reflection to ensure that muxer resources can be released even
// if stopping fails. // if stopping fails.
@SuppressLint("PrivateApi") @SuppressLint("PrivateApi")
......
...@@ -110,4 +110,16 @@ public interface Muxer { ...@@ -110,4 +110,16 @@ public interface Muxer {
* forCancellation} is false. * forCancellation} is false.
*/ */
void release(boolean forCancellation) throws MuxerException; void release(boolean forCancellation) throws MuxerException;
/**
* Returns the maximum delay allowed between output samples, in milliseconds, or {@link
* C#TIME_UNSET} if there is no maximum.
*
* <p>This is the maximum delay between samples of any track. They can be of the same or of
* different track types.
*
* <p>This value is used to abort the transformation when the maximum delay is reached. Note that
* there is no guarantee that the transformation will be aborted exactly at that time.
*/
long getMaxDelayBetweenSamplesMs();
} }
...@@ -19,6 +19,7 @@ package com.google.android.exoplayer2.transformer; ...@@ -19,6 +19,7 @@ package com.google.android.exoplayer2.transformer;
import static com.google.android.exoplayer2.util.Assertions.checkState; import static com.google.android.exoplayer2.util.Assertions.checkState;
import static com.google.android.exoplayer2.util.Util.maxValue; import static com.google.android.exoplayer2.util.Util.maxValue;
import static com.google.android.exoplayer2.util.Util.minValue; import static com.google.android.exoplayer2.util.Util.minValue;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.util.SparseIntArray; import android.util.SparseIntArray;
import android.util.SparseLongArray; import android.util.SparseLongArray;
...@@ -29,6 +30,10 @@ import com.google.android.exoplayer2.util.MimeTypes; ...@@ -29,6 +30,10 @@ import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** /**
* A wrapper around a media muxer. * A wrapper around a media muxer.
...@@ -47,26 +52,33 @@ import java.nio.ByteBuffer; ...@@ -47,26 +52,33 @@ import java.nio.ByteBuffer;
private final Muxer muxer; private final Muxer muxer;
private final Muxer.Factory muxerFactory; private final Muxer.Factory muxerFactory;
private final Transformer.AsyncErrorListener asyncErrorListener;
private final SparseIntArray trackTypeToIndex; private final SparseIntArray trackTypeToIndex;
private final SparseIntArray trackTypeToSampleCount; private final SparseIntArray trackTypeToSampleCount;
private final SparseLongArray trackTypeToTimeUs; private final SparseLongArray trackTypeToTimeUs;
private final SparseLongArray trackTypeToBytesWritten; private final SparseLongArray trackTypeToBytesWritten;
private final ScheduledExecutorService abortScheduledExecutorService;
private int trackCount; private int trackCount;
private int trackFormatCount; private int trackFormatCount;
private boolean isReady; private boolean isReady;
private @C.TrackType int previousTrackType; private @C.TrackType int previousTrackType;
private long minTrackTimeUs; private long minTrackTimeUs;
private @MonotonicNonNull ScheduledFuture<?> abortScheduledFuture;
private boolean isAborted;
public MuxerWrapper(Muxer muxer, Muxer.Factory muxerFactory) { public MuxerWrapper(
Muxer muxer, Muxer.Factory muxerFactory, Transformer.AsyncErrorListener asyncErrorListener) {
this.muxer = muxer; this.muxer = muxer;
this.muxerFactory = muxerFactory; this.muxerFactory = muxerFactory;
this.asyncErrorListener = asyncErrorListener;
trackTypeToIndex = new SparseIntArray(); trackTypeToIndex = new SparseIntArray();
trackTypeToSampleCount = new SparseIntArray(); trackTypeToSampleCount = new SparseIntArray();
trackTypeToTimeUs = new SparseLongArray(); trackTypeToTimeUs = new SparseLongArray();
trackTypeToBytesWritten = new SparseLongArray(); trackTypeToBytesWritten = new SparseLongArray();
previousTrackType = C.TRACK_TYPE_NONE; previousTrackType = C.TRACK_TYPE_NONE;
abortScheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
} }
/** /**
...@@ -131,6 +143,7 @@ import java.nio.ByteBuffer; ...@@ -131,6 +143,7 @@ import java.nio.ByteBuffer;
trackFormatCount++; trackFormatCount++;
if (trackFormatCount == trackCount) { if (trackFormatCount == trackCount) {
isReady = true; isReady = true;
resetAbortTimer();
} }
} }
...@@ -168,6 +181,7 @@ import java.nio.ByteBuffer; ...@@ -168,6 +181,7 @@ import java.nio.ByteBuffer;
trackTypeToTimeUs.put(trackType, presentationTimeUs); trackTypeToTimeUs.put(trackType, presentationTimeUs);
} }
resetAbortTimer();
muxer.writeSampleData(trackIndex, data, isKeyFrame, presentationTimeUs); muxer.writeSampleData(trackIndex, data, isKeyFrame, presentationTimeUs);
previousTrackType = trackType; previousTrackType = trackType;
return true; return true;
...@@ -195,6 +209,7 @@ import java.nio.ByteBuffer; ...@@ -195,6 +209,7 @@ import java.nio.ByteBuffer;
*/ */
public void release(boolean forCancellation) throws Muxer.MuxerException { public void release(boolean forCancellation) throws Muxer.MuxerException {
isReady = false; isReady = false;
abortScheduledExecutorService.shutdownNow();
muxer.release(forCancellation); muxer.release(forCancellation);
} }
...@@ -257,4 +272,31 @@ import java.nio.ByteBuffer; ...@@ -257,4 +272,31 @@ import java.nio.ByteBuffer;
} }
return trackTimeUs - minTrackTimeUs <= MAX_TRACK_WRITE_AHEAD_US; return trackTimeUs - minTrackTimeUs <= MAX_TRACK_WRITE_AHEAD_US;
} }
private void resetAbortTimer() {
long maxDelayBetweenSamplesMs = muxer.getMaxDelayBetweenSamplesMs();
if (maxDelayBetweenSamplesMs == C.TIME_UNSET) {
return;
}
if (abortScheduledFuture != null) {
abortScheduledFuture.cancel(/* mayInterruptIfRunning= */ false);
}
abortScheduledFuture =
abortScheduledExecutorService.schedule(
() -> {
if (isAborted) {
return;
}
isAborted = true;
asyncErrorListener.onTransformationException(
TransformationException.createForMuxer(
new IllegalStateException(
"No output sample written in the last "
+ maxDelayBetweenSamplesMs
+ " milliseconds. Aborting transformation."),
TransformationException.ERROR_CODE_MUXING_FAILED));
},
maxDelayBetweenSamplesMs,
MILLISECONDS);
}
} }
...@@ -724,7 +724,9 @@ public final class Transformer { ...@@ -724,7 +724,9 @@ public final class Transformer {
if (player != null) { if (player != null) {
throw new IllegalStateException("There is already a transformation in progress."); throw new IllegalStateException("There is already a transformation in progress.");
} }
MuxerWrapper muxerWrapper = new MuxerWrapper(muxer, muxerFactory); TransformerPlayerListener playerListener = new TransformerPlayerListener(mediaItem, looper);
MuxerWrapper muxerWrapper =
new MuxerWrapper(muxer, muxerFactory, /* asyncErrorListener= */ playerListener);
this.muxerWrapper = muxerWrapper; this.muxerWrapper = muxerWrapper;
DefaultTrackSelector trackSelector = new DefaultTrackSelector(context); DefaultTrackSelector trackSelector = new DefaultTrackSelector(context);
trackSelector.setParameters( trackSelector.setParameters(
...@@ -741,7 +743,6 @@ public final class Transformer { ...@@ -741,7 +743,6 @@ public final class Transformer {
DEFAULT_BUFFER_FOR_PLAYBACK_MS / 10, DEFAULT_BUFFER_FOR_PLAYBACK_MS / 10,
DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS / 10) DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS / 10)
.build(); .build();
TransformerPlayerListener playerListener = new TransformerPlayerListener(mediaItem, looper);
ExoPlayer.Builder playerBuilder = ExoPlayer.Builder playerBuilder =
new ExoPlayer.Builder( new ExoPlayer.Builder(
context, context,
......
...@@ -63,6 +63,11 @@ public final class TestMuxer implements Muxer, Dumper.Dumpable { ...@@ -63,6 +63,11 @@ public final class TestMuxer implements Muxer, Dumper.Dumpable {
muxer.release(forCancellation); muxer.release(forCancellation);
} }
@Override
public long getMaxDelayBetweenSamplesMs() {
return muxer.getMaxDelayBetweenSamplesMs();
}
// Dumper.Dumpable implementation. // Dumper.Dumpable implementation.
@Override @Override
......
...@@ -478,6 +478,20 @@ public final class TransformerEndToEndTest { ...@@ -478,6 +478,20 @@ public final class TransformerEndToEndTest {
} }
@Test @Test
public void startTransformation_withUnsetMaxDelayBetweenSamples_completesSuccessfully()
throws Exception {
Muxer.Factory muxerFactory = new TestMuxerFactory(/* maxDelayBetweenSamplesMs= */ C.TIME_UNSET);
Transformer transformer =
createTransformerBuilder(/* enableFallback= */ false).setMuxerFactory(muxerFactory).build();
MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO);
transformer.startTransformation(mediaItem, outputPath);
TransformerTestRunner.runUntilCompleted(transformer);
DumpFileAsserts.assertOutput(context, testMuxer, getDumpFileName(FILE_AUDIO_VIDEO));
}
@Test
public void startTransformation_afterCancellation_completesSuccessfully() throws Exception { public void startTransformation_afterCancellation_completesSuccessfully() throws Exception {
Transformer transformer = createTransformerBuilder(/* enableFallback= */ false).build(); Transformer transformer = createTransformerBuilder(/* enableFallback= */ false).build();
MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO); MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO);
...@@ -862,6 +876,10 @@ public final class TransformerEndToEndTest { ...@@ -862,6 +876,10 @@ public final class TransformerEndToEndTest {
defaultMuxerFactory = new DefaultMuxer.Factory(); defaultMuxerFactory = new DefaultMuxer.Factory();
} }
public TestMuxerFactory(long maxDelayBetweenSamplesMs) {
defaultMuxerFactory = new DefaultMuxer.Factory(maxDelayBetweenSamplesMs);
}
@Override @Override
public Muxer create(String path) throws IOException { public Muxer create(String path) throws IOException {
testMuxer = new TestMuxer(path, defaultMuxerFactory); testMuxer = new TestMuxer(path, defaultMuxerFactory);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment