Commit c0bbfe92 by samrobinson Committed by Marc Baechinger

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
parent 965606f7
...@@ -29,7 +29,6 @@ import androidx.annotation.IntRange; ...@@ -29,7 +29,6 @@ import androidx.annotation.IntRange;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; 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.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;
...@@ -49,6 +48,14 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -49,6 +48,14 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
*/ */
/* package */ final class MuxerWrapper { /* 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. * The maximum difference between the track positions, in microseconds.
* *
...@@ -60,7 +67,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -60,7 +67,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@Nullable private final String outputPath; @Nullable private final String outputPath;
@Nullable private final ParcelFileDescriptor outputParcelFileDescriptor; @Nullable private final ParcelFileDescriptor outputParcelFileDescriptor;
private final Muxer.Factory muxerFactory; private final Muxer.Factory muxerFactory;
private final Consumer<TransformationException> errorConsumer; private final Listener listener;
private final SparseIntArray trackTypeToIndex; private final SparseIntArray trackTypeToIndex;
private final SparseIntArray trackTypeToSampleCount; private final SparseIntArray trackTypeToSampleCount;
private final SparseLongArray trackTypeToTimeUs; private final SparseLongArray trackTypeToTimeUs;
...@@ -81,7 +88,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -81,7 +88,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@Nullable String outputPath, @Nullable String outputPath,
@Nullable ParcelFileDescriptor outputParcelFileDescriptor, @Nullable ParcelFileDescriptor outputParcelFileDescriptor,
Muxer.Factory muxerFactory, Muxer.Factory muxerFactory,
Consumer<TransformationException> errorConsumer) { Listener listener) {
if (outputPath == null && outputParcelFileDescriptor == null) { if (outputPath == null && outputParcelFileDescriptor == null) {
throw new NullPointerException("Both output path and ParcelFileDescriptor are null"); throw new NullPointerException("Both output path and ParcelFileDescriptor are null");
} }
...@@ -89,7 +96,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -89,7 +96,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
this.outputPath = outputPath; this.outputPath = outputPath;
this.outputParcelFileDescriptor = outputParcelFileDescriptor; this.outputParcelFileDescriptor = outputParcelFileDescriptor;
this.muxerFactory = muxerFactory; this.muxerFactory = muxerFactory;
this.errorConsumer = errorConsumer; this.listener = listener;
trackTypeToIndex = new SparseIntArray(); trackTypeToIndex = new SparseIntArray();
trackTypeToSampleCount = new SparseIntArray(); trackTypeToSampleCount = new SparseIntArray();
...@@ -216,10 +223,18 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -216,10 +223,18 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* @param trackType The {@link C.TrackType track type}. * @param trackType The {@link C.TrackType track type}.
*/ */
public void endTrack(@C.TrackType int trackType) { public void endTrack(@C.TrackType int trackType) {
listener.onTrackEnded(
trackType,
getTrackAverageBitrate(trackType),
trackTypeToSampleCount.get(trackType, /* valueIfKeyNotFound= */ 0));
trackTypeToIndex.delete(trackType); trackTypeToIndex.delete(trackType);
if (trackTypeToIndex.size() == 0) { if (trackTypeToIndex.size() == 0) {
abortScheduledExecutorService.shutdownNow(); abortScheduledExecutorService.shutdownNow();
isEnded = true; if (!isEnded) {
isEnded = true;
listener.onEnded(getDurationMs(), getCurrentOutputSizeBytes());
}
} }
} }
...@@ -247,58 +262,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -247,58 +262,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. * Returns whether the muxer can write a sample of the given track type.
* *
* @param trackType The track type, defined by the {@code TRACK_TYPE_*} constants in {@link C}. * @param trackType The track type, defined by the {@code TRACK_TYPE_*} constants in {@link C}.
...@@ -340,7 +303,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -340,7 +303,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
return; return;
} }
isAborted = true; isAborted = true;
errorConsumer.accept( listener.onTransformationError(
TransformationException.createForMuxer( TransformationException.createForMuxer(
new IllegalStateException( new IllegalStateException(
"No output sample written in the last " "No output sample written in the last "
...@@ -363,4 +326,47 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -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);
}
} }
...@@ -109,6 +109,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -109,6 +109,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private final ConditionVariable dequeueBufferConditionVariable; private final ConditionVariable dequeueBufferConditionVariable;
private final MuxerWrapper muxerWrapper; private final MuxerWrapper muxerWrapper;
private final ConditionVariable transformerConditionVariable; private final ConditionVariable transformerConditionVariable;
private final TransformationResult.Builder transformationResultBuilder;
@Nullable private DecoderInputBuffer pendingInputBuffer; @Nullable private DecoderInputBuffer pendingInputBuffer;
private boolean isDrainingPipelines; private boolean isDrainingPipelines;
...@@ -169,12 +170,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -169,12 +170,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
silentSamplePipelineIndex = C.INDEX_UNSET; silentSamplePipelineIndex = C.INDEX_UNSET;
dequeueBufferConditionVariable = new ConditionVariable(); dequeueBufferConditionVariable = new ConditionVariable();
muxerWrapper = muxerWrapper =
new MuxerWrapper( new MuxerWrapper(outputPath, outputParcelFileDescriptor, muxerFactory, componentListener);
outputPath,
outputParcelFileDescriptor,
muxerFactory,
/* errorConsumer= */ componentListener::onTransformationError);
transformerConditionVariable = new ConditionVariable(); transformerConditionVariable = new ConditionVariable();
transformationResultBuilder = new TransformationResult.Builder();
// It's safe to use "this" because we don't send a message before exiting the constructor. // It's safe to use "this" because we don't send a message before exiting the constructor.
@SuppressWarnings("nullness:methodref.receiver.bound") @SuppressWarnings("nullness:methodref.receiver.bound")
HandlerWrapper internalHandler = HandlerWrapper internalHandler =
...@@ -288,24 +286,18 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -288,24 +286,18 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
while (samplePipelines.get(i).processData()) {} while (samplePipelines.get(i).processData()) {}
} }
if (muxerWrapper.isEnded()) { if (!muxerWrapper.isEnded()) {
internalHandler
.obtainMessage(
MSG_END, END_REASON_COMPLETED, /* unused */ 0, /* transformationException */ null)
.sendToTarget();
} else {
internalHandler.sendEmptyMessageDelayed(MSG_DRAIN_PIPELINES, DRAIN_PIPELINES_DELAY_MS); internalHandler.sendEmptyMessageDelayed(MSG_DRAIN_PIPELINES, DRAIN_PIPELINES_DELAY_MS);
} }
} }
private void endInternal( private void endInternal(
@EndReason int endReason, @Nullable TransformationException transformationException) { @EndReason int endReason, @Nullable TransformationException transformationException) {
TransformationResult.Builder transformationResultBuilder = transformationResultBuilder
new TransformationResult.Builder() .setAudioDecoderName(decoderFactory.getAudioDecoderName())
.setAudioDecoderName(decoderFactory.getAudioDecoderName()) .setVideoDecoderName(decoderFactory.getVideoDecoderName())
.setVideoDecoderName(decoderFactory.getVideoDecoderName()) .setAudioEncoderName(encoderFactory.getAudioEncoderName())
.setAudioEncoderName(encoderFactory.getAudioEncoderName()) .setVideoEncoderName(encoderFactory.getVideoEncoderName());
.setVideoEncoderName(encoderFactory.getVideoEncoderName());
boolean forCancellation = endReason == END_REASON_CANCELLED; boolean forCancellation = endReason == END_REASON_CANCELLED;
@Nullable TransformationException releaseTransformationException = null; @Nullable TransformationException releaseTransformationException = null;
...@@ -321,15 +313,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -321,15 +313,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
assetLoader.release(); assetLoader.release();
} finally { } finally {
try { 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++) { for (int i = 0; i < samplePipelines.size(); i++) {
samplePipelines.get(i).release(); samplePipelines.get(i).release();
} }
...@@ -378,7 +361,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -378,7 +361,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
transformerConditionVariable.open(); transformerConditionVariable.open();
} }
private class ComponentListener implements AssetLoader.Listener { private class ComponentListener implements AssetLoader.Listener, MuxerWrapper.Listener {
private final MediaItem mediaItem; private final MediaItem mediaItem;
private final FallbackListener fallbackListener; private final FallbackListener fallbackListener;
...@@ -395,6 +378,15 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -395,6 +378,15 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
durationUs = C.TIME_UNSET; 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. // AssetLoader.Listener implementation.
@Override @Override
...@@ -472,12 +464,31 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -472,12 +464,31 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
onTransformationError(transformationException); 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 internalHandler
.obtainMessage(MSG_END, END_REASON_ERROR, /* unused */ 0, transformationException) .obtainMessage(
MSG_END, END_REASON_COMPLETED, /* unused */ 0, /* transformationException */ null)
.sendToTarget(); .sendToTarget();
} }
// Private methods.
private SamplePipeline getSamplePipeline( private SamplePipeline getSamplePipeline(
Format inputFormat, Format inputFormat,
@AssetLoader.SupportedOutputTypes int supportedOutputTypes, @AssetLoader.SupportedOutputTypes int supportedOutputTypes,
......
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