Add a MuxerWrapper listener for events.

Events on the wrapper should be propagated to TransformerInternal as
soon as they occur, switching round the process so TransformerInternal
does not have to query MuxerWrapper.

This CL is a prerequisite for the child CL, where MuxerWrapper can
simplify the internal state and logic.

PiperOrigin-RevId: 497267202
This commit is contained in:
samrobinson 2022-12-23 01:11:29 +00:00 committed by Marc Baechinger
parent 965606f7a7
commit c0bbfe929a
2 changed files with 104 additions and 87 deletions

View file

@ -29,7 +29,6 @@ import androidx.annotation.IntRange;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.Consumer;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
@ -49,6 +48,14 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
*/
/* package */ final class MuxerWrapper {
public interface Listener {
void onTrackEnded(@C.TrackType int trackType, int averageBitrate, int sampleCount);
void onEnded(long durationMs, long fileSizeBytes);
void onTransformationError(TransformationException transformationException);
}
/**
* The maximum difference between the track positions, in microseconds.
*
@ -60,7 +67,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@Nullable private final String outputPath;
@Nullable private final ParcelFileDescriptor outputParcelFileDescriptor;
private final Muxer.Factory muxerFactory;
private final Consumer<TransformationException> errorConsumer;
private final Listener listener;
private final SparseIntArray trackTypeToIndex;
private final SparseIntArray trackTypeToSampleCount;
private final SparseLongArray trackTypeToTimeUs;
@ -81,7 +88,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@Nullable String outputPath,
@Nullable ParcelFileDescriptor outputParcelFileDescriptor,
Muxer.Factory muxerFactory,
Consumer<TransformationException> errorConsumer) {
Listener listener) {
if (outputPath == null && outputParcelFileDescriptor == null) {
throw new NullPointerException("Both output path and ParcelFileDescriptor are null");
}
@ -89,7 +96,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
this.outputPath = outputPath;
this.outputParcelFileDescriptor = outputParcelFileDescriptor;
this.muxerFactory = muxerFactory;
this.errorConsumer = errorConsumer;
this.listener = listener;
trackTypeToIndex = new SparseIntArray();
trackTypeToSampleCount = new SparseIntArray();
@ -216,10 +223,18 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* @param trackType The {@link C.TrackType track type}.
*/
public void endTrack(@C.TrackType int trackType) {
listener.onTrackEnded(
trackType,
getTrackAverageBitrate(trackType),
trackTypeToSampleCount.get(trackType, /* valueIfKeyNotFound= */ 0));
trackTypeToIndex.delete(trackType);
if (trackTypeToIndex.size() == 0) {
abortScheduledExecutorService.shutdownNow();
isEnded = true;
if (!isEnded) {
isEnded = true;
listener.onEnded(getDurationMs(), getCurrentOutputSizeBytes());
}
}
}
@ -246,58 +261,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
}
}
/**
* Returns the average bitrate of data written to the track of the provided {@code trackType}, or
* {@link C#RATE_UNSET_INT} if there is no track data.
*/
public int getTrackAverageBitrate(@C.TrackType int trackType) {
long trackDurationUs = trackTypeToTimeUs.get(trackType, /* valueIfKeyNotFound= */ -1);
long trackBytes = trackTypeToBytesWritten.get(trackType, /* valueIfKeyNotFound= */ -1);
if (trackDurationUs <= 0 || trackBytes <= 0) {
return C.RATE_UNSET_INT;
}
// The number of bytes written is not a timestamp, however this utility method provides
// overflow-safe multiplication & division.
return (int)
Util.scaleLargeTimestamp(
/* timestamp= */ trackBytes,
/* multiplier= */ C.BITS_PER_BYTE * C.MICROS_PER_SECOND,
/* divisor= */ trackDurationUs);
}
/** Returns the number of samples written to the track of the provided {@code trackType}. */
public int getTrackSampleCount(@C.TrackType int trackType) {
return trackTypeToSampleCount.get(trackType, /* valueIfKeyNotFound= */ 0);
}
/**
* Returns the duration of the longest track in milliseconds, or {@link C#TIME_UNSET} if there is
* no track.
*/
public long getDurationMs() {
if (trackTypeToTimeUs.size() == 0) {
return C.TIME_UNSET;
}
return Util.usToMs(maxValue(trackTypeToTimeUs));
}
/** Returns the current size in bytes of the output, or {@link C#LENGTH_UNSET} if unavailable. */
public long getCurrentOutputSizeBytes() {
long fileSize = C.LENGTH_UNSET;
if (outputPath != null) {
fileSize = new File(outputPath).length();
} else if (outputParcelFileDescriptor != null) {
fileSize = outputParcelFileDescriptor.getStatSize();
}
if (fileSize <= 0) {
fileSize = C.LENGTH_UNSET;
}
return fileSize;
}
/**
* Returns whether the muxer can write a sample of the given track type.
*
@ -340,7 +303,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
return;
}
isAborted = true;
errorConsumer.accept(
listener.onTransformationError(
TransformationException.createForMuxer(
new IllegalStateException(
"No output sample written in the last "
@ -363,4 +326,47 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
}
}
}
/**
* Returns the duration of the longest track in milliseconds, or {@link C#TIME_UNSET} if there is
* no track.
*/
private long getDurationMs() {
if (trackTypeToTimeUs.size() == 0) {
return C.TIME_UNSET;
}
return Util.usToMs(maxValue(trackTypeToTimeUs));
}
/** Returns the current size in bytes of the output, or {@link C#LENGTH_UNSET} if unavailable. */
private long getCurrentOutputSizeBytes() {
long fileSize = C.LENGTH_UNSET;
if (outputPath != null) {
fileSize = new File(outputPath).length();
} else if (outputParcelFileDescriptor != null) {
fileSize = outputParcelFileDescriptor.getStatSize();
}
return fileSize > 0 ? fileSize : C.LENGTH_UNSET;
}
/**
* Returns the average bitrate of data written to the track of the provided {@code trackType}, or
* {@link C#RATE_UNSET_INT} if there is no track data.
*/
private int getTrackAverageBitrate(@C.TrackType int trackType) {
long trackDurationUs = trackTypeToTimeUs.get(trackType, /* valueIfKeyNotFound= */ -1);
long trackBytes = trackTypeToBytesWritten.get(trackType, /* valueIfKeyNotFound= */ -1);
if (trackDurationUs <= 0 || trackBytes <= 0) {
return C.RATE_UNSET_INT;
}
// The number of bytes written is not a timestamp, however this utility method provides
// overflow-safe multiplication & division.
return (int)
Util.scaleLargeTimestamp(
/* timestamp= */ trackBytes,
/* multiplier= */ C.BITS_PER_BYTE * C.MICROS_PER_SECOND,
/* divisor= */ trackDurationUs);
}
}

View file

@ -109,6 +109,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private final ConditionVariable dequeueBufferConditionVariable;
private final MuxerWrapper muxerWrapper;
private final ConditionVariable transformerConditionVariable;
private final TransformationResult.Builder transformationResultBuilder;
@Nullable private DecoderInputBuffer pendingInputBuffer;
private boolean isDrainingPipelines;
@ -169,12 +170,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
silentSamplePipelineIndex = C.INDEX_UNSET;
dequeueBufferConditionVariable = new ConditionVariable();
muxerWrapper =
new MuxerWrapper(
outputPath,
outputParcelFileDescriptor,
muxerFactory,
/* errorConsumer= */ componentListener::onTransformationError);
new MuxerWrapper(outputPath, outputParcelFileDescriptor, muxerFactory, componentListener);
transformerConditionVariable = new ConditionVariable();
transformationResultBuilder = new TransformationResult.Builder();
// It's safe to use "this" because we don't send a message before exiting the constructor.
@SuppressWarnings("nullness:methodref.receiver.bound")
HandlerWrapper internalHandler =
@ -288,24 +286,18 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
while (samplePipelines.get(i).processData()) {}
}
if (muxerWrapper.isEnded()) {
internalHandler
.obtainMessage(
MSG_END, END_REASON_COMPLETED, /* unused */ 0, /* transformationException */ null)
.sendToTarget();
} else {
if (!muxerWrapper.isEnded()) {
internalHandler.sendEmptyMessageDelayed(MSG_DRAIN_PIPELINES, DRAIN_PIPELINES_DELAY_MS);
}
}
private void endInternal(
@EndReason int endReason, @Nullable TransformationException transformationException) {
TransformationResult.Builder transformationResultBuilder =
new TransformationResult.Builder()
.setAudioDecoderName(decoderFactory.getAudioDecoderName())
.setVideoDecoderName(decoderFactory.getVideoDecoderName())
.setAudioEncoderName(encoderFactory.getAudioEncoderName())
.setVideoEncoderName(encoderFactory.getVideoEncoderName());
transformationResultBuilder
.setAudioDecoderName(decoderFactory.getAudioDecoderName())
.setVideoDecoderName(decoderFactory.getVideoDecoderName())
.setAudioEncoderName(encoderFactory.getAudioEncoderName())
.setVideoEncoderName(encoderFactory.getVideoEncoderName());
boolean forCancellation = endReason == END_REASON_CANCELLED;
@Nullable TransformationException releaseTransformationException = null;
@ -321,15 +313,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
assetLoader.release();
} finally {
try {
if (endReason == END_REASON_COMPLETED) {
transformationResultBuilder
.setDurationMs(muxerWrapper.getDurationMs())
.setFileSizeBytes(muxerWrapper.getCurrentOutputSizeBytes())
.setAverageAudioBitrate(muxerWrapper.getTrackAverageBitrate(C.TRACK_TYPE_AUDIO))
.setAverageVideoBitrate(muxerWrapper.getTrackAverageBitrate(C.TRACK_TYPE_VIDEO))
.setVideoFrameCount(muxerWrapper.getTrackSampleCount(C.TRACK_TYPE_VIDEO));
}
for (int i = 0; i < samplePipelines.size(); i++) {
samplePipelines.get(i).release();
}
@ -378,7 +361,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
transformerConditionVariable.open();
}
private class ComponentListener implements AssetLoader.Listener {
private class ComponentListener implements AssetLoader.Listener, MuxerWrapper.Listener {
private final MediaItem mediaItem;
private final FallbackListener fallbackListener;
@ -395,6 +378,15 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
durationUs = C.TIME_UNSET;
}
// AssetLoader.Listener and MuxerWrapper.Listener implementation.
@Override
public void onTransformationError(TransformationException transformationException) {
internalHandler
.obtainMessage(MSG_END, END_REASON_ERROR, /* unused */ 0, transformationException)
.sendToTarget();
}
// AssetLoader.Listener implementation.
@Override
@ -472,12 +464,31 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
onTransformationError(transformationException);
}
public void onTransformationError(TransformationException transformationException) {
// MuxerWrapper.Listener implementation.
@Override
public void onTrackEnded(@C.TrackType int trackType, int averageBitrate, int sampleCount) {
if (trackType == C.TRACK_TYPE_AUDIO) {
transformationResultBuilder.setAverageAudioBitrate(averageBitrate);
} else if (trackType == C.TRACK_TYPE_VIDEO) {
transformationResultBuilder
.setVideoFrameCount(sampleCount)
.setAverageVideoBitrate(averageBitrate);
}
}
@Override
public void onEnded(long durationMs, long fileSizeBytes) {
transformationResultBuilder.setDurationMs(durationMs).setFileSizeBytes(fileSizeBytes);
internalHandler
.obtainMessage(MSG_END, END_REASON_ERROR, /* unused */ 0, transformationException)
.obtainMessage(
MSG_END, END_REASON_COMPLETED, /* unused */ 0, /* transformationException */ null)
.sendToTarget();
}
// Private methods.
private SamplePipeline getSamplePipeline(
Format inputFormat,
@AssetLoader.SupportedOutputTypes int supportedOutputTypes,