Add support for setting video duration in InAppMuxer
Similar support was previously added in FrameworkMuxer. PiperOrigin-RevId: 670193986
This commit is contained in:
parent
748e4e5230
commit
f0fa7640ca
@ -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) {
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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.
|
||||
*
|
||||
|
Loading…
x
Reference in New Issue
Block a user