Commit d97de5b9 by hschlueter Committed by Ian Baker

Use microseconds not nanoseconds for GlFrameProcessor.

This requires an additional nanos to micros conversion because
the SurfaceTexture uses nanos. But as the timestamps from the
MediaCodec decoder (propagated in DefaultCodec#releaseOutputBuffer) are
in microseconds no precision is lost here.

Also add test that checks output video duration.

PiperOrigin-RevId: 438010490
parent 2adf0f67
......@@ -1122,6 +1122,25 @@ public final class Util {
}
/**
* Returns the maximum value in the given {@link SparseLongArray}.
*
* @param sparseLongArray The {@link SparseLongArray}.
* @return The maximum value.
* @throws NoSuchElementException If the array is empty.
*/
@RequiresApi(18)
public static long maxValue(SparseLongArray sparseLongArray) {
if (sparseLongArray.size() == 0) {
throw new NoSuchElementException();
}
long max = Long.MIN_VALUE;
for (int i = 0; i < sparseLongArray.size(); i++) {
max = max(max, sparseLongArray.valueAt(i));
}
return max;
}
/**
* Converts a time in microseconds to the corresponding time in milliseconds, preserving {@link
* C#TIME_UNSET} and {@link C#TIME_END_OF_SOURCE} values.
*
......
......@@ -21,6 +21,7 @@ import static androidx.media3.common.util.Util.escapeFileName;
import static androidx.media3.common.util.Util.getCodecsOfType;
import static androidx.media3.common.util.Util.getStringForTime;
import static androidx.media3.common.util.Util.gzip;
import static androidx.media3.common.util.Util.maxValue;
import static androidx.media3.common.util.Util.minValue;
import static androidx.media3.common.util.Util.parseXsDateTime;
import static androidx.media3.common.util.Util.parseXsDuration;
......@@ -748,6 +749,21 @@ public class UtilTest {
}
@Test
public void sparseLongArrayMaxValue_returnsMaxValue() {
SparseLongArray sparseLongArray = new SparseLongArray();
sparseLongArray.put(0, 2);
sparseLongArray.put(25, 10);
sparseLongArray.put(42, 1);
assertThat(maxValue(sparseLongArray)).isEqualTo(10);
}
@Test
public void sparseLongArrayMaxValue_emptyArray_throws() {
assertThrows(NoSuchElementException.class, () -> maxValue(new SparseLongArray()));
}
@Test
public void parseXsDuration_returnsParsedDurationInMillis() {
assertThat(parseXsDuration("PT150.279S")).isEqualTo(150279L);
assertThat(parseXsDuration("PT1.500S")).isEqualTo(1500L);
......
......@@ -94,7 +94,7 @@ public final class AdvancedFrameProcessorPixelTest {
advancedFrameProcessor.initialize(inputTexId);
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(FIRST_FRAME_PNG_ASSET_STRING);
advancedFrameProcessor.updateProgramAndDraw(/* presentationTimeNs= */ 0);
advancedFrameProcessor.updateProgramAndDraw(/* presentationTimeUs= */ 0);
Bitmap actualBitmap =
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(width, height);
......@@ -118,7 +118,7 @@ public final class AdvancedFrameProcessorPixelTest {
Bitmap expectedBitmap =
BitmapTestUtil.readBitmap(TRANSLATE_RIGHT_EXPECTED_OUTPUT_PNG_ASSET_STRING);
advancedFrameProcessor.updateProgramAndDraw(/* presentationTimeNs= */ 0);
advancedFrameProcessor.updateProgramAndDraw(/* presentationTimeUs= */ 0);
Bitmap actualBitmap =
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(width, height);
......@@ -141,7 +141,7 @@ public final class AdvancedFrameProcessorPixelTest {
Bitmap expectedBitmap =
BitmapTestUtil.readBitmap(SCALE_NARROW_EXPECTED_OUTPUT_PNG_ASSET_STRING);
advancedFrameProcessor.updateProgramAndDraw(/* presentationTimeNs= */ 0);
advancedFrameProcessor.updateProgramAndDraw(/* presentationTimeUs= */ 0);
Bitmap actualBitmap =
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(width, height);
......@@ -163,7 +163,7 @@ public final class AdvancedFrameProcessorPixelTest {
advancedFrameProcessor.initialize(inputTexId);
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ROTATE_90_EXPECTED_OUTPUT_PNG_ASSET_STRING);
advancedFrameProcessor.updateProgramAndDraw(/* presentationTimeNs= */ 0);
advancedFrameProcessor.updateProgramAndDraw(/* presentationTimeUs= */ 0);
Bitmap actualBitmap =
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(width, height);
......
......@@ -309,6 +309,9 @@ public class TransformerAndroidTestRunner {
TransformationResult transformationResult = testResult.transformationResult;
JSONObject transformationResultJson = new JSONObject();
if (transformationResult.durationMs != C.LENGTH_UNSET) {
transformationResultJson.put("durationMs", transformationResult.durationMs);
}
if (transformationResult.fileSizeBytes != C.LENGTH_UNSET) {
transformationResultJson.put("fileSizeBytes", transformationResult.fileSizeBytes);
}
......
......@@ -58,4 +58,25 @@ public class TransformerEndToEndTest {
checkNotNull(muxerFactory.getLastFrameCountingMuxerCreated());
assertThat(frameCountingMuxer.getFrameCount()).isEqualTo(expectedFrameCount);
}
@Test
public void videoOnly_completesWithConsistentDuration() throws Exception {
Context context = ApplicationProvider.getApplicationContext();
Transformer transformer =
new Transformer.Builder(context)
.setRemoveAudio(true)
.setTransformationRequest(
new TransformationRequest.Builder().setResolution(480).build())
.setEncoderFactory(
new DefaultEncoderFactory(EncoderSelector.DEFAULT, /* enableFallback= */ false))
.build();
long expectedDurationMs = 967;
TransformationTestResult result =
new TransformerAndroidTestRunner.Builder(context, transformer)
.build()
.run(/* testId= */ "videoOnly_completesWithConsistentDuration", AVC_VIDEO_URI_STRING);
assertThat(result.transformationResult.durationMs).isEqualTo(expectedDurationMs);
}
}
......@@ -124,7 +124,7 @@ public final class AdvancedFrameProcessor implements GlFrameProcessor {
}
@Override
public void updateProgramAndDraw(long presentationTimeNs) {
public void updateProgramAndDraw(long presentationTimeUs) {
checkStateNotNull(glProgram);
glProgram.use();
glProgram.bindAttributesAndUniforms();
......
......@@ -101,7 +101,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
@Override
public void updateProgramAndDraw(long presentationTimeNs) {
public void updateProgramAndDraw(long presentationTimeUs) {
checkStateNotNull(glProgram);
glProgram.use();
glProgram.bindAttributesAndUniforms();
......
......@@ -412,7 +412,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
inputSurfaceTexture.getTransformMatrix(textureTransformMatrix);
externalCopyFrameProcessor.setTextureTransformMatrix(textureTransformMatrix);
long presentationTimeNs = inputSurfaceTexture.getTimestamp();
externalCopyFrameProcessor.updateProgramAndDraw(presentationTimeNs);
long presentationTimeUs = presentationTimeNs / 1000;
externalCopyFrameProcessor.updateProgramAndDraw(presentationTimeUs);
for (int i = 0; i < frameProcessors.size() - 1; i++) {
Size outputSize = inputSizes.get(i + 1);
......@@ -423,11 +424,11 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
framebuffers[i + 1],
outputSize.getWidth(),
outputSize.getHeight());
frameProcessors.get(i).updateProgramAndDraw(presentationTimeNs);
frameProcessors.get(i).updateProgramAndDraw(presentationTimeUs);
}
if (!frameProcessors.isEmpty()) {
GlUtil.focusEglSurface(eglDisplay, eglContext, eglSurface, outputWidth, outputHeight);
getLast(frameProcessors).updateProgramAndDraw(presentationTimeNs);
getLast(frameProcessors).updateProgramAndDraw(presentationTimeUs);
}
EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, presentationTimeNs);
......
......@@ -60,9 +60,9 @@ public interface GlFrameProcessor {
* <p>The frame processor must be {@linkplain #initialize(int) initialized}. The caller is
* responsible for focussing the correct render target before calling this method.
*
* @param presentationTimeNs The presentation timestamp of the current frame, in nanoseconds.
* @param presentationTimeUs The presentation timestamp of the current frame, in microseconds.
*/
void updateProgramAndDraw(long presentationTimeNs);
void updateProgramAndDraw(long presentationTimeUs);
/** Releases all resources. */
void release();
......
......@@ -17,6 +17,7 @@
package androidx.media3.transformer;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Util.maxValue;
import static androidx.media3.common.util.Util.minValue;
import android.util.SparseIntArray;
......@@ -240,4 +241,9 @@ 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));
}
}
......@@ -152,8 +152,8 @@ public final class PresentationFrameProcessor implements GlFrameProcessor {
}
@Override
public void updateProgramAndDraw(long presentationTimeNs) {
checkStateNotNull(advancedFrameProcessor).updateProgramAndDraw(presentationTimeNs);
public void updateProgramAndDraw(long presentationTimeUs) {
checkStateNotNull(advancedFrameProcessor).updateProgramAndDraw(presentationTimeUs);
}
@Override
......
......@@ -176,8 +176,8 @@ public final class ScaleToFitFrameProcessor implements GlFrameProcessor {
}
@Override
public void updateProgramAndDraw(long presentationTimeNs) {
checkStateNotNull(advancedFrameProcessor).updateProgramAndDraw(presentationTimeNs);
public void updateProgramAndDraw(long presentationTimeUs) {
checkStateNotNull(advancedFrameProcessor).updateProgramAndDraw(presentationTimeUs);
}
@Override
......
......@@ -27,17 +27,30 @@ public final class TransformationResult {
/** A builder for {@link TransformationResult} instances. */
public static final class Builder {
private long durationMs;
private long fileSizeBytes;
private int averageAudioBitrate;
private int averageVideoBitrate;
public Builder() {
durationMs = C.TIME_UNSET;
fileSizeBytes = C.LENGTH_UNSET;
averageAudioBitrate = C.RATE_UNSET_INT;
averageVideoBitrate = C.RATE_UNSET_INT;
}
/**
* Sets the duration of the video in milliseconds.
*
* <p>Input must be positive or {@link C#TIME_UNSET}.
*/
public Builder setDurationMs(long durationMs) {
checkArgument(durationMs > 0 || durationMs == C.TIME_UNSET);
this.durationMs = durationMs;
return this;
}
/**
* Sets the file size in bytes.
*
* <p>Input must be positive or {@link C#LENGTH_UNSET}.
......@@ -71,10 +84,13 @@ public final class TransformationResult {
}
public TransformationResult build() {
return new TransformationResult(fileSizeBytes, averageAudioBitrate, averageVideoBitrate);
return new TransformationResult(
durationMs, fileSizeBytes, averageAudioBitrate, averageVideoBitrate);
}
}
/** The duration of the video 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;
/**
......@@ -87,7 +103,8 @@ public final class TransformationResult {
public final int averageVideoBitrate;
private TransformationResult(
long fileSizeBytes, int averageAudioBitrate, int averageVideoBitrate) {
long durationMs, long fileSizeBytes, int averageAudioBitrate, int averageVideoBitrate) {
this.durationMs = durationMs;
this.fileSizeBytes = fileSizeBytes;
this.averageAudioBitrate = averageAudioBitrate;
this.averageVideoBitrate = averageVideoBitrate;
......@@ -95,6 +112,7 @@ public final class TransformationResult {
public Builder buildUpon() {
return new Builder()
.setDurationMs(durationMs)
.setFileSizeBytes(fileSizeBytes)
.setAverageAudioBitrate(averageAudioBitrate)
.setAverageVideoBitrate(averageVideoBitrate);
......@@ -109,14 +127,16 @@ public final class TransformationResult {
return false;
}
TransformationResult result = (TransformationResult) o;
return fileSizeBytes == result.fileSizeBytes
return durationMs == result.durationMs
&& fileSizeBytes == result.fileSizeBytes
&& averageAudioBitrate == result.averageAudioBitrate
&& averageVideoBitrate == result.averageVideoBitrate;
}
@Override
public int hashCode() {
int result = (int) fileSizeBytes;
int result = (int) durationMs;
result = 31 * result + (int) fileSizeBytes;
result = 31 * result + averageAudioBitrate;
result = 31 * result + averageVideoBitrate;
return result;
......
......@@ -1002,6 +1002,7 @@ public final class Transformer {
} else {
TransformationResult result =
new TransformationResult.Builder()
.setDurationMs(muxerWrapper.getDurationMs())
.setAverageAudioBitrate(muxerWrapper.getTrackAverageBitrate(C.TRACK_TYPE_AUDIO))
.setAverageVideoBitrate(muxerWrapper.getTrackAverageBitrate(C.TRACK_TYPE_VIDEO))
.build();
......
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