diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 75bd5d5089..534e9a4200 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -29,6 +29,10 @@ by the user of the device. Apps can opt-out of contributing to platform diagnostics for Transformer with `Transformer.Builder.setUsePlatformDiagnostics(false)`. + * Split `InAppMuxer` into `InAppMp4Muxer` and `InAppFragmentedMp4Muxer`. + `InAppMp4Muxer` is to be used for producing a non-fragmented MP4 file, + while `InAppFragmentedMp4Muxer` is to be used for producing a fragmented + MP4 file. * Track Selection: * Extractors: * Fix handling of NAL units with lengths expressed in 1 or 2 bytes (rather diff --git a/demos/composition/src/main/java/androidx/media3/demo/composition/CompositionPreviewActivity.java b/demos/composition/src/main/java/androidx/media3/demo/composition/CompositionPreviewActivity.java index 7fbf831d17..397e12c64a 100644 --- a/demos/composition/src/main/java/androidx/media3/demo/composition/CompositionPreviewActivity.java +++ b/demos/composition/src/main/java/androidx/media3/demo/composition/CompositionPreviewActivity.java @@ -57,7 +57,8 @@ import androidx.media3.transformer.EditedMediaItemSequence; import androidx.media3.transformer.Effects; import androidx.media3.transformer.ExportException; import androidx.media3.transformer.ExportResult; -import androidx.media3.transformer.InAppMuxer; +import androidx.media3.transformer.InAppFragmentedMp4Muxer; +import androidx.media3.transformer.InAppMp4Muxer; import androidx.media3.transformer.JsonUtil; import androidx.media3.transformer.Transformer; import androidx.media3.ui.PlayerView; @@ -362,21 +363,20 @@ public final class CompositionPreviewActivity extends AppCompatActivity { enableDebugTracingCheckBox.setOnCheckedChangeListener( (buttonView, isChecked) -> DebugTraceUtil.enableTracing = isChecked); - // Connect producing fragmented MP4 to using Media3 Muxer - CheckBox useMedia3MuxerCheckBox = - exportSettingsDialogView.findViewById(R.id.use_media3_muxer_checkbox); - CheckBox produceFragmentedMp4CheckBox = - exportSettingsDialogView.findViewById(R.id.produce_fragmented_mp4_checkbox); - useMedia3MuxerCheckBox.setOnCheckedChangeListener( - (buttonView, isChecked) -> { - if (!isChecked) { - produceFragmentedMp4CheckBox.setChecked(false); - } - }); - produceFragmentedMp4CheckBox.setOnCheckedChangeListener( + CheckBox useMedia3Mp4MuxerCheckBox = + exportSettingsDialogView.findViewById(R.id.use_media3_mp4_muxer_checkbox); + CheckBox useMedia3FragmentedMp4MuxerCheckBox = + exportSettingsDialogView.findViewById(R.id.use_media3_fragmented_mp4_muxer_checkbox); + useMedia3Mp4MuxerCheckBox.setOnCheckedChangeListener( (buttonView, isChecked) -> { if (isChecked) { - useMedia3MuxerCheckBox.setChecked(true); + useMedia3FragmentedMp4MuxerCheckBox.setChecked(false); + } + }); + useMedia3FragmentedMp4MuxerCheckBox.setOnCheckedChangeListener( + (buttonView, isChecked) -> { + if (isChecked) { + useMedia3Mp4MuxerCheckBox.setChecked(false); } }); @@ -419,15 +419,15 @@ public final class CompositionPreviewActivity extends AppCompatActivity { transformerBuilder.setVideoMimeType(selectedVideoMimeType); } - CheckBox useMedia3MuxerCheckBox = - exportSettingsDialogView.findViewById(R.id.use_media3_muxer_checkbox); - CheckBox produceFragmentedMp4CheckBox = - exportSettingsDialogView.findViewById(R.id.produce_fragmented_mp4_checkbox); - if (useMedia3MuxerCheckBox.isChecked()) { - transformerBuilder.setMuxerFactory( - new InAppMuxer.Factory.Builder() - .setOutputFragmentedMp4(produceFragmentedMp4CheckBox.isChecked()) - .build()); + CheckBox useMedia3Mp4MuxerCheckBox = + exportSettingsDialogView.findViewById(R.id.use_media3_mp4_muxer_checkbox); + CheckBox useMedia3FragmentedMp4MuxerCheckBox = + exportSettingsDialogView.findViewById(R.id.use_media3_fragmented_mp4_muxer_checkbox); + if (useMedia3Mp4MuxerCheckBox.isChecked()) { + transformerBuilder.setMuxerFactory(new InAppMp4Muxer.Factory()); + } + if (useMedia3FragmentedMp4MuxerCheckBox.isChecked()) { + transformerBuilder.setMuxerFactory(new InAppFragmentedMp4Muxer.Factory()); } transformer = diff --git a/demos/composition/src/main/res/layout/export_settings.xml b/demos/composition/src/main/res/layout/export_settings.xml index 9500e55519..f5276c048b 100644 --- a/demos/composition/src/main/res/layout/export_settings.xml +++ b/demos/composition/src/main/res/layout/export_settings.xml @@ -79,12 +79,12 @@ android:orientation="horizontal" android:gravity="center_vertical"> Output audio MIME type Output video MIME type Enable debug tracing - Use Media3 muxer - Produce fragmented MP4 + Use Media3 Mp4Muxer + Use Media3 FragmentedMp4Muxer diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java index e297d09b9a..aac26162ff 100644 --- a/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java +++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java @@ -77,8 +77,8 @@ public final class ConfigurationActivity extends AppCompatActivity { public static final String ENABLE_ANALYZER_MODE = "enable_analyzer_mode"; public static final String ENABLE_DEBUG_PREVIEW = "enable_debug_preview"; public static final String ABORT_SLOW_EXPORT = "abort_slow_export"; - public static final String USE_MEDIA3_MUXER = "use_media3_muxer"; - public static final String PRODUCE_FRAGMENTED_MP4 = "produce_fragmented_mp4"; + public static final String USE_MEDIA3_MP4_MUXER = "use_media3_mp4_muxer"; + public static final String USE_MEDIA3_FRAGMENTED_MP4_MUXER = "use_media3_fragmented_mp4_muxer"; public static final String HDR_MODE = "hdr_mode"; public static final String AUDIO_EFFECTS_SELECTIONS = "audio_effects_selections"; public static final String VIDEO_EFFECTS_SELECTIONS = "video_effects_selections"; @@ -177,8 +177,8 @@ public final class ConfigurationActivity extends AppCompatActivity { private CheckBox enableDebugPreviewCheckBox; private CheckBox enableDebugTracingCheckBox; private CheckBox abortSlowExportCheckBox; - private CheckBox useMedia3Muxer; - private CheckBox produceFragmentedMp4CheckBox; + private CheckBox useMedia3Mp4Muxer; + private CheckBox useMedia3FragmentedMp4Muxer; private Spinner hdrModeSpinner; private Button selectAudioEffectsButton; private Button selectVideoEffectsButton; @@ -303,18 +303,18 @@ public final class ConfigurationActivity extends AppCompatActivity { (buttonView, isChecked) -> DebugTraceUtil.enableTracing = isChecked); abortSlowExportCheckBox = findViewById(R.id.abort_slow_export_checkbox); - useMedia3Muxer = findViewById(R.id.use_media3_muxer_checkbox); - produceFragmentedMp4CheckBox = findViewById(R.id.produce_fragmented_mp4_checkbox); - useMedia3Muxer.setOnCheckedChangeListener( - (buttonView, isChecked) -> { - if (!isChecked) { - produceFragmentedMp4CheckBox.setChecked(false); - } - }); - produceFragmentedMp4CheckBox.setOnCheckedChangeListener( + useMedia3Mp4Muxer = findViewById(R.id.use_media3_mp4_muxer_checkbox); + useMedia3FragmentedMp4Muxer = findViewById(R.id.use_media3_fragmented_mp4_muxer_checkbox); + useMedia3Mp4Muxer.setOnCheckedChangeListener( (buttonView, isChecked) -> { if (isChecked) { - useMedia3Muxer.setChecked(true); + useMedia3FragmentedMp4Muxer.setChecked(false); + } + }); + useMedia3FragmentedMp4Muxer.setOnCheckedChangeListener( + (buttonView, isChecked) -> { + if (isChecked) { + useMedia3Mp4Muxer.setChecked(false); } }); @@ -407,8 +407,8 @@ public final class ConfigurationActivity extends AppCompatActivity { bundle.putBoolean(ENABLE_ANALYZER_MODE, enableAnalyzerModeCheckBox.isChecked()); bundle.putBoolean(ENABLE_DEBUG_PREVIEW, enableDebugPreviewCheckBox.isChecked()); bundle.putBoolean(ABORT_SLOW_EXPORT, abortSlowExportCheckBox.isChecked()); - bundle.putBoolean(USE_MEDIA3_MUXER, useMedia3Muxer.isChecked()); - bundle.putBoolean(PRODUCE_FRAGMENTED_MP4, produceFragmentedMp4CheckBox.isChecked()); + bundle.putBoolean(USE_MEDIA3_MP4_MUXER, useMedia3Mp4Muxer.isChecked()); + bundle.putBoolean(USE_MEDIA3_FRAGMENTED_MP4_MUXER, useMedia3FragmentedMp4Muxer.isChecked()); String selectedHdrMode = String.valueOf(hdrModeSpinner.getSelectedItem()); bundle.putInt(HDR_MODE, HDR_MODE_DESCRIPTIONS.get(selectedHdrMode)); bundle.putBooleanArray(AUDIO_EFFECTS_SELECTIONS, audioEffectsSelections); diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java index eb2259bfea..9b1ec9eb02 100644 --- a/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java +++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java @@ -88,7 +88,8 @@ import androidx.media3.transformer.Effects; import androidx.media3.transformer.ExperimentalAnalyzerModeFactory; import androidx.media3.transformer.ExportException; import androidx.media3.transformer.ExportResult; -import androidx.media3.transformer.InAppMuxer; +import androidx.media3.transformer.InAppFragmentedMp4Muxer; +import androidx.media3.transformer.InAppMp4Muxer; import androidx.media3.transformer.JsonUtil; import androidx.media3.transformer.ProgressHolder; import androidx.media3.transformer.Transformer; @@ -302,12 +303,12 @@ public final class TransformerActivity extends AppCompatActivity { transformerBuilder.setMaxDelayBetweenMuxerSamplesMs(C.TIME_UNSET); } - if (bundle.getBoolean(ConfigurationActivity.USE_MEDIA3_MUXER)) { - transformerBuilder.setMuxerFactory( - new InAppMuxer.Factory.Builder() - .setOutputFragmentedMp4( - bundle.getBoolean(ConfigurationActivity.PRODUCE_FRAGMENTED_MP4)) - .build()); + if (bundle.getBoolean(ConfigurationActivity.USE_MEDIA3_MP4_MUXER)) { + transformerBuilder.setMuxerFactory(new InAppMp4Muxer.Factory()); + } + + if (bundle.getBoolean(ConfigurationActivity.USE_MEDIA3_FRAGMENTED_MP4_MUXER)) { + transformerBuilder.setMuxerFactory(new InAppFragmentedMp4Muxer.Factory()); } if (bundle.getBoolean(ConfigurationActivity.ENABLE_DEBUG_PREVIEW)) { diff --git a/demos/transformer/src/main/res/layout/configuration_activity.xml b/demos/transformer/src/main/res/layout/configuration_activity.xml index bc2997b184..9678ef0dbf 100644 --- a/demos/transformer/src/main/res/layout/configuration_activity.xml +++ b/demos/transformer/src/main/res/layout/configuration_activity.xml @@ -239,18 +239,18 @@ android:layout_weight="1"> + android:text="@string/use_media3_mp4_muxer" /> + android:text="@string/use_media3_fragmented_mp4_muxer" /> Enable debug preview Enable debug tracing Abort slow export - Use Media3 muxer - Produce fragmented MP4 + Use Media3 Mp4Muxer + Use Media3 FragmentedMp4Muxer Trim HDR mode Add audio effects diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java index 06b62dc3c8..d5be46e789 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java @@ -1385,7 +1385,7 @@ public final class AndroidTestUtil { /** Returns a {@link Muxer.Factory} depending upon the API level. */ public static Muxer.Factory getMuxerFactoryBasedOnApi() { // MediaMuxer supports B-frame from API > 24. - return SDK_INT > 24 ? new DefaultMuxer.Factory() : new InAppMuxer.Factory.Builder().build(); + return SDK_INT > 24 ? new DefaultMuxer.Factory() : new InAppMp4Muxer.Factory(); } private static boolean canDecode(Format format) throws MediaCodecUtil.DecoderQueryException { diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java index 767e17edfe..fb9d24ae21 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java @@ -1815,7 +1815,7 @@ public class TransformerEndToEndTest { context, new Transformer.Builder(context) .setVideoMimeType(MimeTypes.VIDEO_H265) - .setMuxerFactory(new InAppMuxer.Factory.Builder().build()) + .setMuxerFactory(new InAppMp4Muxer.Factory()) .build()) .build() .run(testId, editedMediaItem); @@ -2112,9 +2112,7 @@ public class TransformerEndToEndTest { public void transmux_audioWithEditListUsingInAppMuxer_preservesDuration() throws Exception { Context context = ApplicationProvider.getApplicationContext(); Transformer transformer = - new Transformer.Builder(context) - .setMuxerFactory(new InAppMuxer.Factory.Builder().build()) - .build(); + new Transformer.Builder(context).setMuxerFactory(new InAppMp4Muxer.Factory()).build(); MediaItem mediaItem = MediaItem.fromUri(Uri.parse("asset:///media/mp4/long_edit_list_audioonly.mp4")); @@ -2393,7 +2391,7 @@ public class TransformerEndToEndTest { // The MediaMuxer is not writing the bitrate hence use the InAppMuxer. Transformer transformer = new Transformer.Builder(context) - .setMuxerFactory(new InAppMuxer.Factory.Builder().build()) + .setMuxerFactory(new InAppMp4Muxer.Factory()) .setEncoderFactory( new DefaultEncoderFactory.Builder(context) .setRequestedAudioEncoderSettings( diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerWithInAppMuxerEndToEndAndroidTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerWithInAppMp4MuxerEndToEndAndroidTest.java similarity index 92% rename from libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerWithInAppMuxerEndToEndAndroidTest.java rename to libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerWithInAppMp4MuxerEndToEndAndroidTest.java index fc457e7503..74fba5f27c 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerWithInAppMuxerEndToEndAndroidTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerWithInAppMp4MuxerEndToEndAndroidTest.java @@ -38,9 +38,9 @@ import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameter; import org.junit.runners.Parameterized.Parameters; -/** End-to-end instrumentation test for {@link Transformer} with {@link InAppMuxer}. */ +/** End-to-end instrumentation test for {@link Transformer} with {@link InAppMp4Muxer}. */ @RunWith(Parameterized.class) -public class TransformerWithInAppMuxerEndToEndAndroidTest { +public class TransformerWithInAppMp4MuxerEndToEndAndroidTest { private static final String MP4_FILE_ASSET_DIRECTORY = "asset:///media/mp4/"; private static final String H264_MP4 = "sample_no_bframes.mp4"; private static final String H265_MP4 = "h265_with_metadata_track.mp4"; @@ -66,9 +66,7 @@ public class TransformerWithInAppMuxerEndToEndAndroidTest { /* inputFormat= */ MP4_ASSET.videoFormat, /* outputFormat= */ MP4_ASSET.videoFormat); Transformer transformer = - new Transformer.Builder(context) - .setMuxerFactory(new InAppMuxer.Factory.Builder().build()) - .build(); + new Transformer.Builder(context).setMuxerFactory(new InAppMp4Muxer.Factory()).build(); ImmutableList videoEffects = ImmutableList.of(RgbFilter.createGrayscaleFilter()); MediaItem mediaItem = MediaItem.fromUri(Uri.parse(MP4_FILE_ASSET_DIRECTORY + inputFile)); EditedMediaItem editedMediaItem = @@ -91,9 +89,7 @@ public class TransformerWithInAppMuxerEndToEndAndroidTest { assumeTrue(checkNotNull(inputFile).equals(H264_MP4)); String testId = "audioEditing_completesSuccessfully"; Transformer transformer = - new Transformer.Builder(context) - .setMuxerFactory(new InAppMuxer.Factory.Builder().build()) - .build(); + new Transformer.Builder(context).setMuxerFactory(new InAppMp4Muxer.Factory()).build(); ChannelMixingAudioProcessor channelMixingAudioProcessor = new ChannelMixingAudioProcessor(); channelMixingAudioProcessor.putChannelMixingMatrix( ChannelMixingMatrix.create(/* inputChannelCount= */ 1, /* outputChannelCount= */ 2)); diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformerWithInAppMuxerEndToEndMhTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformerWithInAppMp4MuxerEndToEndMhTest.java similarity index 91% rename from libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformerWithInAppMuxerEndToEndMhTest.java rename to libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformerWithInAppMp4MuxerEndToEndMhTest.java index c7b88c5583..29e16d8c98 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformerWithInAppMuxerEndToEndMhTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformerWithInAppMp4MuxerEndToEndMhTest.java @@ -27,7 +27,7 @@ import androidx.media3.effect.RgbFilter; import androidx.media3.transformer.EditedMediaItem; import androidx.media3.transformer.Effects; import androidx.media3.transformer.ExportTestResult; -import androidx.media3.transformer.InAppMuxer; +import androidx.media3.transformer.InAppMp4Muxer; import androidx.media3.transformer.Transformer; import androidx.media3.transformer.TransformerAndroidTestRunner; import androidx.test.core.app.ApplicationProvider; @@ -40,9 +40,9 @@ import org.junit.Test; import org.junit.rules.TestName; import org.junit.runner.RunWith; -/** End-to-end instrumentation test for {@link Transformer} with {@link InAppMuxer}. */ +/** End-to-end instrumentation test for {@link Transformer} with {@link InAppMp4Muxer}. */ @RunWith(AndroidJUnit4.class) -public class TransformerWithInAppMuxerEndToEndMhTest { +public class TransformerWithInAppMp4MuxerEndToEndMhTest { @Rule public final TestName testName = new TestName(); private String testId; @@ -61,9 +61,7 @@ public class TransformerWithInAppMuxerEndToEndMhTest { /* inputFormat= */ MP4_ASSET_AV1_VIDEO.videoFormat, /* outputFormat= */ null); Transformer transformer = - new Transformer.Builder(context) - .setMuxerFactory(new InAppMuxer.Factory.Builder().build()) - .build(); + new Transformer.Builder(context).setMuxerFactory(new InAppMp4Muxer.Factory()).build(); ImmutableList videoEffects = ImmutableList.of(RgbFilter.createGrayscaleFilter()); MediaItem mediaItem = MediaItem.fromUri(Uri.parse(MP4_ASSET_AV1_VIDEO.uri)); EditedMediaItem editedMediaItem = diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/InAppFragmentedMp4Muxer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/InAppFragmentedMp4Muxer.java new file mode 100644 index 0000000000..5c8c9765e4 --- /dev/null +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/InAppFragmentedMp4Muxer.java @@ -0,0 +1,194 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.transformer; + +import android.media.MediaCodec; +import android.media.MediaCodec.BufferInfo; +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; +import androidx.media3.muxer.Muxer; +import androidx.media3.muxer.MuxerException; +import androidx.media3.muxer.MuxerUtil; +import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.nio.ByteBuffer; +import java.util.Locale; + +/** {@link Muxer} implementation that uses a {@link FragmentedMp4Muxer}. */ +// TODO: b/372417042 - Add E2E tests for producing fragmented MP4 output. +@UnstableApi +public final class InAppFragmentedMp4Muxer implements Muxer { + /** {@link Muxer.Factory} for {@link InAppFragmentedMp4Muxer}. */ + public static final class Factory implements Muxer.Factory { + // TODO: b/372417042 - Move these lists to FragmentedMp4Muxer. + /** A list of supported video sample MIME types. */ + private static final ImmutableList SUPPORTED_VIDEO_SAMPLE_MIME_TYPES = + ImmutableList.of( + MimeTypes.VIDEO_AV1, + MimeTypes.VIDEO_H263, + MimeTypes.VIDEO_H264, + MimeTypes.VIDEO_H265, + MimeTypes.VIDEO_MP4V); + + /** A list of supported audio sample MIME types. */ + private static final ImmutableList SUPPORTED_AUDIO_SAMPLE_MIME_TYPES = + ImmutableList.of( + MimeTypes.AUDIO_AAC, + MimeTypes.AUDIO_AMR_NB, + MimeTypes.AUDIO_AMR_WB, + MimeTypes.AUDIO_OPUS, + MimeTypes.AUDIO_VORBIS); + + private final long fragmentDurationMs; + + private long videoDurationUs; + + /** Creates an instance with default values. */ + public Factory() { + this(C.TIME_UNSET); + } + + /** + * Creates an instance. + * + * @param fragmentDurationMs The fragment duration (in milliseconds). + */ + public Factory(long fragmentDurationMs) { + 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 will be 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. + */ + @CanIgnoreReturnValue + public Factory setVideoDurationUs(long videoDurationUs) { + this.videoDurationUs = videoDurationUs; + return this; + } + + @Override + public InAppFragmentedMp4Muxer create(String path) throws MuxerException { + FileOutputStream outputStream; + try { + outputStream = new FileOutputStream(path); + } catch (FileNotFoundException e) { + throw new MuxerException("Error creating file output stream", e); + } + + FragmentedMp4Muxer.Builder builder = new FragmentedMp4Muxer.Builder(outputStream); + if (fragmentDurationMs != C.TIME_UNSET) { + builder.setFragmentDurationMs(fragmentDurationMs); + } + FragmentedMp4Muxer muxer = builder.build(); + + return new InAppFragmentedMp4Muxer(muxer, videoDurationUs); + } + + @Override + public ImmutableList getSupportedSampleMimeTypes(@C.TrackType int trackType) { + if (trackType == C.TRACK_TYPE_VIDEO) { + return SUPPORTED_VIDEO_SAMPLE_MIME_TYPES; + } else if (trackType == C.TRACK_TYPE_AUDIO) { + return SUPPORTED_AUDIO_SAMPLE_MIME_TYPES; + } + return ImmutableList.of(); + } + } + + private static final String TAG = "InAppFragmentedMp4Muxer"; + private static final int TRACK_ID_UNSET = -1; + + private final FragmentedMp4Muxer muxer; + private final long videoDurationUs; + + private int videoTrackId; + + private InAppFragmentedMp4Muxer(FragmentedMp4Muxer muxer, long videoDurationUs) { + this.muxer = muxer; + this.videoDurationUs = videoDurationUs; + videoTrackId = TRACK_ID_UNSET; + } + + @Override + public int addTrack(Format format) { + int trackId = muxer.addTrack(format); + if (MimeTypes.isVideo(format.sampleMimeType)) { + muxer.addMetadataEntry(new Mp4OrientationData(format.rotationDegrees)); + videoTrackId = trackId; + } + return trackId; + } + + @Override + public void writeSampleData(int trackId, ByteBuffer byteBuffer, BufferInfo bufferInfo) + throws MuxerException { + if (videoDurationUs != C.TIME_UNSET + && trackId == videoTrackId + && 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(trackId, byteBuffer, bufferInfo); + } + + @Override + public void addMetadataEntry(Metadata.Entry metadataEntry) { + if (MuxerUtil.isMetadataSupported(metadataEntry)) { + muxer.addMetadataEntry(metadataEntry); + } + } + + @Override + public void close() throws MuxerException { + if (videoDurationUs != C.TIME_UNSET && videoTrackId != TRACK_ID_UNSET) { + BufferInfo bufferInfo = new BufferInfo(); + bufferInfo.set( + /* newOffset= */ 0, + /* newSize= */ 0, + videoDurationUs, + MediaCodec.BUFFER_FLAG_END_OF_STREAM); + writeSampleData(videoTrackId, ByteBuffer.allocateDirect(0), bufferInfo); + } + muxer.close(); + } +} diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/InAppMuxer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/InAppMp4Muxer.java similarity index 69% rename from libraries/transformer/src/main/java/androidx/media3/transformer/InAppMuxer.java rename to libraries/transformer/src/main/java/androidx/media3/transformer/InAppMp4Muxer.java index ddfd1fc325..61a7c77e91 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/InAppMuxer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/InAppMp4Muxer.java @@ -27,7 +27,6 @@ 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; import androidx.media3.muxer.Mp4Muxer; import androidx.media3.muxer.Muxer; import androidx.media3.muxer.MuxerException; @@ -41,10 +40,9 @@ import java.util.LinkedHashSet; import java.util.Locale; import java.util.Set; -/** {@link Muxer} implementation that uses an {@link Mp4Muxer} or {@link FragmentedMp4Muxer}. */ +/** {@link Muxer} implementation that uses an {@link Mp4Muxer}. */ @UnstableApi -public final class InAppMuxer implements Muxer { - +public final class InAppMp4Muxer implements Muxer { /** Provides {@linkplain Metadata.Entry metadata} to add in the output MP4 file. */ public interface MetadataProvider { @@ -60,57 +58,9 @@ public final class InAppMuxer implements Muxer { void updateMetadataEntries(Set metadataEntries); } - /** {@link Muxer.Factory} for {@link InAppMuxer}. */ + /** {@link Muxer.Factory} for {@link InAppMp4Muxer}. */ public static final class Factory implements Muxer.Factory { - - /** A builder for {@link Factory} instances. */ - public static final class Builder { - @Nullable private MetadataProvider metadataProvider; - private boolean outputFragmentedMp4; - private long fragmentDurationMs; - - /** Creates a {@link Builder} instance with default values. */ - public Builder() { - fragmentDurationMs = C.TIME_UNSET; - } - - /** - * Sets an implementation of {@link MetadataProvider}. - * - *

The default value is {@code null}. - * - *

If the value is not set then the {@linkplain Metadata.Entry metadata} from the input - * file is set as it is in the output file. - */ - @CanIgnoreReturnValue - public Builder setMetadataProvider(MetadataProvider metadataProvider) { - this.metadataProvider = metadataProvider; - return this; - } - - /** Sets whether to output a fragmented MP4. */ - @CanIgnoreReturnValue - public Builder setOutputFragmentedMp4(boolean outputFragmentedMp4) { - this.outputFragmentedMp4 = outputFragmentedMp4; - return this; - } - - /** - * Sets the fragment duration (in milliseconds) if the output file is {@link - * #setOutputFragmentedMp4(boolean) fragmented}. - */ - @CanIgnoreReturnValue - public Builder setFragmentDurationMs(long fragmentDurationMs) { - this.fragmentDurationMs = fragmentDurationMs; - return this; - } - - /** Builds a {@link Factory} instance. */ - public Factory build() { - return new Factory(metadataProvider, outputFragmentedMp4, fragmentDurationMs); - } - } - + // TODO: b/372417042 - Move these lists to Mp4Muxer. /** A list of supported video sample MIME types. */ private static final ImmutableList SUPPORTED_VIDEO_SAMPLE_MIME_TYPES = ImmutableList.of( @@ -130,18 +80,21 @@ public final class InAppMuxer implements Muxer { MimeTypes.AUDIO_VORBIS); @Nullable private final MetadataProvider metadataProvider; - private final boolean outputFragmentedMp4; - private final long fragmentDurationMs; private long videoDurationUs; - private Factory( - @Nullable MetadataProvider metadataProvider, - boolean outputFragmentedMp4, - long fragmentDurationMs) { + /** Creates an instance with default values. */ + public Factory() { + this(/* metadataProvider= */ null); + } + + /** + * Creates an instance. + * + * @param metadataProvider A {@link MetadataProvider}. + */ + public Factory(@Nullable MetadataProvider metadataProvider) { this.metadataProvider = metadataProvider; - this.outputFragmentedMp4 = outputFragmentedMp4; - this.fragmentDurationMs = fragmentDurationMs; videoDurationUs = C.TIME_UNSET; } @@ -167,7 +120,7 @@ public final class InAppMuxer implements Muxer { } @Override - public InAppMuxer create(String path) throws MuxerException { + public InAppMp4Muxer create(String path) throws MuxerException { FileOutputStream outputStream; try { outputStream = new FileOutputStream(path); @@ -175,23 +128,14 @@ public final class InAppMuxer implements Muxer { throw new MuxerException("Error creating file output stream", e); } - 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( - LAST_SAMPLE_DURATION_BEHAVIOR_SET_FROM_END_OF_STREAM_BUFFER_OR_DUPLICATE_PREVIOUS); - } - muxer = builder.build(); + Mp4Muxer.Builder builder = new Mp4Muxer.Builder(outputStream); + if (videoDurationUs != C.TIME_UNSET) { + builder.setLastSampleDurationBehavior( + LAST_SAMPLE_DURATION_BEHAVIOR_SET_FROM_END_OF_STREAM_BUFFER_OR_DUPLICATE_PREVIOUS); } + Mp4Muxer muxer = builder.build(); - return new InAppMuxer(muxer, metadataProvider, videoDurationUs); + return new InAppMp4Muxer(muxer, metadataProvider, videoDurationUs); } @Override @@ -205,18 +149,18 @@ public final class InAppMuxer implements Muxer { } } - private static final String TAG = "InAppMuxer"; + private static final String TAG = "InAppMp4Muxer"; private static final int TRACK_ID_UNSET = -1; - private final Muxer muxer; + private final Mp4Muxer muxer; @Nullable private final MetadataProvider metadataProvider; private final long videoDurationUs; private final Set metadataEntries; private int videoTrackId; - private InAppMuxer( - Muxer muxer, @Nullable MetadataProvider metadataProvider, long videoDurationUs) { + private InAppMp4Muxer( + Mp4Muxer muxer, @Nullable MetadataProvider metadataProvider, long videoDurationUs) { this.muxer = muxer; this.metadataProvider = metadataProvider; this.videoDurationUs = videoDurationUs; diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/EncodedSampleExporterTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/EncodedSampleExporterTest.java index b81ae93a95..7970c89dc7 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/EncodedSampleExporterTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/EncodedSampleExporterTest.java @@ -66,7 +66,7 @@ public final class EncodedSampleExporterTest { new TransformationRequest.Builder().build(), new MuxerWrapper( /* outputPath= */ "unused", - new InAppMuxer.Factory.Builder().build(), + new InAppMp4Muxer.Factory(), mock(MuxerWrapper.Listener.class), MuxerWrapper.MUXER_MODE_DEFAULT, /* dropSamplesBeforeFirstVideoSample= */ false, diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMuxerEndToEndParameterizedTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMp4MuxerEndToEndParameterizedTest.java similarity index 88% rename from libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMuxerEndToEndParameterizedTest.java rename to libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMp4MuxerEndToEndParameterizedTest.java index 1113e5f21a..8bf9b0ed42 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMuxerEndToEndParameterizedTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMp4MuxerEndToEndParameterizedTest.java @@ -39,9 +39,9 @@ import org.robolectric.ParameterizedRobolectricTestRunner; import org.robolectric.ParameterizedRobolectricTestRunner.Parameter; import org.robolectric.ParameterizedRobolectricTestRunner.Parameters; -/** End to end parameterized tests for {@link Transformer} with {@link InAppMuxer}. */ +/** End to end parameterized tests for {@link Transformer} with {@link InAppMp4Muxer}. */ @RunWith(ParameterizedRobolectricTestRunner.class) -public class TransformerWithInAppMuxerEndToEndParameterizedTest { +public class TransformerWithInAppMp4MuxerEndToEndParameterizedTest { private static final String H263_3GP = "mp4/bbb_176x144_128kbps_15fps_h263.3gp"; private static final String H264_MP4 = "mp4/sample_no_bframes.mp4"; @@ -84,15 +84,13 @@ public class TransformerWithInAppMuxerEndToEndParameterizedTest { @Test public void transmux_mp4File_outputMatchesExpected() throws Exception { Muxer.Factory inAppMuxerFactory = - new InAppMuxer.Factory.Builder() - .setMetadataProvider( - metadataEntries -> - // Add timestamp to make output file deterministic. - metadataEntries.add( - new Mp4TimestampData( - /* creationTimestampSeconds= */ 3_000_000_000L, - /* modificationTimestampSeconds= */ 4_000_000_000L))) - .build(); + new InAppMp4Muxer.Factory( + metadataEntries -> + // Add timestamp to make output file deterministic. + metadataEntries.add( + new Mp4TimestampData( + /* creationTimestampSeconds= */ 3_000_000_000L, + /* modificationTimestampSeconds= */ 4_000_000_000L))); Transformer transformer = new TestTransformerBuilder(context).setMuxerFactory(inAppMuxerFactory).build(); diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMuxerEndToEndNonParameterizedTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMp4MuxerEndToEndTest.java similarity index 86% rename from libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMuxerEndToEndNonParameterizedTest.java rename to libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMp4MuxerEndToEndTest.java index 39f2abc4b4..0baa1ca0ef 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMuxerEndToEndNonParameterizedTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMp4MuxerEndToEndTest.java @@ -47,9 +47,9 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; -/** End-to-end test for {@link Transformer} with {@link InAppMuxer}. */ +/** End-to-end test for {@link Transformer} with {@link InAppMp4Muxer}. */ @RunWith(AndroidJUnit4.class) -public class TransformerWithInAppMuxerEndToEndNonParameterizedTest { +public class TransformerWithInAppMp4MuxerEndToEndTest { private static final String MP4_FILE_PATH = "asset:///media/mp4/sample_no_bframes.mp4"; @Rule public final TemporaryFolder outputDir = new TemporaryFolder(); @@ -67,15 +67,13 @@ public class TransformerWithInAppMuxerEndToEndNonParameterizedTest { String tsFilePath = "asset:///media/ts/sample_h264.ts"; String tsFileName = "ts/sample_h264.ts"; Muxer.Factory inAppMuxerFactory = - new InAppMuxer.Factory.Builder() - .setMetadataProvider( - metadataEntries -> - // Add timestamp to make output file deterministic. - metadataEntries.add( - new Mp4TimestampData( - /* creationTimestampSeconds= */ 3_000_000_000L, - /* modificationTimestampSeconds= */ 4_000_000_000L))) - .build(); + new InAppMp4Muxer.Factory( + metadataEntries -> + // Add timestamp to make output file deterministic. + metadataEntries.add( + new Mp4TimestampData( + /* creationTimestampSeconds= */ 3_000_000_000L, + /* modificationTimestampSeconds= */ 4_000_000_000L))); Transformer transformer = new TestTransformerBuilder(context).setMuxerFactory(inAppMuxerFactory).build(); @@ -104,14 +102,11 @@ public class TransformerWithInAppMuxerEndToEndNonParameterizedTest { Mp4LocationData expectedLocationData = new Mp4LocationData(/* latitude= */ 45f, /* longitude= */ -90f); Muxer.Factory inAppMuxerFactory = - new InAppMuxer.Factory.Builder() - .setMetadataProvider( - metadataEntries -> { - metadataEntries.removeIf( - (Metadata.Entry entry) -> entry instanceof Mp4LocationData); - metadataEntries.add(expectedLocationData); - }) - .build(); + new InAppMp4Muxer.Factory( + metadataEntries -> { + metadataEntries.removeIf((Metadata.Entry entry) -> entry instanceof Mp4LocationData); + metadataEntries.add(expectedLocationData); + }); Transformer transformer = new TestTransformerBuilder(context).setMuxerFactory(inAppMuxerFactory).build(); MediaItem mediaItem = MediaItem.fromUri(Uri.parse(MP4_FILE_PATH)); @@ -130,9 +125,7 @@ public class TransformerWithInAppMuxerEndToEndNonParameterizedTest { String xmpSampleData = "media/xmp/sample_datetime_xmp.xmp"; byte[] xmpData = androidx.media3.test.utils.TestUtil.getByteArray(context, xmpSampleData); Muxer.Factory inAppMuxerFactory = - new InAppMuxer.Factory.Builder() - .setMetadataProvider(metadataEntries -> metadataEntries.add(new XmpData(xmpData))) - .build(); + new InAppMp4Muxer.Factory(metadataEntries -> metadataEntries.add(new XmpData(xmpData))); Transformer transformer = new TestTransformerBuilder(context).setMuxerFactory(inAppMuxerFactory).build(); MediaItem mediaItem = MediaItem.fromUri(Uri.parse(MP4_FILE_PATH)); @@ -153,9 +146,7 @@ public class TransformerWithInAppMuxerEndToEndNonParameterizedTest { /* value= */ Util.toByteArray(captureFps), MdtaMetadataEntry.TYPE_INDICATOR_FLOAT32); Muxer.Factory inAppMuxerFactory = - new InAppMuxer.Factory.Builder() - .setMetadataProvider(metadataEntries -> metadataEntries.add(expectedCaptureFps)) - .build(); + new InAppMp4Muxer.Factory(metadataEntries -> metadataEntries.add(expectedCaptureFps)); Transformer transformer = new TestTransformerBuilder(context).setMuxerFactory(inAppMuxerFactory).build(); MediaItem mediaItem = MediaItem.fromUri(Uri.parse(MP4_FILE_PATH)); @@ -181,9 +172,7 @@ public class TransformerWithInAppMuxerEndToEndNonParameterizedTest { /* creationTimestampSeconds= */ 3_000_000_000L, /* modificationTimestampSeconds= */ 4_000_000_000L); Muxer.Factory inAppMuxerFactory = - new InAppMuxer.Factory.Builder() - .setMetadataProvider(metadataEntries -> metadataEntries.add(expectedTimestampData)) - .build(); + new InAppMp4Muxer.Factory(metadataEntries -> metadataEntries.add(expectedTimestampData)); Transformer transformer = new TestTransformerBuilder(context).setMuxerFactory(inAppMuxerFactory).build(); @@ -212,13 +201,11 @@ public class TransformerWithInAppMuxerEndToEndNonParameterizedTest { /* value= */ Util.toByteArray(600.0f), MdtaMetadataEntry.TYPE_INDICATOR_FLOAT32); Muxer.Factory inAppMuxerFactory = - new InAppMuxer.Factory.Builder() - .setMetadataProvider( - metadataEntries -> { - metadataEntries.add(expectedStringMetadata); - metadataEntries.add(expectedFloatMetadata); - }) - .build(); + new InAppMp4Muxer.Factory( + metadataEntries -> { + metadataEntries.add(expectedStringMetadata); + metadataEntries.add(expectedFloatMetadata); + }); Transformer transformer = new TestTransformerBuilder(context).setMuxerFactory(inAppMuxerFactory).build(); MediaItem mediaItem = MediaItem.fromUri(Uri.parse(MP4_FILE_PATH)); @@ -248,7 +235,7 @@ public class TransformerWithInAppMuxerEndToEndNonParameterizedTest { @Test public void transmux_withSettingVideoDuration_writesCorrectVideoDuration() throws Exception { - InAppMuxer.Factory inAppMuxerFactory = new InAppMuxer.Factory.Builder().build(); + InAppMp4Muxer.Factory inAppMuxerFactory = new InAppMp4Muxer.Factory(); long expectedDurationUs = 2_000_000L; inAppMuxerFactory.setVideoDurationUs(expectedDurationUs); Transformer transformer =