diff --git a/libraries/common/src/main/java/androidx/media3/common/util/Util.java b/libraries/common/src/main/java/androidx/media3/common/util/Util.java index 6cffae7fbd..a0619d1bfa 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/Util.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/Util.java @@ -1121,6 +1121,25 @@ public final class Util { 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 * C#TIME_UNSET} and {@link C#TIME_END_OF_SOURCE} values. diff --git a/libraries/common/src/test/java/androidx/media3/common/util/UtilTest.java b/libraries/common/src/test/java/androidx/media3/common/util/UtilTest.java index 3ffc4bfa4f..a2056e1542 100644 --- a/libraries/common/src/test/java/androidx/media3/common/util/UtilTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/util/UtilTest.java @@ -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; @@ -747,6 +748,21 @@ public class UtilTest { 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 public void parseXsDuration_returnsParsedDurationInMillis() { assertThat(parseXsDuration("PT150.279S")).isEqualTo(150279L); diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AdvancedFrameProcessorPixelTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AdvancedFrameProcessorPixelTest.java index 895d10af18..9fba1d9482 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AdvancedFrameProcessorPixelTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AdvancedFrameProcessorPixelTest.java @@ -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); diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerAndroidTestRunner.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerAndroidTestRunner.java index 499a015696..9b7a52625d 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerAndroidTestRunner.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerAndroidTestRunner.java @@ -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); } 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 d44f514b9d..cdf4e0d902 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java @@ -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); + } } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/AdvancedFrameProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/AdvancedFrameProcessor.java index dc9c73dd55..fb9b22519d 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/AdvancedFrameProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/AdvancedFrameProcessor.java @@ -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(); diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/ExternalCopyFrameProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/ExternalCopyFrameProcessor.java index 71bc45ea1d..502f1fb167 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/ExternalCopyFrameProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/ExternalCopyFrameProcessor.java @@ -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(); diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java index 8346bde888..9645a1d52a 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java @@ -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); diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/GlFrameProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/GlFrameProcessor.java index 84477c06e6..ad9099d8f1 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/GlFrameProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/GlFrameProcessor.java @@ -60,9 +60,9 @@ public interface GlFrameProcessor { *

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(); 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 5babefd873..2a0a946910 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/MuxerWrapper.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/MuxerWrapper.java @@ -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)); + } } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/PresentationFrameProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/PresentationFrameProcessor.java index f6941f2e41..8b0bf17c8d 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/PresentationFrameProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/PresentationFrameProcessor.java @@ -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 diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/ScaleToFitFrameProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/ScaleToFitFrameProcessor.java index 0875e706c4..22a59a5156 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/ScaleToFitFrameProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/ScaleToFitFrameProcessor.java @@ -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 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 b5ece274a6..2bf98abe1d 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationResult.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationResult.java @@ -27,16 +27,29 @@ 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. + * + *

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. * @@ -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; 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 cf59d3c23e..28e7c4f5c3 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java @@ -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();