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
This commit is contained in:
hschlueter 2022-03-29 14:36:10 +01:00 committed by Ian Baker
parent 2adf0f67d8
commit d97de5b9f0
14 changed files with 106 additions and 19 deletions

View File

@ -1121,6 +1121,25 @@ public final class Util {
return min; return min;
} }
/**
* 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 * Converts a time in microseconds to the corresponding time in milliseconds, preserving {@link
* C#TIME_UNSET} and {@link C#TIME_END_OF_SOURCE} values. * C#TIME_UNSET} and {@link C#TIME_END_OF_SOURCE} values.

View File

@ -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.getCodecsOfType;
import static androidx.media3.common.util.Util.getStringForTime; import static androidx.media3.common.util.Util.getStringForTime;
import static androidx.media3.common.util.Util.gzip; 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.minValue;
import static androidx.media3.common.util.Util.parseXsDateTime; import static androidx.media3.common.util.Util.parseXsDateTime;
import static androidx.media3.common.util.Util.parseXsDuration; import static androidx.media3.common.util.Util.parseXsDuration;
@ -747,6 +748,21 @@ public class UtilTest {
assertThrows(NoSuchElementException.class, () -> minValue(new SparseLongArray())); assertThrows(NoSuchElementException.class, () -> minValue(new SparseLongArray()));
} }
@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 @Test
public void parseXsDuration_returnsParsedDurationInMillis() { public void parseXsDuration_returnsParsedDurationInMillis() {
assertThat(parseXsDuration("PT150.279S")).isEqualTo(150279L); assertThat(parseXsDuration("PT150.279S")).isEqualTo(150279L);

View File

@ -94,7 +94,7 @@ public final class AdvancedFrameProcessorPixelTest {
advancedFrameProcessor.initialize(inputTexId); advancedFrameProcessor.initialize(inputTexId);
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(FIRST_FRAME_PNG_ASSET_STRING); Bitmap expectedBitmap = BitmapTestUtil.readBitmap(FIRST_FRAME_PNG_ASSET_STRING);
advancedFrameProcessor.updateProgramAndDraw(/* presentationTimeNs= */ 0); advancedFrameProcessor.updateProgramAndDraw(/* presentationTimeUs= */ 0);
Bitmap actualBitmap = Bitmap actualBitmap =
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(width, height); BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(width, height);
@ -118,7 +118,7 @@ public final class AdvancedFrameProcessorPixelTest {
Bitmap expectedBitmap = Bitmap expectedBitmap =
BitmapTestUtil.readBitmap(TRANSLATE_RIGHT_EXPECTED_OUTPUT_PNG_ASSET_STRING); BitmapTestUtil.readBitmap(TRANSLATE_RIGHT_EXPECTED_OUTPUT_PNG_ASSET_STRING);
advancedFrameProcessor.updateProgramAndDraw(/* presentationTimeNs= */ 0); advancedFrameProcessor.updateProgramAndDraw(/* presentationTimeUs= */ 0);
Bitmap actualBitmap = Bitmap actualBitmap =
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(width, height); BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(width, height);
@ -141,7 +141,7 @@ public final class AdvancedFrameProcessorPixelTest {
Bitmap expectedBitmap = Bitmap expectedBitmap =
BitmapTestUtil.readBitmap(SCALE_NARROW_EXPECTED_OUTPUT_PNG_ASSET_STRING); BitmapTestUtil.readBitmap(SCALE_NARROW_EXPECTED_OUTPUT_PNG_ASSET_STRING);
advancedFrameProcessor.updateProgramAndDraw(/* presentationTimeNs= */ 0); advancedFrameProcessor.updateProgramAndDraw(/* presentationTimeUs= */ 0);
Bitmap actualBitmap = Bitmap actualBitmap =
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(width, height); BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(width, height);
@ -163,7 +163,7 @@ public final class AdvancedFrameProcessorPixelTest {
advancedFrameProcessor.initialize(inputTexId); advancedFrameProcessor.initialize(inputTexId);
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ROTATE_90_EXPECTED_OUTPUT_PNG_ASSET_STRING); Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ROTATE_90_EXPECTED_OUTPUT_PNG_ASSET_STRING);
advancedFrameProcessor.updateProgramAndDraw(/* presentationTimeNs= */ 0); advancedFrameProcessor.updateProgramAndDraw(/* presentationTimeUs= */ 0);
Bitmap actualBitmap = Bitmap actualBitmap =
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(width, height); BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(width, height);

View File

@ -309,6 +309,9 @@ public class TransformerAndroidTestRunner {
TransformationResult transformationResult = testResult.transformationResult; TransformationResult transformationResult = testResult.transformationResult;
JSONObject transformationResultJson = new JSONObject(); JSONObject transformationResultJson = new JSONObject();
if (transformationResult.durationMs != C.LENGTH_UNSET) {
transformationResultJson.put("durationMs", transformationResult.durationMs);
}
if (transformationResult.fileSizeBytes != C.LENGTH_UNSET) { if (transformationResult.fileSizeBytes != C.LENGTH_UNSET) {
transformationResultJson.put("fileSizeBytes", transformationResult.fileSizeBytes); transformationResultJson.put("fileSizeBytes", transformationResult.fileSizeBytes);
} }

View File

@ -58,4 +58,25 @@ public class TransformerEndToEndTest {
checkNotNull(muxerFactory.getLastFrameCountingMuxerCreated()); checkNotNull(muxerFactory.getLastFrameCountingMuxerCreated());
assertThat(frameCountingMuxer.getFrameCount()).isEqualTo(expectedFrameCount); 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);
}
} }

View File

@ -124,7 +124,7 @@ public final class AdvancedFrameProcessor implements GlFrameProcessor {
} }
@Override @Override
public void updateProgramAndDraw(long presentationTimeNs) { public void updateProgramAndDraw(long presentationTimeUs) {
checkStateNotNull(glProgram); checkStateNotNull(glProgram);
glProgram.use(); glProgram.use();
glProgram.bindAttributesAndUniforms(); glProgram.bindAttributesAndUniforms();

View File

@ -101,7 +101,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
@Override @Override
public void updateProgramAndDraw(long presentationTimeNs) { public void updateProgramAndDraw(long presentationTimeUs) {
checkStateNotNull(glProgram); checkStateNotNull(glProgram);
glProgram.use(); glProgram.use();
glProgram.bindAttributesAndUniforms(); glProgram.bindAttributesAndUniforms();

View File

@ -412,7 +412,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
inputSurfaceTexture.getTransformMatrix(textureTransformMatrix); inputSurfaceTexture.getTransformMatrix(textureTransformMatrix);
externalCopyFrameProcessor.setTextureTransformMatrix(textureTransformMatrix); externalCopyFrameProcessor.setTextureTransformMatrix(textureTransformMatrix);
long presentationTimeNs = inputSurfaceTexture.getTimestamp(); long presentationTimeNs = inputSurfaceTexture.getTimestamp();
externalCopyFrameProcessor.updateProgramAndDraw(presentationTimeNs); long presentationTimeUs = presentationTimeNs / 1000;
externalCopyFrameProcessor.updateProgramAndDraw(presentationTimeUs);
for (int i = 0; i < frameProcessors.size() - 1; i++) { for (int i = 0; i < frameProcessors.size() - 1; i++) {
Size outputSize = inputSizes.get(i + 1); Size outputSize = inputSizes.get(i + 1);
@ -423,11 +424,11 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
framebuffers[i + 1], framebuffers[i + 1],
outputSize.getWidth(), outputSize.getWidth(),
outputSize.getHeight()); outputSize.getHeight());
frameProcessors.get(i).updateProgramAndDraw(presentationTimeNs); frameProcessors.get(i).updateProgramAndDraw(presentationTimeUs);
} }
if (!frameProcessors.isEmpty()) { if (!frameProcessors.isEmpty()) {
GlUtil.focusEglSurface(eglDisplay, eglContext, eglSurface, outputWidth, outputHeight); GlUtil.focusEglSurface(eglDisplay, eglContext, eglSurface, outputWidth, outputHeight);
getLast(frameProcessors).updateProgramAndDraw(presentationTimeNs); getLast(frameProcessors).updateProgramAndDraw(presentationTimeUs);
} }
EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, presentationTimeNs); EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, presentationTimeNs);

View File

@ -60,9 +60,9 @@ public interface GlFrameProcessor {
* <p>The frame processor must be {@linkplain #initialize(int) initialized}. The caller is * <p>The frame processor must be {@linkplain #initialize(int) initialized}. The caller is
* responsible for focussing the correct render target before calling this method. * 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. */ /** Releases all resources. */
void release(); void release();

View File

@ -17,6 +17,7 @@
package androidx.media3.transformer; package androidx.media3.transformer;
import static androidx.media3.common.util.Assertions.checkState; 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 static androidx.media3.common.util.Util.minValue;
import android.util.SparseIntArray; import android.util.SparseIntArray;
@ -240,4 +241,9 @@ import java.nio.ByteBuffer;
} }
return trackTimeUs - minTrackTimeUs <= MAX_TRACK_WRITE_AHEAD_US; 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));
}
} }

View File

@ -152,8 +152,8 @@ public final class PresentationFrameProcessor implements GlFrameProcessor {
} }
@Override @Override
public void updateProgramAndDraw(long presentationTimeNs) { public void updateProgramAndDraw(long presentationTimeUs) {
checkStateNotNull(advancedFrameProcessor).updateProgramAndDraw(presentationTimeNs); checkStateNotNull(advancedFrameProcessor).updateProgramAndDraw(presentationTimeUs);
} }
@Override @Override

View File

@ -176,8 +176,8 @@ public final class ScaleToFitFrameProcessor implements GlFrameProcessor {
} }
@Override @Override
public void updateProgramAndDraw(long presentationTimeNs) { public void updateProgramAndDraw(long presentationTimeUs) {
checkStateNotNull(advancedFrameProcessor).updateProgramAndDraw(presentationTimeNs); checkStateNotNull(advancedFrameProcessor).updateProgramAndDraw(presentationTimeUs);
} }
@Override @Override

View File

@ -27,16 +27,29 @@ public final class TransformationResult {
/** A builder for {@link TransformationResult} instances. */ /** A builder for {@link TransformationResult} instances. */
public static final class Builder { public static final class Builder {
private long durationMs;
private long fileSizeBytes; private long fileSizeBytes;
private int averageAudioBitrate; private int averageAudioBitrate;
private int averageVideoBitrate; private int averageVideoBitrate;
public Builder() { public Builder() {
durationMs = C.TIME_UNSET;
fileSizeBytes = C.LENGTH_UNSET; fileSizeBytes = C.LENGTH_UNSET;
averageAudioBitrate = C.RATE_UNSET_INT; averageAudioBitrate = C.RATE_UNSET_INT;
averageVideoBitrate = 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. * Sets the file size in bytes.
* *
@ -71,10 +84,13 @@ public final class TransformationResult {
} }
public TransformationResult build() { 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. */ /** The size of the file in bytes, or {@link C#LENGTH_UNSET} if unset or unknown. */
public final long fileSizeBytes; public final long fileSizeBytes;
/** /**
@ -87,7 +103,8 @@ public final class TransformationResult {
public final int averageVideoBitrate; public final int averageVideoBitrate;
private TransformationResult( private TransformationResult(
long fileSizeBytes, int averageAudioBitrate, int averageVideoBitrate) { long durationMs, long fileSizeBytes, int averageAudioBitrate, int averageVideoBitrate) {
this.durationMs = durationMs;
this.fileSizeBytes = fileSizeBytes; this.fileSizeBytes = fileSizeBytes;
this.averageAudioBitrate = averageAudioBitrate; this.averageAudioBitrate = averageAudioBitrate;
this.averageVideoBitrate = averageVideoBitrate; this.averageVideoBitrate = averageVideoBitrate;
@ -95,6 +112,7 @@ public final class TransformationResult {
public Builder buildUpon() { public Builder buildUpon() {
return new Builder() return new Builder()
.setDurationMs(durationMs)
.setFileSizeBytes(fileSizeBytes) .setFileSizeBytes(fileSizeBytes)
.setAverageAudioBitrate(averageAudioBitrate) .setAverageAudioBitrate(averageAudioBitrate)
.setAverageVideoBitrate(averageVideoBitrate); .setAverageVideoBitrate(averageVideoBitrate);
@ -109,14 +127,16 @@ public final class TransformationResult {
return false; return false;
} }
TransformationResult result = (TransformationResult) o; TransformationResult result = (TransformationResult) o;
return fileSizeBytes == result.fileSizeBytes return durationMs == result.durationMs
&& fileSizeBytes == result.fileSizeBytes
&& averageAudioBitrate == result.averageAudioBitrate && averageAudioBitrate == result.averageAudioBitrate
&& averageVideoBitrate == result.averageVideoBitrate; && averageVideoBitrate == result.averageVideoBitrate;
} }
@Override @Override
public int hashCode() { public int hashCode() {
int result = (int) fileSizeBytes; int result = (int) durationMs;
result = 31 * result + (int) fileSizeBytes;
result = 31 * result + averageAudioBitrate; result = 31 * result + averageAudioBitrate;
result = 31 * result + averageVideoBitrate; result = 31 * result + averageVideoBitrate;
return result; return result;

View File

@ -1002,6 +1002,7 @@ public final class Transformer {
} else { } else {
TransformationResult result = TransformationResult result =
new TransformationResult.Builder() new TransformationResult.Builder()
.setDurationMs(muxerWrapper.getDurationMs())
.setAverageAudioBitrate(muxerWrapper.getTrackAverageBitrate(C.TRACK_TYPE_AUDIO)) .setAverageAudioBitrate(muxerWrapper.getTrackAverageBitrate(C.TRACK_TYPE_AUDIO))
.setAverageVideoBitrate(muxerWrapper.getTrackAverageBitrate(C.TRACK_TYPE_VIDEO)) .setAverageVideoBitrate(muxerWrapper.getTrackAverageBitrate(C.TRACK_TYPE_VIDEO))
.build(); .build();