Commit d4a9e3d9 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 d0d0935f
...@@ -1120,6 +1120,25 @@ public final class Util { ...@@ -1120,6 +1120,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 * 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.
* *
......
...@@ -21,6 +21,7 @@ import static com.google.android.exoplayer2.util.Util.escapeFileName; ...@@ -21,6 +21,7 @@ import static com.google.android.exoplayer2.util.Util.escapeFileName;
import static com.google.android.exoplayer2.util.Util.getCodecsOfType; import static com.google.android.exoplayer2.util.Util.getCodecsOfType;
import static com.google.android.exoplayer2.util.Util.getStringForTime; import static com.google.android.exoplayer2.util.Util.getStringForTime;
import static com.google.android.exoplayer2.util.Util.gzip; import static com.google.android.exoplayer2.util.Util.gzip;
import static com.google.android.exoplayer2.util.Util.maxValue;
import static com.google.android.exoplayer2.util.Util.minValue; import static com.google.android.exoplayer2.util.Util.minValue;
import static com.google.android.exoplayer2.util.Util.parseXsDateTime; import static com.google.android.exoplayer2.util.Util.parseXsDateTime;
import static com.google.android.exoplayer2.util.Util.parseXsDuration; import static com.google.android.exoplayer2.util.Util.parseXsDuration;
...@@ -748,6 +749,21 @@ public class UtilTest { ...@@ -748,6 +749,21 @@ public class UtilTest {
} }
@Test @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() { public void parseXsDuration_returnsParsedDurationInMillis() {
assertThat(parseXsDuration("PT150.279S")).isEqualTo(150279L); assertThat(parseXsDuration("PT150.279S")).isEqualTo(150279L);
assertThat(parseXsDuration("PT1.500S")).isEqualTo(1500L); assertThat(parseXsDuration("PT1.500S")).isEqualTo(1500L);
......
...@@ -94,7 +94,7 @@ public final class AdvancedFrameProcessorPixelTest { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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);
......
...@@ -309,6 +309,9 @@ public class TransformerAndroidTestRunner { ...@@ -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);
} }
......
...@@ -58,4 +58,25 @@ public class TransformerEndToEndTest { ...@@ -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);
}
} }
...@@ -122,7 +122,7 @@ public final class AdvancedFrameProcessor implements GlFrameProcessor { ...@@ -122,7 +122,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();
......
...@@ -101,7 +101,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -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();
......
...@@ -412,7 +412,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -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; ...@@ -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);
......
...@@ -58,9 +58,9 @@ public interface GlFrameProcessor { ...@@ -58,9 +58,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();
......
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
package com.google.android.exoplayer2.transformer; package com.google.android.exoplayer2.transformer;
import static com.google.android.exoplayer2.util.Assertions.checkState; import static com.google.android.exoplayer2.util.Assertions.checkState;
import static com.google.android.exoplayer2.util.Util.maxValue;
import static com.google.android.exoplayer2.util.Util.minValue; import static com.google.android.exoplayer2.util.Util.minValue;
import android.util.SparseIntArray; import android.util.SparseIntArray;
...@@ -240,4 +241,9 @@ import java.nio.ByteBuffer; ...@@ -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));
}
} }
...@@ -150,8 +150,8 @@ public final class PresentationFrameProcessor implements GlFrameProcessor { ...@@ -150,8 +150,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
......
...@@ -174,8 +174,8 @@ public final class ScaleToFitFrameProcessor implements GlFrameProcessor { ...@@ -174,8 +174,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
......
...@@ -25,17 +25,30 @@ public final class TransformationResult { ...@@ -25,17 +25,30 @@ 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.
* *
* <p>Input must be positive or {@link C#LENGTH_UNSET}. * <p>Input must be positive or {@link C#LENGTH_UNSET}.
...@@ -69,10 +82,13 @@ public final class TransformationResult { ...@@ -69,10 +82,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;
/** /**
...@@ -85,7 +101,8 @@ public final class TransformationResult { ...@@ -85,7 +101,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;
...@@ -93,6 +110,7 @@ public final class TransformationResult { ...@@ -93,6 +110,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);
...@@ -107,14 +125,16 @@ public final class TransformationResult { ...@@ -107,14 +125,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;
......
...@@ -1000,6 +1000,7 @@ public final class Transformer { ...@@ -1000,6 +1000,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();
......
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