Commit a3f981ae by samrobinson Committed by Ian Baker

Output from the Transformer the average audio & video bitrates.

PiperOrigin-RevId: 426956151
parent 60d9ae47
......@@ -39,6 +39,7 @@ import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.transformer.ProgressHolder;
import com.google.android.exoplayer2.transformer.TransformationException;
import com.google.android.exoplayer2.transformer.TransformationRequest;
import com.google.android.exoplayer2.transformer.TransformationResult;
import com.google.android.exoplayer2.transformer.Transformer;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.StyledPlayerView;
......@@ -223,7 +224,8 @@ public final class TransformerActivity extends AppCompatActivity {
.addListener(
new Transformer.Listener() {
@Override
public void onTransformationCompleted(MediaItem mediaItem) {
public void onTransformationCompleted(
MediaItem mediaItem, TransformationResult transformationResult) {
TransformerActivity.this.onTransformationCompleted(filePath);
}
......
......@@ -65,7 +65,7 @@ app is notified of events via the listener passed to the `Transformer` builder.
Transformer.Listener transformerListener =
new Transformer.Listener() {
@Override
public void onTransformationCompleted(MediaItem inputMediaItem) {
public void onTransformationCompleted(MediaItem inputMediaItem, TransformationResult transformationResult) {
playOutput();
}
......
......@@ -69,6 +69,9 @@ public final class C {
/** Represents an unset or unknown rate. */
public static final float RATE_UNSET = -Float.MAX_VALUE;
/** Represents an unset or unknown integer rate. */
public static final int RATE_UNSET_INT = Integer.MIN_VALUE + 1;
/** Represents an unset or unknown length. */
public static final int LENGTH_UNSET = -1;
......
......@@ -24,6 +24,7 @@ import android.net.Uri;
import android.os.Build;
import androidx.annotation.Nullable;
import androidx.test.platform.app.InstrumentationRegistry;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
import java.io.File;
import java.io.FileWriter;
......@@ -39,50 +40,6 @@ public final class AndroidTestUtil {
public static final String REMOTE_MP4_10_SECONDS_URI_STRING =
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/android-screens-10s.mp4";
/** Information about the result of successfully running a transformer. */
public static final class TransformationResult {
/** A builder for {@link TransformationResult} instances. */
public static final class Builder {
private final String testId;
@Nullable private Long fileSizeBytes;
public Builder(String testId) {
this.testId = testId;
}
public Builder setFileSizeBytes(long fileSizeBytes) {
this.fileSizeBytes = fileSizeBytes;
return this;
}
public TransformationResult build() {
return new TransformationResult(testId, fileSizeBytes);
}
}
public final String testId;
@Nullable public final Long fileSizeBytes;
private TransformationResult(String testId, @Nullable Long fileSizeBytes) {
this.testId = testId;
this.fileSizeBytes = fileSizeBytes;
}
/**
* Returns all the analysis data from the test.
*
* <p>If a value was not generated, it will not be part of the return value.
*/
public String getFormattedAnalysis() {
String analysis = "test=" + testId;
if (fileSizeBytes != null) {
analysis += ", fileSizeBytes=" + fileSizeBytes;
}
return analysis;
}
}
/**
* Transforms the {@code uriString} with the {@link Transformer}.
*
......@@ -98,6 +55,7 @@ public final class AndroidTestUtil {
Context context, String testId, Transformer transformer, String uriString, int timeoutSeconds)
throws Exception {
AtomicReference<@NullableType Exception> exceptionReference = new AtomicReference<>();
AtomicReference<TransformationResult> resultReference = new AtomicReference<>();
CountDownLatch countDownLatch = new CountDownLatch(1);
Transformer testTransformer =
......@@ -106,7 +64,9 @@ public final class AndroidTestUtil {
.addListener(
new Transformer.Listener() {
@Override
public void onTransformationCompleted(MediaItem inputMediaItem) {
public void onTransformationCompleted(
MediaItem inputMediaItem, TransformationResult result) {
resultReference.set(result);
countDownLatch.countDown();
}
......@@ -141,19 +101,21 @@ public final class AndroidTestUtil {
}
TransformationResult result =
new TransformationResult.Builder(testId).setFileSizeBytes(outputVideoFile.length()).build();
resultReference.get().buildUpon().setFileSizeBytes(outputVideoFile.length()).build();
writeTransformationResultToFile(context, result);
writeResultToFile(context, testId, result);
return result;
}
private static void writeTransformationResultToFile(Context context, TransformationResult result)
private static void writeResultToFile(Context context, String testId, TransformationResult result)
throws IOException {
File analysisFile =
createExternalCacheFile(context, /* fileName= */ result.testId + "-result.txt");
File analysisFile = createExternalCacheFile(context, /* fileName= */ testId + "-result.txt");
try (FileWriter fileWriter = new FileWriter(analysisFile)) {
String fileContents =
result.getFormattedAnalysis()
"test="
+ testId
+ ", "
+ getFormattedResult(result)
+ ", deviceFingerprint="
+ Build.FINGERPRINT
+ ", deviceBrand="
......@@ -166,6 +128,22 @@ public final class AndroidTestUtil {
}
}
/** Formats a {@link TransformationResult} into a comma separated String. */
public static String getFormattedResult(TransformationResult result) {
String analysis = "";
if (result.fileSizeBytes != C.LENGTH_UNSET) {
analysis += "fileSizeBytes=" + result.fileSizeBytes;
}
if (result.averageAudioBitrate != C.RATE_UNSET_INT) {
analysis += ", averageAudioBitrate=" + result.averageAudioBitrate;
}
if (result.averageVideoBitrate != C.RATE_UNSET_INT) {
analysis += ", averageVideoBitrate=" + result.averageVideoBitrate;
}
return analysis;
}
private static File createExternalCacheFile(Context context, String fileName) throws IOException {
File file = new File(context.getExternalCacheDir(), fileName);
checkState(!file.exists() || file.delete(), "Could not delete file: " + file.getAbsolutePath());
......
......@@ -24,6 +24,7 @@ import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.transformer.AndroidTestUtil;
import com.google.android.exoplayer2.transformer.TransformationRequest;
import com.google.android.exoplayer2.transformer.TransformationResult;
import com.google.android.exoplayer2.transformer.Transformer;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
......@@ -57,7 +58,7 @@ public final class RepeatedTranscodeTransformationTest {
Set<Long> differentOutputSizesBytes = new HashSet<>();
for (int i = 0; i < TRANSCODE_COUNT; i++) {
// Use a long video in case an error occurs a while after the start of the video.
AndroidTestUtil.TransformationResult result =
TransformationResult result =
runTransformer(
context,
/* testId= */ "repeatedTranscode_givesConsistentLengthOutput_" + i,
......@@ -91,7 +92,7 @@ public final class RepeatedTranscodeTransformationTest {
Set<Long> differentOutputSizesBytes = new HashSet<>();
for (int i = 0; i < TRANSCODE_COUNT; i++) {
// Use a long video in case an error occurs a while after the start of the video.
AndroidTestUtil.TransformationResult result =
TransformationResult result =
runTransformer(
context,
/* testId= */ "repeatedTranscodeNoAudio_givesConsistentLengthOutput_" + i,
......@@ -122,7 +123,7 @@ public final class RepeatedTranscodeTransformationTest {
Set<Long> differentOutputSizesBytes = new HashSet<>();
for (int i = 0; i < TRANSCODE_COUNT; i++) {
// Use a long video in case an error occurs a while after the start of the video.
AndroidTestUtil.TransformationResult result =
TransformationResult result =
runTransformer(
context,
/* testId= */ "repeatedTranscodeNoVideo_givesConsistentLengthOutput_" + i,
......
......@@ -48,6 +48,7 @@ import java.nio.ByteBuffer;
private final Muxer.Factory muxerFactory;
private final SparseIntArray trackTypeToIndex;
private final SparseLongArray trackTypeToTimeUs;
private final SparseLongArray trackTypeToBytesWritten;
private final String containerMimeType;
private int trackCount;
......@@ -62,6 +63,7 @@ import java.nio.ByteBuffer;
this.containerMimeType = containerMimeType;
trackTypeToIndex = new SparseIntArray();
trackTypeToTimeUs = new SparseLongArray();
trackTypeToBytesWritten = new SparseLongArray();
previousTrackType = C.TRACK_TYPE_NONE;
}
......@@ -121,6 +123,7 @@ import java.nio.ByteBuffer;
int trackIndex = muxer.addTrack(format);
trackTypeToIndex.put(trackType, trackIndex);
trackTypeToTimeUs.put(trackType, 0L);
trackTypeToBytesWritten.put(trackType, 0L);
trackFormatCount++;
if (trackFormatCount == trackCount) {
isReady = true;
......@@ -154,8 +157,11 @@ import java.nio.ByteBuffer;
return false;
}
muxer.writeSampleData(trackIndex, data, isKeyFrame, presentationTimeUs);
trackTypeToBytesWritten.put(
trackType, trackTypeToBytesWritten.get(trackType) + data.remaining());
trackTypeToTimeUs.put(trackType, presentationTimeUs);
muxer.writeSampleData(trackIndex, data, isKeyFrame, presentationTimeUs);
previousTrackType = trackType;
return true;
}
......@@ -168,7 +174,6 @@ import java.nio.ByteBuffer;
*/
public void endTrack(@C.TrackType int trackType) {
trackTypeToIndex.delete(trackType);
trackTypeToTimeUs.delete(trackType);
}
/**
......@@ -192,6 +197,25 @@ import java.nio.ByteBuffer;
}
/**
* 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 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}.
......@@ -208,7 +232,7 @@ import java.nio.ByteBuffer;
if (!isReady) {
return false;
}
if (trackTypeToTimeUs.size() == 1) {
if (trackTypeToIndex.size() == 1) {
return true;
}
if (trackType != previousTrackType) {
......
/*
* 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 com.google.android.exoplayer2.transformer;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
/** Information about the result of a successful transformation. */
public final class TransformationResult {
/** A builder for {@link TransformationResult} instances. */
public static final class Builder {
private long fileSizeBytes;
private int averageAudioBitrate;
private int averageVideoBitrate;
public Builder() {
fileSizeBytes = C.LENGTH_UNSET;
averageAudioBitrate = C.RATE_UNSET_INT;
averageVideoBitrate = C.RATE_UNSET_INT;
}
/**
* Sets the file size in bytes.
*
* <p>Input must be positive or {@link C#LENGTH_UNSET}.
*/
public Builder setFileSizeBytes(long fileSizeBytes) {
checkArgument(fileSizeBytes > 0 || fileSizeBytes == C.LENGTH_UNSET);
this.fileSizeBytes = fileSizeBytes;
return this;
}
/**
* Sets the average audio bitrate.
*
* <p>Input must be positive or {@link C#RATE_UNSET_INT}.
*/
public Builder setAverageAudioBitrate(int averageAudioBitrate) {
checkArgument(averageAudioBitrate > 0 || averageAudioBitrate == C.RATE_UNSET_INT);
this.averageAudioBitrate = averageAudioBitrate;
return this;
}
/**
* Sets the average video bitrate.
*
* <p>Input must be positive or {@link C#RATE_UNSET_INT}.
*/
public Builder setAverageVideoBitrate(int averageVideoBitrate) {
checkArgument(averageVideoBitrate > 0 || averageVideoBitrate == C.RATE_UNSET_INT);
this.averageVideoBitrate = averageVideoBitrate;
return this;
}
public TransformationResult build() {
return new TransformationResult(fileSizeBytes, averageAudioBitrate, averageVideoBitrate);
}
}
/** The size of the file in bytes, or {@link C#LENGTH_UNSET} if unset or unknown. */
public final long fileSizeBytes;
/**
* The average bitrate of the audio track data, or {@link C#RATE_UNSET_INT} if unset or unknown.
*/
public final int averageAudioBitrate;
/**
* The average bitrate of the video track data, or {@link C#RATE_UNSET_INT} if unset or unknown.
*/
public final int averageVideoBitrate;
private TransformationResult(
long fileSizeBytes, int averageAudioBitrate, int averageVideoBitrate) {
this.fileSizeBytes = fileSizeBytes;
this.averageAudioBitrate = averageAudioBitrate;
this.averageVideoBitrate = averageVideoBitrate;
}
public Builder buildUpon() {
return new Builder()
.setFileSizeBytes(fileSizeBytes)
.setAverageAudioBitrate(averageAudioBitrate)
.setAverageVideoBitrate(averageVideoBitrate);
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (!(o instanceof TransformationResult)) {
return false;
}
TransformationResult result = (TransformationResult) o;
return fileSizeBytes == result.fileSizeBytes
&& averageAudioBitrate == result.averageAudioBitrate
&& averageVideoBitrate == result.averageVideoBitrate;
}
@Override
public int hashCode() {
int result = (int) fileSizeBytes;
result = 31 * result + averageAudioBitrate;
result = 31 * result + averageVideoBitrate;
return result;
}
}
......@@ -444,11 +444,21 @@ public final class Transformer {
public interface Listener {
/**
* @deprecated Use {@link #onTransformationCompleted(MediaItem, TransformationResult)} instead.
*/
@Deprecated
default void onTransformationCompleted(MediaItem inputMediaItem) {}
/**
* Called when the transformation is completed successfully.
*
* @param inputMediaItem The {@link MediaItem} for which the transformation is completed.
* @param transformationResult The {@link TransformationResult} of the transformation.
*/
default void onTransformationCompleted(MediaItem inputMediaItem) {}
default void onTransformationCompleted(
MediaItem inputMediaItem, TransformationResult transformationResult) {
onTransformationCompleted(inputMediaItem);
}
/** @deprecated Use {@link #onTransformationError(MediaItem, TransformationException)}. */
@Deprecated
......@@ -733,8 +743,9 @@ public final class Transformer {
* Returns the current {@link ProgressState} and updates {@code progressHolder} with the current
* progress if it is {@link #PROGRESS_STATE_AVAILABLE available}.
*
* <p>After a transformation {@link Listener#onTransformationCompleted(MediaItem) completes}, this
* method returns {@link #PROGRESS_STATE_NO_TRANSFORMATION}.
* <p>After a transformation {@link Listener#onTransformationCompleted(MediaItem,
* TransformationResult) completes}, this method returns {@link
* #PROGRESS_STATE_NO_TRANSFORMATION}.
*
* @param progressHolder A {@link ProgressHolder}, updated to hold the percentage progress if
* {@link #PROGRESS_STATE_AVAILABLE available}.
......@@ -950,9 +961,14 @@ public final class Transformer {
/* eventFlag= */ C.INDEX_UNSET,
listener -> listener.onTransformationError(mediaItem, finalException));
} else {
TransformationResult result =
new TransformationResult.Builder()
.setAverageAudioBitrate(muxerWrapper.getTrackAverageBitrate(C.TRACK_TYPE_AUDIO))
.setAverageVideoBitrate(muxerWrapper.getTrackAverageBitrate(C.TRACK_TYPE_VIDEO))
.build();
listeners.queueEvent(
/* eventFlag= */ C.INDEX_UNSET,
listener -> listener.onTransformationCompleted(mediaItem));
listener -> listener.onTransformationCompleted(mediaItem, result));
}
listeners.flushEvents();
}
......
......@@ -22,6 +22,8 @@ import static com.google.android.exoplayer2.transformer.Transformer.PROGRESS_STA
import static com.google.android.exoplayer2.transformer.Transformer.PROGRESS_STATE_WAITING_FOR_AVAILABILITY;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
......@@ -57,6 +59,7 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.checkerframework.checker.nullness.compatqual.NullableType;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
......@@ -239,9 +242,9 @@ public final class TransformerEndToEndTest {
transformer.startTransformation(mediaItem, outputPath);
TransformerTestRunner.runUntilCompleted(transformer);
verify(mockListener1, times(1)).onTransformationCompleted(mediaItem);
verify(mockListener2, times(1)).onTransformationCompleted(mediaItem);
verify(mockListener3, times(1)).onTransformationCompleted(mediaItem);
verify(mockListener1, times(1)).onTransformationCompleted(eq(mediaItem), any());
verify(mockListener2, times(1)).onTransformationCompleted(eq(mediaItem), any());
verify(mockListener3, times(1)).onTransformationCompleted(eq(mediaItem), any());
}
@Test
......@@ -313,9 +316,9 @@ public final class TransformerEndToEndTest {
transformer2.startTransformation(mediaItem, outputPath);
TransformerTestRunner.runUntilCompleted(transformer2);
verify(mockListener1, times(1)).onTransformationCompleted(mediaItem);
verify(mockListener2, never()).onTransformationCompleted(mediaItem);
verify(mockListener3, times(1)).onTransformationCompleted(mediaItem);
verify(mockListener1, times(1)).onTransformationCompleted(eq(mediaItem), any());
verify(mockListener2, never()).onTransformationCompleted(eq(mediaItem), any());
verify(mockListener3, times(1)).onTransformationCompleted(eq(mediaItem), any());
}
@Test
......@@ -334,6 +337,30 @@ public final class TransformerEndToEndTest {
}
@Test
public void startTransformation_completesWithValidBitrate() throws Exception {
AtomicReference<@NullableType TransformationResult> resultReference = new AtomicReference<>();
Transformer.Listener listener =
new Transformer.Listener() {
@Override
public void onTransformationCompleted(
MediaItem inputMediaItem, TransformationResult transformationResult) {
resultReference.set(transformationResult);
}
};
Transformer transformer =
createTransformerBuilder(/* disableFallback= */ true).addListener(listener).build();
MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_AUDIO_VIDEO);
transformer.startTransformation(mediaItem, outputPath);
TransformerTestRunner.runUntilCompleted(transformer);
@Nullable TransformationResult result = resultReference.get();
assertThat(result).isNotNull();
assertThat(result.averageAudioBitrate).isGreaterThan(0);
assertThat(result.averageVideoBitrate).isGreaterThan(0);
}
@Test
public void startTransformation_withAudioEncoderFormatUnsupported_completesWithError()
throws Exception {
Transformer transformer =
......
......@@ -78,7 +78,8 @@ public final class TransformerTestRunner {
transformer.addListener(
new Transformer.Listener() {
@Override
public void onTransformationCompleted(MediaItem inputMediaItem) {
public void onTransformationCompleted(
MediaItem inputMediaItem, TransformationResult transformationResult) {
transformationCompleted.set(true);
}
......
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