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.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<String> 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.
*
* <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
* output, or {@link C#TIME_UNSET} to not enforce. Only applicable when a video track is
* <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 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) {

View File

@ -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.
*
* <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
@ -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<Metadata.Entry> 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();
}

View File

@ -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.
*