Add support for setting video duration in InAppMuxer

Similar support was previously added in FrameworkMuxer.

PiperOrigin-RevId: 670193986
This commit is contained in:
sheenachhabra 2024-09-02 06:08:36 -07:00 committed by Copybara-Service
parent 748e4e5230
commit f0fa7640ca
3 changed files with 116 additions and 15 deletions

View File

@ -29,6 +29,7 @@ import androidx.media3.common.C;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.Metadata; import androidx.media3.common.Metadata;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.MediaFormatUtil; import androidx.media3.common.util.MediaFormatUtil;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.container.Mp4LocationData; import androidx.media3.container.Mp4LocationData;
@ -39,6 +40,7 @@ import java.io.IOException;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.HashMap; import java.util.HashMap;
import java.util.Locale;
import java.util.Map; import java.util.Map;
/** {@link Muxer} implementation that uses a {@link MediaMuxer}. */ /** {@link Muxer} implementation that uses a {@link MediaMuxer}. */
@ -50,6 +52,7 @@ import java.util.Map;
getSupportedVideoSampleMimeTypes(); getSupportedVideoSampleMimeTypes();
private static final ImmutableList<String> SUPPORTED_AUDIO_SAMPLE_MIME_TYPES = private static final ImmutableList<String> SUPPORTED_AUDIO_SAMPLE_MIME_TYPES =
ImmutableList.of(MimeTypes.AUDIO_AAC, MimeTypes.AUDIO_AMR_NB, MimeTypes.AUDIO_AMR_WB); 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}. */ /** {@link Muxer.Factory} for {@link FrameworkMuxer}. */
public static final class Factory implements Muxer.Factory { 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.
* *
* <p>The default is {@link C#TIME_UNSET}. * <p>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 * <p>The default is {@link C#TIME_UNSET} to not set any duration in the output. In this case
* output, or {@link C#TIME_UNSET} to not enforce. Only applicable when a video track is * 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}. * {@linkplain #addTrack(Format) added}.
* @return This factory. * @return This factory.
*/ */
@ -156,6 +164,13 @@ import java.util.Map;
if (videoDurationUs != C.TIME_UNSET if (videoDurationUs != C.TIME_UNSET
&& trackToken == videoTrackToken && trackToken == videoTrackToken
&& presentationTimeUs > videoDurationUs) { && presentationTimeUs > videoDurationUs) {
Log.w(
TAG,
String.format(
Locale.US,
"Skipped sample with presentation time (%d) > video duration (%d)",
presentationTimeUs,
videoDurationUs));
return; return;
} }
if (!isStarted) { if (!isStarted) {

View File

@ -15,12 +15,16 @@
*/ */
package androidx.media3.transformer; package androidx.media3.transformer;
import static androidx.media3.common.util.Assertions.checkNotNull;
import android.media.MediaCodec;
import android.media.MediaCodec.BufferInfo; import android.media.MediaCodec.BufferInfo;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.Metadata; import androidx.media3.common.Metadata;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.container.Mp4OrientationData; import androidx.media3.container.Mp4OrientationData;
import androidx.media3.muxer.FragmentedMp4Muxer; import androidx.media3.muxer.FragmentedMp4Muxer;
@ -33,6 +37,7 @@ import java.io.FileNotFoundException;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Set; import java.util.Set;
/** {@link Muxer} implementation that uses an {@link Mp4Muxer} or {@link FragmentedMp4Muxer}. */ /** {@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 boolean outputFragmentedMp4;
private final long fragmentDurationMs; private final long fragmentDurationMs;
private long videoDurationUs;
private Factory( private Factory(
@Nullable MetadataProvider metadataProvider, @Nullable MetadataProvider metadataProvider,
boolean outputFragmentedMp4, boolean outputFragmentedMp4,
@ -134,6 +141,28 @@ public final class InAppMuxer implements Muxer {
this.metadataProvider = metadataProvider; this.metadataProvider = metadataProvider;
this.outputFragmentedMp4 = outputFragmentedMp4; this.outputFragmentedMp4 = outputFragmentedMp4;
this.fragmentDurationMs = fragmentDurationMs; this.fragmentDurationMs = fragmentDurationMs;
videoDurationUs = C.TIME_UNSET;
}
/**
* Sets the duration of the video track (in microseconds) in the output.
*
* <p>Only the duration of the last sample is adjusted to achieve the given duration. Duration
* of the other samples remains unchanged.
*
* <p>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 @Override
@ -145,15 +174,23 @@ public final class InAppMuxer implements Muxer {
throw new MuxerException("Error creating file output stream", e); throw new MuxerException("Error creating file output stream", e);
} }
androidx.media3.muxer.Muxer muxer = Muxer muxer = null;
outputFragmentedMp4 if (outputFragmentedMp4) {
? fragmentDurationMs != C.TIME_UNSET FragmentedMp4Muxer.Builder builder = new FragmentedMp4Muxer.Builder(outputStream);
? new FragmentedMp4Muxer.Builder(outputStream) if (fragmentDurationMs != C.TIME_UNSET) {
.setFragmentDurationMs(fragmentDurationMs) builder.setFragmentDurationMs(fragmentDurationMs);
.build() }
: new FragmentedMp4Muxer.Builder(outputStream).build() muxer = builder.build();
: new Mp4Muxer.Builder(outputStream).build(); } else {
return new InAppMuxer(muxer, metadataProvider); 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 @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; @Nullable private final MetadataProvider metadataProvider;
private final long videoDurationUs;
private final Set<Metadata.Entry> metadataEntries; private final Set<Metadata.Entry> metadataEntries;
@Nullable private TrackToken videoTrackToken;
private InAppMuxer( private InAppMuxer(
androidx.media3.muxer.Muxer muxer, @Nullable MetadataProvider metadataProvider) { Muxer muxer, @Nullable MetadataProvider metadataProvider, long videoDurationUs) {
this.muxer = muxer; this.muxer = muxer;
this.metadataProvider = metadataProvider; this.metadataProvider = metadataProvider;
this.videoDurationUs = videoDurationUs;
metadataEntries = new LinkedHashSet<>(); metadataEntries = new LinkedHashSet<>();
} }
@ -183,6 +226,7 @@ public final class InAppMuxer implements Muxer {
TrackToken trackToken = muxer.addTrack(format); TrackToken trackToken = muxer.addTrack(format);
if (MimeTypes.isVideo(format.sampleMimeType)) { if (MimeTypes.isVideo(format.sampleMimeType)) {
muxer.addMetadataEntry(new Mp4OrientationData(format.rotationDegrees)); muxer.addMetadataEntry(new Mp4OrientationData(format.rotationDegrees));
videoTrackToken = trackToken;
} }
return trackToken; return trackToken;
} }
@ -190,6 +234,18 @@ public final class InAppMuxer implements Muxer {
@Override @Override
public void writeSampleData(TrackToken trackToken, ByteBuffer byteBuffer, BufferInfo bufferInfo) public void writeSampleData(TrackToken trackToken, ByteBuffer byteBuffer, BufferInfo bufferInfo)
throws MuxerException { 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); muxer.writeSampleData(trackToken, byteBuffer, bufferInfo);
} }
@ -202,6 +258,15 @@ public final class InAppMuxer implements Muxer {
@Override @Override
public void close() throws MuxerException { 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(); writeMetadata();
muxer.close(); muxer.close();
} }

View File

@ -265,6 +265,27 @@ public class TransformerWithInAppMuxerEndToEndNonParameterizedTest {
assertThat(actualFloatMetadata).isEqualTo(expectedFloatMetadata); 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. * Returns specific {@linkplain Metadata.Entry metadata} from the media file.
* *