diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/MuxerWrapper.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/MuxerWrapper.java index b0dfe8e014..82ec9d3f0c 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/MuxerWrapper.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/MuxerWrapper.java @@ -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 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 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); + } } diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerInternal.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerInternal.java index d33666dbe4..ef5ef719a7 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerInternal.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerInternal.java @@ -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,