Move muxer initialization off application thread

Problem: We are initialising muxer as soon as we start the transformation. Now the startTransformation() method can be called from main thread, but muxer creation is an I/O operation and should be not be done on main thread.

Solution: Added lazy initialisation of muxer object. The actual transformation happens on background thread so the muxer will be initialised lazily from background thread only.

Another way was to provide an initialize() method on MuxerWrapper which will explicitly initialise muxer object but with this approach the caller need to call the initialise method before calling anything else. With current implementation the renderers are calling MuxerWrapper methods on various callbacks (Not sequentially) and also we are sharing same muxer with multiple renderers so It might become confusing for the caller on when to call the initialise() method. Also there are few methods on MuxerWrapper which dont really need muxer object. So in short it might make MuxerWrapper APIs more confusing.

Validation: Verified the transformation from demo app.
PiperOrigin-RevId: 486735787
This commit is contained in:
sheenachhabra 2022-11-07 20:38:44 +00:00 committed by microkatz
parent e510d9903f
commit eb357654bb
7 changed files with 72 additions and 31 deletions

View File

@ -20,7 +20,6 @@ import androidx.media3.common.C;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
/** A default {@link Muxer} implementation. */ /** A default {@link Muxer} implementation. */
@ -53,12 +52,12 @@ public final class DefaultMuxer implements Muxer {
} }
@Override @Override
public Muxer create(String path) throws IOException { public Muxer create(String path) throws MuxerException {
return new DefaultMuxer(muxerFactory.create(path)); return new DefaultMuxer(muxerFactory.create(path));
} }
@Override @Override
public Muxer create(ParcelFileDescriptor parcelFileDescriptor) throws IOException { public Muxer create(ParcelFileDescriptor parcelFileDescriptor) throws MuxerException {
return new DefaultMuxer(muxerFactory.create(parcelFileDescriptor)); return new DefaultMuxer(muxerFactory.create(parcelFileDescriptor));
} }

View File

@ -63,18 +63,28 @@ import java.nio.ByteBuffer;
} }
@Override @Override
public FrameworkMuxer create(String path) throws IOException { public FrameworkMuxer create(String path) throws MuxerException {
MediaMuxer mediaMuxer = new MediaMuxer(path, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); MediaMuxer mediaMuxer;
try {
mediaMuxer = new MediaMuxer(path, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
} catch (IOException e) {
throw new MuxerException("Error creating muxer", e);
}
return new FrameworkMuxer(mediaMuxer, maxDelayBetweenSamplesMs); return new FrameworkMuxer(mediaMuxer, maxDelayBetweenSamplesMs);
} }
@RequiresApi(26) @RequiresApi(26)
@Override @Override
public FrameworkMuxer create(ParcelFileDescriptor parcelFileDescriptor) throws IOException { public FrameworkMuxer create(ParcelFileDescriptor parcelFileDescriptor) throws MuxerException {
MediaMuxer mediaMuxer = MediaMuxer mediaMuxer;
try {
mediaMuxer =
new MediaMuxer( new MediaMuxer(
parcelFileDescriptor.getFileDescriptor(), parcelFileDescriptor.getFileDescriptor(),
MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
} catch (IOException e) {
throw new MuxerException("Error creating muxer", e);
}
return new FrameworkMuxer(mediaMuxer, maxDelayBetweenSamplesMs); return new FrameworkMuxer(mediaMuxer, maxDelayBetweenSamplesMs);
} }

View File

@ -21,7 +21,6 @@ import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
/** /**
@ -57,9 +56,9 @@ public interface Muxer {
* *
* @param path The path to the output file. * @param path The path to the output file.
* @throws IllegalArgumentException If the path is invalid. * @throws IllegalArgumentException If the path is invalid.
* @throws IOException If an error occurs opening the output file for writing. * @throws MuxerException If an error occurs opening the output file for writing.
*/ */
Muxer create(String path) throws IOException; Muxer create(String path) throws MuxerException;
/** /**
* Returns a new muxer writing to a file descriptor. * Returns a new muxer writing to a file descriptor.
@ -69,9 +68,9 @@ public interface Muxer {
* muxer is released. It is the responsibility of the caller to close the * muxer is released. It is the responsibility of the caller to close the
* ParcelFileDescriptor. This can be done after this method returns. * ParcelFileDescriptor. This can be done after this method returns.
* @throws IllegalArgumentException If the file descriptor is invalid. * @throws IllegalArgumentException If the file descriptor is invalid.
* @throws IOException If an error occurs opening the output file descriptor for writing. * @throws MuxerException If an error occurs opening the output file descriptor for writing.
*/ */
Muxer create(ParcelFileDescriptor parcelFileDescriptor) throws IOException; Muxer create(ParcelFileDescriptor parcelFileDescriptor) throws MuxerException;
/** /**
* Returns the supported sample {@linkplain MimeTypes MIME types} for the given {@link * Returns the supported sample {@linkplain MimeTypes MIME types} for the given {@link

View File

@ -16,11 +16,13 @@
package androidx.media3.transformer; package androidx.media3.transformer;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Util.maxValue; import static androidx.media3.common.util.Util.maxValue;
import static androidx.media3.common.util.Util.minValue; import static androidx.media3.common.util.Util.minValue;
import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.os.ParcelFileDescriptor;
import android.util.SparseIntArray; import android.util.SparseIntArray;
import android.util.SparseLongArray; import android.util.SparseLongArray;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -33,7 +35,9 @@ import java.nio.ByteBuffer;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/** /**
* A wrapper around a media muxer. * A wrapper around a media muxer.
@ -50,7 +54,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
*/ */
private static final long MAX_TRACK_WRITE_AHEAD_US = Util.msToUs(500); private static final long MAX_TRACK_WRITE_AHEAD_US = Util.msToUs(500);
private final Muxer muxer; @Nullable private final String outputPath;
@Nullable private final ParcelFileDescriptor outputParcelFileDescriptor;
private final Muxer.Factory muxerFactory; private final Muxer.Factory muxerFactory;
private final Transformer.AsyncErrorListener asyncErrorListener; private final Transformer.AsyncErrorListener asyncErrorListener;
private final SparseIntArray trackTypeToIndex; private final SparseIntArray trackTypeToIndex;
@ -66,10 +71,19 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private long minTrackTimeUs; private long minTrackTimeUs;
private @MonotonicNonNull ScheduledFuture<?> abortScheduledFuture; private @MonotonicNonNull ScheduledFuture<?> abortScheduledFuture;
private boolean isAborted; private boolean isAborted;
private @MonotonicNonNull Muxer muxer;
public MuxerWrapper( public MuxerWrapper(
Muxer muxer, Muxer.Factory muxerFactory, Transformer.AsyncErrorListener asyncErrorListener) { @Nullable String outputPath,
this.muxer = muxer; @Nullable ParcelFileDescriptor outputParcelFileDescriptor,
Muxer.Factory muxerFactory,
Transformer.AsyncErrorListener asyncErrorListener) {
if (outputPath == null && outputParcelFileDescriptor == null) {
throw new NullPointerException("Both output path and ParcelFileDescriptor are null");
}
this.outputPath = outputPath;
this.outputParcelFileDescriptor = outputParcelFileDescriptor;
this.muxerFactory = muxerFactory; this.muxerFactory = muxerFactory;
this.asyncErrorListener = asyncErrorListener; this.asyncErrorListener = asyncErrorListener;
@ -135,6 +149,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
trackTypeToIndex.get(trackType, /* valueIfKeyNotFound= */ C.INDEX_UNSET) == C.INDEX_UNSET, trackTypeToIndex.get(trackType, /* valueIfKeyNotFound= */ C.INDEX_UNSET) == C.INDEX_UNSET,
"There is already a track of type " + trackType); "There is already a track of type " + trackType);
ensureMuxerInitialized();
int trackIndex = muxer.addTrack(format); int trackIndex = muxer.addTrack(format);
trackTypeToIndex.put(trackType, trackIndex); trackTypeToIndex.put(trackType, trackIndex);
trackTypeToSampleCount.put(trackType, 0); trackTypeToSampleCount.put(trackType, 0);
@ -181,6 +197,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
trackTypeToTimeUs.put(trackType, presentationTimeUs); trackTypeToTimeUs.put(trackType, presentationTimeUs);
} }
checkNotNull(muxer);
resetAbortTimer(); resetAbortTimer();
muxer.writeSampleData(trackIndex, data, isKeyFrame, presentationTimeUs); muxer.writeSampleData(trackIndex, data, isKeyFrame, presentationTimeUs);
previousTrackType = trackType; previousTrackType = trackType;
@ -213,8 +230,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public void release(boolean forCancellation) throws Muxer.MuxerException { public void release(boolean forCancellation) throws Muxer.MuxerException {
isReady = false; isReady = false;
abortScheduledExecutorService.shutdownNow(); abortScheduledExecutorService.shutdownNow();
if (muxer != null) {
muxer.release(forCancellation); muxer.release(forCancellation);
} }
}
/** Returns the number of {@link #registerTrack() registered} tracks. */ /** Returns the number of {@link #registerTrack() registered} tracks. */
public int getTrackCount() { public int getTrackCount() {
@ -276,6 +295,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return trackTimeUs - minTrackTimeUs <= MAX_TRACK_WRITE_AHEAD_US; return trackTimeUs - minTrackTimeUs <= MAX_TRACK_WRITE_AHEAD_US;
} }
@RequiresNonNull("muxer")
private void resetAbortTimer() { private void resetAbortTimer() {
long maxDelayBetweenSamplesMs = muxer.getMaxDelayBetweenSamplesMs(); long maxDelayBetweenSamplesMs = muxer.getMaxDelayBetweenSamplesMs();
if (maxDelayBetweenSamplesMs == C.TIME_UNSET) { if (maxDelayBetweenSamplesMs == C.TIME_UNSET) {
@ -302,4 +322,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
maxDelayBetweenSamplesMs, maxDelayBetweenSamplesMs,
MILLISECONDS); MILLISECONDS);
} }
@EnsuresNonNull("muxer")
private void ensureMuxerInitialized() throws Muxer.MuxerException {
if (muxer == null) {
if (outputPath != null) {
muxer = muxerFactory.create(outputPath);
} else {
checkNotNull(outputParcelFileDescriptor);
muxer = muxerFactory.create(outputParcelFileDescriptor);
}
}
}
} }

View File

@ -677,7 +677,7 @@ public final class Transformer {
* @throws IllegalArgumentException If the path is invalid. * @throws IllegalArgumentException If the path is invalid.
* @throws IllegalStateException If this method is called from the wrong thread. * @throws IllegalStateException If this method is called from the wrong thread.
* @throws IllegalStateException If a transformation is already in progress. * @throws IllegalStateException If a transformation is already in progress.
* @throws IOException If an error occurs opening the output file for writing. * @throws IOException If {@link MediaItem} is not supported.
*/ */
public void startTransformation(MediaItem mediaItem, String path) throws IOException { public void startTransformation(MediaItem mediaItem, String path) throws IOException {
if (!mediaItem.clippingConfiguration.equals(MediaItem.ClippingConfiguration.UNSET) if (!mediaItem.clippingConfiguration.equals(MediaItem.ClippingConfiguration.UNSET)
@ -688,7 +688,7 @@ public final class Transformer {
} }
this.outputPath = path; this.outputPath = path;
this.outputParcelFileDescriptor = null; this.outputParcelFileDescriptor = null;
startTransformation(mediaItem, muxerFactory.create(path)); startTransformationInternal(mediaItem);
} }
/** /**
@ -711,24 +711,26 @@ public final class Transformer {
* @throws IllegalArgumentException If the file descriptor is invalid. * @throws IllegalArgumentException If the file descriptor is invalid.
* @throws IllegalStateException If this method is called from the wrong thread. * @throws IllegalStateException If this method is called from the wrong thread.
* @throws IllegalStateException If a transformation is already in progress. * @throws IllegalStateException If a transformation is already in progress.
* @throws IOException If an error occurs opening the output file for writing.
*/ */
@RequiresApi(26) @RequiresApi(26)
public void startTransformation(MediaItem mediaItem, ParcelFileDescriptor parcelFileDescriptor) public void startTransformation(MediaItem mediaItem, ParcelFileDescriptor parcelFileDescriptor) {
throws IOException {
this.outputParcelFileDescriptor = parcelFileDescriptor; this.outputParcelFileDescriptor = parcelFileDescriptor;
this.outputPath = null; this.outputPath = null;
startTransformation(mediaItem, muxerFactory.create(parcelFileDescriptor)); startTransformationInternal(mediaItem);
} }
private void startTransformation(MediaItem mediaItem, Muxer muxer) { private void startTransformationInternal(MediaItem mediaItem) {
verifyApplicationThread(); verifyApplicationThread();
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.");
} }
TransformerPlayerListener playerListener = new TransformerPlayerListener(mediaItem, looper); TransformerPlayerListener playerListener = new TransformerPlayerListener(mediaItem, looper);
MuxerWrapper muxerWrapper = MuxerWrapper muxerWrapper =
new MuxerWrapper(muxer, muxerFactory, /* asyncErrorListener= */ playerListener); new MuxerWrapper(
outputPath,
outputParcelFileDescriptor,
muxerFactory,
/* asyncErrorListener= */ playerListener);
this.muxerWrapper = muxerWrapper; this.muxerWrapper = muxerWrapper;
DefaultTrackSelector trackSelector = new DefaultTrackSelector(context); DefaultTrackSelector trackSelector = new DefaultTrackSelector(context);
trackSelector.setParameters( trackSelector.setParameters(

View File

@ -18,7 +18,6 @@ package androidx.media3.transformer;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.test.utils.DumpableFormat; import androidx.media3.test.utils.DumpableFormat;
import androidx.media3.test.utils.Dumper; import androidx.media3.test.utils.Dumper;
import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -35,7 +34,7 @@ public final class TestMuxer implements Muxer, Dumper.Dumpable {
private final List<Dumper.Dumpable> dumpables; private final List<Dumper.Dumpable> dumpables;
/** Creates a new test muxer. */ /** Creates a new test muxer. */
public TestMuxer(String path, Muxer.Factory muxerFactory) throws IOException { public TestMuxer(String path, Muxer.Factory muxerFactory) throws MuxerException {
muxer = muxerFactory.create(path); muxer = muxerFactory.create(path);
dumpables = new ArrayList<>(); dumpables = new ArrayList<>();
} }

View File

@ -912,13 +912,13 @@ public final class TransformerEndToEndTest {
} }
@Override @Override
public Muxer create(String path) throws IOException { public Muxer create(String path) throws Muxer.MuxerException {
testMuxer = new TestMuxer(path, defaultMuxerFactory); testMuxer = new TestMuxer(path, defaultMuxerFactory);
return testMuxer; return testMuxer;
} }
@Override @Override
public Muxer create(ParcelFileDescriptor parcelFileDescriptor) throws IOException { public Muxer create(ParcelFileDescriptor parcelFileDescriptor) throws Muxer.MuxerException {
testMuxer = new TestMuxer("FD:" + parcelFileDescriptor.getFd(), defaultMuxerFactory); testMuxer = new TestMuxer("FD:" + parcelFileDescriptor.getFd(), defaultMuxerFactory);
return testMuxer; return testMuxer;
} }