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.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) {
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
*
|
*
|
||||||
|
Loading…
x
Reference in New Issue
Block a user