diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameworkMuxer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameworkMuxer.java index 2146bf2abc..6668f380bc 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameworkMuxer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameworkMuxer.java @@ -29,6 +29,7 @@ import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.Metadata; import androidx.media3.common.MimeTypes; +import androidx.media3.common.util.Log; import androidx.media3.common.util.MediaFormatUtil; import androidx.media3.common.util.Util; import androidx.media3.container.Mp4LocationData; @@ -39,6 +40,7 @@ import java.io.IOException; import java.lang.reflect.Field; import java.nio.ByteBuffer; import java.util.HashMap; +import java.util.Locale; import java.util.Map; /** {@link Muxer} implementation that uses a {@link MediaMuxer}. */ @@ -50,6 +52,7 @@ import java.util.Map; getSupportedVideoSampleMimeTypes(); private static final ImmutableList SUPPORTED_AUDIO_SAMPLE_MIME_TYPES = ImmutableList.of(MimeTypes.AUDIO_AAC, MimeTypes.AUDIO_AMR_NB, MimeTypes.AUDIO_AMR_WB); + private static final String TAG = "FrameworkMuxer"; /** {@link Muxer.Factory} for {@link FrameworkMuxer}. */ public static final class Factory implements Muxer.Factory { @@ -60,12 +63,17 @@ import java.util.Map; } /** - * Sets the duration of the video track (in microseconds) to enforce in the output. + * Sets the duration of the video track (in microseconds) in the output. * - *

The default is {@link C#TIME_UNSET}. + *

Only the duration of the last sample is adjusted to achieve the given duration. Duration + * of the other samples remains unchanged. * - * @param videoDurationUs The duration of the video track (in microseconds) to enforce in the - * output, or {@link C#TIME_UNSET} to not enforce. Only applicable when a video track is + *

The default is {@link C#TIME_UNSET} to not set any duration in the output. In this case + * the video track duration is determined by the samples written to it and the duration of the + * last sample would be the same as that of the sample before that. + * + * @param videoDurationUs The duration of the video track (in microseconds) in the output, or + * {@link C#TIME_UNSET} to not set any duration. Only applicable when a video track is * {@linkplain #addTrack(Format) added}. * @return This factory. */ @@ -156,6 +164,13 @@ import java.util.Map; if (videoDurationUs != C.TIME_UNSET && trackToken == videoTrackToken && presentationTimeUs > videoDurationUs) { + Log.w( + TAG, + String.format( + Locale.US, + "Skipped sample with presentation time (%d) > video duration (%d)", + presentationTimeUs, + videoDurationUs)); return; } if (!isStarted) { diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/InAppMuxer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/InAppMuxer.java index ce40aa13ca..6c81224d3d 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/InAppMuxer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/InAppMuxer.java @@ -15,12 +15,16 @@ */ package androidx.media3.transformer; +import static androidx.media3.common.util.Assertions.checkNotNull; + +import android.media.MediaCodec; import android.media.MediaCodec.BufferInfo; import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.Metadata; import androidx.media3.common.MimeTypes; +import androidx.media3.common.util.Log; import androidx.media3.common.util.UnstableApi; import androidx.media3.container.Mp4OrientationData; import androidx.media3.muxer.FragmentedMp4Muxer; @@ -33,6 +37,7 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.nio.ByteBuffer; import java.util.LinkedHashSet; +import java.util.Locale; import java.util.Set; /** {@link Muxer} implementation that uses an {@link Mp4Muxer} or {@link FragmentedMp4Muxer}. */ @@ -127,6 +132,8 @@ public final class InAppMuxer implements Muxer { private final boolean outputFragmentedMp4; private final long fragmentDurationMs; + private long videoDurationUs; + private Factory( @Nullable MetadataProvider metadataProvider, boolean outputFragmentedMp4, @@ -134,6 +141,28 @@ public final class InAppMuxer implements Muxer { this.metadataProvider = metadataProvider; this.outputFragmentedMp4 = outputFragmentedMp4; this.fragmentDurationMs = fragmentDurationMs; + videoDurationUs = C.TIME_UNSET; + } + + /** + * Sets the duration of the video track (in microseconds) in the output. + * + *

Only the duration of the last sample is adjusted to achieve the given duration. Duration + * of the other samples remains unchanged. + * + *

The default is {@link C#TIME_UNSET} to not set any duration in the output. In this case + * the video track duration is determined by the samples written to it and the duration of the + * last sample is set to 0. + * + * @param videoDurationUs The duration of the video track (in microseconds) in the output, or + * {@link C#TIME_UNSET} to not set any duration. Only applicable when a video track is + * {@linkplain #addTrack(Format) added}. + * @return This factory. + */ + @CanIgnoreReturnValue + public Factory setVideoDurationUs(long videoDurationUs) { + this.videoDurationUs = videoDurationUs; + return this; } @Override @@ -145,15 +174,23 @@ public final class InAppMuxer implements Muxer { throw new MuxerException("Error creating file output stream", e); } - androidx.media3.muxer.Muxer muxer = - outputFragmentedMp4 - ? fragmentDurationMs != C.TIME_UNSET - ? new FragmentedMp4Muxer.Builder(outputStream) - .setFragmentDurationMs(fragmentDurationMs) - .build() - : new FragmentedMp4Muxer.Builder(outputStream).build() - : new Mp4Muxer.Builder(outputStream).build(); - return new InAppMuxer(muxer, metadataProvider); + Muxer muxer = null; + if (outputFragmentedMp4) { + FragmentedMp4Muxer.Builder builder = new FragmentedMp4Muxer.Builder(outputStream); + if (fragmentDurationMs != C.TIME_UNSET) { + builder.setFragmentDurationMs(fragmentDurationMs); + } + muxer = builder.build(); + } else { + Mp4Muxer.Builder builder = new Mp4Muxer.Builder(outputStream); + if (videoDurationUs != C.TIME_UNSET) { + builder.setLastSampleDurationBehavior( + Mp4Muxer.LAST_SAMPLE_DURATION_BEHAVIOR_USING_END_OF_STREAM_FLAG); + } + muxer = builder.build(); + } + + return new InAppMuxer(muxer, metadataProvider, videoDurationUs); } @Override @@ -167,14 +204,20 @@ public final class InAppMuxer implements Muxer { } } - private final androidx.media3.muxer.Muxer muxer; + private static final String TAG = "InAppMuxer"; + + private final Muxer muxer; @Nullable private final MetadataProvider metadataProvider; + private final long videoDurationUs; private final Set metadataEntries; + @Nullable private TrackToken videoTrackToken; + private InAppMuxer( - androidx.media3.muxer.Muxer muxer, @Nullable MetadataProvider metadataProvider) { + Muxer muxer, @Nullable MetadataProvider metadataProvider, long videoDurationUs) { this.muxer = muxer; this.metadataProvider = metadataProvider; + this.videoDurationUs = videoDurationUs; metadataEntries = new LinkedHashSet<>(); } @@ -183,6 +226,7 @@ public final class InAppMuxer implements Muxer { TrackToken trackToken = muxer.addTrack(format); if (MimeTypes.isVideo(format.sampleMimeType)) { muxer.addMetadataEntry(new Mp4OrientationData(format.rotationDegrees)); + videoTrackToken = trackToken; } return trackToken; } @@ -190,6 +234,18 @@ public final class InAppMuxer implements Muxer { @Override public void writeSampleData(TrackToken trackToken, ByteBuffer byteBuffer, BufferInfo bufferInfo) throws MuxerException { + if (videoDurationUs != C.TIME_UNSET + && trackToken == videoTrackToken + && bufferInfo.presentationTimeUs > videoDurationUs) { + Log.w( + TAG, + String.format( + Locale.US, + "Skipped sample with presentation time (%d) > video duration (%d)", + bufferInfo.presentationTimeUs, + videoDurationUs)); + return; + } muxer.writeSampleData(trackToken, byteBuffer, bufferInfo); } @@ -202,6 +258,15 @@ public final class InAppMuxer implements Muxer { @Override public void close() throws MuxerException { + if (videoDurationUs != C.TIME_UNSET && videoTrackToken != null) { + BufferInfo bufferInfo = new BufferInfo(); + bufferInfo.set( + /* newOffset= */ 0, + /* newSize= */ 0, + videoDurationUs, + MediaCodec.BUFFER_FLAG_END_OF_STREAM); + writeSampleData(checkNotNull(videoTrackToken), ByteBuffer.allocateDirect(0), bufferInfo); + } writeMetadata(); muxer.close(); } diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMuxerEndToEndNonParameterizedTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMuxerEndToEndNonParameterizedTest.java index 07eea41d18..8a529819de 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMuxerEndToEndNonParameterizedTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMuxerEndToEndNonParameterizedTest.java @@ -265,6 +265,27 @@ public class TransformerWithInAppMuxerEndToEndNonParameterizedTest { assertThat(actualFloatMetadata).isEqualTo(expectedFloatMetadata); } + @Test + public void transmux_withSettingVideoDuration_writesCorrectVideoDuration() throws Exception { + InAppMuxer.Factory inAppMuxerFactory = new InAppMuxer.Factory.Builder().build(); + long expectedDurationUs = 2_000_000L; + inAppMuxerFactory.setVideoDurationUs(expectedDurationUs); + Transformer transformer = + new Transformer.Builder(context) + .setClock(new FakeClock(/* isAutoAdvancing= */ true)) + .setMuxerFactory(inAppMuxerFactory) + .build(); + MediaItem mediaItem = MediaItem.fromUri(Uri.parse(MP4_FILE_PATH)); + + transformer.start(mediaItem, outputPath); + TransformerTestRunner.runLooper(transformer); + + FakeExtractorOutput fakeExtractorOutput = + androidx.media3.test.utils.TestUtil.extractAllSamplesFromFilePath( + new Mp4Extractor(new DefaultSubtitleParserFactory()), outputPath); + assertThat(fakeExtractorOutput.seekMap.getDurationUs()).isEqualTo(expectedDurationUs); + } + /** * Returns specific {@linkplain Metadata.Entry metadata} from the media file. *