Skip to content

Commit

Permalink
Add muxer timer to detect when generating an output sample is too slow
Browse files Browse the repository at this point in the history
This allows to throw when the Transformer is stuck or is too slow.

PiperOrigin-RevId: 484179037
  • Loading branch information
kim-vde authored and microkatz committed Oct 31, 2022
1 parent c9585d0 commit 376ee77
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,27 @@ public final class DefaultMuxer implements Muxer {

/** A {@link Muxer.Factory} for {@link DefaultMuxer}. */
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;

/**
* Creates an instance with {@link Muxer#getMaxDelayBetweenSamplesMs() maxDelayBetweenSamplesMs}
* set to {@link #DEFAULT_MAX_DELAY_BETWEEN_SAMPLES_MS}.
*/
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
Expand Down Expand Up @@ -71,4 +88,9 @@ public void writeSampleData(
public void release(boolean forCancellation) throws MuxerException {
muxer.release(forCancellation);
}

@Override
public long getMaxDelayBetweenSamplesMs() {
return muxer.getMaxDelayBetweenSamplesMs();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,17 @@

/** {@link Muxer.Factory} for {@link FrameworkMuxer}. */
public static final class Factory implements Muxer.Factory {

private final long maxDelayBetweenSamplesMs;

public Factory(long maxDelayBetweenSamplesMs) {
this.maxDelayBetweenSamplesMs = maxDelayBetweenSamplesMs;
}

@Override
public FrameworkMuxer create(String path) throws IOException {
MediaMuxer mediaMuxer = new MediaMuxer(path, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
return new FrameworkMuxer(mediaMuxer);
return new FrameworkMuxer(mediaMuxer, maxDelayBetweenSamplesMs);
}

@RequiresApi(26)
Expand All @@ -68,7 +75,7 @@ public FrameworkMuxer create(ParcelFileDescriptor parcelFileDescriptor) throws I
new MediaMuxer(
parcelFileDescriptor.getFileDescriptor(),
MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
return new FrameworkMuxer(mediaMuxer);
return new FrameworkMuxer(mediaMuxer, maxDelayBetweenSamplesMs);
}

@Override
Expand All @@ -83,13 +90,15 @@ public ImmutableList<String> getSupportedSampleMimeTypes(@C.TrackType int trackT
}

private final MediaMuxer mediaMuxer;
private final long maxDelayBetweenSamplesMs;
private final MediaCodec.BufferInfo bufferInfo;
private final SparseLongArray trackIndexToLastPresentationTimeUs;

private boolean isStarted;

private FrameworkMuxer(MediaMuxer mediaMuxer) {
private FrameworkMuxer(MediaMuxer mediaMuxer, long maxDelayBetweenSamplesMs) {
this.mediaMuxer = mediaMuxer;
this.maxDelayBetweenSamplesMs = maxDelayBetweenSamplesMs;
bufferInfo = new MediaCodec.BufferInfo();
trackIndexToLastPresentationTimeUs = new SparseLongArray();
}
Expand Down Expand Up @@ -183,6 +192,11 @@ public void release(boolean forCancellation) throws MuxerException {
}
}

@Override
public long getMaxDelayBetweenSamplesMs() {
return maxDelayBetweenSamplesMs;
}

// Accesses MediaMuxer state via reflection to ensure that muxer resources can be released even
// if stopping fails.
@SuppressLint("PrivateApi")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,16 @@ void writeSampleData(int trackIndex, ByteBuffer data, boolean isKeyFrame, long p
* forCancellation} is false.
*/
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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
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.minValue;
import static java.util.concurrent.TimeUnit.MILLISECONDS;

import android.util.SparseIntArray;
import android.util.SparseLongArray;
Expand All @@ -29,6 +30,10 @@
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
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.
Expand All @@ -47,26 +52,33 @@

private final Muxer muxer;
private final Muxer.Factory muxerFactory;
private final Transformer.AsyncErrorListener asyncErrorListener;
private final SparseIntArray trackTypeToIndex;
private final SparseIntArray trackTypeToSampleCount;
private final SparseLongArray trackTypeToTimeUs;
private final SparseLongArray trackTypeToBytesWritten;
private final ScheduledExecutorService abortScheduledExecutorService;

private int trackCount;
private int trackFormatCount;
private boolean isReady;
private @C.TrackType int previousTrackType;
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.muxerFactory = muxerFactory;
this.asyncErrorListener = asyncErrorListener;

trackTypeToIndex = new SparseIntArray();
trackTypeToSampleCount = new SparseIntArray();
trackTypeToTimeUs = new SparseLongArray();
trackTypeToBytesWritten = new SparseLongArray();
previousTrackType = C.TRACK_TYPE_NONE;
abortScheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
}

/**
Expand Down Expand Up @@ -131,6 +143,7 @@ public void addTrackFormat(Format format) throws Muxer.MuxerException {
trackFormatCount++;
if (trackFormatCount == trackCount) {
isReady = true;
resetAbortTimer();
}
}

Expand Down Expand Up @@ -168,6 +181,7 @@ public boolean writeSample(
trackTypeToTimeUs.put(trackType, presentationTimeUs);
}

resetAbortTimer();
muxer.writeSampleData(trackIndex, data, isKeyFrame, presentationTimeUs);
previousTrackType = trackType;
return true;
Expand Down Expand Up @@ -195,6 +209,7 @@ public void endTrack(@C.TrackType int trackType) {
*/
public void release(boolean forCancellation) throws Muxer.MuxerException {
isReady = false;
abortScheduledExecutorService.shutdownNow();
muxer.release(forCancellation);
}

Expand Down Expand Up @@ -257,4 +272,31 @@ private boolean canWriteSampleOfType(int trackType) {
}
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -724,7 +724,9 @@ private void startTransformation(MediaItem mediaItem, Muxer muxer) {
if (player != null) {
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;
DefaultTrackSelector trackSelector = new DefaultTrackSelector(context);
trackSelector.setParameters(
Expand All @@ -741,7 +743,6 @@ private void startTransformation(MediaItem mediaItem, Muxer muxer) {
DEFAULT_BUFFER_FOR_PLAYBACK_MS / 10,
DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS / 10)
.build();
TransformerPlayerListener playerListener = new TransformerPlayerListener(mediaItem, looper);
ExoPlayer.Builder playerBuilder =
new ExoPlayer.Builder(
context,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ public void release(boolean forCancellation) throws MuxerException {
muxer.release(forCancellation);
}

@Override
public long getMaxDelayBetweenSamplesMs() {
return muxer.getMaxDelayBetweenSamplesMs();
}

// Dumper.Dumpable implementation.

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,20 @@ public void startTransformation_withAudioMuxerFormatFallback_completesSuccessful
.onFallbackApplied(mediaItem, originalTransformationRequest, fallbackTransformationRequest);
}

@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 {
Transformer transformer = createTransformerBuilder(/* enableFallback= */ false).build();
Expand Down Expand Up @@ -862,6 +876,10 @@ public TestMuxerFactory() {
defaultMuxerFactory = new DefaultMuxer.Factory();
}

public TestMuxerFactory(long maxDelayBetweenSamplesMs) {
defaultMuxerFactory = new DefaultMuxer.Factory(maxDelayBetweenSamplesMs);
}

@Override
public Muxer create(String path) throws IOException {
testMuxer = new TestMuxer(path, defaultMuxerFactory);
Expand Down

0 comments on commit 376ee77

Please sign in to comment.