diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameCountingMuxer.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameCountingMuxer.java deleted file mode 100644 index 27e05ace84..0000000000 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameCountingMuxer.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package androidx.media3.transformer; - -import android.os.ParcelFileDescriptor; -import androidx.annotation.Nullable; -import androidx.media3.common.C; -import androidx.media3.common.Format; -import androidx.media3.common.MimeTypes; -import com.google.common.collect.ImmutableList; -import java.io.IOException; -import java.nio.ByteBuffer; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; - -/** - * An implementation of {@link Muxer} that forwards operations to another {@link Muxer}, counting - * the number of frames as they go past. - */ -/* package */ final class FrameCountingMuxer implements Muxer { - public static final class Factory implements Muxer.Factory { - - private final Muxer.Factory muxerFactory; - private @MonotonicNonNull FrameCountingMuxer frameCountingMuxer; - - public Factory(Muxer.Factory muxerFactory) { - this.muxerFactory = muxerFactory; - } - - @Override - public Muxer create(String path, String outputMimeType) throws IOException { - frameCountingMuxer = new FrameCountingMuxer(muxerFactory.create(path, outputMimeType)); - return frameCountingMuxer; - } - - @Override - public Muxer create(ParcelFileDescriptor parcelFileDescriptor, String outputMimeType) - throws IOException { - frameCountingMuxer = - new FrameCountingMuxer(muxerFactory.create(parcelFileDescriptor, outputMimeType)); - return frameCountingMuxer; - } - - @Override - public boolean supportsOutputMimeType(String mimeType) { - return muxerFactory.supportsOutputMimeType(mimeType); - } - - @Override - public boolean supportsSampleMimeType(@Nullable String sampleMimeType, String outputMimeType) { - return muxerFactory.supportsSampleMimeType(sampleMimeType, outputMimeType); - } - - @Override - public ImmutableList getSupportedSampleMimeTypes( - @C.TrackType int trackType, String containerMimeType) { - return muxerFactory.getSupportedSampleMimeTypes(trackType, containerMimeType); - } - - @Nullable - public FrameCountingMuxer getLastFrameCountingMuxerCreated() { - return frameCountingMuxer; - } - } - - private final Muxer muxer; - private int videoTrackIndex; - private int frameCount; - - private FrameCountingMuxer(Muxer muxer) throws IOException { - this.muxer = muxer; - } - - @Override - public int addTrack(Format format) throws MuxerException { - int trackIndex = muxer.addTrack(format); - if (MimeTypes.isVideo(format.sampleMimeType)) { - videoTrackIndex = trackIndex; - } - return trackIndex; - } - - @Override - public void writeSampleData( - int trackIndex, ByteBuffer data, boolean isKeyFrame, long presentationTimeUs) - throws MuxerException { - muxer.writeSampleData(trackIndex, data, isKeyFrame, presentationTimeUs); - if (trackIndex == videoTrackIndex) { - frameCount++; - } - } - - @Override - public void release(boolean forCancellation) throws MuxerException { - muxer.release(forCancellation); - } - - /* Returns the number of frames written for the video track. */ - public int getFrameCount() { - return frameCount; - } -} diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformationTestResult.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformationTestResult.java index d74e5484d9..c2bd3241e2 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformationTestResult.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformationTestResult.java @@ -31,7 +31,6 @@ public class TransformationTestResult { @Nullable private String filePath; @Nullable private Exception analysisException; - private long elapsedTimeMs; private double ssim; @@ -103,8 +102,12 @@ public class TransformationTestResult { } public final TransformationResult transformationResult; - @Nullable public final String filePath; + /** + * The average rate (per second) at which frames are processed by the transformer, or {@link + * C#RATE_UNSET} if unset or unknown. + */ + public final float throughputFps; /** * The amount of time taken to perform the transformation in milliseconds. {@link C#TIME_UNSET} if * unset. @@ -133,6 +136,12 @@ public class TransformationTestResult { if (transformationResult.averageVideoBitrate != C.RATE_UNSET_INT) { jsonObject.put("averageVideoBitrate", transformationResult.averageVideoBitrate); } + if (transformationResult.videoFrameCount > 0) { + jsonObject.put("videoFrameCount", transformationResult.videoFrameCount); + } + if (throughputFps != C.RATE_UNSET) { + jsonObject.put("throughputFps", throughputFps); + } if (elapsedTimeMs != C.TIME_UNSET) { jsonObject.put("elapsedTimeMs", elapsedTimeMs); } @@ -156,5 +165,9 @@ public class TransformationTestResult { this.elapsedTimeMs = elapsedTimeMs; this.ssim = ssim; this.analysisException = analysisException; + this.throughputFps = + elapsedTimeMs != C.TIME_UNSET && transformationResult.videoFrameCount > 0 + ? 1000f * transformationResult.videoFrameCount / elapsedTimeMs + : C.RATE_UNSET; } } diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java index cdf4e0d902..d1a9182e87 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java @@ -15,7 +15,7 @@ */ package androidx.media3.transformer; -import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_URI_STRING; import static com.google.common.truth.Truth.assertThat; import android.content.Context; @@ -31,18 +31,13 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class TransformerEndToEndTest { - private static final String AVC_VIDEO_URI_STRING = "asset:///media/mp4/sample.mp4"; - @Test public void videoEditing_completesWithConsistentFrameCount() throws Exception { Context context = ApplicationProvider.getApplicationContext(); - FrameCountingMuxer.Factory muxerFactory = - new FrameCountingMuxer.Factory(new FrameworkMuxer.Factory()); Transformer transformer = new Transformer.Builder(context) .setTransformationRequest( new TransformationRequest.Builder().setResolution(480).build()) - .setMuxerFactory(muxerFactory) .setEncoderFactory( new DefaultEncoderFactory(EncoderSelector.DEFAULT, /* enableFallback= */ false)) .build(); @@ -50,13 +45,14 @@ public class TransformerEndToEndTest { // ffprobe -count_frames -select_streams v:0 -show_entries stream=nb_read_frames sample.mp4 int expectedFrameCount = 30; - new TransformerAndroidTestRunner.Builder(context, transformer) - .build() - .run(/* testId= */ "videoEditing_completesWithConsistentFrameCount", AVC_VIDEO_URI_STRING); + TransformationTestResult result = + new TransformerAndroidTestRunner.Builder(context, transformer) + .build() + .run( + /* testId= */ "videoEditing_completesWithConsistentFrameCount", + MP4_ASSET_URI_STRING); - FrameCountingMuxer frameCountingMuxer = - checkNotNull(muxerFactory.getLastFrameCountingMuxerCreated()); - assertThat(frameCountingMuxer.getFrameCount()).isEqualTo(expectedFrameCount); + assertThat(result.transformationResult.videoFrameCount).isEqualTo(expectedFrameCount); } @Test @@ -75,7 +71,7 @@ public class TransformerEndToEndTest { TransformationTestResult result = new TransformerAndroidTestRunner.Builder(context, transformer) .build() - .run(/* testId= */ "videoOnly_completesWithConsistentDuration", AVC_VIDEO_URI_STRING); + .run(/* testId= */ "videoOnly_completesWithConsistentDuration", MP4_ASSET_URI_STRING); assertThat(result.transformationResult.durationMs).isEqualTo(expectedDurationMs); } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/MuxerWrapper.java b/libraries/transformer/src/main/java/androidx/media3/transformer/MuxerWrapper.java index ce8af780ac..97941b5ccf 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/MuxerWrapper.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/MuxerWrapper.java @@ -48,6 +48,7 @@ import java.nio.ByteBuffer; private final Muxer muxer; private final Muxer.Factory muxerFactory; private final SparseIntArray trackTypeToIndex; + private final SparseIntArray trackTypeToSampleCount; private final SparseLongArray trackTypeToTimeUs; private final SparseLongArray trackTypeToBytesWritten; private final String containerMimeType; @@ -62,7 +63,9 @@ import java.nio.ByteBuffer; this.muxer = muxer; this.muxerFactory = muxerFactory; this.containerMimeType = containerMimeType; + trackTypeToIndex = new SparseIntArray(); + trackTypeToSampleCount = new SparseIntArray(); trackTypeToTimeUs = new SparseLongArray(); trackTypeToBytesWritten = new SparseLongArray(); previousTrackType = C.TRACK_TYPE_NONE; @@ -123,6 +126,7 @@ import java.nio.ByteBuffer; int trackIndex = muxer.addTrack(format); trackTypeToIndex.put(trackType, trackIndex); + trackTypeToSampleCount.put(trackType, 0); trackTypeToTimeUs.put(trackType, 0L); trackTypeToBytesWritten.put(trackType, 0L); trackFormatCount++; @@ -158,6 +162,7 @@ import java.nio.ByteBuffer; return false; } + trackTypeToSampleCount.put(trackType, trackTypeToSampleCount.get(trackType) + 1); trackTypeToBytesWritten.put( trackType, trackTypeToBytesWritten.get(trackType) + data.remaining()); if (trackTypeToTimeUs.get(trackType) < presentationTimeUs) { @@ -218,6 +223,16 @@ import java.nio.ByteBuffer; /* 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. */ + public long getDurationMs() { + return Util.usToMs(maxValue(trackTypeToTimeUs)); + } + /** * Returns whether the muxer can write a sample of the given track type. * @@ -243,9 +258,4 @@ import java.nio.ByteBuffer; } return trackTimeUs - minTrackTimeUs <= MAX_TRACK_WRITE_AHEAD_US; } - - /** Returns the duration of the longest track in milliseconds. */ - public long getDurationMs() { - return Util.usToMs(maxValue(trackTypeToTimeUs)); - } } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationResult.java b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationResult.java index 2bf98abe1d..7aba68e647 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationResult.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationResult.java @@ -31,6 +31,7 @@ public final class TransformationResult { private long fileSizeBytes; private int averageAudioBitrate; private int averageVideoBitrate; + private int videoFrameCount; public Builder() { durationMs = C.TIME_UNSET; @@ -83,13 +84,24 @@ public final class TransformationResult { return this; } + /** + * Sets the number of video frames. + * + *

Input must be positive or {@code 0}. + */ + public Builder setVideoFrameCount(int videoFrameCount) { + checkArgument(videoFrameCount >= 0); + this.videoFrameCount = videoFrameCount; + return this; + } + public TransformationResult build() { return new TransformationResult( - durationMs, fileSizeBytes, averageAudioBitrate, averageVideoBitrate); + durationMs, fileSizeBytes, averageAudioBitrate, averageVideoBitrate, videoFrameCount); } } - /** The duration of the video in milliseconds, or {@link C#TIME_UNSET} if unset or unknown. */ + /** The duration of the file in milliseconds, or {@link C#TIME_UNSET} if unset or unknown. */ public final long durationMs; /** The size of the file in bytes, or {@link C#LENGTH_UNSET} if unset or unknown. */ public final long fileSizeBytes; @@ -101,13 +113,20 @@ public final class TransformationResult { * The average bitrate of the video track data, or {@link C#RATE_UNSET_INT} if unset or unknown. */ public final int averageVideoBitrate; + /** The number of video frames. */ + public final int videoFrameCount; private TransformationResult( - long durationMs, long fileSizeBytes, int averageAudioBitrate, int averageVideoBitrate) { + long durationMs, + long fileSizeBytes, + int averageAudioBitrate, + int averageVideoBitrate, + int videoFrameCount) { this.durationMs = durationMs; this.fileSizeBytes = fileSizeBytes; this.averageAudioBitrate = averageAudioBitrate; this.averageVideoBitrate = averageVideoBitrate; + this.videoFrameCount = videoFrameCount; } public Builder buildUpon() { @@ -115,7 +134,8 @@ public final class TransformationResult { .setDurationMs(durationMs) .setFileSizeBytes(fileSizeBytes) .setAverageAudioBitrate(averageAudioBitrate) - .setAverageVideoBitrate(averageVideoBitrate); + .setAverageVideoBitrate(averageVideoBitrate) + .setVideoFrameCount(videoFrameCount); } @Override @@ -130,7 +150,8 @@ public final class TransformationResult { return durationMs == result.durationMs && fileSizeBytes == result.fileSizeBytes && averageAudioBitrate == result.averageAudioBitrate - && averageVideoBitrate == result.averageVideoBitrate; + && averageVideoBitrate == result.averageVideoBitrate + && videoFrameCount == result.videoFrameCount; } @Override @@ -139,6 +160,7 @@ public final class TransformationResult { result = 31 * result + (int) fileSizeBytes; result = 31 * result + averageAudioBitrate; result = 31 * result + averageVideoBitrate; + result = 31 * result + videoFrameCount; return result; } } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java index 28e7c4f5c3..c812cdab23 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java @@ -704,7 +704,6 @@ public final class Transformer { if (player != null) { throw new IllegalStateException("There is already a transformation in progress."); } - MuxerWrapper muxerWrapper = new MuxerWrapper(muxer, muxerFactory, containerMimeType); this.muxerWrapper = muxerWrapper; DefaultTrackSelector trackSelector = new DefaultTrackSelector(context); @@ -1005,7 +1004,9 @@ public final class Transformer { .setDurationMs(muxerWrapper.getDurationMs()) .setAverageAudioBitrate(muxerWrapper.getTrackAverageBitrate(C.TRACK_TYPE_AUDIO)) .setAverageVideoBitrate(muxerWrapper.getTrackAverageBitrate(C.TRACK_TYPE_VIDEO)) + .setVideoFrameCount(muxerWrapper.getTrackSampleCount(C.TRACK_TYPE_VIDEO)) .build(); + listeners.queueEvent( /* eventFlag= */ C.INDEX_UNSET, listener -> listener.onTransformationCompleted(mediaItem, result));